You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by wy...@apache.org on 2026/09/08 05:26:29 UTC

(maka) branch main updated: feat(workhub): restore chat identity and reuse standard composer (#5003)

This is an automated email from the ASF dual-hosted git repository.

M4n5ter pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/maka.git


The following commit(s) were added to refs/heads/main by this push:
     new 431ca4257a feat(workhub): restore chat identity and reuse standard composer (#5003)
431ca4257a is described below

commit 431ca4257a23860e2ecd6e438de7214b88fb542b
Author: ChengBo Zhang <66...@users.noreply.github.com>
AuthorDate: Tue Sep 8 13:26:24 2026 +0800

    feat(workhub): restore chat identity and reuse standard composer (#5003)
    
    Reuse the standard Composer for WorkHub attachments and per-Work configuration, align conversation layouts, and link Work identity across messages and navigation. Preserve attachment ownership when delegating and advance protocol compatibility to epoch 133.
    
    Generated-by: Codex
---
 apps/desktop/e2e/workhub-reconstruction.spec.ts    |  13 ++
 apps/desktop/renderer-architecture.json            |  42 ++---
 .../main/__tests__/composer-directories.test.ts    |   2 +-
 .../main/__tests__/new-task-staged-content.test.ts |   2 +-
 .../runtime-host-workhub-ipc-main.test.ts          |   2 +
 .../src/main/__tests__/workhub-controller.test.ts  |  19 +++
 .../main/__tests__/workhub-surface-flow.test.ts    |   3 +
 .../src/main/runtime-host-desktop-candidate.ts     |   1 +
 .../src/main/runtime-host-workhub-ipc-main.ts      |  16 ++
 apps/desktop/src/preload/bridge-contract.d.ts      |   1 +
 apps/desktop/src/preload/preload.ts                |   4 +
 apps/desktop/src/renderer/app-shell.tsx            |  41 ++---
 apps/desktop/src/renderer/composer-attachments.ts  |  59 +------
 .../composition/desktop-feature-services.tsx       |   5 +
 .../controller/use-composer-attachments.ts         |  34 ++++
 .../src/renderer/features/conversation/index.ts    |   5 +
 .../model/slash-command-presentation.ts            |  51 ++++++
 .../tools/side-chat/quote-companion-panel.tsx      |   5 +-
 .../desktop/src/renderer/features/workhub/index.ts |   6 +
 .../renderer/features/workhub/services-context.tsx |  35 ++++
 .../features/workhub/ui/workhub-composer.tsx       | 171 +++++++++++++++++++
 .../workhub/ui/workhub-navigation-rail.tsx         |  15 +-
 .../features/workhub/ui/workhub-prompt-rail.tsx    |  17 +-
 .../features/workhub/ui/workhub-work-identity.tsx  |  42 +++++
 apps/desktop/src/renderer/locales/workhub-copy.ts  |  64 +++++++
 apps/desktop/src/renderer/pending-items.ts         |  56 +-----
 .../desktop/create-workhub-composer-services.ts}   |  15 +-
 apps/desktop/src/renderer/styles/workhub.css       | 154 +++++++++++++++--
 apps/desktop/src/renderer/workhub-controller.ts    |  14 +-
 .../src/renderer/workhub-coordination-port.ts      |   2 +
 apps/desktop/src/renderer/workhub-surface.tsx      | 107 ++++++++++--
 apps/desktop/stories/workhub.stories.tsx           | 190 +++++++++++++++++++++
 docs/astryx-surface-file-inventory.md              |   5 +-
 docs/astryx-surface-file-inventory.paths           |   3 +
 packages/core/src/session.ts                       |  50 +++++-
 .../workhub-coordination-action-gate.test.ts       |  27 +++
 .../workhub-coordination-protocol.test.ts          |  59 +++++++
 .../__tests__/workhub-message-attachments.test.ts  | 101 +++++++++++
 packages/runtime-host/src/protocol/index.ts        |   4 +-
 .../src/protocol/workhub-coordination.ts           |  48 +++++-
 .../src/server/execution-composition.ts            |  28 ++-
 .../src/server/workhub-coordination-action-gate.ts |  33 +++-
 .../src/server/workhub-coordination-coordinator.ts |  16 +-
 .../src/server/workhub-message-attachments.ts      |  60 +++++++
 packages/ui/package.json                           |   5 +-
 .../ui/src}/composer-attachments.ts                |   0
 packages/ui/src/composer.tsx                       |   6 +-
 .../renderer => packages/ui/src}/pending-items.ts  |   0
 packages/ui/src/prompt-anchor-rail.tsx             |  15 +-
 .../ui/src}/use-composer-attachments.ts            |  19 ++-
 50 files changed, 1429 insertions(+), 243 deletions(-)

diff --git a/apps/desktop/e2e/workhub-reconstruction.spec.ts b/apps/desktop/e2e/workhub-reconstruction.spec.ts
index 397ca975e3..3acf19c471 100644
--- a/apps/desktop/e2e/workhub-reconstruction.spec.ts
+++ b/apps/desktop/e2e/workhub-reconstruction.spec.ts
@@ -58,6 +58,14 @@ test('WorkHub rebuilds delegated execution feedback after navigating away and ba
   await workHubComposer.press('Enter');
   const routedTurn = page.locator('.workhub-turn', { hasText: routedPrompt });
   await expect(routedTurn.locator('.workhub-submitted')).toBeVisible();
+  await expect(routedTurn.locator('.workhub-message-identity')).toContainText(sessionName!);
+  const workIdentity = await routedTurn.getAttribute('data-work-session-id');
+  expect(workIdentity).toBeTruthy();
+  const identityColor = await routedTurn.evaluate((element) =>
+    getComputedStyle(element).getPropertyValue('--workhub-work-color'),
+  );
+  expect(identityColor).toContain('oklch');
+
   await routedTurn.locator('.workhub-submitted > button').click();
   await expect(page.getByRole('region', { name: 'WorkHub' })).toBeHidden();
 
@@ -74,6 +82,11 @@ test('WorkHub rebuilds delegated execution feedback after navigating away and ba
     page.locator('.workhub-projected-turn', { hasText: routedPrompt })
       .locator('.workhub-submitted-state'),
   ).toHaveText('关联有效 · 已完成');
+  await expect(routedTurn).toHaveAttribute('data-work-session-id', workIdentity!);
+  expect(await routedTurn.evaluate((element) =>
+    getComputedStyle(element).getPropertyValue('--workhub-work-color'),
+  )).toBe(identityColor);
+
 });
 
 test('WorkHub replaces the exact linked delegation across Sessions', async ({
diff --git a/apps/desktop/renderer-architecture.json b/apps/desktop/renderer-architecture.json
index 51b23b7387..9a6a332a4b 100644
--- a/apps/desktop/renderer-architecture.json
+++ b/apps/desktop/renderer-architecture.json
@@ -223,7 +223,6 @@
     "src/renderer/use-app-shell-session-list.ts",
     "src/renderer/use-app-shell-session-ui-reads.ts",
     "src/renderer/use-app-shell-session-workspace.ts",
-    "src/renderer/use-composer-attachments.ts",
     "src/renderer/use-deep-research-run.ts",
     "src/renderer/use-delayed-flag.ts",
     "src/renderer/use-external-store-selector.ts",
@@ -281,7 +280,6 @@
     "src/renderer/features/workbar/tools/side-chat/quote-companion-panel.tsx -> src/renderer/composer-mentions",
     "src/renderer/features/workbar/tools/side-chat/quote-companion-panel.tsx -> src/renderer/scroll-motion-policy",
     "src/renderer/features/workbar/tools/side-chat/quote-companion-panel.tsx -> src/renderer/turn-footer-actions",
-    "src/renderer/features/workbar/tools/side-chat/quote-companion-panel.tsx -> src/renderer/use-composer-attachments",
     "src/renderer/features/workbar/tools/side-chat/use-quote-companion.ts -> src/renderer/settled-message-merge",
     "src/renderer/features/workbar/tools/terminal/session-terminal-panel.tsx -> src/renderer/theme",
     "src/renderer/features/workbar/ui/workbar-surface.tsx -> src/renderer/work-board-panel"
@@ -718,7 +716,7 @@
         "nonTriviaTokens": 1395
       },
       "src/renderer/app-shell.tsx": {
-        "importDeclarations": 79,
+        "importDeclarations": 77,
         "bridgePaths": {
           "window.maka.attachments": 1,
           "window.maka.attachments.readBytes": 1,
@@ -830,6 +828,7 @@
           "./error-boundary": 1,
           "./features/app-update/index.js": 1,
           "./features/conversation": 1,
+          "./features/conversation/index.js": 1,
           "./features/goals": 1,
           "./features/module-hub": 1,
           "./features/session-collaboration": 1,
@@ -860,7 +859,6 @@
           "./use-app-shell-composer-quotes": 1,
           "./use-app-shell-session-ui-reads": 1,
           "./use-app-shell-session-workspace": 1,
-          "./use-composer-attachments": 1,
           "./use-new-task-choice": 1,
           "./use-onboarding-snapshot": 1,
           "./use-project-context": 1,
@@ -891,11 +889,10 @@
           "@maka/core/slash-command-catalog": 1,
           "@maka/core/ui-locale": 1,
           "@maka/ui": 1,
-          "@maka/ui/icons": 1,
           "react": 1
         },
-        "importSpecifiers": 121,
-        "nonTriviaTokens": 14630
+        "importSpecifiers": 116,
+        "nonTriviaTokens": 14544
       },
       "src/renderer/use-app-shell-composer-quotes.ts": {
         "importDeclarations": 2,
@@ -1237,7 +1234,9 @@
         "lifecycleMethods": {},
         "unresolvedDependencies": 0,
         "actionFactories": [],
-        "dependencyPaths": {}
+        "dependencyPaths": {
+          "./features/conversation/index.js": 1
+        }
       },
       "src/renderer/composer-defaults.ts": {
         "bridgePaths": {},
@@ -2036,7 +2035,9 @@
         "lifecycleMethods": {},
         "unresolvedDependencies": 0,
         "actionFactories": [],
-        "dependencyPaths": {}
+        "dependencyPaths": {
+          "./features/conversation/index.js": 1
+        }
       },
       "src/renderer/pending-session-view.ts": {
         "bridgePaths": {},
@@ -4168,29 +4169,6 @@
           "react": 1
         }
       },
-      "src/renderer/use-composer-attachments.ts": {
-        "bridgePaths": {},
-        "environmentCapabilities": {},
-        "hookCalls": {
-          "useEffect": 2,
-          "useRef": 2,
-          "useState": 2,
-          "useUiLocale": 1
-        },
-        "lifecycleMethods": {},
-        "unresolvedDependencies": 0,
-        "actionFactories": [],
-        "dependencyPaths": {
-          "./composer-attachments.js": 1,
-          "./locales/conversation-copy.js": 1,
-          "./locales/shell-copy.js": 1,
-          "./pending-items.js": 1,
-          "@maka/core/attachments": 1,
-          "@maka/core/events": 1,
-          "@maka/ui": 1,
-          "react": 1
-        }
-      },
       "src/renderer/use-deep-research-run.ts": {
         "bridgePaths": {
           "window.maka.deepResearch.get": 1,
diff --git a/apps/desktop/src/main/__tests__/composer-directories.test.ts b/apps/desktop/src/main/__tests__/composer-directories.test.ts
index b82d888e14..f3b093de2c 100644
--- a/apps/desktop/src/main/__tests__/composer-directories.test.ts
+++ b/apps/desktop/src/main/__tests__/composer-directories.test.ts
@@ -25,7 +25,7 @@ import { normalizeSessionSendCommand } from '../permission-response-guard.js';
 import {
   useComposerAttachments,
   type ComposerAttachmentService,
-} from '../../renderer/use-composer-attachments.js';
+} from '../../renderer/features/conversation/index.js';
 import { cleanupFakeDom, installReactRenderer } from './fake-dom.js';
 
 afterEach(cleanupFakeDom);
diff --git a/apps/desktop/src/main/__tests__/new-task-staged-content.test.ts b/apps/desktop/src/main/__tests__/new-task-staged-content.test.ts
index b0fa5ee43e..b5e9755d52 100644
--- a/apps/desktop/src/main/__tests__/new-task-staged-content.test.ts
+++ b/apps/desktop/src/main/__tests__/new-task-staged-content.test.ts
@@ -29,7 +29,7 @@ import { getDesktopConversationCopy } from '../../renderer/locales/conversation-
 import {
   useComposerAttachments,
   type ComposerAttachmentService,
-} from '../../renderer/use-composer-attachments.js';
+} from '../../renderer/features/conversation/index.js';
 import { useAppShellComposerQuotes } from '../../renderer/use-app-shell-composer-quotes.js';
 import {
   composerModelSupportsVision,
diff --git a/apps/desktop/src/main/__tests__/runtime-host-workhub-ipc-main.test.ts b/apps/desktop/src/main/__tests__/runtime-host-workhub-ipc-main.test.ts
index 70a84dedf9..33e8313b62 100644
--- a/apps/desktop/src/main/__tests__/runtime-host-workhub-ipc-main.test.ts
+++ b/apps/desktop/src/main/__tests__/runtime-host-workhub-ipc-main.test.ts
@@ -94,6 +94,7 @@ test('projects WorkHub coordination resolution through its dedicated IPC domain'
   assert.deepEqual(
     await handlers.get('workhub:act')?.({}, {
       actionId: 'create-action',
+      newWorkDefaults: { permissionMode: 'bypass' },
       userText: 'Start accessibility review',
       proposal: { disposition: 'create_new', title: 'Accessibility review' },
       create: {
@@ -112,6 +113,7 @@ test('projects WorkHub coordination resolution through its dedicated IPC domain'
   );
   assert.deepEqual(actions, [{
     actionId: 'create-action',
+    newWorkDefaults: { permissionMode: 'bypass' },
     userText: 'Start accessibility review',
     proposal: { disposition: 'create_new', title: 'Accessibility review' },
     create: {
diff --git a/apps/desktop/src/main/__tests__/workhub-controller.test.ts b/apps/desktop/src/main/__tests__/workhub-controller.test.ts
index b8f0fc0fb1..cddaaeb527 100644
--- a/apps/desktop/src/main/__tests__/workhub-controller.test.ts
+++ b/apps/desktop/src/main/__tests__/workhub-controller.test.ts
@@ -3273,3 +3273,22 @@ test('deterministic routing preserves executable instructions after the model te
   assert.equal(result.kind, 'submitted');
   if (result.kind === 'submitted') assert.equal(result.target.sessionId, 'ledger');
 });
+
+
+test('composer defaults apply only to creation while attachments follow explicit and automatic routing', async () => {
+  const actions: WorkHubCoordinationActInput[] = [];
+  const newWorkDefaults = { model: { llmConnectionId: 'chosen', llmConnectionSlug: 'chosen', model: 'chosen-model' }, permissionMode: 'bypass' as const };
+  const attachments: NonNullable<WorkHubCoordinationActInput['attachments']> = [{ name: 'requirements.txt', kind: 'other', mimeType: 'text/plain', bytes: 12, ref: { kind: 'session_file', sessionId: 'maka_workhub_coordination', relativePath: 'file-1' } }];
+  const existing = createWorkHubController({ sessions: port([session('payments')]), onAct: (input) => actions.push(input) });
+  await existing.submit({ requestId: 'explicit-composer', text: 'Continue payments', explicitTarget: { sessionId: 'payments' }, newWorkDefaults, attachments });
+  assert.equal(actions[0]?.proposal.disposition, 'delegate_existing');
+  assert.equal(actions[0]?.newWorkDefaults, undefined);
+  assert.deepEqual(actions[0]?.attachments, attachments);
+  const freshPort = port([]);
+  freshPort.create = async () => session('created-work');
+  const fresh = createWorkHubController({ sessions: freshPort, onAct: (input) => actions.push(input) });
+  await fresh.submit({ requestId: 'new-composer', text: 'Create a new Session for an accessibility audit', newWorkDefaults, attachments });
+  assert.equal(actions[1]?.proposal.disposition, 'create_new');
+  assert.deepEqual(actions[1]?.newWorkDefaults, newWorkDefaults);
+  assert.deepEqual(actions[1]?.attachments, attachments);
+});
diff --git a/apps/desktop/src/main/__tests__/workhub-surface-flow.test.ts b/apps/desktop/src/main/__tests__/workhub-surface-flow.test.ts
index 772fb55f39..fceaff86c7 100644
--- a/apps/desktop/src/main/__tests__/workhub-surface-flow.test.ts
+++ b/apps/desktop/src/main/__tests__/workhub-surface-flow.test.ts
@@ -154,6 +154,9 @@ test('durable delegation renders every projected target state as a navigable res
     );
     assert.match(markup, /<button/u);
     assert.match(markup, /Payments/u);
+    assert.match(markup, /data-work-session-id="payment"/u);
+    assert.match(markup, /workhub-message-identity/u);
+    assert.match(markup, /--workhub-work-hue:/u);
     assert.match(markup, /Active link/u);
     assert.match(markup, new RegExp(label, 'u'));
     assert.match(markup, new RegExp(`data-state="${state}"`, 'u'));
diff --git a/apps/desktop/src/main/runtime-host-desktop-candidate.ts b/apps/desktop/src/main/runtime-host-desktop-candidate.ts
index 32dfc95ce3..3fd058bf61 100644
--- a/apps/desktop/src/main/runtime-host-desktop-candidate.ts
+++ b/apps/desktop/src/main/runtime-host-desktop-candidate.ts
@@ -866,6 +866,7 @@ export async function createDesktopRuntimeHostCandidate(
     });
     if (target.access === 'owner') {
       registerRuntimeHostWorkHubIpc(client, ipc, {
+        attachmentIngest: { approvals: deps.attachmentApprovals, stat: deps.stat, resizeImage: deps.resizeImage },
         resolveCreateProject: () => deps.resolveSessionCreateProject({}, target),
         emitSessionsChanged,
       });
diff --git a/apps/desktop/src/main/runtime-host-workhub-ipc-main.ts b/apps/desktop/src/main/runtime-host-workhub-ipc-main.ts
index 1ebca81bf6..7bb4283f0a 100644
--- a/apps/desktop/src/main/runtime-host-workhub-ipc-main.ts
+++ b/apps/desktop/src/main/runtime-host-workhub-ipc-main.ts
@@ -25,11 +25,14 @@ import type {
   WorkspaceTarget,
 } from '@maka/runtime-host/protocol';
 import { RuntimeHostOperationError } from '@maka/runtime-host/client';
+import { WORKHUB_COORDINATION_SESSION_ID } from '@maka/core/session';
+import { prepareIngestItems, resolveAttachmentRefs } from './attachment-ingest.js';
 import type { DesktopRuntimeHostClient } from './runtime-host-client.js';
 import type { ReconnectableReadIpcMain } from './ipc-reconnect-policy.js';
 
 type RuntimeHostWorkHubClient = Pick<
   DesktopRuntimeHostClient,
+  | 'ingestAttachment'
   | 'actWorkHubCoordination'
   | 'listWorkHubCoordinationCandidates'
   | 'recordWorkHubCoordination'
@@ -39,6 +42,7 @@ type RuntimeHostWorkHubClient = Pick<
 type RendererWorkHubActionInput = Omit<WorkHubCoordinationActInput, 'create'>;
 
 export interface RuntimeHostWorkHubIpcOptions {
+  attachmentIngest?: Pick<Parameters<typeof prepareIngestItems>[0], 'approvals' | 'stat'> & { resizeImage?: (bytes: Uint8Array) => Promise<Uint8Array> };
   resolveCreateProject(): Promise<WorkspaceTarget>;
   emitSessionsChanged(reason: 'created' | 'status-change', sessionId: string): void;
 }
@@ -56,6 +60,16 @@ export function registerRuntimeHostWorkHubIpc(
     client.recordWorkHubCoordination(input),
   );
   ipcMain.handle('workhub:candidates', () => client.listWorkHubCoordinationCandidates());
+  ipcMain.handle('workhub:prepareAttachments', async (event, items: unknown) => {
+    if (!options.attachmentIngest) throw new Error('WorkHub attachments are unavailable');
+    const prepared = await prepareIngestItems({ ...options.attachmentIngest, senderId: event.sender.id, items });
+    const refs = await resolveAttachmentRefs({
+      files: prepared.files,
+      resizeImage: options.attachmentIngest.resizeImage,
+      snapshot: ({ name, mimeType, content }) => client.ingestAttachment({ sessionId: WORKHUB_COORDINATION_SESSION_ID, name, mimeType, content }),
+    });
+    return prepared.commit(() => refs);
+  });
   ipcMain.handle('workhub:act', async (_event, rawInput: RendererWorkHubActionInput) => {
     try {
       const proposal = rawInput?.proposal;
@@ -63,6 +77,7 @@ export function registerRuntimeHostWorkHubIpc(
         actionId: rawInput?.actionId,
         userText: rawInput?.userText,
         proposal,
+        ...(rawInput?.attachments ? { attachments: rawInput.attachments } : {}),
         ...(rawInput?.confirmation === undefined
           ? {}
           : { confirmation: rawInput.confirmation }),
@@ -78,6 +93,7 @@ export function registerRuntimeHostWorkHubIpc(
       if (createsTarget) {
         result = await client.actWorkHubCoordination({
           ...base,
+          ...(rawInput.newWorkDefaults ? { newWorkDefaults: rawInput.newWorkDefaults } : {}),
           create: {
             workspace: await options.resolveCreateProject(),
           },
diff --git a/apps/desktop/src/preload/bridge-contract.d.ts b/apps/desktop/src/preload/bridge-contract.d.ts
index 85e6b0a70e..42e131e946 100644
--- a/apps/desktop/src/preload/bridge-contract.d.ts
+++ b/apps/desktop/src/preload/bridge-contract.d.ts
@@ -1007,6 +1007,7 @@ export interface MakaBridge {
     ): () => void;
   };
   workHub: {
+    prepareAttachments(coordinationSessionId: string, items: RendererIngestInput[]): Promise<AttachmentRef[]>;
     /** Resolve the active Runtime Host's stable coordination conversation. */
     resolveCoordinationSession(): Promise<string>;
     /** Persist one deterministic clarification or routing summary. */
diff --git a/apps/desktop/src/preload/preload.ts b/apps/desktop/src/preload/preload.ts
index 99790e2880..ff2eaacdc2 100644
--- a/apps/desktop/src/preload/preload.ts
+++ b/apps/desktop/src/preload/preload.ts
@@ -2000,6 +2000,10 @@ const makaBridge = {
     },
   },
   workHub: {
+    async prepareAttachments(coordinationSessionId: string, items: Parameters<MakaBridge['workHub']['prepareAttachments']>[1]) {
+      const scope = await resolveDesktopWorkHubCoordinationCreateScope(coordinationSessionId, runtimeHostSessionRef);
+      return ipcRenderer.invoke('workhub:prepareAttachments', scope, await encodeIngestItems(items));
+    },
     resolveCoordinationSession(): Promise<string> {
       return resolveDesktopWorkHubCoordinationSession(
         activeRuntimeHostRef,
diff --git a/apps/desktop/src/renderer/app-shell.tsx b/apps/desktop/src/renderer/app-shell.tsx
index 24518aa030..cf79cdd9b4 100644
--- a/apps/desktop/src/renderer/app-shell.tsx
+++ b/apps/desktop/src/renderer/app-shell.tsx
@@ -35,7 +35,6 @@ import type {
 } from '@maka/core/events';
 import type { OrchestrationMode } from '@maka/core/orchestration';
 import type { ChatDefaultPermissionMode } from '@maka/core/settings';
-import type { SlashCommandIdForSurface } from '@maka/core/slash-command-catalog';
 import type { UiLocale, UiLocalePreference } from '@maka/core/ui-locale';
 import { collapseSessionRevisions } from '@maka/core/session-revisions';
 import { isLinkedSubagentSession } from '@maka/core/session';
@@ -66,7 +65,6 @@ import {
   reconcileInteractions,
 } from '@maka/ui';
 import type { ConnectionEvent } from '@maka/core/connections';
-import { GitBranch, MessageCircleQuestion, Minimize2, Network } from '@maka/ui/icons';
 import { Button } from '@astryxdesign/core/Button';
 import { useKeyboardHelp } from './keyboard-help';
 import { useCommandPalette } from './command-palette';
@@ -195,7 +193,7 @@ import {
 import * as liveContent from './live-content-seed';
 import { loadComposerDefaults, saveComposerDefaults } from './composer-defaults';
 import { useTurnActionRegistry } from './use-turn-action-registry';
-import { useComposerAttachments } from './use-composer-attachments';
+import { useComposerAttachments, desktopSlashCommandPresentation } from './features/conversation/index.js';
 import { useAppShellComposerQuotes } from './use-app-shell-composer-quotes';
 import {
   type ComposerMentionsSurfaceInput,
@@ -552,7 +550,7 @@ function AppShellContent({
   const sessionHostConnections = useShellConnections({
     toastApi,
     uiLocale,
-    target: { kind: 'session', sessionId: ownerActiveId },
+    target: { kind: 'session', sessionId: workHubActive ? workHubCoordinationSessionId : ownerActiveId },
   });
   const startupConnectionSnapshot = onboarding.snapshot;
   const newTaskUsesDefaultHost = taskEntry.selectors.usesDefaultHost;
@@ -568,7 +566,7 @@ function AppShellContent({
           }
         : defaultHostConnections.snapshot;
   }
-  const activeConnectionSnapshot = activeId
+  const activeConnectionSnapshot = workHubActive || activeId
     ? sessionHostConnections.snapshot
     : newTaskConnectionSnapshot;
   const connections = activeConnectionSnapshot.connections;
@@ -1222,31 +1220,7 @@ function AppShellContent({
           streaming: turnActive || activeStreamingLive,
         }),
       );
-      const presentation: Record<
-        SlashCommandIdForSurface<'desktop'>,
-        Omit<ComposerSlashCommandOption, 'id'>
-      > = {
-        compact: {
-          ...shellCopy.slashCommands.compact,
-          keywords: ['compact', 'context', '压缩', '上下文'],
-          Icon: Minimize2,
-        },
-        side: {
-          ...shellCopy.slashCommands.side,
-          keywords: ['side', 'btw', '侧聊', '追问'],
-          Icon: MessageCircleQuestion,
-        },
-        swarm: {
-          ...shellCopy.slashCommands.swarm,
-          keywords: ['swarm', 'multi-agent', '多智能体'],
-          Icon: Network,
-        },
-        graph: {
-          ...shellCopy.slashCommands.graph,
-          keywords: ['graph', 'agent graph', '智能体图'],
-          Icon: GitBranch,
-        },
-      };
+      const presentation = desktopSlashCommandPresentation(shellCopy.slashCommands);
       return availableCommands.map(({ id }) => ({ id, ...presentation[id] }));
     },
     [activeId, activeStreamingLive, shellCopy.slashCommands, turnActive],
@@ -2655,6 +2629,13 @@ function AppShellContent({
                     locale={uiLocale}
                     {...(activeId ? { initialFocusSessionId: activeId } : {})}
                     onOpenSession={openSessionInChat}
+                    composerServices={{
+                      sessions,
+                      modelChoices: chatModelChoices,
+                      defaults: { model: newChatModel, permissionMode: newTaskPermissionMode },
+                      confirmBypass: () => confirmBypassPermission(toastApi, uiLocale),
+                      onOpenModelSettings: () => openSettingsSection('models'),
+                    }}
                   />
                 ) : (
                   <WorkHubCoordinationStatus
diff --git a/apps/desktop/src/renderer/composer-attachments.ts b/apps/desktop/src/renderer/composer-attachments.ts
index d978b4ec87..900879e9ff 100644
--- a/apps/desktop/src/renderer/composer-attachments.ts
+++ b/apps/desktop/src/renderer/composer-attachments.ts
@@ -17,61 +17,4 @@
  * under the License.
  */
 
-import type { AttachmentRef } from '@maka/core/events';
-
-export type PendingAttachment = {
-  /** Unique per staged item; keys preview ownership and cleanup. */
-  stagingKey: string;
-  displayName: string;
-  mimeType?: string;
-  kind: AttachmentRef['kind'];
-  size: number;
-  /** Present only after the URL has decoded successfully. */
-  previewUrl?: string;
-  source:
-    | { type: 'approval'; approvalId: string; name: string }
-    | { type: 'file'; file: File }
-    | { type: 'retained'; attachment: AttachmentRef };
-};
-
-export type ComposerIngestInput =
-  | { approvalId: string; name: string; mimeType?: string }
-  | { file: File };
-
-/** Stable identity across preview-URL merges. */
-export function pendingAttachmentSourceKey(
-  attachment: PendingAttachment,
-): unknown {
-  if (attachment.source.type === 'approval') {
-    return `approval:${attachment.source.approvalId}`;
-  }
-  if (attachment.source.type === 'file') return attachment.source.file;
-  return `retained:${JSON.stringify(attachment.source.attachment)}`;
-}
-
-export function toComposerIngestItems(
-  pending: readonly PendingAttachment[],
-): ComposerIngestInput[] {
-  return pending.flatMap((item) => {
-    if (item.source.type === 'retained') return [];
-    return [
-      item.source.type === 'approval'
-        ? {
-            approvalId: item.source.approvalId,
-            name: item.source.name,
-            ...(item.mimeType ? { mimeType: item.mimeType } : {}),
-          }
-        : { file: item.source.file },
-    ];
-  });
-}
-
-export function retainedAttachmentRefs(
-  pending: readonly PendingAttachment[],
-): AttachmentRef[] {
-  return pending.flatMap((item) =>
-    item.source.type === 'retained'
-      ? [structuredClone(item.source.attachment)]
-      : [],
-  );
-}
+export { toComposerIngestItems, retainedAttachmentRefs, type PendingAttachment } from './features/conversation/index.js';
diff --git a/apps/desktop/src/renderer/composition/desktop-feature-services.tsx b/apps/desktop/src/renderer/composition/desktop-feature-services.tsx
index d12a42a7ca..70a402938b 100644
--- a/apps/desktop/src/renderer/composition/desktop-feature-services.tsx
+++ b/apps/desktop/src/renderer/composition/desktop-feature-services.tsx
@@ -27,6 +27,7 @@ import { ModuleHubServicesProvider } from '../features/module-hub';
 import { RuntimeHostManagementServicesProvider } from '../features/runtime-host-management';
 import { SessionCollaborationServicesProvider } from '../features/session-collaboration';
 import { SessionNavigationServicesProvider } from '../features/session-navigation';
+import { WorkHubComposerServicesProvider } from '../features/workhub/index.js';
 import { SessionSettingsServicesProvider } from '../features/session-settings';
 import { TaskEntryServicesProvider } from '../features/task-entry';
 import { WorkbarServicesProvider } from '../features/workbar';
@@ -39,6 +40,7 @@ import { createDesktopSessionCollaborationServices } from '../platform/desktop/c
 import { createDesktopSessionNavigationServices } from '../platform/desktop/create-session-navigation-services';
 import { createDesktopSessionSettingsServices } from '../platform/desktop/create-session-settings-services';
 import { createDesktopTaskEntryServices } from '../platform/desktop/create-task-entry-services';
+import { createDesktopWorkHubComposerServices } from '../platform/desktop/create-workhub-composer-services.js';
 import { createDesktopWorkbarServices } from '../platform/desktop/create-workbar-services';
 
 export function createDesktopFeatureServices() {
@@ -54,6 +56,7 @@ export function createDesktopFeatureServices() {
     sessionSettings: createDesktopSessionSettingsServices(),
     taskEntry: createDesktopTaskEntryServices(),
     workbar: createDesktopWorkbarServices(),
+    workhub: createDesktopWorkHubComposerServices(),
   };
 }
 
@@ -68,6 +71,7 @@ export function DesktopFeatureServicesProvider(props: {
           <SessionCollaborationServicesProvider services={props.services.sessionCollaboration}>
             <SessionNavigationServicesProvider services={props.services.sessionNavigation}>
               <SessionSettingsServicesProvider services={props.services.sessionSettings}>
+              <WorkHubComposerServicesProvider services={props.services.workhub}>
                 <TaskEntryServicesProvider services={props.services.taskEntry}>
                   <ModuleHubServicesProvider services={props.services.moduleHub}>
                     <GoalServicesProvider services={props.services.goal}>
@@ -79,6 +83,7 @@ export function DesktopFeatureServicesProvider(props: {
                     </GoalServicesProvider>
                   </ModuleHubServicesProvider>
                 </TaskEntryServicesProvider>
+              </WorkHubComposerServicesProvider>
               </SessionSettingsServicesProvider>
             </SessionNavigationServicesProvider>
           </SessionCollaborationServicesProvider>
diff --git a/apps/desktop/src/renderer/features/conversation/controller/use-composer-attachments.ts b/apps/desktop/src/renderer/features/conversation/controller/use-composer-attachments.ts
new file mode 100644
index 0000000000..c47feb32d9
--- /dev/null
+++ b/apps/desktop/src/renderer/features/conversation/controller/use-composer-attachments.ts
@@ -0,0 +1,34 @@
+/*
+ * 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 { useUiLocale } from '@maka/ui';
+import { useComposerAttachments as useSharedComposerAttachments } from '@maka/ui/use-composer-attachments';
+import { getDesktopConversationCopy } from '../../../locales/conversation-copy.js';
+import { localizedShellErrorMessage } from '../../../locales/shell-copy.js';
+export type { ComposerAttachmentService } from '@maka/ui/use-composer-attachments';
+
+/** Desktop localization for the shared staging and preview lifecycle. */
+export function useComposerAttachments(options: Omit<Parameters<typeof useSharedComposerAttachments>[0], 'copy' | 'formatError'>) {
+  const locale = useUiLocale();
+  return useSharedComposerAttachments({
+    ...options,
+    copy: getDesktopConversationCopy(locale).actions,
+    formatError: (error, fallback) => localizedShellErrorMessage(error, fallback, locale),
+  });
+}
diff --git a/apps/desktop/src/renderer/features/conversation/index.ts b/apps/desktop/src/renderer/features/conversation/index.ts
index ee44a48d4c..95a408f8d2 100644
--- a/apps/desktop/src/renderer/features/conversation/index.ts
+++ b/apps/desktop/src/renderer/features/conversation/index.ts
@@ -52,3 +52,8 @@ export type { ConversationServices } from './ports.js';
 export { ConversationServicesProvider } from './services.js';
 export { SessionLocalMessages } from './controller/session-local-messages.js';
 export { restoreTranscriptTailAfterSend } from './controller/transcript-reading-position.js';
+
+export { useComposerAttachments, type ComposerAttachmentService } from './controller/use-composer-attachments.js';
+export { toComposerIngestItems, retainedAttachmentRefs, type PendingAttachment } from '@maka/ui/composer-attachments';
+export { NEW_TASK_PENDING_KEY, selectPending, appendPending, removePending, removePendingItems, clearPending, type PendingByKey } from '@maka/ui/pending-items';
+export { desktopSlashCommandPresentation } from './model/slash-command-presentation.js';
diff --git a/apps/desktop/src/renderer/features/conversation/model/slash-command-presentation.ts b/apps/desktop/src/renderer/features/conversation/model/slash-command-presentation.ts
new file mode 100644
index 0000000000..a66df2d2d9
--- /dev/null
+++ b/apps/desktop/src/renderer/features/conversation/model/slash-command-presentation.ts
@@ -0,0 +1,51 @@
+/*
+ * 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 { GitBranch, MessageCircleQuestion, Minimize2, Network } from '@maka/ui/icons';
+import type { ComposerSlashCommandOption } from '@maka/ui';
+import type { SlashCommandIdForSurface } from '@maka/core/slash-command-catalog';
+import type { getShellCopy } from '../../../locales/shell-copy.js';
+export function desktopSlashCommandPresentation(copy: ReturnType<typeof getShellCopy>['app']['slashCommands']) {
+  const presentation: Record<
+        SlashCommandIdForSurface<'desktop'>,
+        Omit<ComposerSlashCommandOption, 'id'>
+      > = {
+        compact: {
+          ...copy.compact,
+          keywords: ['compact', 'context', '压缩', '上下文'],
+          Icon: Minimize2,
+        },
+        side: {
+          ...copy.side,
+          keywords: ['side', 'btw', '侧聊', '追问'],
+          Icon: MessageCircleQuestion,
+        },
+        swarm: {
+          ...copy.swarm,
+          keywords: ['swarm', 'multi-agent', '多智能体'],
+          Icon: Network,
+        },
+        graph: {
+          ...copy.graph,
+          keywords: ['graph', 'agent graph', '智能体图'],
+          Icon: GitBranch,
+        },
+      };
+  return presentation;
+}
diff --git a/apps/desktop/src/renderer/features/workbar/tools/side-chat/quote-companion-panel.tsx b/apps/desktop/src/renderer/features/workbar/tools/side-chat/quote-companion-panel.tsx
index d49fafb4a0..353fe2f510 100644
--- a/apps/desktop/src/renderer/features/workbar/tools/side-chat/quote-companion-panel.tsx
+++ b/apps/desktop/src/renderer/features/workbar/tools/side-chat/quote-companion-panel.tsx
@@ -36,7 +36,8 @@ import {
 import type { SessionSummary } from '@maka/core/session';
 import { generalizedErrorMessageForLocale } from '@maka/core/redaction';
 import { useQuoteCompanion } from './use-quote-companion';
-import { useComposerAttachments } from '../../../../use-composer-attachments';
+import { useComposerAttachments } from '@maka/ui/use-composer-attachments';
+import { localizedShellErrorMessage } from '../../../../locales/shell-copy.js';
 import { useComposerMentionsContext } from '../../../../composer-mentions.js';
 import { preflightAttachmentItems } from '../../../../attachment-preflight';
 import { toComposerIngestItems } from '../../../../composer-attachments';
@@ -137,6 +138,8 @@ export function QuoteCompanionPanel(props: {
     removeAttachment,
     clearSubmittedAttachments,
   } = useComposerAttachments({
+    copy: getDesktopConversationCopy(locale).actions,
+    formatError: (error, fallback) => localizedShellErrorMessage(error, fallback, locale),
     draftKey,
     toastApi: toast,
     service: attachments,
diff --git a/apps/desktop/src/renderer/features/workhub/index.ts b/apps/desktop/src/renderer/features/workhub/index.ts
index 10f1cf43e5..e3e7d8f734 100644
--- a/apps/desktop/src/renderer/features/workhub/index.ts
+++ b/apps/desktop/src/renderer/features/workhub/index.ts
@@ -23,3 +23,9 @@ export * from './model/routing-strategy.js';
 export { WorkHubNavigationRail } from './ui/workhub-navigation-rail.js';
 
 export { WorkHubPromptRail } from './ui/workhub-prompt-rail.js';
+
+export { WorkHubHighlightContext, WorkHubHighlightProvider, workHubIdentityHue } from './ui/workhub-work-identity.js';
+
+export { WorkHubComposer, type WorkHubComposerServices, type WorkHubComposerSelection } from './ui/workhub-composer.js';
+
+export { WorkHubComposerServicesProvider } from './services-context.js';
diff --git a/apps/desktop/src/renderer/features/workhub/services-context.tsx b/apps/desktop/src/renderer/features/workhub/services-context.tsx
new file mode 100644
index 0000000000..fd71321192
--- /dev/null
+++ b/apps/desktop/src/renderer/features/workhub/services-context.tsx
@@ -0,0 +1,35 @@
+/*
+ * 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 { createServicesContext } from '../../application/contracts/feature-services.js';
+import type { WorkHubCreateDefaults } from '@maka/core/session';
+import type { ChatDefaultPermissionMode } from '@maka/core/settings';
+
+export interface WorkHubComposerPort {
+  attachments: {
+    pickFiles(): Promise<{ ok: true; files: Array<{ approvalId: string; name: string; mimeType?: string; size: number }> } | { ok: false; reason: 'cancelled' }>;
+    previewApproval(approvalId: string): Promise<{ ok: true; base64: string; mimeType: string } | { ok: false; reason: string }>;
+  };
+  prepareAttachments(sessionId: string, items: Array<{ approvalId: string; name: string; mimeType?: string } | { file: File }>): Promise<import('@maka/core/events').AttachmentRef[]>;
+  setModelConfiguration(sessionId: string, input: NonNullable<WorkHubCreateDefaults['model']> & { thinkingLevel: null }): Promise<unknown>;
+  setPermissionMode(sessionId: string, mode: ChatDefaultPermissionMode): Promise<unknown>;
+}
+const { Provider, useServices } = createServicesContext<WorkHubComposerPort>('WorkHubComposerServicesProvider');
+export const WorkHubComposerServicesProvider = Provider;
+export const useWorkHubComposerServices = useServices;
diff --git a/apps/desktop/src/renderer/features/workhub/ui/workhub-composer.tsx b/apps/desktop/src/renderer/features/workhub/ui/workhub-composer.tsx
new file mode 100644
index 0000000000..2ac3781a36
--- /dev/null
+++ b/apps/desktop/src/renderer/features/workhub/ui/workhub-composer.tsx
@@ -0,0 +1,171 @@
+/*
+ * 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 { useRef, useState } from 'react';
+import { Composer, Selector, useToast, type ComposerProps, type ChatModelChoice } from '@maka/ui';
+import type { SessionSummary, WorkHubCreateDefaults } from '@maka/core/session';
+import { isChatDefaultPermissionMode } from '@maka/core/settings';
+import { useComposerAttachments } from '@maka/ui/use-composer-attachments';
+import { toComposerIngestItems } from '@maka/ui/composer-attachments';
+import { MAX_ATTACHMENT_BYTES, MAX_ATTACHMENT_COUNT } from '@maka/core/attachments';
+import type { AttachmentRef } from '@maka/core/events';
+import { getWorkHubComposerCopy } from '../../../locales/workhub-copy.js';
+import { getDesktopConversationCopy } from '../../../locales/conversation-copy.js';
+import { localizedShellErrorMessage } from '../../../locales/shell-copy.js';
+import type { UiLocale } from '@maka/core/ui-locale';
+import { useWorkHubComposerServices } from '../services-context.js';
+import { workHubIdentityHue } from './workhub-work-identity.js';
+
+export interface WorkHubComposerServices {
+  sessions: readonly SessionSummary[];
+  modelChoices: ChatModelChoice[];
+  defaults: WorkHubCreateDefaults;
+  confirmBypass(): Promise<boolean>;
+  onOpenModelSettings(): void;
+}
+
+export interface WorkHubComposerSelection {
+  attachments?: AttachmentRef[];
+  explicitTarget?: { sessionId: string };
+  newWorkDefaults?: WorkHubCreateDefaults;
+}
+
+/** Bind the shared Composer to an explicit Work or to creation defaults. */
+type WorkHubComposerProps = Omit<ComposerProps, 'onSend'> & {
+  services?: WorkHubComposerServices;
+  locale: UiLocale;
+  attachmentScope?: string;
+  onSend(text: string, selection: WorkHubComposerSelection, onAccepted?: () => void): Promise<boolean>;
+};
+
+export function WorkHubComposer(props: WorkHubComposerProps) {
+  if (!props.services) return <Composer {...props} onSend={(text) => props.onSend(text, {})} />;
+  return <ConfiguredWorkHubComposer {...props} />;
+}
+
+function ConfiguredWorkHubComposer({ services, locale, attachmentScope, onSend, ...composer }: WorkHubComposerProps) {
+  const settings = useWorkHubComposerServices();
+  const [selectedId, setSelectedId] = useState('');
+  const [defaults, setDefaults] = useState<WorkHubCreateDefaults>(services?.defaults ?? {});
+  const [changing, setChanging] = useState(false);
+  const [submitting, setSubmitting] = useState(false);
+  const toast = useToast();
+  const staged = useComposerAttachments({
+    draftKey: `workhub:${attachmentScope}`,
+    toastApi: toast,
+    service: settings.attachments,
+    copy: getDesktopConversationCopy(locale).actions,
+    formatError: (error, fallback) => localizedShellErrorMessage(error, fallback, locale),
+  });
+  const uploaded = useRef(new Map<string, AttachmentRef>());
+  const copy = getWorkHubComposerCopy(locale);
+  const selected = services?.sessions.find((session) => session.id === selectedId);
+  const unavailable = Boolean(selectedId && !selected);
+  const defaultChoice = services?.modelChoices.find((choice) => defaults.model &&
+    choice.connectionId === defaults.model.llmConnectionId && choice.connectionSlug === defaults.model.llmConnectionSlug && choice.model === defaults.model.model)
+    ?? services?.modelChoices.find((choice) => choice.isDefault);
+  const defaultModel = defaultChoice ? { llmConnectionId: defaultChoice.connectionId, llmConnectionSlug: defaultChoice.connectionSlug, model: defaultChoice.model } : undefined;
+  const busy = changing || submitting || Boolean(composer.sendBlocked);
+  const settingsLocked = busy || unavailable || selected?.status === 'running' || selected?.status === 'waiting_for_user';
+  const change = async (operation: () => Promise<unknown>) => {
+    setChanging(true);
+    try { await operation(); }
+    catch { toast.error(copy.settingsUpdateFailed); }
+    finally { setChanging(false); }
+  };
+  return <>
+    <div className="workhub-composer-scope workhub-work-identity" style={selected ? { color: `oklch(var(--workhub-tone) ${workHubIdentityHue(selected.id)})` } : undefined}>
+      <div className="workhub-composer-target">
+        <span>{copy.sendTo}</span>
+        <Selector
+          size="sm"
+          label={copy.currentWork}
+          isLabelHidden
+          value={selectedId}
+          isDisabled={busy}
+          options={[
+            { value: '', label: copy.routeAutomatically },
+            ...(unavailable ? [{ value: selectedId, label: copy.workUnavailable }] : []),
+            ...(services?.sessions.filter((session) => !session.isArchived).map((session) => ({ value: session.id, label: session.name || session.id })) ?? []),
+          ]}
+          onChange={(option) => setSelectedId(option)}
+        />
+      </div>
+      <span>{selected ? (copy.selectedWorkSettings) : (copy.newWorkSettings)}</span>
+    </div>
+    <Composer {...composer}
+      sendBlocked={busy || unavailable}
+      allowAttachmentOnlySend
+      pendingAttachments={staged.pendingAttachments}
+      onPickAttachments={staged.pickAttachments}
+      onAttachFilePaths={staged.attachFilePaths}
+      onRemoveAttachment={staged.removeAttachment}
+      onSend={async (text) => {
+        const snapshot = [...staged.pendingAttachments];
+        setSubmitting(true);
+        try {
+          if (snapshot.length > MAX_ATTACHMENT_COUNT || snapshot.some((item) => item.size > MAX_ATTACHMENT_BYTES)) {
+            throw new Error(copy.attachmentLimitExceeded);
+          }
+          const attachments: AttachmentRef[] = [];
+          for (const item of snapshot) {
+            let ref = uploaded.current.get(item.stagingKey);
+            if (!ref) {
+              [ref] = await settings.prepareAttachments(attachmentScope!, toComposerIngestItems([item]));
+              if (!ref) throw new Error('Attachment upload did not return a reference');
+              uploaded.current.set(item.stagingKey, ref);
+            }
+            attachments.push(ref);
+          }
+          const accepted = await onSend(text.trim() || (copy.attachmentPrompt), {
+            ...(selectedId ? { explicitTarget: { sessionId: selectedId } } : { newWorkDefaults: { ...defaults, model: defaultModel } }),
+            ...(attachments.length ? { attachments } : {}),
+          }, () => {
+            staged.clearSubmittedAttachments(snapshot);
+            for (const item of snapshot) uploaded.current.delete(item.stagingKey);
+          });
+          return accepted;
+        } catch (error) {
+          toast.error(copy.sendFailed, localizedShellErrorMessage(error, copy.tryAgain, locale));
+          return false;
+        } finally { setSubmitting(false); }
+      }}
+      activeSession={selected}
+      modelChoices={services?.modelChoices}
+      noModelConnection={!selected && services?.modelChoices.length === 0}
+      activeModelConnectionId={selected?.llmConnectionId}
+      activeModelConnectionSlug={selected?.llmConnectionSlug}
+      activeModel={selected?.model}
+      modelLabel={selected?.model ?? defaultModel?.model}
+      onModelChange={selected && services ? (model) => change(() => settings.setModelConfiguration(selected.id, { ...model, thinkingLevel: null })) : undefined}
+      modelSwitchAvailability={settingsLocked ? { available: false, pending: changing, reason: 'pending' } : undefined}
+      newChatModel={defaultModel}
+      onPickNewChatModel={services ? (model) => { setDefaults((current) => ({ ...current, model })); } : undefined}
+      onOpenModelSettings={services?.onOpenModelSettings}
+      permissionMode={selected?.permissionMode ?? defaults.permissionMode ?? 'ask'}
+      permissionModeDisabledReason={settingsLocked ? (copy.settingsLocked) : undefined}
+      onPermissionModeChange={services ? (mode) => change(async () => {
+        if (!isChatDefaultPermissionMode(mode)) return;
+        if (mode === 'bypass' && !await services.confirmBypass()) return;
+        if (selected) await settings.setPermissionMode(selected.id, mode);
+        else setDefaults((current) => ({ ...current, permissionMode: mode }));
+      }) : undefined}
+    />
+  </>;
+}
diff --git a/apps/desktop/src/renderer/features/workhub/ui/workhub-navigation-rail.tsx b/apps/desktop/src/renderer/features/workhub/ui/workhub-navigation-rail.tsx
index 857bb5fe14..9a20668f76 100644
--- a/apps/desktop/src/renderer/features/workhub/ui/workhub-navigation-rail.tsx
+++ b/apps/desktop/src/renderer/features/workhub/ui/workhub-navigation-rail.tsx
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-import { useState } from 'react';
+import { useContext, useState, type CSSProperties } from 'react';
 import type { WorkHubRailCopy } from '../../../locales/workhub-copy.js';
 import type { UiLocale } from '@maka/core/ui-locale';
 import { Button, dotForStatus, presentSessionStatus } from '@maka/ui';
@@ -29,6 +29,8 @@ import {
   type WorkHubWorkFilter,
 } from '../model/anchor-rail.js';
 
+import { WorkHubHighlightContext, workHubIdentityHue } from './workhub-work-identity.js';
+
 export function WorkHubNavigationRail(props: {
   readonly locale: UiLocale;
   readonly sessions: readonly WorkHubAnchorSession[];
@@ -37,6 +39,7 @@ export function WorkHubNavigationRail(props: {
   readonly copy: WorkHubRailCopy;
   readonly onOpenSession: (sessionId: string) => void;
 }) {
+  const highlight = useContext(WorkHubHighlightContext);
   const [filter, setFilter] = useState<WorkHubWorkFilter>('all');
   const anchors = deriveWorkHubAnchors({
     sessions: props.sessions,
@@ -79,7 +82,15 @@ export function WorkHubNavigationRail(props: {
               return (
                 <ListItem
                   key={anchor.target.sessionId}
-                  label={anchor.sessionName}
+                  className="workhub-work-identity workhub-navigation-item"
+                  style={{ '--workhub-work-hue': workHubIdentityHue(anchor.target.sessionId) } as CSSProperties}
+                  data-work-session-id={anchor.target.sessionId}
+                  data-work-highlighted={highlight.sessionId === anchor.target.sessionId}
+                  onMouseEnter={() => highlight.highlight(anchor.target.sessionId)}
+                  onMouseLeave={() => highlight.highlight(undefined)}
+                  onFocus={() => highlight.highlight(anchor.target.sessionId)}
+                  onBlur={() => highlight.highlight(undefined)}
+                  label={<span className="workhub-navigation-label">{anchor.sessionName}</span>}
                   description={`${anchor.target.sessionId === props.focusSessionId ? props.copy.focused : anchor.projectName} · ${state}`}
                   startContent={variant ? <StatusDot variant={variant} label={state} /> : undefined}
                   isSelected={anchor.target.sessionId === props.focusSessionId}
diff --git a/apps/desktop/src/renderer/features/workhub/ui/workhub-prompt-rail.tsx b/apps/desktop/src/renderer/features/workhub/ui/workhub-prompt-rail.tsx
index fb466370da..23f49b02ba 100644
--- a/apps/desktop/src/renderer/features/workhub/ui/workhub-prompt-rail.tsx
+++ b/apps/desktop/src/renderer/features/workhub/ui/workhub-prompt-rail.tsx
@@ -17,12 +17,25 @@
  * under the License.
  */
 
+import { useContext } from 'react';
+import { WorkHubHighlightContext, workHubIdentityHue } from './workhub-work-identity.js';
 import { useChatLayoutContext } from '@astryxdesign/core/Chat';
 import { PromptAnchorRail, type PromptAnchorRailTurn } from '@maka/ui';
 
 /** Uses the enclosing chat layout's scroller, never the Session navigation. */
-export function WorkHubPromptRail({ turns }: { turns: readonly PromptAnchorRailTurn[] }) {
+export function WorkHubPromptRail({ turns }: { turns: readonly (PromptAnchorRailTurn & { sessionId?: string })[] }) {
+  const highlight = useContext(WorkHubHighlightContext);
   const layout = useChatLayoutContext();
   if (!layout) throw new Error('WorkHubPromptRail requires ChatSurfaceLayout');
-  return <PromptAnchorRail turns={turns} scrollRef={layout.scrollContainerRef} />;
+  return <PromptAnchorRail
+    turns={turns.map((turn) => ({
+      ...turn,
+      accentColor: turn.sessionId
+        ? `oklch(var(--workhub-${highlight.sessionId === turn.sessionId ? 'highlight' : 'tone'}) ${workHubIdentityHue(turn.sessionId)})`
+        : undefined,
+      highlighted: Boolean(turn.sessionId && highlight.sessionId === turn.sessionId),
+    }))}
+    onHighlightTurn={(turn) => highlight.highlight(turns.find((candidate) => candidate.turnId === turn?.turnId)?.sessionId)}
+    scrollRef={layout.scrollContainerRef}
+  />;
 }
diff --git a/apps/desktop/src/renderer/features/workhub/ui/workhub-work-identity.tsx b/apps/desktop/src/renderer/features/workhub/ui/workhub-work-identity.tsx
new file mode 100644
index 0000000000..bc66f69034
--- /dev/null
+++ b/apps/desktop/src/renderer/features/workhub/ui/workhub-work-identity.tsx
@@ -0,0 +1,42 @@
+/*
+ * 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, useState, type ReactNode } from 'react';
+
+export const WorkHubHighlightContext = createContext<{
+  sessionId: string | undefined;
+  highlight(sessionId: string | undefined): void;
+}>({ sessionId: undefined, highlight: () => {} });
+
+/** Stable across refreshes and reordering; color supplements the visible work name. */
+export function workHubIdentityHue(sessionId: string): number {
+  let hash = 0;
+  for (const char of sessionId) hash = (Math.imul(hash, 31) + char.charCodeAt(0)) >>> 0;
+  const hues = [250, 165, 65, 315, 205, 25];
+  return hues[hash % hues.length]!;
+}
+
+
+/** Work identity hover is local presentation state shared by the three rails. */
+export function WorkHubHighlightProvider({ children }: { children: ReactNode }) {
+  const [sessionId, highlight] = useState<string>();
+  return <WorkHubHighlightContext.Provider value={{ sessionId, highlight }}>
+    {children}
+  </WorkHubHighlightContext.Provider>;
+}
diff --git a/apps/desktop/src/renderer/locales/workhub-copy.ts b/apps/desktop/src/renderer/locales/workhub-copy.ts
index d3f4ffdff6..2c717ef125 100644
--- a/apps/desktop/src/renderer/locales/workhub-copy.ts
+++ b/apps/desktop/src/renderer/locales/workhub-copy.ts
@@ -84,3 +84,67 @@ const COPY = {
 export function getWorkHubRailCopy(locale: UiLocale): WorkHubRailCopy {
   return COPY[locale];
 }
+
+interface WorkHubComposerCopy {
+  readonly settingsUpdateFailed: string;
+  readonly sendTo: string;
+  readonly currentWork: string;
+  readonly routeAutomatically: string;
+  readonly workUnavailable: string;
+  readonly selectedWorkSettings: string;
+  readonly newWorkSettings: string;
+  readonly attachmentLimitExceeded: string;
+  readonly attachmentPrompt: string;
+  readonly sendFailed: string;
+  readonly tryAgain: string;
+  readonly settingsLocked: string;
+}
+
+const COMPOSER_COPY = {
+  'zh-CN': {
+    settingsUpdateFailed: '配置更新失败,请重试',
+    sendTo: '发送到',
+    currentWork: '当前 Work',
+    routeAutomatically: '自动识别工作',
+    workUnavailable: '工作不可用',
+    selectedWorkSettings: '模型与权限用于此 Work',
+    newWorkSettings: '模型与权限用于新 Work',
+    attachmentLimitExceeded: '附件数量或大小超过限制',
+    attachmentPrompt: '请查看附件。',
+    sendFailed: '发送失败',
+    tryAgain: '请重试',
+    settingsLocked: '当前无法修改配置',
+  },
+  'zh-TW': {
+    settingsUpdateFailed: '設定更新失敗,請重試',
+    sendTo: '傳送至',
+    currentWork: '目前 Work',
+    routeAutomatically: '自動識別工作',
+    workUnavailable: '工作無法使用',
+    selectedWorkSettings: '模型與權限用於此 Work',
+    newWorkSettings: '模型與權限用於新 Work',
+    attachmentLimitExceeded: '附件數量或大小超過限制',
+    attachmentPrompt: '請查看附件。',
+    sendFailed: '傳送失敗',
+    tryAgain: '請重試',
+    settingsLocked: '目前無法修改設定',
+  },
+  'en': {
+    settingsUpdateFailed: 'Could not update settings. Try again.',
+    sendTo: 'Send to',
+    currentWork: 'Current Work',
+    routeAutomatically: 'Route automatically',
+    workUnavailable: 'Work unavailable',
+    selectedWorkSettings: 'Settings apply to this Work',
+    newWorkSettings: 'Settings apply to new Work',
+    attachmentLimitExceeded: 'Attachment count or size exceeds the limit',
+    attachmentPrompt: 'Please review the attachments.',
+    sendFailed: 'Could not send',
+    tryAgain: 'Try again',
+    settingsLocked: 'Settings are currently locked',
+  },
+} satisfies UiCatalog<WorkHubComposerCopy>;
+
+export function getWorkHubComposerCopy(locale: UiLocale): WorkHubComposerCopy {
+  return COMPOSER_COPY[locale];
+}
diff --git a/apps/desktop/src/renderer/pending-items.ts b/apps/desktop/src/renderer/pending-items.ts
index eb1963ac22..2c5bd8afac 100644
--- a/apps/desktop/src/renderer/pending-items.ts
+++ b/apps/desktop/src/renderer/pending-items.ts
@@ -17,58 +17,4 @@
  * under the License.
  */
 
-/** Generic keyed staging registry shared by composer surfaces. */
-export type PendingByKey<T> = Record<string, T[]>;
-
-/**
- * The bucket the session-less composer stages into, whatever the workspace
- * picker points at (#3408).
- *
- * The rest of the new-task surface is keyed by (profileId, hostId, projectId)
- * since #3122, and staged files and quotes were keyed with it. But they are
- * in-memory intent, not Host state: nothing persists them, and nothing restores
- * them per target. Keying them by the target only made them drop out of the
- * composer when the picker moved.
- *
- * A key that never moves is also the only owner an in-flight submission can
- * safely have. `send()` captures the key it submitted from and clears that key
- * when it resolves, so a key that followed the picker would leave the files it
- * just sent staged under the new target, ready to be sent a second time. This
- * one cannot go stale, so no re-keying rule is needed to keep it honest.
- */
-export const NEW_TASK_PENDING_KEY = 'new-task';
-
-export function selectPending<T>(map: PendingByKey<T>, key: string): T[] {
-  return map[key] ?? [];
-}
-
-export function appendPending<T>(
-  map: PendingByKey<T>,
-  key: string,
-  items: readonly T[],
-): PendingByKey<T> {
-  return { ...map, [key]: [...(map[key] ?? []), ...items] };
-}
-
-export function removePending<T>(map: PendingByKey<T>, key: string, index: number): PendingByKey<T> {
-  const current = map[key] ?? [];
-  return { ...map, [key]: current.filter((_, i) => i !== index) };
-}
-
-export function removePendingItems<T>(
-  map: PendingByKey<T>,
-  key: string,
-  items: readonly T[],
-  identityOf: (item: T) => unknown = (item) => item,
-): PendingByKey<T> {
-  const submitted = new Set(items.map(identityOf));
-  const remaining = (map[key] ?? []).filter((item) => !submitted.has(identityOf(item)));
-  if (remaining.length === 0) return clearPending(map, key);
-  return { ...map, [key]: remaining };
-}
-
-export function clearPending<T>(map: PendingByKey<T>, key: string): PendingByKey<T> {
-  const next = { ...map };
-  delete next[key];
-  return next;
-}
+export { NEW_TASK_PENDING_KEY, selectPending, appendPending, removePending, removePendingItems, clearPending, type PendingByKey } from './features/conversation/index.js';
diff --git a/apps/desktop/src/renderer/features/workhub/index.ts b/apps/desktop/src/renderer/platform/desktop/create-workhub-composer-services.ts
similarity index 67%
copy from apps/desktop/src/renderer/features/workhub/index.ts
copy to apps/desktop/src/renderer/platform/desktop/create-workhub-composer-services.ts
index 10f1cf43e5..af4cb1632a 100644
--- a/apps/desktop/src/renderer/features/workhub/index.ts
+++ b/apps/desktop/src/renderer/platform/desktop/create-workhub-composer-services.ts
@@ -17,9 +17,12 @@
  * under the License.
  */
 
-export * from './model/anchor-rail.js';
-export * from './model/route-policy.js';
-export * from './model/routing-strategy.js';
-export { WorkHubNavigationRail } from './ui/workhub-navigation-rail.js';
-
-export { WorkHubPromptRail } from './ui/workhub-prompt-rail.js';
+import type { MakaBridge } from '../../../preload/bridge-contract.js';
+export function createDesktopWorkHubComposerServices(bridge: MakaBridge = window.maka) {
+  return {
+    attachments: bridge.attachments,
+    prepareAttachments: bridge.workHub.prepareAttachments,
+    setModelConfiguration: bridge.sessions.setModelConfiguration,
+    setPermissionMode: bridge.sessions.setPermissionMode,
+  };
+}
diff --git a/apps/desktop/src/renderer/styles/workhub.css b/apps/desktop/src/renderer/styles/workhub.css
index 5bd40e0efd..bec8666f5d 100644
--- a/apps/desktop/src/renderer/styles/workhub.css
+++ b/apps/desktop/src/renderer/styles/workhub.css
@@ -51,8 +51,7 @@
   box-sizing: border-box;
   align-items: flex-start;
   margin-top: var(--space-6, 24px);
-  padding: 0 var(--space-3, 12px) 24px;
-  border-bottom: 1px solid var(--color-border-subtle, var(--border));
+  padding: 0 var(--space-4, 16px) 12px;
 }
 
 .workhub-header h1,
@@ -76,16 +75,14 @@
 
 .workhub-message-list {
   flex: 1 1 auto;
+  padding-inline: 0;
 }
 
 .workhub-body {
   display: grid;
   width: 100%;
   min-width: 0;
-  grid-template-columns: minmax(180px, 220px) minmax(
-      0,
-      var(--maka-reading-measure)
-    ) minmax(180px, 220px);
+  grid-template-columns: minmax(0, 220px) var(--maka-reading-measure) minmax(0, 220px);
   gap: var(--space-6, 24px);
   justify-content: center;
 }
@@ -174,8 +171,7 @@
   display: flex;
   flex-direction: column;
   gap: 8px;
-  padding: 24px 0;
-  border-bottom: 1px solid var(--color-border-subtle, var(--border));
+  padding: 16px 0;
 }
 
 .workhub-message {
@@ -310,9 +306,6 @@
     padding-inline: 16px;
   }
 
-  .workhub-turn {
-    padding-inline: 4px;
-  }
 }
 
 @media (max-width: 1240px) {
@@ -320,6 +313,12 @@
     display: block;
   }
 
+  .workhub-conversation-shell,
+  .workhub-header {
+    width: min(var(--maka-reading-measure), calc(100% - 2 * (var(--space-6) + var(--space-3))));
+    margin-inline: auto;
+  }
+
   .workhub-anchor-rail {
     position: static;
     width: min(var(--maka-reading-measure), 100%);
@@ -343,3 +342,136 @@
   }
 
 }
+
+/* Identity sits next to the prompt; shared chat bubbles remain neutral. */
+.workhub-message-identity {
+  display: flex;
+  justify-content: flex-end;
+  min-width: 0;
+}
+
+.workhub-message-identity button {
+  height: auto;
+  min-height: 28px;
+  max-width: 100%;
+  white-space: normal;
+  overflow-wrap: anywhere;
+  text-align: right;
+  color: var(--workhub-work-label);
+  font-size: var(--font-size-body-sm, 12px);
+}
+
+.workhub-bound-turn .workhub-user-bubble,
+.workhub-bound-turn .workhub-assistant-bubble {
+  position: relative;
+}
+
+.workhub-header p {
+  display: none;
+}
+
+.workhub-submitted > button {
+  border-bottom: 0;
+}
+
+.workhub-bound-turn .workhub-assistant-bubble {
+  padding-inline-start: var(--space-3, 12px);
+}
+
+.workhub-bound-turn,
+.workhub-work-identity {
+  --workhub-work-color: oklch(0.58 0.1 var(--workhub-work-hue));
+  --workhub-work-label: oklch(0.42 0.075 var(--workhub-work-hue));
+  --workhub-work-highlight: oklch(0.48 0.18 var(--workhub-work-hue));
+}
+
+.dark .workhub-bound-turn,
+.dark .workhub-work-identity {
+  --workhub-work-color: oklch(0.76 0.105 var(--workhub-work-hue));
+  --workhub-work-label: oklch(0.8 0.09 var(--workhub-work-hue));
+  --workhub-work-highlight: oklch(0.85 0.15 var(--workhub-work-hue));
+}
+
+.workhub-work-rail {
+  position: absolute;
+  top: 4px;
+  bottom: 4px;
+  width: 11px;
+}
+
+.workhub-work-rail::before {
+  position: absolute;
+  inset-block: 0;
+  left: 4px;
+  width: 3px;
+  border-radius: var(--radius-pill, 999px);
+  background: var(--workhub-work-color);
+  transition: background-color 180ms ease-out;
+  content: '';
+}
+
+.workhub-user-bubble > .workhub-work-rail {
+  right: -11px;
+}
+
+.workhub-assistant-bubble > .workhub-work-rail {
+  left: -4px;
+}
+
+.workhub-bound-turn[data-work-highlighted='true'],
+.workhub-work-identity[data-work-highlighted='true'] {
+  --workhub-work-color: var(--workhub-work-highlight);
+  --workhub-work-label: var(--workhub-work-highlight);
+}
+
+@media (prefers-reduced-motion: reduce) {
+  .workhub-work-rail::before {
+    transition: none;
+  }
+}
+
+.workhub-surface {
+  --workhub-tone: 0.58 0.1;
+  --workhub-highlight: 0.48 0.18;
+}
+
+.dark .workhub-surface {
+  --workhub-tone: 0.76 0.105;
+  --workhub-highlight: 0.85 0.15;
+}
+
+.workhub-navigation-item {
+  color: var(--workhub-work-label);
+}
+
+.workhub-navigation-item[data-work-highlighted='true'] {
+  background: color-mix(in oklch, var(--workhub-work-color) 8%, transparent);
+}
+
+.workhub-surface .maka-prompt-rail:has([data-highlighted='true']) {
+  opacity: 1;
+}
+
+.workhub-navigation-label {
+  display: block;
+  overflow: hidden;
+  color: var(--workhub-work-label);
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.workhub-composer-scope {
+  box-sizing: border-box;
+  width: min(var(--maka-reading-measure), calc(100% - 2 * var(--space-6)));
+  margin-inline: auto;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px;
+  padding: 0 16px 8px;
+  font-size: var(--font-size-body-sm, 12px);
+  color: var(--color-text-secondary);
+}
+.workhub-composer-target { display: flex; align-items: center; gap: 8px; min-width: 0; }
+.workhub-composer-target > :last-child { max-width: 240px; min-width: 0; }
+@media (max-width: 600px) { .workhub-composer-scope { flex-wrap: wrap; } }
diff --git a/apps/desktop/src/renderer/workhub-controller.ts b/apps/desktop/src/renderer/workhub-controller.ts
index 6eea15a9c6..56f4fbbab0 100644
--- a/apps/desktop/src/renderer/workhub-controller.ts
+++ b/apps/desktop/src/renderer/workhub-controller.ts
@@ -120,6 +120,7 @@ export interface WorkHubProjectedTurn {
 }
 
 export interface WorkHubCoordinationTurn {
+  attachments?: WorkHubCoordinationActInput['attachments'];
   messageId: string;
   turnId: string;
   text: string;
@@ -160,6 +161,8 @@ export interface WorkHubProjection {
 }
 
 export interface WorkHubSubmitInput {
+  attachments?: WorkHubCoordinationActInput['attachments'];
+  newWorkDefaults?: WorkHubCoordinationActInput['newWorkDefaults'];
   requestId: string;
   text: string;
   retryAction?: true;
@@ -583,13 +586,13 @@ export function createWorkHubController(deps: {
         text: input.text,
         sessions: ordinary,
       });
-      const resume = await submitNamedDelegationAction(input, resumeDecision, 'resume', routingStrategy.strategyId);
+      const resume = input.attachments?.length ? undefined : await submitNamedDelegationAction(input, resumeDecision, 'resume', routingStrategy.strategyId);
       if (resume) return resume;
       const stopDecision = submissionPolicy.resolveStop({
         text: input.text,
         sessions: ordinary,
       });
-      const stop = await submitNamedDelegationAction(input, stopDecision, 'stop', routingStrategy.strategyId);
+      const stop = input.attachments?.length ? undefined : await submitNamedDelegationAction(input, stopDecision, 'stop', routingStrategy.strategyId);
       if (stop) return stop;
       const candidateSet = await coordination.candidates();
       const candidateBySessionId = new Map(
@@ -657,6 +660,7 @@ export function createWorkHubController(deps: {
         await coordination.act({
           actionId: input.requestId,
           userText: input.text,
+          ...(input.attachments ? { attachments: input.attachments } : {}),
           proposal: { disposition: 'answer_here' },
         });
         return {
@@ -676,6 +680,8 @@ export function createWorkHubController(deps: {
           ? {
               actionId: input.requestId,
               userText: input.text,
+          ...(input.attachments ? { attachments: input.attachments } : {}),
+              ...(input.newWorkDefaults ? { newWorkDefaults: input.newWorkDefaults } : {}),
               confirmation: { kind: 'user_correction' },
               proposal: {
                 disposition: 'replace',
@@ -686,6 +692,8 @@ export function createWorkHubController(deps: {
           : {
               actionId: input.requestId,
               userText: input.text,
+          ...(input.attachments ? { attachments: input.attachments } : {}),
+              ...(input.newWorkDefaults ? { newWorkDefaults: input.newWorkDefaults } : {}),
               proposal: { disposition: 'create_new', title },
             });
         if (
@@ -731,6 +739,7 @@ export function createWorkHubController(deps: {
         ? {
             actionId: input.requestId,
             userText: input.text,
+          ...(input.attachments ? { attachments: input.attachments } : {}),
             candidateSetId: candidateSet.candidateSetId,
             confirmation: { kind: 'user_correction' },
             proposal: {
@@ -745,6 +754,7 @@ export function createWorkHubController(deps: {
         : {
             actionId: input.requestId,
             userText: input.text,
+          ...(input.attachments ? { attachments: input.attachments } : {}),
             candidateSetId: candidateSet.candidateSetId,
             proposal: {
               disposition: 'delegate_existing',
diff --git a/apps/desktop/src/renderer/workhub-coordination-port.ts b/apps/desktop/src/renderer/workhub-coordination-port.ts
index 954e68286b..88f9c82fd8 100644
--- a/apps/desktop/src/renderer/workhub-coordination-port.ts
+++ b/apps/desktop/src/renderer/workhub-coordination-port.ts
@@ -194,6 +194,7 @@ export function projectWorkHubCoordinationTurns(
         messageId: message.id,
         turnId: message.coordinationTurnId,
         text: boundedWorkHubTimelineText(message.userText),
+        ...(message.attachments ? { attachments: message.attachments } : {}),
         state: 'completed',
         assignment: {
           actionId: message.actionId,
@@ -217,6 +218,7 @@ export function projectWorkHubCoordinationTurns(
         messageId: message.id,
         turnId: message.turnId,
         text,
+        ...(message.attachments ? { attachments: message.attachments } : {}),
         state: stateByTurnId.get(message.turnId) ?? 'running',
         updatedAt: message.ts,
       });
diff --git a/apps/desktop/src/renderer/workhub-surface.tsx b/apps/desktop/src/renderer/workhub-surface.tsx
index db8dd711ce..b62a4f5955 100644
--- a/apps/desktop/src/renderer/workhub-surface.tsx
+++ b/apps/desktop/src/renderer/workhub-surface.tsx
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-import { useCallback, useEffect, useRef, useState, type ReactNode } from 'react';
+import { useContext, useCallback, useEffect, useRef, useState, type CSSProperties, type ReactNode } from 'react';
 import {
   ChatMessage,
   ChatMessageBubble,
@@ -26,7 +26,7 @@ import {
 } from '@astryxdesign/core';
 import { Button } from '@astryxdesign/core/Button';
 import type { UiLocale } from '@maka/core/ui-locale';
-import { ChatSurfaceLayout, Composer } from '@maka/ui';
+import { ChatSurfaceLayout } from '@maka/ui';
 import {
   type WorkHubController,
   type WorkHubCoordinationTurn,
@@ -36,12 +36,13 @@ import {
   type WorkHubSubmission,
   type WorkHubSubmitInput,
 } from './workhub-controller.js';
+import type { AttachmentRef } from '@maka/core/events';
 import { WorkHubCoordinationFailure } from './workhub-coordination-port.js';
 import {
   WorkHubSendLease,
   type WorkHubSendAttempt,
 } from './workhub-send-lease.js';
-import { WorkHubNavigationRail, WorkHubPromptRail } from './features/workhub/index.js';
+import { WorkHubHighlightContext, WorkHubHighlightProvider, workHubIdentityHue, WorkHubNavigationRail, WorkHubPromptRail, WorkHubComposer, type WorkHubComposerServices, type WorkHubComposerSelection } from './features/workhub/index.js';
 import { getWorkHubRailCopy } from './locales/workhub-copy.js';
 
 export interface WorkHubConversationTurn {
@@ -50,6 +51,7 @@ export interface WorkHubConversationTurn {
   state: 'routing' | 'settled' | 'failed';
   outcome?: WorkHubSubmission;
   failure?: WorkHubSurfaceFailure;
+  submissionContext?: WorkHubComposerSelection & { attachments?: AttachmentRef[] };
 }
 
 export type WorkHubSurfaceFailure =
@@ -220,13 +222,16 @@ export async function submitLeasedWorkHubSurfaceInput(input: {
  * The persistent Coordination Session transcript is the primary conversation.
  * Ordinary Sessions remain a read-only status/routing projection.
  */
-export function WorkHubSurface(props: {
+interface WorkHubSurfaceProps {
   controller: WorkHubController;
   leaseScope: string;
   locale: UiLocale;
   initialFocusSessionId?: string;
+  composerServices?: WorkHubComposerServices;
   onOpenSession(sessionId: string): void;
-}) {
+}
+
+export function WorkHubSurface(props: WorkHubSurfaceProps) {
   const copy = workHubCopy(props.locale);
   const railCopy = getWorkHubRailCopy(props.locale);
   const [projection, setProjection] = useState<WorkHubProjection>({ sessions: [], turns: [] });
@@ -355,26 +360,31 @@ export function WorkHubSurface(props: {
     });
   }, [copy, projection, props.controller, refresh, routeGate]);
 
-  const send = useCallback(async (value: string) => {
+  const send = useCallback(async (value: string, selection: WorkHubComposerSelection, onAccepted?: () => void) => {
     const text = value.trim();
     if (!text || !initialLoadSettled || !conversationReady || routeGate.pending) return false;
-    return submitLeasedWorkHubSurfaceInput({
+    const submissionContext = selection;
+    const accepted = await submitLeasedWorkHubSurfaceInput({
       lease: sendLease,
       text,
       submit: async (attempt) => {
         const { requestId } = attempt;
         setTurns((current) => current.some((turn) => turn.requestId === requestId)
           ? current.map((turn) => turn.requestId === requestId
-            ? { requestId, text: attempt.text, state: 'routing' }
+            ? { requestId, text: attempt.text, state: 'routing', submissionContext }
             : turn)
-          : [...current, { requestId, text: attempt.text, state: 'routing' }]);
-        return route({
+          : [...current, { requestId, text: attempt.text, state: 'routing', submissionContext }]);
+        const result = await route({
           requestId,
           text: attempt.text,
+          ...submissionContext,
           ...(attempt.retrying ? { retryAction: true as const } : {}),
         });
+        if (workHubSubmissionClearsDraft(result)) onAccepted?.();
+        return result;
       },
     });
+    return accepted;
   }, [conversationReady, initialLoadSettled, route, routeGate, sendLease]);
   const visible = visibleWorkHubConversation(coordination.turns, turns);
   const visibleCoordinationTurns = visible.coordination;
@@ -383,16 +393,24 @@ export function WorkHubSurface(props: {
   const surfaceReady = initialLoadSettled && conversationReady;
 
   return (
+    <WorkHubHighlightProvider>
     <ChatSurfaceLayout
       className="workhub-surface"
       composer={(
-        <Composer
+        <WorkHubComposer
+          services={props.composerServices ? {
+            ...props.composerServices,
+            sessions: props.composerServices.sessions.filter((session) => projection.sessions.some((work) => work.target.sessionId === session.id)),
+          } : undefined}
+          locale={props.locale}
+          attachmentScope={props.leaseScope}
           draftKey="workhub"
           draftPersistence={sendLease}
           onSend={send}
           onStop={() => {}}
           sendBlocked={pending || !surfaceReady}
           modelLabel="WorkHub"
+          showStaticModelUnavailableStatus={false}
         />
       )}
     >
@@ -423,10 +441,13 @@ export function WorkHubSurface(props: {
                 turnId: `workhub-message-${turn.messageId}`,
                 label: turn.text,
                 reply: turn.result,
+                sessionId: turn.assignment?.targetSessionId ?? turn.stop?.targetSessionId,
               })),
               ...visibleLocalTurns.map((turn) => ({
                 turnId: `workhub-request-${turn.requestId}`,
                 label: turn.text,
+                sessionId: turn.outcome?.kind === 'submitted' || turn.outcome?.kind === 'stop' || turn.outcome?.kind === 'resume'
+                  ? turn.outcome.target.sessionId : undefined,
               })),
             ]} />
             <ChatMessageList
@@ -473,6 +494,7 @@ export function WorkHubSurface(props: {
                         submit: (attempt) => route({
                           requestId: attempt.requestId,
                           text: attempt.text,
+                          ...turn.submissionContext,
                           explicitTarget: target,
                           ...(attempt.retrying ? { retryAction: true as const } : {}),
                           ...(turn.outcome?.kind === 'clarification' && turn.outcome.correction
@@ -493,6 +515,7 @@ export function WorkHubSurface(props: {
         </div>
       </section>
     </ChatSurfaceLayout>
+    </WorkHubHighlightProvider>
   );
 }
 
@@ -518,12 +541,14 @@ export function WorkHubCoordinationStatus(props: {
     <ChatSurfaceLayout
       className="workhub-surface"
       composer={(
-        <Composer
+        <WorkHubComposer
+          locale={props.locale}
           draftKey="workhub"
           onSend={async () => false}
           onStop={() => {}}
           sendBlocked
           modelLabel="WorkHub"
+          showStaticModelUnavailableStatus={false}
         />
       )}
     >
@@ -609,11 +634,18 @@ export function WorkHubCoordinationTurnView(props: {
     <WorkHubMessageFrame
       anchorId={`workhub-message-${props.turn.messageId}`}
       text={props.turn.text}
+      attachments={props.turn.attachments}
       state={props.turn.stop?.outcome ?? (assignment?.linkState === 'active'
         ? assignment.feedbackState
         : assignment?.linkState ?? props.turn.state)}
       linkState={assignment?.linkState}
       projected
+      work={assignment || props.turn.stop ? {
+        sessionId: assignment?.targetSessionId ?? props.turn.stop!.targetSessionId,
+        name: session?.sessionName ?? stoppedSession?.sessionName ?? assignment?.targetSessionName ?? props.turn.stop!.targetSessionName,
+        projectName: session?.projectName ?? stoppedSession?.projectName,
+      } : undefined}
+      onOpenSession={props.onOpenSession}
     >
       {props.turn.stop ? (
         <SubmittedWorkView
@@ -728,7 +760,18 @@ function WorkHubTurnView(props: {
     : undefined;
 
   return (
-    <WorkHubMessageFrame anchorId={`workhub-request-${turn.requestId}`} text={turn.text} state={turn.state}>
+    <WorkHubMessageFrame
+      anchorId={`workhub-request-${turn.requestId}`}
+      text={turn.text}
+      attachments={turn.submissionContext?.attachments}
+      state={turn.state}
+      work={submitted ? {
+        sessionId: submitted.target.sessionId,
+        name: target?.sessionName ?? copy.sessionFallback,
+        projectName: target?.projectName,
+      } : undefined}
+      onOpenSession={props.onOpenSession}
+    >
           {turn.state === 'routing' ? (
             <p className="workhub-status" role="status">{copy.routing}</p>
           ) : turn.state === 'failed' ? (
@@ -811,22 +854,57 @@ function WorkHubTurnView(props: {
 function WorkHubMessageFrame(props: {
   anchorId: string;
   text: string;
+  attachments?: AttachmentRef[];
   state: string;
   linkState?: WorkHubDelegationLinkState;
   projected?: boolean;
+  work?: { sessionId: string; name: string; projectName?: string };
+  onOpenSession?(sessionId: string): void;
   children: ReactNode;
 }) {
+  const highlight = useContext(WorkHubHighlightContext);
+  const work = props.work;
+  const rail = work ? (
+    <span
+      className="workhub-work-rail"
+      aria-hidden="true"
+      onMouseEnter={() => highlight.highlight(work.sessionId)}
+      onMouseLeave={() => highlight.highlight(undefined)}
+    />
+  ) : null;
   return (
     <section
-      className={`workhub-turn${props.projected ? ' workhub-projected-turn' : ''}`}
+      className={`workhub-turn${props.projected ? ' workhub-projected-turn' : ''}${props.work ? ' workhub-bound-turn' : ''}`}
+      style={props.work ? { '--workhub-work-hue': workHubIdentityHue(props.work.sessionId) } as CSSProperties : undefined}
+      aria-label={props.text}
       data-turn-id={props.anchorId}
       data-transcript-turn-id={props.anchorId}
+      data-work-session-id={props.work?.sessionId}
+      data-work-highlighted={Boolean(work && highlight.sessionId === work.sessionId)}
       data-state={props.state}
       data-link-state={props.linkState}
     >
+      {props.work ? (
+        <div className="workhub-message-identity">
+          <Button
+            variant="ghost"
+            label={[props.work.projectName, props.work.name].filter(Boolean).join(' / ')}
+            onMouseEnter={() => highlight.highlight(work!.sessionId)}
+            onMouseLeave={() => highlight.highlight(undefined)}
+            onFocus={() => highlight.highlight(work!.sessionId)}
+            onBlur={() => highlight.highlight(undefined)}
+            onClick={() => props.onOpenSession?.(props.work!.sessionId)}
+          >
+            {props.work.projectName ? <span>{props.work.projectName}<span aria-hidden="true"> / </span></span> : null}
+            <strong>{props.work.name}</strong>
+          </Button>
+        </div>
+      ) : null}
       <ChatMessage sender="user" className="workhub-message">
         <ChatMessageBubble className="maka-chat-message-bubble maka-chat-message-bubble-user workhub-user-bubble">
+          {rail}
           <p>{props.text}</p>
+          {props.attachments?.length ? <ul className="workhub-message-attachments">{props.attachments.map((attachment, index) => <li key={index}>{attachment.name}</li>)}</ul> : null}
         </ChatMessageBubble>
       </ChatMessage>
       <ChatMessage sender="assistant" className="workhub-message">
@@ -835,6 +913,7 @@ function WorkHubMessageFrame(props: {
           width="100%"
           className="maka-chat-message-bubble maka-chat-message-bubble-assistant workhub-assistant-bubble"
         >
+          {rail}
           {props.children}
         </ChatMessageBubble>
       </ChatMessage>
diff --git a/apps/desktop/stories/workhub.stories.tsx b/apps/desktop/stories/workhub.stories.tsx
index c447e788cb..6fa53ec942 100644
--- a/apps/desktop/stories/workhub.stories.tsx
+++ b/apps/desktop/stories/workhub.stories.tsx
@@ -17,6 +17,10 @@
  * under the License.
  */
 
+import { ToastProvider } from '@maka/ui';
+import { useState } from 'react';
+import type { SessionSummary } from '@maka/core/session';
+import { WorkHubComposerServicesProvider } from '../src/renderer/features/workhub/index.js';
 import type { Meta, StoryObj } from '@storybook/react-vite';
 import { expect, fn, userEvent, within, waitFor } from 'storybook/test';
 import type {
@@ -275,3 +279,189 @@ export const ConversationPromptAnchors: Story = {
 export const ConversationPromptAnchorsNarrow: Story = {
   ...ConversationPromptAnchors,
 };
+
+// Repeated messages for one work must highlight together, independently of
+// intervening work. This is the same durable assignment seam as production.
+export const WorkIdentityAcrossTurns: Story = {
+  render: () => {
+    const first = submittedTurn();
+    const other: WorkHubCoordinationTurn = {
+      ...first, messageId: 'other-message', turnId: 'other-turn',
+      text: '检查另一个工作的界面布局。',
+      assignment: { ...first.assignment!, targetSessionId: 'other-work', targetSessionName: '界面布局' },
+    };
+    const followup = { ...first, messageId: 'followup-message', turnId: 'followup-turn', text: '继续支付回调,检查失败重试。' };
+    return <Surface turns={[first, other, followup]} />;
+  },
+  play: async ({ canvasElement }) => {
+    await waitFor(() => expect(canvasElement.querySelectorAll('.workhub-bound-turn')).toHaveLength(3));
+    const turns = Array.from(canvasElement.querySelectorAll<HTMLElement>('.workhub-bound-turn'));
+    const rail = turns[0]!.querySelector<HTMLElement>('.workhub-work-rail')!;
+    await userEvent.hover(rail);
+    await waitFor(() => expect(turns.map(turn => turn.dataset.workHighlighted)).toEqual(['true', 'false', 'true']));
+    await userEvent.unhover(rail);
+    await waitFor(() => expect(turns.map(turn => turn.dataset.workHighlighted)).toEqual(['false', 'false', 'false']));
+    const ticks = Array.from(canvasElement.querySelectorAll<HTMLElement>('.maka-prompt-rail-tick'));
+    expect(ticks).toHaveLength(3);
+    const navigation = canvasElement.querySelector<HTMLElement>('.workhub-navigation-item')!;
+    await userEvent.hover(navigation);
+    await waitFor(() => {
+      expect(turns.map(turn => turn.dataset.workHighlighted)).toEqual(['true', 'false', 'true']);
+      expect(ticks.map(tick => tick.dataset.highlighted)).toEqual(['true', undefined, 'true']);
+      expect(getComputedStyle(ticks[0]!).color).toBe(getComputedStyle(ticks[2]!).color);
+      expect(getComputedStyle(navigation.querySelector('.workhub-navigation-label')!).color)
+        .toBe(getComputedStyle(ticks[0]!).color);
+      expect(getComputedStyle(ticks[0]!).color).not.toBe(getComputedStyle(ticks[1]!).color);
+    });
+    await userEvent.unhover(navigation);
+    await userEvent.hover(ticks[1]!);
+    await waitFor(() => {
+      expect(turns.map(turn => turn.dataset.workHighlighted)).toEqual(['false', 'true', 'false']);
+      expect(navigation.dataset.workHighlighted).toBe('false');
+    });
+    await userEvent.unhover(ticks[1]!);
+    ticks[0]!.focus();
+    await waitFor(() => expect(navigation.dataset.workHighlighted).toBe('true'));
+    ticks[0]!.blur();
+    await userEvent.hover(rail);
+    await waitFor(() => {
+      expect(navigation.dataset.workHighlighted).toBe('true');
+      expect(ticks.map(tick => tick.dataset.highlighted)).toEqual(['true', undefined, 'true']);
+    });
+    await userEvent.unhover(rail);
+  },
+};
+
+const composerWrites = { model: fn(), permission: fn(), send: fn(), upload: fn() };
+const composerModels = ['model-a', 'model-b'].map((model, index) => ({
+  connectionId: 'connection-test', connectionSlug: 'test', providerType: 'openai' as const,
+  providerLabel: 'OpenAI', model, label: model, isDefault: index === 0, thinkingLevels: [],
+}));
+function ConfiguredComposerSurface({ failFirst = false }: { failFirst?: boolean }) {
+  const [failures] = useState(() => ({ remaining: failFirst ? 1 : 0 }));
+  const [session, setSession] = useState<SessionSummary>({
+    id: TARGET_SESSION_ID, name: SESSION_NAME, isFlagged: false, isArchived: false, labels: [],
+    hasUnread: false, status: 'active', backend: 'ai-sdk', llmConnectionId: 'connection-test',
+    llmConnectionSlug: 'test', connectionLocked: false, model: 'model-a', permissionMode: 'ask',
+  });
+  const [fixture] = useState<WorkHubController>(() => ({
+    ...controller([submittedTurn()]),
+    submit: async (input) => {
+      composerWrites.send(input);
+      if (failures.remaining-- > 0) throw new Error('Temporary Host failure');
+      return { kind: 'discussion', requestId: input.requestId, text: input.text, strategyId: 'wh-r2.4-session-context-continuity' };
+    },
+  }));
+  return <ToastProvider><WorkHubComposerServicesProvider services={{
+    attachments: {
+      pickFiles: async () => ({ ok: true, files: [{ approvalId: 'file-1', name: 'requirements.txt', size: 12, mimeType: 'text/plain' }] }),
+      previewApproval: async () => ({ ok: false, reason: 'not-image' }),
+    },
+    prepareAttachments: async (sessionId, items) => {
+      composerWrites.upload(sessionId, items);
+      return [{ name: 'requirements.txt', kind: 'other', mimeType: 'text/plain', bytes: 12, ref: { kind: 'session_file', sessionId: 'maka_workhub_coordination', relativePath: 'artifact-1' } }];
+    },
+    setModelConfiguration: async (sessionId, model) => {
+      composerWrites.model(sessionId, model);
+      setSession((current) => ({ ...current, ...model, thinkingLevel: undefined }));
+    },
+    setPermissionMode: async (sessionId, permissionMode) => {
+      composerWrites.permission(sessionId, permissionMode);
+      setSession((current) => ({ ...current, permissionMode }));
+    },
+  }}>
+    <div className="maka-detail-with-artifacts" style={{ height: '100dvh' }}><div className="mainColumn">
+      <WorkHubSurface controller={fixture} leaseScope="composer-story" locale={LOCALE} onOpenSession={() => {}} composerServices={{
+        sessions: [session], modelChoices: composerModels,
+        defaults: { model: { llmConnectionId: 'connection-test', llmConnectionSlug: 'test', model: 'model-a' }, permissionMode: 'ask' },
+        confirmBypass: async () => true, onOpenModelSettings: () => {},
+      }} />
+    </div></div>
+  </WorkHubComposerServicesProvider></ToastProvider>;
+}
+
+// Production Composer, with only native file picking and Host writes replaced.
+export const StandardComposer: Story = {
+  render: () => <ConfiguredComposerSurface />,
+  play: async ({ canvasElement }) => {
+    Object.values(composerWrites).forEach((spy) => spy.mockClear());
+    const canvas = within(canvasElement);
+    await waitFor(() => expect(canvas.getByRole('combobox', { name: '当前 Work' })).toBeEnabled());
+    await expect(canvas.getByText('模型与权限用于新 Work')).toBeVisible();
+    await expect(canvas.getByRole('button', { name: '添加上下文' })).toBeEnabled();
+    await userEvent.click(canvas.getByRole('combobox', { name: '当前 Work' }));
+    await userEvent.click(within(canvasElement.ownerDocument.body).getByRole('option', { name: SESSION_NAME }));
+    await expect(canvas.getByText('模型与权限用于此 Work')).toBeVisible();
+    const page = within(canvasElement.ownerDocument.body);
+    await userEvent.click(canvas.getByRole('button', { name: /切换当前任务模型/ }));
+    await userEvent.click(page.getByRole('menuitemradio', { name: 'model-b' }));
+    await waitFor(() => expect(composerWrites.model).toHaveBeenCalledWith(TARGET_SESSION_ID, expect.objectContaining({ model: 'model-b' })));
+    await userEvent.click(canvas.getByRole('button', { name: /权限模式/ }));
+    await userEvent.click(page.getByRole('menuitemradio', { name: '完全权限' }));
+    await waitFor(() => expect(composerWrites.permission).toHaveBeenCalledWith(TARGET_SESSION_ID, 'bypass'));
+    await userEvent.click(canvas.getByRole('combobox', { name: '当前 Work' }));
+    await userEvent.click(page.getByRole('option', { name: '自动识别工作' }));
+    await userEvent.click(canvas.getByRole('button', { name: /选择新任务模型/ }));
+    await userEvent.click(page.getByRole('menuitemradio', { name: 'model-b' }));
+    await userEvent.click(canvas.getByRole('button', { name: '添加上下文' }));
+    await userEvent.click(page.getByRole('menuitem', { name: /添加文件/ }));
+    await waitFor(() => expect(canvas.getByText('requirements.txt')).toBeVisible());
+    const editor = canvasElement.querySelector('[contenteditable="true"]') as HTMLElement;
+    await userEvent.click(editor);
+    await userEvent.type(editor, 'Review requirements');
+    await userEvent.keyboard('{Enter}');
+    await waitFor(() => expect(composerWrites.send).toHaveBeenCalledWith(expect.objectContaining({
+      newWorkDefaults: expect.objectContaining({ model: expect.objectContaining({ model: 'model-b' }), permissionMode: 'ask' }),
+      attachments: [expect.objectContaining({ name: 'requirements.txt' })],
+    })));
+    expect(composerWrites.send.mock.lastCall?.[0].explicitTarget).toBeUndefined();
+    expect(composerWrites.model).toHaveBeenCalledTimes(1);
+    expect(composerWrites.permission).toHaveBeenCalledTimes(1);
+    const composer = canvasElement.querySelector('.maka-composer-astryx') as HTMLElement;
+    await waitFor(() => expect(canvasElement.querySelectorAll('.maka-composer-attachment-token')).toHaveLength(0));
+    await userEvent.click(canvas.getByRole('button', { name: '添加上下文' }));
+    await userEvent.click(page.getByRole('menuitem', { name: /添加文件/ }));
+    await waitFor(() => expect(canvasElement.querySelectorAll('.maka-composer-attachment-token')).toHaveLength(1));
+    await userEvent.click(editor);
+    await userEvent.keyboard('{Enter}');
+    await waitFor(() => expect(composerWrites.send).toHaveBeenCalledTimes(2));
+    expect(composerWrites.send.mock.lastCall?.[0].text).toBe('请查看附件。');
+    await waitFor(() => expect(canvasElement.querySelectorAll('.maka-composer-attachment-token')).toHaveLength(0));
+    const scopeBox = canvasElement.querySelector('.workhub-composer-scope')!.getBoundingClientRect();
+    const plateBox = composer.firstElementChild!.getBoundingClientRect();
+    expect(Math.abs(scopeBox.left - plateBox.left)).toBeLessThanOrEqual(1);
+    expect(Math.abs(scopeBox.right - plateBox.right)).toBeLessThanOrEqual(1);
+    const conversationBox = canvasElement.querySelector('.workhub-conversation-shell')!.getBoundingClientRect();
+    expect(Math.abs(conversationBox.left - plateBox.left)).toBeLessThanOrEqual(1);
+    expect(Math.abs(conversationBox.right - plateBox.right)).toBeLessThanOrEqual(1);
+    canvasElement.dataset.workhubComposerVerified = 'true';
+
+  },
+};
+
+export const StandardComposerNarrow: Story = { ...StandardComposer, parameters: { viewport: { defaultViewport: 'tablet' } } };
+
+export const ComposerRetainsFailedAttachment: Story = {
+  render: () => <ConfiguredComposerSurface failFirst />,
+  play: async ({ canvasElement }) => {
+    Object.values(composerWrites).forEach((spy) => spy.mockClear());
+    const canvas = within(canvasElement);
+    const page = within(canvasElement.ownerDocument.body);
+    await waitFor(() => expect(canvas.getByRole('combobox', { name: '当前 Work' })).toBeEnabled());
+    await userEvent.click(canvas.getByRole('button', { name: '添加上下文' }));
+    await userEvent.click(page.getByRole('menuitem', { name: /添加文件/ }));
+    const editor = canvasElement.querySelector('[contenteditable="true"]') as HTMLElement;
+    await userEvent.click(editor);
+    await userEvent.type(editor, 'Review requirements');
+    await userEvent.keyboard('{Enter}');
+    await waitFor(() => expect(canvasElement.querySelector('.workhub-turn[data-state="failed"]')).not.toBeNull());
+    const composer = canvasElement.querySelector('.maka-composer-astryx') as HTMLElement;
+    expect(within(composer).getByText('requirements.txt')).toBeVisible();
+    await userEvent.keyboard('{Enter}');
+    await waitFor(() => expect(composerWrites.send).toHaveBeenCalledTimes(2));
+    await waitFor(() => expect(canvasElement.querySelectorAll('.maka-composer-attachment-token')).toHaveLength(0));
+    expect(composerWrites.upload).toHaveBeenCalledTimes(1);
+    expect(composerWrites.send.mock.calls[0]?.[0].requestId).toBe(composerWrites.send.mock.calls[1]?.[0].requestId);
+    canvasElement.dataset.workhubComposerVerified = 'true';
+  },
+};
diff --git a/docs/astryx-surface-file-inventory.md b/docs/astryx-surface-file-inventory.md
index 4d02b3de02..fe3841cf0f 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:** 256 files — blocker 0, reimplementation 0, polish 1, aligned 255.
+**Totals:** 259 files — blocker 0, reimplementation 0, polish 1, aligned 258.
 
 ## Exclusions (explicit)
 
@@ -97,8 +97,11 @@ Wiki bar: Design Conventions · API Use-the-System · Theming · Container Paddi
 | `apps/desktop/src/renderer/features/workbar/ui/workbar-host.tsx` | shell-chrome-or-panel | Card, ResizeHandle, Spinner | aligned — uses Astryx (Card, ResizeHandle, Spinner) | aligned |
 | `apps/desktop/src/renderer/features/workbar/ui/workbar-surface.tsx` | shell-chrome-or-panel | Badge, Card, DropdownMenu, DropdownMenuItem, Heading, Icon, Kbd, List, ListItem, Section, Spinner, Tab, TabList | aligned — uses Astryx (Badge, Card, DropdownMenu, DropdownMenuItem, Heading, Icon, Kbd, List) | aligned |
 | `apps/desktop/src/renderer/features/workbar/ui/workbar-toggle.tsx` | shell-chrome-or-panel | Icon, IconButton, Tooltip | aligned — uses Astryx (Icon, IconButton, Tooltip) | aligned |
+| `apps/desktop/src/renderer/features/workhub/services-context.tsx` | other | none | aligned — no raw controls; no Astryx JSX usage | aligned |
+| `apps/desktop/src/renderer/features/workhub/ui/workhub-composer.tsx` | shell-chrome-or-panel | Selector | aligned — uses Astryx (Selector) | aligned |
 | `apps/desktop/src/renderer/features/workhub/ui/workhub-navigation-rail.tsx` | other | Button, List, ListItem, StatusDot | aligned — uses Astryx (Button, List, ListItem, StatusDot) | aligned |
 | `apps/desktop/src/renderer/features/workhub/ui/workhub-prompt-rail.tsx` | other | none | aligned — no raw controls; no Astryx JSX usage | aligned |
+| `apps/desktop/src/renderer/features/workhub/ui/workhub-work-identity.tsx` | other | none | aligned — no raw controls; no Astryx JSX usage | aligned |
 | `apps/desktop/src/renderer/keyboard-help.tsx` | dialog-overlay | Dialog, DialogHeader, Heading, Kbd, Layout, LayoutContent | aligned — uses Astryx (Dialog, DialogHeader, Heading, Kbd, Layout, LayoutContent) | aligned |
 | `apps/desktop/src/renderer/live-turn-reconciler.tsx` | other | none | aligned — no raw controls; no Astryx JSX usage | aligned |
 | `apps/desktop/src/renderer/maka-tokens.css` | styles | n/a (css) | aligned — no off-rhythm control heights flagged | aligned |
diff --git a/docs/astryx-surface-file-inventory.paths b/docs/astryx-surface-file-inventory.paths
index fd44277143..da07d479ce 100644
--- a/docs/astryx-surface-file-inventory.paths
+++ b/docs/astryx-surface-file-inventory.paths
@@ -68,8 +68,11 @@ apps/desktop/src/renderer/features/workbar/ui/side-chat-close-confirmation.tsx
 apps/desktop/src/renderer/features/workbar/ui/workbar-host.tsx
 apps/desktop/src/renderer/features/workbar/ui/workbar-surface.tsx
 apps/desktop/src/renderer/features/workbar/ui/workbar-toggle.tsx
+apps/desktop/src/renderer/features/workhub/services-context.tsx
+apps/desktop/src/renderer/features/workhub/ui/workhub-composer.tsx
 apps/desktop/src/renderer/features/workhub/ui/workhub-navigation-rail.tsx
 apps/desktop/src/renderer/features/workhub/ui/workhub-prompt-rail.tsx
+apps/desktop/src/renderer/features/workhub/ui/workhub-work-identity.tsx
 apps/desktop/src/renderer/keyboard-help.tsx
 apps/desktop/src/renderer/live-turn-reconciler.tsx
 apps/desktop/src/renderer/maka-tokens.css
diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts
index b495caed0d..e06eddfe00 100644
--- a/packages/core/src/session.ts
+++ b/packages/core/src/session.ts
@@ -27,6 +27,7 @@ import {
   decodeMessageContent,
   TOOL_ACTIVITY_KINDS,
   type MessageContent,
+  type AttachmentRef,
   type ToolActivityKind,
   type ToolResultContent,
 } from './events.js';
@@ -930,9 +931,39 @@ export type WorkHubDelegationWorkspace =
   | { readonly kind: 'project'; readonly projectId: string }
   | { readonly kind: 'host_path'; readonly path: string };
 
+/** User-selected creation defaults; never applied to an existing Work. */
+export interface WorkHubCreateDefaults {
+  readonly model?: {
+    readonly llmConnectionId: string;
+    readonly llmConnectionSlug: string;
+    readonly model: string;
+  };
+  readonly permissionMode?: PermissionMode;
+}
+
+export function isWorkHubCreateDefaults(value: unknown): value is WorkHubCreateDefaults {
+  if (
+    !isRecord(value) ||
+    Object.keys(value).some((key) => key !== 'model' && key !== 'permissionMode')
+  )
+    return false;
+  if (value.permissionMode !== undefined && !isPermissionMode(value.permissionMode)) return false;
+  if (value.model === undefined) return true;
+  const model = value.model;
+  return (
+    isRecord(model) &&
+    Object.keys(model).length === 3 &&
+    ['llmConnectionId', 'llmConnectionSlug', 'model'].every(
+      (key) =>
+        typeof model[key] === 'string' && model[key].trim().length > 0 && model[key].length <= 512,
+    )
+  );
+}
+
 export interface WorkHubDelegationCreateSpec {
   readonly title: string;
   readonly workspace: WorkHubDelegationWorkspace;
+  readonly defaults?: WorkHubCreateDefaults;
 }
 
 interface WorkHubCoordinationMessageEnvelope {
@@ -951,6 +982,7 @@ interface WorkHubCoordinationMessageEnvelope {
   disposition: WorkHubDelegationDisposition;
   /** Exact target payload; retained so retry does not depend on renderer memory. */
   userText: string;
+  attachments?: AttachmentRef[];
   /** Present exactly for create_new. */
   create?: WorkHubDelegationCreateSpec;
 }
@@ -1292,7 +1324,7 @@ const WORKHUB_DELEGATION_ASSIGNED_MESSAGE_SHAPE =
       'targetMessageId',
       'targetSessionName',
     ],
-    ['create', 'steered', 'replacesActionId', 'replacesDelegationId'],
+    ['attachments', 'create', 'steered', 'replacesActionId', 'replacesDelegationId'],
   );
 const WORKHUB_DELEGATION_REPLACEMENT_REQUESTED_MESSAGE_SHAPE =
   defineObjectShape<WorkHubDelegationReplacementRequestedMessage>()(
@@ -1315,7 +1347,7 @@ const WORKHUB_DELEGATION_REPLACEMENT_REQUESTED_MESSAGE_SHAPE =
       'replacedTargetMessageId',
       'targetSessionName',
     ],
-    ['create'],
+    ['attachments', 'create'],
   );
 const WORKHUB_DELEGATION_SUPERSEDED_MESSAGE_SHAPE =
   defineObjectShape<WorkHubDelegationSupersededMessage>()(
@@ -1396,7 +1428,7 @@ const WORKHUB_DELEGATION_STOP_RESOLVED_MESSAGE_SHAPE =
   );
 const WORKHUB_DELEGATION_CREATE_SHAPE = defineObjectShape<WorkHubDelegationCreateSpec>()(
   ['title', 'workspace'],
-  [],
+  ['defaults'],
 );
 const WORKHUB_DELEGATION_PROJECT_WORKSPACE_SHAPE = defineObjectShape<
   Extract<WorkHubDelegationWorkspace, { kind: 'project' }>
@@ -1660,6 +1692,7 @@ function isWorkHubCoordinationMessage(message: Record<string, unknown>): boolean
     typeof message.targetSessionId === 'string' &&
     typeof message.userText === 'string' &&
     message.userText.trim().length > 0 &&
+    isWorkHubMessageAttachments(message.attachments) &&
     ((message.disposition === 'delegate_existing' && message.create === undefined) ||
       (message.disposition === 'create_new' && isWorkHubDelegationCreateSpec(message.create))) &&
     (message.disposition === 'delegate_existing' || message.disposition === 'create_new');
@@ -1712,10 +1745,21 @@ function isWorkHubActionIdentity(message: Record<string, unknown>): boolean {
   );
 }
 
+function isWorkHubMessageAttachments(value: unknown): boolean {
+  if (value === undefined) return true;
+  try {
+    decodeMessageContent({ text: '', attachments: value });
+    return true;
+  } catch {
+    return false;
+  }
+}
+
 function isWorkHubDelegationCreateSpec(value: unknown): value is WorkHubDelegationCreateSpec {
   if (
     !isRecord(value) ||
     !hasExactShape(value, WORKHUB_DELEGATION_CREATE_SHAPE) ||
+    (value.defaults !== undefined && !isWorkHubCreateDefaults(value.defaults)) ||
     typeof value.title !== 'string' ||
     value.title.trim().length === 0 ||
     !isRecord(value.workspace)
diff --git a/packages/runtime-host/src/__tests__/workhub-coordination-action-gate.test.ts b/packages/runtime-host/src/__tests__/workhub-coordination-action-gate.test.ts
index 22d7fd17b4..3d818742d2 100644
--- a/packages/runtime-host/src/__tests__/workhub-coordination-action-gate.test.ts
+++ b/packages/runtime-host/src/__tests__/workhub-coordination-action-gate.test.ts
@@ -1279,6 +1279,23 @@ describe('WorkHub Coordination Action Gate', () => {
     );
     const input = {
       actionId: 'create',
+      attachments: [
+        {
+          name: 'requirements.txt',
+          kind: 'other' as const,
+          mimeType: 'text/plain',
+          bytes: 12,
+          ref: {
+            kind: 'session_file' as const,
+            sessionId: 'maka_workhub_coordination',
+            relativePath: 'artifact-1',
+          },
+        },
+      ],
+      newWorkDefaults: {
+        model: { llmConnectionId: 'conn', llmConnectionSlug: 'test', model: 'chosen-model' },
+        permissionMode: 'ask' as const,
+      },
       userText: 'Create an accessibility audit',
       proposal: { disposition: 'create_new' as const, title: 'Accessibility audit' },
       create: { workspace: { kind: 'host_path' as const, path: '/workspace' } },
@@ -1291,15 +1308,24 @@ describe('WorkHub Coordination Action Gate', () => {
     assert.deepEqual(restartedReplay, first);
     assert.equal(effects.assignments.length, 2);
     assert.deepEqual(effects.assignments[0], effects.assignments[1]);
+    assert.deepEqual(effects.assignments[0]?.attachments, input.attachments);
     assert.match(effects.assignments[0]!.targetSessionId, /^whs_[a-f0-9]{48}$/u);
     assert.deepEqual(effects.assignments[0]!.create, {
       title: 'Accessibility audit',
       workspace: input.create.workspace,
+      defaults: input.newWorkDefaults,
     });
     await assert.rejects(
       gate.act({ ...input, proposal: { disposition: 'create_new', title: 'Different' } }, CONTEXT),
       (error) => error instanceof WorkHubActionGateFailure && error.code === 'action_conflict',
     );
+    await assert.rejects(
+      new WorkHubCoordinationActionGate(effects).act(
+        { ...input, newWorkDefaults: { ...input.newWorkDefaults, permissionMode: 'bypass' } },
+        CONTEXT,
+      ),
+      (error) => error instanceof WorkHubActionGateFailure && error.code === 'action_conflict',
+    );
     assert.equal(effects.assignments.length, 2);
   });
 
@@ -3032,6 +3058,7 @@ function assignmentRecord(
     delegationId: `delegation-${input.actionId}`,
     disposition: input.disposition,
     userText: input.userText,
+    ...(input.attachments ? { attachments: input.attachments } : {}),
     ...(input.create ? { create: input.create } : {}),
     ...(input.replacesActionId ? { replacesActionId: input.replacesActionId } : {}),
     ...(input.replacesDelegationId ? { replacesDelegationId: input.replacesDelegationId } : {}),
diff --git a/packages/runtime-host/src/__tests__/workhub-coordination-protocol.test.ts b/packages/runtime-host/src/__tests__/workhub-coordination-protocol.test.ts
index 04a51c1043..30c0f9b40e 100644
--- a/packages/runtime-host/src/__tests__/workhub-coordination-protocol.test.ts
+++ b/packages/runtime-host/src/__tests__/workhub-coordination-protocol.test.ts
@@ -507,3 +507,62 @@ test('WorkHub Coordination action results preserve the admitted disposition', ()
     );
   }
 });
+
+test('WorkHub decodes attachment context and user-selected creation defaults without strategy authority', () => {
+  const attachments = [
+    {
+      name: 'requirements.txt',
+      kind: 'other',
+      mimeType: 'text/plain',
+      bytes: 12,
+      ref: {
+        kind: 'session_file',
+        sessionId: 'maka_workhub_coordination',
+        relativePath: 'artifact-1',
+      },
+    },
+  ];
+  const input = {
+    actionId: 'composer-action',
+    userText: 'Create an audit',
+    proposal: { disposition: 'create_new', title: 'Audit' },
+    create: { workspace: { kind: 'host_path', path: '/workspace' } },
+    newWorkDefaults: {
+      model: {
+        llmConnectionId: 'connection-1',
+        llmConnectionSlug: 'primary',
+        model: 'chosen-model',
+      },
+      permissionMode: 'ask',
+    },
+    attachments,
+  };
+  assert.deepEqual(decodeWorkHubCoordinationActInput(input), input);
+  assert.deepEqual(
+    decodeWorkHubCoordinationAnswerInput({ turnId: 'answer-1', text: 'Review file', attachments })
+      .attachments,
+    attachments,
+  );
+  assert.throws(() =>
+    decodeWorkHubCoordinationActInput({
+      ...input,
+      newWorkDefaults: { permissionMode: 'invented' },
+    }),
+  );
+  assert.throws(() =>
+    decodeWorkHubCoordinationActInput({
+      ...input,
+      newWorkDefaults: { ...input.newWorkDefaults, workspace: '/forged' },
+    }),
+  );
+  assert.throws(() =>
+    decodeWorkHubCoordinationActInput({
+      ...input,
+      proposal: { disposition: 'answer_here' },
+      create: undefined,
+    }),
+  );
+  assert.throws(() =>
+    decodeWorkHubCoordinationActInput({ ...input, attachments: Array(9).fill(attachments[0]) }),
+  );
+});
diff --git a/packages/runtime-host/src/__tests__/workhub-message-attachments.test.ts b/packages/runtime-host/src/__tests__/workhub-message-attachments.test.ts
new file mode 100644
index 0000000000..6a094772cd
--- /dev/null
+++ b/packages/runtime-host/src/__tests__/workhub-message-attachments.test.ts
@@ -0,0 +1,101 @@
+/*
+ * 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 assert from 'node:assert/strict';
+import { mkdtemp, rm } from 'node:fs/promises';
+import { tmpdir } from 'node:os';
+import { join } from 'node:path';
+import { test } from 'node:test';
+import { openInteractiveArtifactStoreForWrite } from '@maka/storage/artifact-stores';
+import { resolveStorageRoot, tryAcquireInteractiveRootOwner } from '@maka/storage/root-authority';
+import { WORKHUB_COORDINATION_SESSION_ID } from '@maka/core/session';
+import type { AttachmentRef } from '@maka/core/events';
+import { HostArtifactCoordinator } from '../server/artifact-coordinator.js';
+import { SessionAdmissionGate } from '../server/session-admission-gate.js';
+import { copyWorkHubAttachmentsToTarget } from '../server/workhub-message-attachments.js';
+
+test('delegation copies only selected canonical attachments into the target Work', async () => {
+  const root = await mkdtemp(join(tmpdir(), 'workhub-attachments-'));
+  const capability = await resolveStorageRoot({ path: root, kind: 'interactive' });
+  const owner = await tryAcquireInteractiveRootOwner(capability);
+  assert.ok(owner);
+  try {
+    const store = await openInteractiveArtifactStoreForWrite(owner.lease);
+    try {
+      const selected = await store.create({
+        sessionId: WORKHUB_COORDINATION_SESSION_ID,
+        turnId: 'upload-1',
+        name: 'requirements.txt',
+        kind: 'file',
+        source: 'user_upload',
+        mimeType: 'text/plain',
+        content: 'requirements',
+      });
+      await store.create({
+        sessionId: WORKHUB_COORDINATION_SESSION_ID,
+        turnId: 'upload-2',
+        name: 'unrelated.txt',
+        kind: 'file',
+        source: 'user_upload',
+        mimeType: 'text/plain',
+        content: 'private draft',
+      });
+      const artifacts = new HostArtifactCoordinator(store, () => {}, new SessionAdmissionGate(), {
+        probeSessionRemoval: async () => ({ kind: 'present' }),
+      });
+      const source: AttachmentRef = {
+        name: selected.name,
+        kind: 'other',
+        mimeType: 'text/plain',
+        bytes: selected.sizeBytes,
+        ref: {
+          kind: 'session_file',
+          sessionId: WORKHUB_COORDINATION_SESSION_ID,
+          relativePath: selected.id,
+        },
+      };
+      const copied = await copyWorkHubAttachmentsToTarget(store, artifacts, 'target-work', [
+        source,
+      ]);
+      assert.equal(await artifacts.validateTurnAttachments('target-work', copied), undefined);
+      assert.equal(copied[0]!.ref.kind, 'session_file');
+      if (copied[0]!.ref.kind !== 'session_file') throw new Error('Expected canonical ref');
+      const binary = await store.readTextInSession('target-work', copied[0]!.ref.relativePath);
+      assert.deepEqual(binary, { ok: true, text: 'requirements' });
+      assert.notEqual(copied[0]!.ref.relativePath, selected.id);
+      await assert.rejects(
+        copyWorkHubAttachmentsToTarget(store, artifacts, 'target-work', [
+          { ...source, bytes: 999 },
+        ]),
+        /metadata/,
+      );
+      await assert.rejects(
+        copyWorkHubAttachmentsToTarget(store, artifacts, 'target-work', [
+          { ...source, ref: { ...source.ref, sessionId: 'foreign-work' } } as AttachmentRef,
+        ]),
+        /different Session/,
+      );
+    } finally {
+      store.close();
+    }
+  } finally {
+    await owner.close();
+    await rm(root, { recursive: true, force: true });
+  }
+});
diff --git a/packages/runtime-host/src/protocol/index.ts b/packages/runtime-host/src/protocol/index.ts
index ac0d00407f..3078a049b2 100644
--- a/packages/runtime-host/src/protocol/index.ts
+++ b/packages/runtime-host/src/protocol/index.ts
@@ -101,8 +101,10 @@ export const RUNTIME_HOST_REGISTRATION_SCHEMA_VERSION = 1 as const;
 export const RUNTIME_HOST_PROTOCOL_VERSION = 0 as const;
 // Increment when the same protocol version no longer guarantees safe Client-Host
 // interoperability. Mismatches are rejected before domain commands are admitted.
+export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 133 as const;
+// 133: WorkHub actions carry attachments and new-Work model/permission defaults.
+// Epoch-132 peers reject these additional fields on strict action shapes.
 // 132: new Tool Result archives use versioned ledger references, not Artifact payloads.
-export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 132 as const;
 // 131: Logical model steps bind durable Request Composition identities.
 // 130: Turn contributions carry the optional bounded `failureMessage` diagnostic.
 // Epoch-129 peers reject this added field on the strict contribution shape.
diff --git a/packages/runtime-host/src/protocol/workhub-coordination.ts b/packages/runtime-host/src/protocol/workhub-coordination.ts
index ebbd1092ef..fb9b6a3e5a 100644
--- a/packages/runtime-host/src/protocol/workhub-coordination.ts
+++ b/packages/runtime-host/src/protocol/workhub-coordination.ts
@@ -17,6 +17,9 @@
  * under the License.
  */
 
+import type { AttachmentRef } from '@maka/core/events';
+import { decodeMessageContent } from './turn.js';
+import { isWorkHubCreateDefaults, type WorkHubCreateDefaults } from '@maka/core/session';
 import {
   requireCount,
   requireEntityId,
@@ -80,6 +83,7 @@ export interface WorkHubCoordinationResolveResult {
 export interface WorkHubCoordinationAnswerInput {
   readonly turnId: string;
   readonly text: string;
+  readonly attachments?: AttachmentRef[];
 }
 
 export interface WorkHubCoordinationRecordInput {
@@ -177,6 +181,8 @@ export interface WorkHubCoordinationCreateContext {
 export interface WorkHubCoordinationActInput {
   readonly actionId: string;
   readonly userText: string;
+  readonly newWorkDefaults?: WorkHubCreateDefaults;
+  readonly attachments?: AttachmentRef[];
   readonly proposal: WorkHubCoordinationProposal;
   readonly candidateSetId?: string;
   readonly create?: WorkHubCoordinationCreateContext;
@@ -295,8 +301,19 @@ export function decodeWorkHubCoordinationResolveResult(
 export function decodeWorkHubCoordinationAnswerInput(
   value: unknown,
 ): WorkHubCoordinationAnswerInput {
-  const input = requireExactRecord(value, 'WorkHub Coordination answer input', ['turnId', 'text']);
+  const input = requireShapedRecord(
+    value,
+    'WorkHub Coordination answer input',
+    ['turnId', 'text'],
+    ['attachments'],
+  );
   return {
+    ...(input.attachments !== undefined
+      ? {
+          attachments: decodeMessageContent({ text: input.text, attachments: input.attachments })
+            .attachments!,
+        }
+      : {}),
     turnId: requireEntityId(input.turnId, 'WorkHub Coordination Turn id'),
     text: requireUtf8String(
       input.text,
@@ -367,9 +384,25 @@ export function decodeWorkHubCoordinationActInput(value: unknown): WorkHubCoordi
     value,
     'WorkHub Coordination action input',
     ['actionId', 'userText', 'proposal'],
-    ['candidateSetId', 'create', 'confirmation'],
+    ['candidateSetId', 'create', 'confirmation', 'newWorkDefaults', 'attachments'],
   );
   const proposal = decodeWorkHubCoordinationProposal(input.proposal);
+  if (
+    input.newWorkDefaults !== undefined &&
+    (!isWorkHubCreateDefaults(input.newWorkDefaults) ||
+      !(
+        proposal.disposition === 'create_new' ||
+        (proposal.disposition === 'replace' && proposal.target.disposition === 'create_new')
+      ))
+  ) {
+    throw invalidProtocolFrame('Invalid WorkHub creation defaults');
+  }
+  if (
+    input.attachments !== undefined &&
+    !['answer_here', 'delegate_existing', 'create_new', 'replace'].includes(proposal.disposition)
+  ) {
+    throw invalidProtocolFrame('This WorkHub action does not accept attachments');
+  }
   const base = {
     actionId: requireEntityId(input.actionId, 'WorkHub Coordination action id'),
     userText: requireUtf8String(
@@ -378,6 +411,17 @@ export function decodeWorkHubCoordinationActInput(value: unknown): WorkHubCoordi
       WORKHUB_COORDINATION_TEXT_MAX_BYTES,
     ),
     proposal,
+    ...(input.attachments !== undefined
+      ? {
+          attachments: decodeMessageContent({
+            text: input.userText,
+            attachments: input.attachments,
+          }).attachments!,
+        }
+      : {}),
+    ...(input.newWorkDefaults !== undefined
+      ? { newWorkDefaults: input.newWorkDefaults as WorkHubCreateDefaults }
+      : {}),
   };
   if (proposal.disposition === 'delegate_existing') {
     if (
diff --git a/packages/runtime-host/src/server/execution-composition.ts b/packages/runtime-host/src/server/execution-composition.ts
index 9caabd01ad..f098c3ab81 100644
--- a/packages/runtime-host/src/server/execution-composition.ts
+++ b/packages/runtime-host/src/server/execution-composition.ts
@@ -17,6 +17,7 @@
  * under the License.
  */
 
+import { copyWorkHubAttachmentsToTarget } from './workhub-message-attachments.js';
 import { createHash, randomUUID } from 'node:crypto';
 import { MAX_READ_IMAGE_BYTES } from '@maka/core/attachments';
 import type { ContextOffloadLimits } from '@maka/core/context-offload';
@@ -1560,7 +1561,17 @@ export async function createExecutionRuntimeHostComposition(
                   sessionId: input.targetSessionId,
                   workspace: input.create.workspace,
                   name: input.create.title,
-                  modelTarget: { kind: 'default' },
+                  modelTarget: input.create.defaults?.model
+                    ? {
+                        kind: 'explicit',
+                        connectionId: input.create.defaults.model.llmConnectionId,
+                        connectionSlug: input.create.defaults.model.llmConnectionSlug,
+                        model: input.create.defaults.model.model,
+                      }
+                    : { kind: 'default' },
+                  ...(input.create.defaults?.permissionMode
+                    ? { permissionMode: input.create.defaults.permissionMode }
+                    : {}),
                   collaborationMode: 'agent',
                   orchestrationMode: 'default',
                 })
@@ -1570,7 +1581,19 @@ export async function createExecutionRuntimeHostComposition(
             .digest('hex')
             .slice(0, 48);
           const messageId = `whm_${suffix}`;
-          const content = normalizeMessageContent({ text: input.userText });
+          const targetAttachments =
+            !durable && input.attachments?.length
+              ? await copyWorkHubAttachmentsToTarget(
+                  openedArtifactStore,
+                  artifacts,
+                  input.targetSessionId,
+                  input.attachments,
+                )
+              : input.attachments;
+          const content = normalizeMessageContent({
+            text: input.userText,
+            ...(targetAttachments ? { attachments: targetAttachments } : {}),
+          });
           const persisted =
             durable ??
             (await sessionAdmission.runMany(
@@ -1628,6 +1651,7 @@ export async function createExecutionRuntimeHostComposition(
                     delegationId,
                     disposition: input.disposition,
                     userText: input.userText,
+                    ...(input.attachments ? { attachments: input.attachments } : {}),
                     ...(steered ? { steered: true as const } : {}),
                     ...(input.create ? { create: input.create } : {}),
                     ...(input.replacesActionId && input.replacesDelegationId
diff --git a/packages/runtime-host/src/server/workhub-coordination-action-gate.ts b/packages/runtime-host/src/server/workhub-coordination-action-gate.ts
index 8d701e46d9..b9c9e88019 100644
--- a/packages/runtime-host/src/server/workhub-coordination-action-gate.ts
+++ b/packages/runtime-host/src/server/workhub-coordination-action-gate.ts
@@ -17,6 +17,7 @@
  * under the License.
  */
 
+import type { AttachmentRef } from '@maka/core/events';
 import { createHash } from 'node:crypto';
 import type {
   SessionHeader,
@@ -116,7 +117,11 @@ export interface WorkHubActionGateEffects {
     delegationId: string,
   ): Promise<WorkHubDelegationStopResolvedMessage | undefined>;
   answer(
-    input: { readonly turnId: string; readonly text: string },
+    input: {
+      readonly turnId: string;
+      readonly text: string;
+      readonly attachments?: AttachmentRef[];
+    },
     context: ConnectionContext,
   ): Promise<void>;
   clarify(input: {
@@ -181,6 +186,7 @@ export interface WorkHubDelegationAssignmentInput {
   readonly targetSessionName: string;
   readonly disposition: WorkHubDelegationDisposition;
   readonly userText: string;
+  readonly attachments?: AttachmentRef[];
   readonly create?: WorkHubDelegationCreateSpec;
   readonly replacesActionId?: string;
   readonly replacesDelegationId?: string;
@@ -360,7 +366,14 @@ export class WorkHubCoordinationActionGate {
     if (proposal.disposition === 'answer_here') {
       const turnId = coordinationTurnId(input.actionId, 'answer');
       await this.#claimAction(input.actionId, 'answer_here', fingerprint, turnId);
-      await this.#effects.answer({ turnId, text: input.userText }, context);
+      await this.#effects.answer(
+        {
+          turnId,
+          text: input.userText,
+          ...(input.attachments ? { attachments: input.attachments } : {}),
+        },
+        context,
+      );
       return { disposition: 'answer_here', coordinationTurnId: turnId };
     }
     if (proposal.disposition === 'clarify') {
@@ -762,7 +775,12 @@ export class WorkHubCoordinationActionGate {
         targetSessionName: target.title,
         disposition: 'create_new',
         userText: input.userText,
-        create: { title: target.title, workspace: input.create.workspace },
+        ...(input.attachments ? { attachments: input.attachments } : {}),
+        create: {
+          title: target.title,
+          workspace: input.create.workspace,
+          ...(input.newWorkDefaults ? { defaults: input.newWorkDefaults } : {}),
+        },
         replacesActionId: replaced.actionId,
         replacesDelegationId: replaced.delegationId,
         replacedTargetSessionId: replaced.targetSessionId,
@@ -807,6 +825,7 @@ export class WorkHubCoordinationActionGate {
       targetSessionName: destination.sessionName,
       disposition: 'delegate_existing',
       userText: input.userText,
+      ...(input.attachments ? { attachments: input.attachments } : {}),
       replacesActionId: replaced.actionId,
       replacesDelegationId: replaced.delegationId,
       replacedTargetSessionId: replaced.targetSessionId,
@@ -1085,6 +1104,7 @@ function delegationAssignment(
     targetSessionName,
     disposition: input.proposal.disposition,
     userText: input.userText,
+    ...(input.attachments ? { attachments: input.attachments } : {}),
   } as const;
   if (input.proposal.disposition === 'delegate_existing') return base;
   if (!create) {
@@ -1098,6 +1118,7 @@ function delegationAssignment(
     create: {
       title: input.proposal.title,
       workspace: create.workspace,
+      ...(input.newWorkDefaults ? { defaults: input.newWorkDefaults } : {}),
     },
   };
 }
@@ -1137,6 +1158,8 @@ function digest(value: unknown): `sha256:${string}` {
 function actionFingerprint(input: WorkHubCoordinationActInput): `sha256:${string}` {
   return digest({
     userText: input.userText,
+    ...(input.attachments ? { attachments: input.attachments } : {}),
+    ...(input.newWorkDefaults ? { newWorkDefaults: input.newWorkDefaults } : {}),
     disposition: input.proposal.disposition,
     ...(input.proposal.disposition === 'delegate_existing'
       ? { candidateRef: input.proposal.candidateRef }
@@ -1171,6 +1194,8 @@ function replacementActionFingerprint(
   }
   return digest({
     userText: input.userText,
+    ...(input.attachments ? { attachments: input.attachments } : {}),
+    ...(input.newWorkDefaults ? { newWorkDefaults: input.newWorkDefaults } : {}),
     disposition: input.proposal.disposition,
     replacesActionId: input.proposal.replacesActionId,
     target: {
@@ -1261,6 +1286,7 @@ function assignmentInputFromRecord(
     targetSessionName: assignment.targetSessionName,
     disposition: assignment.disposition,
     userText: assignment.userText,
+    ...(assignment.attachments ? { attachments: assignment.attachments } : {}),
     ...(assignment.create ? { create: assignment.create } : {}),
     ...(assignment.replacesActionId && assignment.replacesDelegationId
       ? {
@@ -1281,6 +1307,7 @@ function assignmentInputFromReplacement(
     targetSessionName: replacement.targetSessionName,
     disposition: replacement.disposition,
     userText: replacement.userText,
+    ...(replacement.attachments ? { attachments: replacement.attachments } : {}),
     ...(replacement.create ? { create: replacement.create } : {}),
     replacesActionId: replacement.replacesActionId,
     replacesDelegationId: replacement.replacesDelegationId,
diff --git a/packages/runtime-host/src/server/workhub-coordination-coordinator.ts b/packages/runtime-host/src/server/workhub-coordination-coordinator.ts
index 6c953ed7e3..1084180500 100644
--- a/packages/runtime-host/src/server/workhub-coordination-coordinator.ts
+++ b/packages/runtime-host/src/server/workhub-coordination-coordinator.ts
@@ -197,7 +197,7 @@ export class HostWorkHubCoordinationCoordinator {
       readStopRequest: (delegationId) => this.#stores.readWorkHubStopRequest(delegationId),
       readStopResolution: (delegationId) => this.#stores.readWorkHubStopResolution(delegationId),
       answer: async (input, context) => {
-        const outcome = await this.#answer({ turnId: input.turnId, text: input.text }, context);
+        const outcome = await this.#answer(input, context);
         if (!outcome.ok) {
           throw new WorkHubActionEffectFailure(outcome.error.code, outcome.error.message);
         }
@@ -248,6 +248,7 @@ export class HostWorkHubCoordinationCoordinator {
         targetSessionName: input.targetSessionName,
         disposition: input.disposition,
         userText: input.userText,
+        ...(input.attachments ? { attachments: input.attachments } : {}),
         replacesActionId: input.replacesActionId,
         replacesDelegationId: input.replacesDelegationId,
         replacedTargetSessionId: input.replacedTargetSessionId,
@@ -642,7 +643,10 @@ export class HostWorkHubCoordinationCoordinator {
         turnId: input.turnId,
         execution: {
           kind: 'workhub_coordination',
-          inputDigest: digest({ text: input.text }),
+          inputDigest: digest({
+            text: input.text,
+            ...(input.attachments ? { attachments: input.attachments } : {}),
+          }),
         },
         archivedMessage: 'WorkHub Coordination Session is unavailable',
         // A recorded summary owns its Turn identity durably but is admitted
@@ -662,7 +666,13 @@ export class HostWorkHubCoordinationCoordinator {
           }
           return recorded.length > 0
             ? { kind: 'rejected', outcome: turnIdentityConflict() }
-            : { kind: 'ready', content: normalizeMessageContent({ text: input.text }) };
+            : {
+                kind: 'ready',
+                content: normalizeMessageContent({
+                  text: input.text,
+                  ...(input.attachments ? { attachments: input.attachments } : {}),
+                }),
+              };
         },
       },
       context,
diff --git a/packages/runtime-host/src/server/workhub-message-attachments.ts b/packages/runtime-host/src/server/workhub-message-attachments.ts
new file mode 100644
index 0000000000..5972453a79
--- /dev/null
+++ b/packages/runtime-host/src/server/workhub-message-attachments.ts
@@ -0,0 +1,60 @@
+/*
+ * 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 type { AttachmentRef } from '@maka/core/events';
+import { WORKHUB_COORDINATION_SESSION_ID } from '@maka/core/session';
+import type { InteractiveArtifactStoreWriter } from '@maka/storage/artifact-stores';
+import type { HostArtifactCoordinator } from './artifact-coordinator.js';
+import { WorkHubActionEffectFailure } from './workhub-coordination-action-gate.js';
+
+/** Preserve canonical ownership when a Coordination message is delegated. */
+export async function copyWorkHubAttachmentsToTarget(
+  store: Pick<InteractiveArtifactStoreWriter, 'copyConversationArtifacts'>,
+  artifacts: Pick<HostArtifactCoordinator, 'validateTurnAttachments'>,
+  targetSessionId: string,
+  attachments: readonly AttachmentRef[],
+): Promise<AttachmentRef[]> {
+  const invalid = await artifacts.validateTurnAttachments(
+    WORKHUB_COORDINATION_SESSION_ID,
+    attachments,
+  );
+  if (invalid) throw new WorkHubActionEffectFailure('operation_conflict', invalid);
+  const ids = attachments.map((attachment) => {
+    if (attachment.ref.kind !== 'session_file') throw new Error('Invalid WorkHub attachment');
+    return attachment.ref.relativePath;
+  });
+  const copied = await store.copyConversationArtifacts({
+    sourceSessionId: WORKHUB_COORDINATION_SESSION_ID,
+    targetSessionId,
+    turnIds: [],
+    includeArtifactIds: ids,
+  });
+  return attachments.map((attachment, index) => {
+    const relativePath = copied.artifactIds.get(ids[index]!);
+    if (!relativePath)
+      throw new WorkHubActionEffectFailure(
+        'operation_conflict',
+        'WorkHub attachment was not copied',
+      );
+    return {
+      ...attachment,
+      ref: { kind: 'session_file', sessionId: targetSessionId, relativePath },
+    };
+  });
+}
diff --git a/packages/ui/package.json b/packages/ui/package.json
index d9f5513126..22e7137cda 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -12,7 +12,10 @@
     "./assistant-stream": "./dist/assistant-stream.js",
     "./icons": "./dist/icons.js",
     "./maka-uri": "./dist/maka-uri.js",
-    "./styles.css": "./src/styles.css"
+    "./styles.css": "./src/styles.css",
+    "./composer-attachments": "./dist/composer-attachments.js",
+    "./pending-items": "./dist/pending-items.js",
+    "./use-composer-attachments": "./dist/use-composer-attachments.js"
   },
   "scripts": {
     "clean": "node ../../scripts/clean-paths.mjs dist tsconfig.tsbuildinfo",
diff --git a/apps/desktop/src/renderer/composer-attachments.ts b/packages/ui/src/composer-attachments.ts
similarity index 100%
copy from apps/desktop/src/renderer/composer-attachments.ts
copy to packages/ui/src/composer-attachments.ts
diff --git a/packages/ui/src/composer.tsx b/packages/ui/src/composer.tsx
index de7c8407e8..2692348122 100644
--- a/packages/ui/src/composer.tsx
+++ b/packages/ui/src/composer.tsx
@@ -314,6 +314,8 @@ export const Composer = forwardRef<
     pendingDirectories?: readonly import('@maka/core/events').DirectoryReference[];
     onRemoveDirectory?(index: number): void;
     onAttachFilePaths?(files: File[]): void | Promise<void>;
+    /** Hosts that can submit context without a text prompt opt in. */
+    allowAttachmentOnlySend?: boolean;
     pendingAttachments?: readonly {
       displayName: string;
       kind: AttachmentRef['kind'];
@@ -1265,7 +1267,7 @@ export const Composer = forwardRef<
     // `text`. The optional metadata below is a send-time rendering snapshot of
     // file chips that still exist in the editor, not a second draft state.
     const text = composerWireText(textPort.getValue());
-    if (!text) return;
+    if (!text && !(props.allowAttachmentOnlySend && props.pendingAttachments?.length)) return;
     const editable = editableNode();
     const workspaceFileReferences = editable ? workspaceFileReferencePositions(editable) : [];
     const submittedDraftKey = activeDraftKey();
@@ -1453,7 +1455,7 @@ export const Composer = forwardRef<
     props.sendBlocked ||
     sendPending ||
     importActionBusy ||
-    !text.trim() ||
+    (!text.trim() && !(props.allowAttachmentOnlySend && props.pendingAttachments?.length)) ||
     noModelConnection;
   // The disabled Send is explanatory only in the no-model dead-end; other
   // disabled reasons (empty draft, in-flight import) keep the neutral label.
diff --git a/apps/desktop/src/renderer/pending-items.ts b/packages/ui/src/pending-items.ts
similarity index 100%
copy from apps/desktop/src/renderer/pending-items.ts
copy to packages/ui/src/pending-items.ts
diff --git a/packages/ui/src/prompt-anchor-rail.tsx b/packages/ui/src/prompt-anchor-rail.tsx
index 29e80605cc..b397ad069c 100644
--- a/packages/ui/src/prompt-anchor-rail.tsx
+++ b/packages/ui/src/prompt-anchor-rail.tsx
@@ -212,6 +212,9 @@ export function observeActivePromptRailVisibility(
 }
 
 export interface PromptAnchorRailTurn {
+  /** Optional host identity color; ordinary Session ticks remain neutral. */
+  accentColor?: string;
+  highlighted?: boolean;
   turnId: string;
   label: string;
   reply?: string;
@@ -240,6 +243,8 @@ export function mergePromptAnchorRailTurns(
 }
 
 export interface PromptAnchorRailProps {
+  /** Presentation-only hover/focus linkage; never navigates the transcript. */
+  onHighlightTurn?: (turn: PromptAnchorRailTurn | undefined) => void;
   turns: readonly PromptAnchorRailTurn[];
   scrollRef: RefObject<HTMLElement | null>;
   /** When the indexed Turn is outside the Host's active transcript range. */
@@ -376,7 +381,7 @@ export function selectPromptRailTickForMountedTurn(input: {
 }
 
 /** Right-edge rail: bounded prompt landmarks that scroll to `[data-turn-id]`. */
-export const PromptAnchorRail = memo(function PromptAnchorRail({ turns, scrollRef, onNavigateFallback, onNavigateStart }: PromptAnchorRailProps): React.ReactElement | null {
+export const PromptAnchorRail = memo(function PromptAnchorRail({ turns, scrollRef, onNavigateFallback, onNavigateStart, onHighlightTurn }: PromptAnchorRailProps): React.ReactElement | null {
   const copy = getConversationCopy(useUiLocale()).sessions;
   const [activeSelection, setActiveSelection] = useState<{
     turnId: string;
@@ -743,7 +748,7 @@ export const PromptAnchorRail = memo(function PromptAnchorRail({ turns, scrollRe
         className="maka-prompt-rail"
         aria-label={copy.promptRailAriaLabel}
         ref={railRef}
-        onPointerLeave={() => setHoveredIndex(null)}
+        onPointerLeave={() => { setHoveredIndex(null); onHighlightTurn?.(undefined); }}
       >
         {railTurns.map((turn, index) => {
           const isActive = turn.turnId === activeRailTurnId;
@@ -775,12 +780,16 @@ export const PromptAnchorRail = memo(function PromptAnchorRail({ turns, scrollRe
                 label={copy.jumpToPrompt(preview)}
                 className="maka-prompt-rail-tick"
                 data-prompt-turn-id={turn.turnId}
+                data-highlighted={turn.highlighted || undefined}
                 data-active={isActive ? 'true' : undefined}
                 aria-current={isActive ? 'true' : undefined}
                 onClick={() => jumpTo(turn)}
-                onPointerEnter={() => setHoveredIndex(index)}
+                onPointerEnter={() => { setHoveredIndex(index); onHighlightTurn?.(turn); }}
+                onFocus={() => onHighlightTurn?.(turn)}
+                onBlur={() => onHighlightTurn?.(undefined)}
                 style={
                   {
+                    color: turn.accentColor,
                     '--maka-prompt-rail-index': index,
                     '--maka-prompt-rail-scale': scale,
                   } as CSSProperties
diff --git a/apps/desktop/src/renderer/use-composer-attachments.ts b/packages/ui/src/use-composer-attachments.ts
similarity index 97%
rename from apps/desktop/src/renderer/use-composer-attachments.ts
rename to packages/ui/src/use-composer-attachments.ts
index 4f762d8847..e5110fb283 100644
--- a/apps/desktop/src/renderer/use-composer-attachments.ts
+++ b/packages/ui/src/use-composer-attachments.ts
@@ -29,13 +29,10 @@ import {
   type AttachmentRef,
   type DirectoryReference,
 } from '@maka/core/events';
-import { useUiLocale } from '@maka/ui';
 import {
   pendingAttachmentSourceKey,
   type PendingAttachment,
 } from './composer-attachments.js';
-import { getDesktopConversationCopy } from './locales/conversation-copy.js';
-import { localizedShellErrorMessage } from './locales/shell-copy.js';
 import {
   appendPending,
   removePending,
@@ -174,7 +171,16 @@ function releasePreviewUrl(url: string | undefined): void {
   if (url?.startsWith('blob:')) URL.revokeObjectURL(url);
 }
 
+export interface ComposerAttachmentCopy {
+  attachmentFailedTitle: string;
+  tryAgain: string;
+  imageAttachmentNotDirectTitle: string;
+  imageAttachmentNotDirectDescription: string;
+}
+
 export function useComposerAttachments(options: {
+  copy: ComposerAttachmentCopy;
+  formatError(error: unknown, fallback: string): string;
   draftKey: string;
   directoryHostId?: string;
   toastApi: ToastApi;
@@ -187,8 +193,7 @@ export function useComposerAttachments(options: {
       }
     | undefined;
 }) {
-  const uiLocale = useUiLocale();
-  const copy = getDesktopConversationCopy(uiLocale).actions;
+  const copy = options.copy;
   const [pendingState, setPendingState] = useState<ComposerPendingState>({
     attachments: {},
     directories: {},
@@ -332,7 +337,7 @@ export function useComposerAttachments(options: {
     } catch (error) {
       options.toastApi.error(
         copy.attachmentFailedTitle,
-        localizedShellErrorMessage(error, copy.tryAgain, uiLocale),
+        options.formatError(error, copy.tryAgain),
       );
     }
   }
@@ -365,7 +370,7 @@ export function useComposerAttachments(options: {
     } catch (error) {
       owner.toastApi.error(
         copy.attachmentFailedTitle,
-        localizedShellErrorMessage(error, copy.tryAgain, uiLocale),
+        options.formatError(error, copy.tryAgain),
       );
     }
   }


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org