From 3dc2ee85a592e83db898d2546d0728ae9c2d220a Mon Sep 17 00:00:00 2001 From: chihumyum Date: Wed, 2 Sep 2026 01:02:56 +0800 Subject: [PATCH] refactor(desktop): move Task Entry controller below AppShell Generated-by: OpenAI Codex --- apps/desktop/renderer-architecture.json | 5 +- .../__tests__/task-entry-boundary.test.ts | 48 ++- .../task-entry-provider-scope.test.ts | 189 ++++++++++++ apps/desktop/src/renderer/app-shell.tsx | 60 ++-- .../renderer/features/task-entry/README.md | 9 +- .../controller/use-task-entry-controller.ts | 7 +- .../src/renderer/features/task-entry/index.ts | 5 +- .../src/renderer/features/task-entry/ports.ts | 6 + .../renderer/features/task-entry/testing.ts | 6 + .../task-entry/ui/task-entry-host.tsx | 7 +- .../task-entry/ui/task-entry-provider.tsx | 289 ++++++++++++++++++ docs/astryx-surface-file-inventory.md | 3 +- docs/astryx-surface-file-inventory.paths | 1 + scripts/check-app-shell-hooks.mjs | 1 - 14 files changed, 590 insertions(+), 46 deletions(-) create mode 100644 apps/desktop/src/main/__tests__/task-entry-provider-scope.test.ts create mode 100644 apps/desktop/src/renderer/features/task-entry/ui/task-entry-provider.tsx diff --git a/apps/desktop/renderer-architecture.json b/apps/desktop/renderer-architecture.json index 669e5507ae..09c8f7db0e 100644 --- a/apps/desktop/renderer-architecture.json +++ b/apps/desktop/renderer-architecture.json @@ -868,7 +868,6 @@ "useStableActions": 6, "useState": 17, "useSystemUiLocale": 1, - "useTaskEntryController": 1, "useTaskSubmissionReadiness": 1, "useToast": 1, "useTurnActionRegistry": 1, @@ -979,8 +978,8 @@ "@maka/ui/icons": 1, "react": 1 }, - "importSpecifiers": 184, - "nonTriviaTokens": 15687 + "importSpecifiers": 183, + "nonTriviaTokens": 15680 }, "src/renderer/use-app-shell-composer-quotes.ts": { "importDeclarations": 3, diff --git a/apps/desktop/src/main/__tests__/task-entry-boundary.test.ts b/apps/desktop/src/main/__tests__/task-entry-boundary.test.ts index 028d1b55d8..8b879d13b4 100644 --- a/apps/desktop/src/main/__tests__/task-entry-boundary.test.ts +++ b/apps/desktop/src/main/__tests__/task-entry-boundary.test.ts @@ -83,6 +83,39 @@ describe('Task Entry feature boundary', () => { const productionEntry = readFileSync(join(featureRoot, 'index.ts'), 'utf8'); assert.equal(productionEntry.includes('createFakeTaskEntryServices'), false); assert.equal(productionEntry.includes("from './testing"), false); + assert.equal(productionEntry.includes('useTaskEntryController'), false); + assert.equal(productionEntry.includes('TaskEntryProvider,'), false); + assert.equal(productionEntry.includes('useTaskEntryOwnership'), false); + }); + + it('keeps the controller owned by TaskEntryProvider and out of renderer roots', () => { + const controllerOwner = join(featureRoot, 'ui', 'task-entry-provider.tsx'); + const consumers: string[] = []; + for (const path of sourceFiles(join(desktopRoot, 'src', 'renderer'))) { + if (!/\.tsx?$/.test(path) || path.endsWith('use-task-entry-controller.ts')) continue; + const source = readFileSync(path, 'utf8'); + if (/\buseTaskEntryController\s*\(/.test(source)) { + consumers.push(relative(desktopRoot, path)); + } + } + assert.deepEqual(consumers, [relative(desktopRoot, controllerOwner)]); + }); + + it('keeps the controller module behind TaskEntryProvider and the testing entry', () => { + const importers: string[] = []; + for (const path of sourceFiles(featureRoot)) { + if (!/\.tsx?$/.test(path)) continue; + const source = readFileSync(path, 'utf8'); + for (const match of source.matchAll(/from\s+['"]([^'"]+)['"]/g)) { + if (match[1]?.includes('controller/use-task-entry-controller')) { + importers.push(relative(desktopRoot, path)); + } + } + } + assert.deepEqual(importers.sort(), [ + 'src/renderer/features/task-entry/testing.ts', + 'src/renderer/features/task-entry/ui/task-entry-provider.tsx', + ]); }); it('keeps Task Entry catalog, picker, and directory handoff ownership out of AppShell', () => { @@ -96,10 +129,21 @@ describe('Task Entry feature boundary', () => { 'newTaskDraftKey(', 'RemoteProjectDirectoryDialog', 'const workspacePicker: WorkspacePickerModel', + 'useTaskEntryController', + 'useTaskEntryShellProjection', + 'taskEntry.host', + 'taskEntry.owner', + ''), true); + for (const required of [ + '', + '', + ]) { + assert.equal(appShell.includes(required), true, required); + } }); }); diff --git a/apps/desktop/src/main/__tests__/task-entry-provider-scope.test.ts b/apps/desktop/src/main/__tests__/task-entry-provider-scope.test.ts new file mode 100644 index 0000000000..a26743c806 --- /dev/null +++ b/apps/desktop/src/main/__tests__/task-entry-provider-scope.test.ts @@ -0,0 +1,189 @@ +/* + * 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 { strict as assert } from 'node:assert'; +import { afterEach, describe, it } from 'node:test'; +import { act, createElement, Fragment } from 'react'; +import { LocaleProvider, ToastProvider } from '@maka/ui'; +import { cleanupFakeDom, installReactRenderer } from './fake-dom.js'; +import { + createFakeTaskEntryServices, + TaskEntryRoot, + TaskEntryServicesProvider, + TaskEntryWorkspacePickerConsumer, + useTaskEntryHostModel, + type TaskEntryCatalog, + type TaskEntryHost, + type TaskEntryShellProjection, + type TaskEntryServices, +} from '../../renderer/features/task-entry/testing.js'; + +let shellRenders = 0; +let frameRenders = 0; +let workspaceRenders = 0; +let hostRenders = 0; +let latestTaskEntry: TaskEntryShellProjection | undefined; +let latestDirectoryHostId: string | undefined; +let latestWorkspaceGroupCount = 0; + +function project(id: string) { + return { + id, + name: id, + locations: [{ path: `/tmp/${id}`, isWorktree: false }], + available: true, + preferredPath: `/tmp/${id}`, + }; +} + +function remoteHost(): Extract { + return { + profile: { id: 'remote', name: 'Remote', kind: 'remote' }, + hostId: 'host-remote', + readiness: 'ready', + state: 'available', + projects: [project('project-a')], + capabilities: { + chooseClientDirectory: false, + chooseHostDirectory: true, + selectNoProject: false, + }, + selectedProjectId: 'project-a', + chatDefaults: { permissionMode: 'ask', thinkingLevel: 'high' }, + }; +} + +function catalog(): TaskEntryCatalog { + return { defaultProfileId: 'remote', hosts: [remoteHost()] }; +} + +function WorkspaceProbe() { + return createElement(TaskEntryWorkspacePickerConsumer, { + manageProjects() {}, + children: (workspacePicker) => { + workspaceRenders += 1; + latestWorkspaceGroupCount = workspacePicker.groups.length; + return null; + }, + }); +} + +function HostProbe() { + const host = useTaskEntryHostModel(); + hostRenders += 1; + latestDirectoryHostId = host.directoryHost?.hostId; + return null; +} + +function FrameProbe() { + frameRenders += 1; + return createElement(Fragment, null, createElement(WorkspaceProbe), createElement(HostProbe)); +} + +function ShellProbe() { + return createElement(TaskEntryRoot, { + children: (taskEntry) => { + shellRenders += 1; + latestTaskEntry = taskEntry; + return createElement(FrameProbe); + }, + }); +} + +function renderProvider( + root: ReturnType['root'], + services: TaskEntryServices, +) { + root.render( + createElement(LocaleProvider, { + locale: 'en', + children: createElement( + ToastProvider, + null, + createElement( + TaskEntryServicesProvider, + { services }, + createElement(ShellProbe), + ), + ), + }), + ); +} + +afterEach(() => { + shellRenders = 0; + frameRenders = 0; + workspaceRenders = 0; + hostRenders = 0; + latestTaskEntry = undefined; + latestDirectoryHostId = undefined; + latestWorkspaceGroupCount = 0; + cleanupFakeDom(); +}); + +describe('TaskEntryProvider render scope', () => { + it('keeps a controller-only directory handoff below the shell frame', async () => { + const { root } = installReactRenderer(); + const services = createFakeTaskEntryServices({ + catalog: { + ...createFakeTaskEntryServices().catalog, + getCatalog: async () => catalog(), + }, + }); + + await act(async () => renderProvider(root, services)); + assert.equal(latestTaskEntry?.selectors.target?.hostId, 'host-remote'); + assert.equal(latestWorkspaceGroupCount, 1); + + const shellBefore = shellRenders; + const frameBefore = frameRenders; + const workspaceBefore = workspaceRenders; + const hostBefore = hostRenders; + await act(async () => latestTaskEntry?.commands.addProject()); + + assert.equal(latestDirectoryHostId, 'host-remote'); + assert.equal(shellRenders, shellBefore); + assert.equal(frameRenders, frameBefore); + assert.equal(workspaceRenders, workspaceBefore); + assert.equal(hostRenders, hostBefore + 1); + + await act(async () => root.unmount()); + }); + + it('retains the shell projection across an equivalent catalog refresh', async () => { + const { root } = installReactRenderer(); + const services = createFakeTaskEntryServices({ + catalog: { + ...createFakeTaskEntryServices().catalog, + getCatalog: async () => catalog(), + }, + }); + + await act(async () => renderProvider(root, services)); + const shellBefore = shellRenders; + const frameBefore = frameRenders; + await act(async () => latestTaskEntry?.commands.refresh()); + + assert.equal(shellRenders, shellBefore); + assert.equal(frameRenders, frameBefore); + assert.equal(latestTaskEntry?.selectors.target?.projectId, 'project-a'); + + await act(async () => root.unmount()); + }); +}); diff --git a/apps/desktop/src/renderer/app-shell.tsx b/apps/desktop/src/renderer/app-shell.tsx index 0a685bbab4..3bf61f2475 100644 --- a/apps/desktop/src/renderer/app-shell.tsx +++ b/apps/desktop/src/renderer/app-shell.tsx @@ -101,7 +101,7 @@ import { type SessionNavigationPorts, type SessionNavigationRowActions, } from './features/session-navigation'; -import { TaskEntryHost, useTaskEntryController } from './features/task-entry'; +import * as TaskEntry from './features/task-entry'; import { useNewTaskChoice } from './use-new-task-choice'; import { SessionCollaborationDialog } from './session-collaboration-dialog'; import * as SessionCollaboration from './features/session-collaboration'; @@ -259,6 +259,10 @@ type AppShellProps = { initialOnboardingSnapshot?: OnboardingSnapshot | null; }; +type TaskEntryShellProjection = Parameters< + ComponentProps['children'] +>[0]; + export function AppShell({ initialOnboardingSnapshot = null }: AppShellProps = {}) { const [uiLocalePreference, setUiLocalePreference] = useState('auto'); const [uiLocaleOverride, setUiLocaleOverride] = useState(null); @@ -289,13 +293,18 @@ export function AppShell({ initialOnboardingSnapshot = null }: AppShellProps = { - + + {(taskEntry) => ( + + )} + @@ -316,12 +325,14 @@ const SESSION_RAIL = ; function AppShellContent({ initialOnboardingSnapshot = null, + taskEntry, uiLocale, uiLocaleOverride, setUiLocaleOverride, setUiLocalePreference, }: { initialOnboardingSnapshot?: OnboardingSnapshot | null; + taskEntry: TaskEntryShellProjection; uiLocale: UiLocale; uiLocaleOverride: UiLocale | null; setUiLocaleOverride: Dispatch>; @@ -386,21 +397,8 @@ function AppShellContent({ } = useSettingsModal(); const onboarding = useOnboardingSnapshot(initialOnboardingSnapshot); - const reportTaskEntryError = useCallback< - Parameters[0]['reportError'] - >( - ({ title, description, profileId }) => { - toastApi.error(title, description, undefined, { profileId }); - }, - [toastApi], - ); - const taskEntry = useTaskEntryController({ - reportError: reportTaskEntryError, - manageProjects: openProjectSettings, - }); - // Named on its own because the rail depends on it: `taskEntry.commands` is a - // fresh object every render, so depending on the bag rather than the command - // would rebuild the rail's Project rows on every AppShell commit (#4109). + // The owner bridge keeps commands stable while TaskEntryProvider swaps the + // current feature-owned implementation below the shell. const { selectLocalProject } = taskEntry.commands; const currentNewTaskDraftKey = taskEntry.selectors.draftKey; // Staged files and quotes do NOT take the target-scoped key: they belong to @@ -1497,7 +1495,6 @@ function AppShellContent({ // Where a NEW chat starts. Built unconditionally and handed to the composer, // which renders it only while no session owns it — the project is fixed once // the first message creates one, so there is nothing to pick after that. - const workspacePicker = taskEntry.selectors.workspacePicker; const taskReadinessWorkspace = activeSession?.cwd ?? taskEntry.selectors.projectPath; const taskReadinessRequest = { ...resolveTaskReadinessModelTarget(activeSession, activeSessionSendOutcome, newChatModel), @@ -2670,8 +2667,9 @@ function AppShellContent({ : 'im_hub'; return ( - // Goal state lives below the shell and wakes only its three readers. Composer - // mentions still wrap the frame so one projection serves every composer. + // Feature controllers live below the shell. Task Entry publishes a stable + // shell projection plus reader-local Host/Workspace Picker projections; + // Goal state likewise wakes only its narrow readers. ) : ( - + {(workspacePicker) => ( + + /> + )} + )} } @@ -3259,7 +3261,7 @@ function AppShellContent({ /> )} - + ; } -export function TaskEntryHost({ model }: { model: TaskEntryHostModel }) { +export function TaskEntryHost() { + return ; +} + +export function TaskEntryHostView({ model }: { model: TaskEntryHostModel }) { return ( void; + +interface TaskEntryOwner { + getState(): TaskEntryController; + subscribe(listener: Listener): () => void; + readonly commands: TaskEntryControllerCommands; +} + +export interface TaskEntryShellProjection { + readonly commands: TaskEntryControllerCommands; + readonly selectors: Omit; +} + +interface TaskEntryProviderProps { + readonly owner: TaskEntryOwner; + readonly children?: ReactNode; +} + +export interface TaskEntryRootProps { + readonly children: (taskEntry: TaskEntryShellProjection) => ReactNode; +} + +const EMPTY_WORKSPACE_PICKER: WorkspacePickerModel = { + pending: true, + groups: [], +}; + +const EMPTY_CONTROLLER: TaskEntryController = { + host: { + closeDirectoryPicker() {}, + async acceptRegisteredProject() {}, + }, + commands: { + async refresh() {}, + selectLocalProject: () => false, + addProject() {}, + async chooseProjectForProfile() {}, + }, + selectors: { + draftKey: taskEntryDraftKey(undefined), + defaultProfileId: 'local', + usesDefaultHost: true, + workspacePicker: EMPTY_WORKSPACE_PICKER, + canAddProject: false, + }, +}; + +const TaskEntryOwnerContext = createContext(null); + +function ignoreManageProjects(): void {} + +function createTaskEntryOwner(): TaskEntryOwner & { + publish(controller: TaskEntryController): void; +} { + let current = EMPTY_CONTROLLER; + const listeners = new Set(); + const owner = { + getState: () => current, + subscribe(listener: Listener): () => void { + listeners.add(listener); + return () => listeners.delete(listener); + }, + commands: { + refresh: () => current.commands.refresh(), + selectLocalProject: (projectId: string) => + current.commands.selectLocalProject(projectId), + addProject: () => current.commands.addProject(), + chooseProjectForProfile: (profileId: string) => + current.commands.chooseProjectForProfile(profileId), + }, + publish(controller: TaskEntryController): void { + if (current === controller) return; + current = controller; + for (const listener of [...listeners]) listener(); + }, + }; + return owner; +} + +function useTaskEntrySelection( + owner: TaskEntryOwner, + select: (controller: TaskEntryController) => T, + isEqual: (previous: T, next: T) => boolean = Object.is, +): T { + const getSnapshot = useMemo(() => { + let cachedController: TaskEntryController | undefined; + let cachedSelection: T | undefined; + return (): T => { + const controller = owner.getState(); + if (controller === cachedController) return cachedSelection as T; + const next = select(controller); + if (cachedController === undefined || !isEqual(cachedSelection as T, next)) { + cachedSelection = next; + } + cachedController = controller; + return cachedSelection as T; + }; + }, [isEqual, owner, select]); + return useSyncExternalStore(owner.subscribe, getSnapshot, getSnapshot); +} + +function sameTarget( + previous: TaskEntryControllerSelectors['target'], + next: TaskEntryControllerSelectors['target'], +): boolean { + return previous === next || Boolean( + previous && + next && + previous.profileId === next.profileId && + previous.hostId === next.hostId && + previous.projectId === next.projectId, + ); +} + +function sameSelectedHost( + previous: TaskEntryControllerSelectors['selectedHost'], + next: TaskEntryControllerSelectors['selectedHost'], +): boolean { + return previous === next || Boolean( + previous && + next && + previous.profileId === next.profileId && + previous.hostId === next.hostId && + previous.name === next.name && + previous.kind === next.kind && + previous.chatDefaults.permissionMode === next.chatDefaults.permissionMode && + previous.chatDefaults.thinkingLevel === next.chatDefaults.thinkingLevel, + ); +} + +const selectShellSelectors = ( + controller: TaskEntryController, +): Omit => { + const { workspacePicker: _workspacePicker, ...selectors } = controller.selectors; + return selectors; +}; + +function sameShellSelectors( + previous: Omit, + next: Omit, +): boolean { + return ( + sameTarget(previous.target, next.target) && + previous.draftKey === next.draftKey && + previous.projectPath === next.projectPath && + sameSelectedHost(previous.selectedHost, next.selectedHost) && + previous.selectedProfileId === next.selectedProfileId && + previous.defaultProfileId === next.defaultProfileId && + previous.usesDefaultHost === next.usesDefaultHost && + previous.canAddProject === next.canAddProject + ); +} + +const selectWorkspacePicker = (controller: TaskEntryController): WorkspacePickerModel => + controller.selectors.workspacePicker; +const selectHost = (controller: TaskEntryController): TaskEntryHostModel => controller.host; + +function sameHost(previous: TaskEntryHostModel, next: TaskEntryHostModel): boolean { + return ( + previous.directoryHost?.profileId === next.directoryHost?.profileId && + previous.directoryHost?.hostId === next.directoryHost?.hostId && + previous.directoryHost?.name === next.directoryHost?.name && + previous.directoryOpener === next.directoryOpener && + previous.closeDirectoryPicker === next.closeDirectoryPicker && + previous.acceptRegisteredProject === next.acceptRegisteredProject + ); +} + +/** + * Creates the stable bridge AppShell reads without owning the Task Entry controller. + * Controller-only updates keep the same shell projection identity and therefore + * stop at the provider or the matching leaf reader. + */ +function useTaskEntryOwnership(): TaskEntryShellProjection & { readonly owner: TaskEntryOwner } { + const owner = useMemo(createTaskEntryOwner, []); + const selectors = useTaskEntrySelection(owner, selectShellSelectors, sameShellSelectors); + return useMemo( + () => ({ owner, commands: owner.commands, selectors }), + [owner, selectors], + ); +} + +/** Owns Task Entry catalog/selection lifecycle below AppShell. */ +export function TaskEntryProvider({ + owner: ownerInput, + children, +}: TaskEntryProviderProps) { + const owner = ownerInput as ReturnType; + const toastApi = useToast(); + const reportError = useCallback( + ({ title, description, profileId }: TaskEntryError) => { + toastApi.error(title, description, undefined, { profileId }); + }, + [toastApi], + ); + const controller = useTaskEntryController({ reportError, manageProjects: ignoreManageProjects }); + useLayoutEffect(() => owner.publish(controller), [controller, owner]); + + return ( + + {children} + + ); +} + +/** Mounts the controller owner and hands only its stable shell projection outward. */ +export function TaskEntryRoot({ children }: TaskEntryRootProps) { + const ownership = useTaskEntryOwnership(); + const taskEntry = useMemo( + () => ({ commands: ownership.commands, selectors: ownership.selectors }), + [ownership.commands, ownership.selectors], + ); + return ( + + {children(taskEntry)} + + ); +} + +function useTaskEntryOwner(): TaskEntryOwner { + const owner = useContext(TaskEntryOwnerContext); + if (!owner) throw new Error('TaskEntryProvider is missing'); + return owner; +} + +export function TaskEntryWorkspacePickerConsumer({ + manageProjects, + children, +}: { + readonly manageProjects: (profileId: string) => void; + readonly children: (workspacePicker: WorkspacePickerModel) => ReactNode; +}) { + const owner = useTaskEntryOwner(); + const controllerPicker = useTaskEntrySelection(owner, selectWorkspacePicker); + const workspacePicker = useMemo( + () => ({ + ...controllerPicker, + groups: controllerPicker.groups.map((group) => + group.onManage + ? { ...group, onManage: () => manageProjects(group.id) } + : group), + }), + [controllerPicker, manageProjects], + ); + return children(workspacePicker); +} + +export function useTaskEntryHostModel(): TaskEntryHostModel { + return useTaskEntrySelection(useTaskEntryOwner(), selectHost, sameHost); +} diff --git a/docs/astryx-surface-file-inventory.md b/docs/astryx-surface-file-inventory.md index 14992542f9..f99d5c86ca 100644 --- a/docs/astryx-surface-file-inventory.md +++ b/docs/astryx-surface-file-inventory.md @@ -6,7 +6,7 @@ Generated against `@astryxdesign/core@0.5.2` (194 component exports). Wiki bar: Design Conventions · API Use-the-System · Theming · Container Padding. -**Totals:** 244 files — blocker 0, reimplementation 0, polish 1, aligned 243. +**Totals:** 245 files — blocker 0, reimplementation 0, polish 1, aligned 244. ## Exclusions (explicit) @@ -68,6 +68,7 @@ Wiki bar: Design Conventions · API Use-the-System · Theming · Container Paddi | `apps/desktop/src/renderer/features/session-settings/services-context.tsx` | shell-chrome-or-panel | none | aligned — no raw controls; no Astryx JSX usage | aligned | | `apps/desktop/src/renderer/features/task-entry/services-context.tsx` | other | none | aligned — no raw controls; no Astryx JSX usage | aligned | | `apps/desktop/src/renderer/features/task-entry/ui/task-entry-host.tsx` | other | none | aligned — no raw controls; no Astryx JSX usage | aligned | +| `apps/desktop/src/renderer/features/task-entry/ui/task-entry-provider.tsx` | other | none | aligned — no raw controls; no Astryx JSX usage | aligned | | `apps/desktop/src/renderer/features/usage/services-context.tsx` | other | none | aligned — no raw controls; no Astryx JSX usage | aligned | | `apps/desktop/src/renderer/features/usage/ui/metric-card.tsx` | other | none | aligned — no raw controls; no Astryx JSX usage | aligned | | `apps/desktop/src/renderer/features/usage/ui/usage-settings-view.tsx` | other | Banner, Button, SegmentedControl, SegmentedControlItem, Selector, Switch, Tab, TabList, TextInput, Tooltip | aligned — uses Astryx (Banner, Button, SegmentedControl, SegmentedControlItem, Selector, Switch, Tab, TabList) | aligned | diff --git a/docs/astryx-surface-file-inventory.paths b/docs/astryx-surface-file-inventory.paths index e256ca9acb..041f4e0441 100644 --- a/docs/astryx-surface-file-inventory.paths +++ b/docs/astryx-surface-file-inventory.paths @@ -39,6 +39,7 @@ apps/desktop/src/renderer/features/session-navigation/ui/session-navigation-prov apps/desktop/src/renderer/features/session-settings/services-context.tsx apps/desktop/src/renderer/features/task-entry/services-context.tsx apps/desktop/src/renderer/features/task-entry/ui/task-entry-host.tsx +apps/desktop/src/renderer/features/task-entry/ui/task-entry-provider.tsx apps/desktop/src/renderer/features/usage/services-context.tsx apps/desktop/src/renderer/features/usage/ui/metric-card.tsx apps/desktop/src/renderer/features/usage/ui/usage-settings-view.tsx diff --git a/scripts/check-app-shell-hooks.mjs b/scripts/check-app-shell-hooks.mjs index a7d81339ce..fc15166159 100644 --- a/scripts/check-app-shell-hooks.mjs +++ b/scripts/check-app-shell-hooks.mjs @@ -149,7 +149,6 @@ export const ALLOWED = { useShellSearch: 1, useStableActions: 6, useState: 15, - useTaskEntryController: 1, useTaskSubmissionReadiness: 1, useToast: 1, // The last of the three `useKeyedPendingRegistry` call sites this entry