diff --git a/apps/desktop/renderer-architecture.json b/apps/desktop/renderer-architecture.json
index 4570aa0895..9915c7e896 100644
--- a/apps/desktop/renderer-architecture.json
+++ b/apps/desktop/renderer-architecture.json
@@ -294,6 +294,13 @@
"owner": "src/renderer/features/module-hub/ui/module-hub-provider.tsx",
"ownerSymbol": "ModuleHubProvider",
"count": 1
+ },
+ {
+ "implementation": "src/renderer/features/task-entry/controller/use-task-entry-controller.ts",
+ "symbol": "useTaskEntryController",
+ "owner": "src/renderer/features/task-entry/ui/task-entry-provider.tsx",
+ "ownerSymbol": "TaskEntryRoot",
+ "count": 1
}
],
"legacyAppShell": {
@@ -792,7 +799,6 @@
"useStableActions": 6,
"useState": 17,
"useSystemUiLocale": 1,
- "useTaskEntryController": 1,
"useTaskSubmissionReadiness": 1,
"useToast": 1,
"useTurnActionRegistry": 1,
@@ -895,8 +901,8 @@
"@maka/ui/icons": 1,
"react": 1
},
- "importSpecifiers": 147,
- "nonTriviaTokens": 15588
+ "importSpecifiers": 146,
+ "nonTriviaTokens": 15568
},
"src/renderer/use-app-shell-composer-quotes.ts": {
"importDeclarations": 2,
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..d112ad86c5 100644
--- a/apps/desktop/src/main/__tests__/task-entry-boundary.test.ts
+++ b/apps/desktop/src/main/__tests__/task-entry-boundary.test.ts
@@ -83,8 +83,11 @@ 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('useTaskEntryOwnership'), false);
});
+
+
it('keeps Task Entry catalog, picker, and directory handoff ownership out of AppShell', () => {
const appShell = readFileSync(
join(desktopRoot, 'src', 'renderer', 'app-shell.tsx'),
@@ -96,10 +99,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..4619ae77ea
--- /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('TaskEntryRoot 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 f6473dd7e7..a3cd6e7e68 100644
--- a/apps/desktop/src/renderer/app-shell.tsx
+++ b/apps/desktop/src/renderer/app-shell.tsx
@@ -25,7 +25,6 @@ import {
useRef,
useState,
type CSSProperties,
- type ComponentProps,
type Dispatch,
type SetStateAction,
} from 'react';
@@ -99,7 +98,8 @@ import {
type SessionNavigationPorts,
type SessionNavigationRowActions,
} from './features/session-navigation';
-import { TaskEntryHost, useTaskEntryController } from './features/task-entry';
+import * as TaskEntry from './features/task-entry';
+import type { TaskEntryShellProjection } from './features/task-entry';
import { useNewTaskChoice } from './use-new-task-choice';
import { SessionCollaborationDialog } from './session-collaboration-dialog';
import * as SessionCollaboration from './features/session-collaboration';
@@ -287,13 +287,18 @@ export function AppShell({ initialOnboardingSnapshot = null }: AppShellProps = {
-
+
+ {(taskEntry) => (
+
+ )}
+
@@ -314,12 +319,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>;
@@ -384,21 +391,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 TaskEntryRoot 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
@@ -1448,7 +1442,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),
@@ -2626,9 +2619,12 @@ function AppShellContent({
: 'im_hub';
return (
- // Goal state and Module Hub ownership both live below the shell. Composer
- // mentions still wrap the frame so one projection serves every composer,
- // including side-chat panels, without rebuilding the frame on catalog moves.
+ // Feature controllers live below the shell. Task Entry publishes a stable
+ // shell projection plus reader-local Host/Workspace Picker projections;
+ // Goal state and Module Hub ownership likewise wake only their narrow
+ // readers. Composer mentions still wrap the frame so one projection serves
+ // every composer, including side-chat panels, without rebuilding the frame
+ // on catalog moves.
) : (
-
+ {(workspacePicker) => (
+
+ />
+ )}
+
)}
>
}
@@ -3229,7 +3229,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;
+}
+
+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. Controller-only updates keep the
+ * same shell projection identity and therefore stop at the owner 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 the Task Entry controller below AppShell and hands only its stable
+ * shell projection outward.
+ *
+ * The render prop is memoized on that projection, so a controller-only update
+ * re-renders this one fiber and reuses the frame element it built last time;
+ * React bails out of the frame, and only the Host and Workspace Picker readers
+ * whose selection changed wake through the owner store. The shell's own reads
+ * arrive as `taskEntry`, whose identity moves only on a semantic change.
+ */
+export function TaskEntryRoot({ children }: TaskEntryRootProps) {
+ const ownership = useTaskEntryOwnership();
+ const owner = ownership.owner 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]);
+ const taskEntry = useMemo(
+ () => ({ commands: ownership.commands, selectors: ownership.selectors }),
+ [ownership.commands, ownership.selectors],
+ );
+ const frame = useMemo(() => children(taskEntry), [children, taskEntry]);
+ return (
+
+ {frame}
+
+ );
+}
+
+function useTaskEntryOwner(): TaskEntryOwner {
+ const owner = useContext(TaskEntryOwnerContext);
+ if (!owner) throw new Error('TaskEntryRoot 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 6a8def3d1f..865709bd82 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:** 249 files — blocker 0, reimplementation 0, polish 1, aligned 248.
+**Totals:** 250 files — blocker 0, reimplementation 0, polish 1, aligned 249.
## Exclusions (explicit)
@@ -72,6 +72,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 a00a5e0e85..275defce51 100644
--- a/docs/astryx-surface-file-inventory.paths
+++ b/docs/astryx-surface-file-inventory.paths
@@ -43,6 +43,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 bc56032921..4ec98cf595 100644
--- a/scripts/check-app-shell-hooks.mjs
+++ b/scripts/check-app-shell-hooks.mjs
@@ -148,7 +148,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