Skip to content

Commit 512b255

Browse files
committed
refactor(desktop): share the feature services context and pass bridge namespaces through
Add application/contracts/feature-services.tsx with createServicesContext, and derive every feature slice's ServicesProvider/useServices pair from it instead of restating the same createContext boilerplate nine times; every exported name, type, and error message is unchanged. Where a Desktop adapter's port is a structural subset of one bridge namespace, hand the namespace through (sessions, newTasks, scheduledTasks, shellRuns, todo, attachments) instead of restating each method. Blocks that rename, guard, filter, or translate stay hand-written: the adapter tests drive Proxy-based bridge recorders without own keys, so a spread would copy nothing there, and passing the object keeps late binding everywhere. Generated-by: Claude Code
1 parent eacfb46 commit 512b255

17 files changed

Lines changed: 109 additions & 200 deletions

File tree

apps/desktop/src/renderer/README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,11 @@ application -> shared contracts + injected ports
6464
preload, main, or `platform/desktop`. Consumers use its public `index` entry;
6565
`testing` is test/Storybook-only.
6666
- `platform/desktop/` is the outer adapter zone for the preload bridge. It
67-
implements narrow inward-facing ports rather than exporting the whole bridge;
67+
implements narrow inward-facing ports rather than exporting the whole bridge:
68+
where a port is a structural subset of one bridge namespace the adapter hands
69+
that namespace through as-is (`sessions: bridge.sessions`) instead of
70+
restating each method, and hand-writes the blocks that rename, guard, or
71+
translate;
6872
composition and adapters consume application public entries, not deep
6973
implementation modules. Adapters may own bridge and browser-environment
7074
access, but never React UI/hooks/class lifecycle, Electron/Node imports, or
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
import { createContext, useContext, type ReactElement, type ReactNode } from 'react';
21+
22+
export interface ServicesContext<S> {
23+
/** Mounts one services object for the feature's subtree. */
24+
readonly Provider: (props: {
25+
readonly services: S;
26+
readonly children?: ReactNode;
27+
}) => ReactElement;
28+
/** Reads the mounted services; throws when the Provider is missing. */
29+
readonly useServices: () => S;
30+
}
31+
32+
/**
33+
* One services context per feature slice.
34+
*
35+
* A slice declares its inward-facing services type in `ports.ts` and gets the
36+
* Provider/hook pair from here instead of restating twenty lines of
37+
* `createContext` boilerplate. `providerName` is the Provider's displayName
38+
* and the name in the error a consumer sees when nothing is mounted, so the
39+
* message stays `<Feature>ServicesProvider is missing` for every slice.
40+
*/
41+
export function createServicesContext<S>(providerName: string): ServicesContext<S> {
42+
const Context = createContext<S | null>(null);
43+
function Provider(props: {
44+
readonly services: S;
45+
readonly children?: ReactNode;
46+
}): ReactElement {
47+
return <Context.Provider value={props.services}>{props.children}</Context.Provider>;
48+
}
49+
Provider.displayName = providerName;
50+
function useServices(): S {
51+
const services = useContext(Context);
52+
if (!services) throw new Error(`${providerName} is missing`);
53+
return services;
54+
}
55+
return { Provider, useServices };
56+
}

apps/desktop/src/renderer/features/connection-settings/services-context.tsx

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,26 +17,18 @@
1717
* under the License.
1818
*/
1919

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

23-
const ConnectionSettingsServicesContext = createContext<ConnectionSettingsServices | null>(null);
24+
const { Provider, useServices } = createServicesContext<ConnectionSettingsServices>(
25+
'ConnectionSettingsServicesProvider',
26+
);
2427

25-
export function ConnectionSettingsServicesProvider(props: {
26-
readonly services: ConnectionSettingsServices;
27-
readonly children?: ReactNode;
28-
}) {
29-
return (
30-
<ConnectionSettingsServicesContext.Provider value={props.services}>
31-
{props.children}
32-
</ConnectionSettingsServicesContext.Provider>
33-
);
34-
}
28+
export const ConnectionSettingsServicesProvider = Provider;
3529

3630
export function useConnectionSettingsServices(): ConnectionSettingsServices {
37-
const services = useContext(ConnectionSettingsServicesContext);
38-
if (!services) throw new Error('ConnectionSettingsServicesProvider is missing');
39-
return services;
31+
return useServices();
4032
}
4133

4234
export function ConnectionSettingsServicesConsumer(props: {

apps/desktop/src/renderer/features/goals/services-context.tsx

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,24 +17,13 @@
1717
* under the License.
1818
*/
1919

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

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

25-
export function GoalServicesProvider(props: {
26-
services: GoalServices;
27-
children?: ReactNode;
28-
}) {
29-
return (
30-
<GoalServicesContext.Provider value={props.services}>
31-
{props.children}
32-
</GoalServicesContext.Provider>
33-
);
34-
}
25+
export const GoalServicesProvider = Provider;
3526

3627
export function useGoalServices(): GoalServices {
37-
const services = useContext(GoalServicesContext);
38-
if (!services) throw new Error('GoalServicesProvider is missing');
39-
return services;
28+
return useServices();
4029
}

apps/desktop/src/renderer/features/module-hub/services-context.tsx

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,24 +17,13 @@
1717
* under the License.
1818
*/
1919

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

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

25-
export function ModuleHubServicesProvider(props: {
26-
services: ModuleHubServices;
27-
children?: ReactNode;
28-
}) {
29-
return (
30-
<ModuleHubServicesContext.Provider value={props.services}>
31-
{props.children}
32-
</ModuleHubServicesContext.Provider>
33-
);
34-
}
25+
export const ModuleHubServicesProvider = Provider;
3526

3627
export function useModuleHubServices(): ModuleHubServices {
37-
const services = useContext(ModuleHubServicesContext);
38-
if (!services) throw new Error('ModuleHubServicesProvider is missing');
39-
return services;
28+
return useServices();
4029
}

apps/desktop/src/renderer/features/runtime-host-management/services-context.tsx

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,24 +17,13 @@
1717
* under the License.
1818
*/
1919

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

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

25-
export function RuntimeHostManagementServicesProvider(props: {
26-
readonly services: RuntimeHostManagementServices;
27-
readonly children?: ReactNode;
28-
}) {
29-
return (
30-
<RuntimeHostManagementServicesContext.Provider value={props.services}>
31-
{props.children}
32-
</RuntimeHostManagementServicesContext.Provider>
33-
);
34-
}
25+
export const RuntimeHostManagementServicesProvider = Provider;
3526

3627
export function useRuntimeHostManagementServices(): RuntimeHostManagementServices {
37-
const services = useContext(RuntimeHostManagementServicesContext);
38-
if (!services) throw new Error('RuntimeHostManagementServicesProvider is missing');
39-
return services;
28+
return useServices();
4029
}

apps/desktop/src/renderer/features/session-collaboration/services-context.tsx

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,25 +17,13 @@
1717
* under the License.
1818
*/
1919

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

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

26-
export function SessionCollaborationServicesProvider(props: {
27-
readonly services: SessionCollaborationServices;
28-
readonly children?: ReactNode;
29-
}) {
30-
return (
31-
<SessionCollaborationServicesContext.Provider value={props.services}>
32-
{props.children}
33-
</SessionCollaborationServicesContext.Provider>
34-
);
35-
}
25+
export const SessionCollaborationServicesProvider = Provider;
3626

3727
export function useSessionCollaborationServices(): SessionCollaborationServices {
38-
const services = useContext(SessionCollaborationServicesContext);
39-
if (!services) throw new Error('SessionCollaborationServicesProvider is missing');
40-
return services;
28+
return useServices();
4129
}

apps/desktop/src/renderer/features/session-navigation/services-context.tsx

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,27 +17,13 @@
1717
* under the License.
1818
*/
1919

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

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

26-
export function SessionNavigationServicesProvider(props: {
27-
services: SessionNavigationServices;
28-
children?: ReactNode;
29-
}) {
30-
return (
31-
<SessionNavigationServicesContext.Provider value={props.services}>
32-
{props.children}
33-
</SessionNavigationServicesContext.Provider>
34-
);
35-
}
25+
export const SessionNavigationServicesProvider = Provider;
3626

3727
export function useSessionNavigationServices(): SessionNavigationServices {
38-
const services = useContext(SessionNavigationServicesContext);
39-
if (!services) {
40-
throw new Error('SessionNavigationServicesProvider is missing');
41-
}
42-
return services;
28+
return useServices();
4329
}

apps/desktop/src/renderer/features/session-settings/services-context.tsx

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,24 +17,13 @@
1717
* under the License.
1818
*/
1919

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

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

25-
export function SessionSettingsServicesProvider(props: {
26-
readonly services: SessionSettingsServices;
27-
readonly children?: ReactNode;
28-
}) {
29-
return (
30-
<SessionSettingsServicesContext.Provider value={props.services}>
31-
{props.children}
32-
</SessionSettingsServicesContext.Provider>
33-
);
34-
}
25+
export const SessionSettingsServicesProvider = Provider;
3526

3627
export function useSessionSettingsServices(): SessionSettingsServices {
37-
const services = useContext(SessionSettingsServicesContext);
38-
if (!services) throw new Error('SessionSettingsServicesProvider is missing');
39-
return services;
28+
return useServices();
4029
}

apps/desktop/src/renderer/features/task-entry/services-context.tsx

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,24 +17,13 @@
1717
* under the License.
1818
*/
1919

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

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

25-
export function TaskEntryServicesProvider(props: {
26-
services: TaskEntryServices;
27-
children?: ReactNode;
28-
}) {
29-
return (
30-
<TaskEntryServicesContext.Provider value={props.services}>
31-
{props.children}
32-
</TaskEntryServicesContext.Provider>
33-
);
34-
}
25+
export const TaskEntryServicesProvider = Provider;
3526

3627
export function useTaskEntryServices(): TaskEntryServices {
37-
const services = useContext(TaskEntryServicesContext);
38-
if (!services) throw new Error('TaskEntryServicesProvider is missing');
39-
return services;
28+
return useServices();
4029
}

0 commit comments

Comments
 (0)