Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion apps/desktop/src/renderer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,11 @@ application -> shared contracts + injected ports
preload, main, or `platform/desktop`. Consumers use its public `index` entry;
`testing` is test/Storybook-only.
- `platform/desktop/` is the outer adapter zone for the preload bridge. It
implements narrow inward-facing ports rather than exporting the whole bridge;
implements narrow inward-facing ports rather than exporting the whole bridge:
where a port is a structural subset of one bridge namespace the adapter hands
that namespace through as-is (`sessions: bridge.sessions`) instead of
restating each method, and hand-writes the blocks that rename, guard, or
translate;
composition and adapters consume application public entries, not deep
implementation modules. Adapters may own bridge and browser-environment
access, but never React UI/hooks/class lifecycle, Electron/Node imports, or
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { createContext, useContext, type ReactElement, type ReactNode } from 'react';

export interface ServicesContext<S> {
/** Mounts one services object for the feature's subtree. */
readonly Provider: (props: {
readonly services: S;
readonly children?: ReactNode;
}) => ReactElement;
/** Reads the mounted services; throws when the Provider is missing. */
readonly useServices: () => S;
}

/**
* One services context per feature slice.
*
* A slice declares its inward-facing services type in `ports.ts` and gets the
* Provider/hook pair from here instead of restating twenty lines of
* `createContext` boilerplate. `providerName` is the Provider's displayName
* and the name in the error a consumer sees when nothing is mounted, so the
* message stays `<Feature>ServicesProvider is missing` for every slice.
*/
export function createServicesContext<S>(providerName: string): ServicesContext<S> {
const Context = createContext<S | null>(null);
function Provider(props: {
readonly services: S;
readonly children?: ReactNode;
}): ReactElement {
return <Context.Provider value={props.services}>{props.children}</Context.Provider>;
}
Provider.displayName = providerName;
function useServices(): S {
const services = useContext(Context);
if (!services) throw new Error(`${providerName} is missing`);
return services;
}
return { Provider, useServices };
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,26 +17,18 @@
* under the License.
*/

import { createContext, useContext, useSyncExternalStore, type ReactNode } from 'react';
import { useSyncExternalStore, type ReactNode } from 'react';
import { createServicesContext } from '../../application/contracts/feature-services.js';
import type { ApiKeyOnboardingBridge, ConnectionSettingsServices } from './ports.js';

const ConnectionSettingsServicesContext = createContext<ConnectionSettingsServices | null>(null);
const { Provider, useServices } = createServicesContext<ConnectionSettingsServices>(
'ConnectionSettingsServicesProvider',
);

export function ConnectionSettingsServicesProvider(props: {
readonly services: ConnectionSettingsServices;
readonly children?: ReactNode;
}) {
return (
<ConnectionSettingsServicesContext.Provider value={props.services}>
{props.children}
</ConnectionSettingsServicesContext.Provider>
);
}
export const ConnectionSettingsServicesProvider = Provider;

export function useConnectionSettingsServices(): ConnectionSettingsServices {
const services = useContext(ConnectionSettingsServicesContext);
if (!services) throw new Error('ConnectionSettingsServicesProvider is missing');
return services;
return useServices();
}

export function ConnectionSettingsServicesConsumer(props: {
Expand Down
19 changes: 4 additions & 15 deletions apps/desktop/src/renderer/features/goals/services-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,13 @@
* under the License.
*/

import { createContext, useContext, type ReactNode } from 'react';
import { createServicesContext } from '../../application/contracts/feature-services.js';
import type { GoalServices } from './ports.js';

const GoalServicesContext = createContext<GoalServices | null>(null);
const { Provider, useServices } = createServicesContext<GoalServices>('GoalServicesProvider');

export function GoalServicesProvider(props: {
services: GoalServices;
children?: ReactNode;
}) {
return (
<GoalServicesContext.Provider value={props.services}>
{props.children}
</GoalServicesContext.Provider>
);
}
export const GoalServicesProvider = Provider;

export function useGoalServices(): GoalServices {
const services = useContext(GoalServicesContext);
if (!services) throw new Error('GoalServicesProvider is missing');
return services;
return useServices();
}
19 changes: 4 additions & 15 deletions apps/desktop/src/renderer/features/module-hub/services-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,13 @@
* under the License.
*/

import { createContext, useContext, type ReactNode } from 'react';
import { createServicesContext } from '../../application/contracts/feature-services.js';
import type { ModuleHubServices } from './ports.js';

const ModuleHubServicesContext = createContext<ModuleHubServices | null>(null);
const { Provider, useServices } = createServicesContext<ModuleHubServices>('ModuleHubServicesProvider');

export function ModuleHubServicesProvider(props: {
services: ModuleHubServices;
children?: ReactNode;
}) {
return (
<ModuleHubServicesContext.Provider value={props.services}>
{props.children}
</ModuleHubServicesContext.Provider>
);
}
export const ModuleHubServicesProvider = Provider;

export function useModuleHubServices(): ModuleHubServices {
const services = useContext(ModuleHubServicesContext);
if (!services) throw new Error('ModuleHubServicesProvider is missing');
return services;
return useServices();
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,13 @@
* under the License.
*/

import { createContext, useContext, type ReactNode } from 'react';
import { createServicesContext } from '../../application/contracts/feature-services.js';
import type { RuntimeHostManagementServices } from './ports.js';

const RuntimeHostManagementServicesContext = createContext<RuntimeHostManagementServices | null>(null);
const { Provider, useServices } = createServicesContext<RuntimeHostManagementServices>('RuntimeHostManagementServicesProvider');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: this line is 128 characters, over the repo's lineWidth: 100 (biome.jsonc:47). CI cannot catch it because apps/desktop/** is excluded from the formatter (biome.jsonc:56), but the rest of desktop keeps to 100, and connection-settings/services-context.tsx in this same PR wraps the identical call. Six of the nine rewritten files are over: module-hub 104, task-entry 104, session-settings 116, session-navigation 120, session-collaboration 126, this one 128. Smallest fix: wrap them the way connection-settings does.


export function RuntimeHostManagementServicesProvider(props: {
readonly services: RuntimeHostManagementServices;
readonly children?: ReactNode;
}) {
return (
<RuntimeHostManagementServicesContext.Provider value={props.services}>
{props.children}
</RuntimeHostManagementServicesContext.Provider>
);
}
export const RuntimeHostManagementServicesProvider = Provider;

export function useRuntimeHostManagementServices(): RuntimeHostManagementServices {
const services = useContext(RuntimeHostManagementServicesContext);
if (!services) throw new Error('RuntimeHostManagementServicesProvider is missing');
return services;
return useServices();
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,13 @@
* under the License.
*/

import { createContext, useContext, type ReactNode } from 'react';
import { createServicesContext } from '../../application/contracts/feature-services.js';
import type { SessionCollaborationServices } from './ports.js';

const SessionCollaborationServicesContext =
createContext<SessionCollaborationServices | null>(null);
const { Provider, useServices } = createServicesContext<SessionCollaborationServices>('SessionCollaborationServicesProvider');

export function SessionCollaborationServicesProvider(props: {
readonly services: SessionCollaborationServices;
readonly children?: ReactNode;
}) {
return (
<SessionCollaborationServicesContext.Provider value={props.services}>
{props.children}
</SessionCollaborationServicesContext.Provider>
);
}
export const SessionCollaborationServicesProvider = Provider;

export function useSessionCollaborationServices(): SessionCollaborationServices {
const services = useContext(SessionCollaborationServicesContext);
if (!services) throw new Error('SessionCollaborationServicesProvider is missing');
return services;
return useServices();
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,13 @@
* under the License.
*/

import { createContext, useContext, type ReactNode } from 'react';
import { createServicesContext } from '../../application/contracts/feature-services.js';
import type { SessionNavigationServices } from './ports.js';

const SessionNavigationServicesContext =
createContext<SessionNavigationServices | null>(null);
const { Provider, useServices } = createServicesContext<SessionNavigationServices>('SessionNavigationServicesProvider');

export function SessionNavigationServicesProvider(props: {
services: SessionNavigationServices;
children?: ReactNode;
}) {
return (
<SessionNavigationServicesContext.Provider value={props.services}>
{props.children}
</SessionNavigationServicesContext.Provider>
);
}
export const SessionNavigationServicesProvider = Provider;

export function useSessionNavigationServices(): SessionNavigationServices {
const services = useContext(SessionNavigationServicesContext);
if (!services) {
throw new Error('SessionNavigationServicesProvider is missing');
}
return services;
return useServices();
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,13 @@
* under the License.
*/

import { createContext, useContext, type ReactNode } from 'react';
import { createServicesContext } from '../../application/contracts/feature-services.js';
import type { SessionSettingsServices } from './ports.js';

const SessionSettingsServicesContext = createContext<SessionSettingsServices | null>(null);
const { Provider, useServices } = createServicesContext<SessionSettingsServices>('SessionSettingsServicesProvider');

export function SessionSettingsServicesProvider(props: {
readonly services: SessionSettingsServices;
readonly children?: ReactNode;
}) {
return (
<SessionSettingsServicesContext.Provider value={props.services}>
{props.children}
</SessionSettingsServicesContext.Provider>
);
}
export const SessionSettingsServicesProvider = Provider;

export function useSessionSettingsServices(): SessionSettingsServices {
const services = useContext(SessionSettingsServicesContext);
if (!services) throw new Error('SessionSettingsServicesProvider is missing');
return services;
return useServices();
}
19 changes: 4 additions & 15 deletions apps/desktop/src/renderer/features/task-entry/services-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,13 @@
* under the License.
*/

import { createContext, useContext, type ReactNode } from 'react';
import { createServicesContext } from '../../application/contracts/feature-services.js';
import type { TaskEntryServices } from './ports.js';

const TaskEntryServicesContext = createContext<TaskEntryServices | null>(null);
const { Provider, useServices } = createServicesContext<TaskEntryServices>('TaskEntryServicesProvider');

export function TaskEntryServicesProvider(props: {
services: TaskEntryServices;
children?: ReactNode;
}) {
return (
<TaskEntryServicesContext.Provider value={props.services}>
{props.children}
</TaskEntryServicesContext.Provider>
);
}
export const TaskEntryServicesProvider = Provider;

export function useTaskEntryServices(): TaskEntryServices {
const services = useContext(TaskEntryServicesContext);
if (!services) throw new Error('TaskEntryServicesProvider is missing');
return services;
return useServices();
}
21 changes: 4 additions & 17 deletions apps/desktop/src/renderer/features/workbar/services-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,26 +17,13 @@
* under the License.
*/

import { createContext, useContext, type ReactNode } from 'react';
import { createServicesContext } from '../../application/contracts/feature-services.js';
import type { WorkbarServices } from './ports.js';

const WorkbarServicesContext = createContext<WorkbarServices | null>(null);
const { Provider, useServices } = createServicesContext<WorkbarServices>('WorkbarServicesProvider');

export function WorkbarServicesProvider(props: {
services: WorkbarServices;
children?: ReactNode;
}) {
return (
<WorkbarServicesContext.Provider value={props.services}>
{props.children}
</WorkbarServicesContext.Provider>
);
}
export const WorkbarServicesProvider = Provider;

export function useWorkbarServices(): WorkbarServices {
const services = useContext(WorkbarServicesContext);
if (!services) {
throw new Error('WorkbarServicesProvider is missing');
}
return services;
return useServices();
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,22 +84,7 @@ export function createDesktopModuleHubServices(
delete: (skillRef, host) => bridge.skills.delete(skillRef, host),
open: (skillId, target, host) => bridge.skills.open(skillId, target, host),
},
scheduledTasks: {
list: (host) => bridge.scheduledTasks.list(host),
create: (input, host) => bridge.scheduledTasks.create(input, host),
update: (id, patch, host) =>
bridge.scheduledTasks.update(id, patch, host),
setEnabled: (id, enabled, host) =>
bridge.scheduledTasks.setEnabled(id, enabled, host),
triggerNow: (id, host) => bridge.scheduledTasks.triggerNow(id, host),
snooze: (id, host) => bridge.scheduledTasks.snooze(id, host),
clearRunHistory: (id, host) =>
bridge.scheduledTasks.clearRunHistory(id, host),
delete: (id, host) => bridge.scheduledTasks.delete(id, host),
subscribeChanges: (handler) =>
bridge.scheduledTasks.subscribeChanges(handler),
subscribeDue: (handler) => bridge.scheduledTasks.subscribeDue(handler),
},
scheduledTasks: bridge.scheduledTasks,
clientSettings: {
supported: clientSettingsSupported,
async getKeepSystemAwake() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,6 @@ export function createDesktopSessionNavigationServices(
bridge: DesktopSessionNavigationBridge = window.maka,
): SessionNavigationServices {
return {
sessions: {
list: () => bridge.sessions.list(),
setFlagged: (sessionId, flagged, options) =>
bridge.sessions.setFlagged(sessionId, flagged, options),
archive: (sessionId, options) =>
bridge.sessions.archive(sessionId, options),
unarchive: (sessionId, options) =>
bridge.sessions.unarchive(sessionId, options),
rename: (sessionId, name, options) =>
bridge.sessions.rename(sessionId, name, options),
remove: (sessionId, options) =>
bridge.sessions.remove(sessionId, options),
previewRemoval: (sessionId) => bridge.sessions.previewRemoval(sessionId),
},
sessions: bridge.sessions,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3, recording the trade-off rather than blocking it. This is the widest of the six pass-throughs: SessionNavigationServices['sessions'] is 7 methods, while the object the rail now holds at runtime is the full window.maka.sessions, 47 methods including send, stop, compact, regenerateTurn, approvePlan and respondToSandboxBoundary. Before this change the adapter object itself was the enforcement; now only the port type is, so a future cast reaches the whole execution surface from a navigation feature. No consumer does that today and the README amendment states the rule, so I am not asking for a change. If you want to keep one adapter hand-written, this is the one that earns it.

};
}
Loading