You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by zh...@apache.org on 2026/09/18 07:50:22 UTC

(maka) 01/05: fix(desktop): make missing WorkHub model setup actionable

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

me2seeks pushed a commit to branch fix/workhub-model-setup
in repository https://gitbox.apache.org/repos/asf/maka.git

commit 26a47eea96ab2ba8f7adbb74bef926247ef51d61
Author: me2seeks <me...@gmail.com>
AuthorDate: Fri Sep 18 15:07:44 2026 +0800

    fix(desktop): make missing WorkHub model setup actionable
    
    Keep Runtime Host fail-closed when WorkHub has no available default model, but carry the expected setup state across Desktop IPC and guide the user to Settings > Models.
    
    Generated-by: OpenAI Codex
---
 .../runtime-host-workhub-ipc-main.test.ts          | 28 ++++++++++++++++++++++
 .../workhub-coordination-session-preload.test.ts   | 12 ++++++++++
 .../main/__tests__/workhub-send-visibility.test.ts | 14 +++++++++--
 .../src/main/runtime-host-workhub-ipc-main.ts      | 20 +++++++++++++---
 apps/desktop/src/main/workhub-presentation.ts      |  5 ++++
 apps/desktop/src/preload/bridge-contract.d.ts      |  2 +-
 apps/desktop/src/preload/preload.ts                |  2 +-
 .../src/preload/workhub-coordination-session.ts    |  6 +++--
 apps/desktop/src/preload/workhub-presentation.ts   |  1 +
 apps/desktop/src/renderer/app-shell.tsx            |  3 ++-
 .../workhub-workspace/coordination-lifecycle.ts    |  7 ++++++
 .../workhub/controller/use-workhub-controller.ts   | 17 +++++++++++--
 .../desktop/src/renderer/features/workhub/index.ts |  6 ++++-
 .../features/workhub/locales/workhub-live-copy.ts  |  6 ++---
 .../features/workhub/ui/main-navigation.tsx        |  3 ++-
 .../renderer/features/workhub/ui/workhub-root.tsx  |  9 ++++++-
 .../platform/desktop/create-workhub-services.ts    |  7 +++++-
 apps/desktop/src/renderer/styles/workhub.css       |  2 ++
 apps/desktop/src/shared/workhub-conversation.d.ts  |  4 ++++
 apps/desktop/src/shared/workhub-presentation.d.ts  |  4 +++-
 apps/desktop/stories/workhub.stories.tsx           |  2 +-
 .../src/protocol/workhub-coordination.ts           |  2 ++
 .../src/server/workhub-coordination-coordinator.ts |  7 ++++--
 23 files changed, 146 insertions(+), 23 deletions(-)

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 f13a98c8c1..7da3a5fea0 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
@@ -19,6 +19,8 @@
 
 import assert from 'node:assert/strict';
 import test from 'node:test';
+import { RuntimeHostOperationError } from '@maka/runtime-host/client';
+import { WORKHUB_COORDINATION_DEFAULT_MODEL_REQUIRED_MESSAGE } from '@maka/runtime-host/protocol';
 import type { IpcHandler } from '../ipc-reconnect-policy.js';
 import { registerRuntimeHostWorkHubIpc } from '../runtime-host-workhub-ipc-main.js';
 
@@ -47,3 +49,29 @@ test('returns a structured WorkHub attachment rejection across IPC', async () =>
   );
   assert.deepEqual(result, { ok: false, code: 'count_limit' });
 });
+
+test('returns a structured model setup state across IPC', async () => {
+  const handlers = new Map<string, IpcHandler>();
+  registerRuntimeHostWorkHubIpc(
+    {
+      resolveWorkHubCoordinationSession: async () => {
+        throw new RuntimeHostOperationError(
+          'workhub.coordination.resolve',
+          'operation_conflict',
+          WORKHUB_COORDINATION_DEFAULT_MODEL_REQUIRED_MESSAGE,
+        );
+      },
+    } as unknown as Parameters<typeof registerRuntimeHostWorkHubIpc>[0],
+    {
+      handle(channel, handler) {
+        handlers.set(channel, handler);
+      },
+    },
+    {},
+  );
+
+  const resolve = handlers.get('workhub:resolveCoordinationSession');
+  assert.ok(resolve);
+  const result = await resolve({ sender: { id: 7 } } as Parameters<IpcHandler>[0]);
+  assert.deepEqual(result, { kind: 'model_required' });
+});
diff --git a/apps/desktop/src/main/__tests__/workhub-coordination-session-preload.test.ts b/apps/desktop/src/main/__tests__/workhub-coordination-session-preload.test.ts
index 4152afcbd8..f60ed5c0e1 100644
--- a/apps/desktop/src/main/__tests__/workhub-coordination-session-preload.test.ts
+++ b/apps/desktop/src/main/__tests__/workhub-coordination-session-preload.test.ts
@@ -46,6 +46,9 @@ test('resolves the Coordination Session through the currently active Runtime Hos
   const second = await resolve();
 
   assert.deepEqual(seen, scopes);
+  assert.equal(typeof first, 'string');
+  assert.equal(typeof second, 'string');
+  if (typeof first !== 'string' || typeof second !== 'string') throw new Error('expected session keys');
   assert.deepEqual(parseDesktopSessionKey(first), {
     hostId: 'host-a',
     sessionId: 'maka_workhub_coordination',
@@ -56,6 +59,15 @@ test('resolves the Coordination Session through the currently active Runtime Hos
   });
 });
 
+test('keeps model setup as a structured WorkHub resolution state', async () => {
+  const result = await resolveDesktopWorkHubCoordinationSession(
+    async () => ({ hostId: 'host-a', targetEpoch: 'epoch-a' }),
+    async () => ({ kind: 'model_required' }),
+  );
+
+  assert.deepEqual(result, { kind: 'model_required' });
+});
+
 test('creates against the Coordination Session Host instead of later UI focus', async () => {
   const coordination = JSON.stringify(['host-a', 'maka_workhub_coordination']);
   const scopeA = { hostId: 'host-a', targetEpoch: 'epoch-a' };
diff --git a/apps/desktop/src/main/__tests__/workhub-send-visibility.test.ts b/apps/desktop/src/main/__tests__/workhub-send-visibility.test.ts
index 4e0beb845a..dd97be3aca 100644
--- a/apps/desktop/src/main/__tests__/workhub-send-visibility.test.ts
+++ b/apps/desktop/src/main/__tests__/workhub-send-visibility.test.ts
@@ -29,7 +29,7 @@ import type { IpcHandler } from '../ipc-reconnect-policy.js';
 import type { DesktopSessionStopResult } from '../../preload/bridge-contract.js';
 import type { AttachmentRef } from '@maka/core/events';
 import type { StoredMessage } from '@maka/core/session';
-import { WorkHubServicesProvider, type WorkHubServices, type WorkHubTranscriptSnapshot } from '../../renderer/features/workhub/index.js';
+import { WorkHubModelConfigurationRequiredError, WorkHubServicesProvider, type WorkHubServices, type WorkHubTranscriptSnapshot } from '../../renderer/features/workhub/index.js';
 import { useWorkHubController } from '../../renderer/features/workhub/testing.js';
 import { cleanupFakeDom, installReactRenderer } from './fake-dom.js';
 
@@ -129,7 +129,7 @@ async function mountController(failFirstRead = false, overrides: Partial<WorkHub
       createElement(WorkHubServicesProvider, { services }, createElement(Probe)),
     }));
   });
-  assert.equal(controller.sessionId, sessionId);
+  if (!overrides.resolve) assert.equal(controller.sessionId, sessionId);
   return {
     get submissions() { return submissions; },
     get controller() { return controller; }, get openCount() { return openCount; },
@@ -148,6 +148,16 @@ async function mountController(failFirstRead = false, overrides: Partial<WorkHub
   };
 }
 
+test('WorkHub presents model setup instead of a retry-only resolution error', async () => {
+  const h = await mountController(false, {
+    resolve: async () => { throw new WorkHubModelConfigurationRequiredError(); },
+  });
+
+  assert.equal(h.controller.modelSetupRequired, true);
+  assert.equal(h.controller.error, undefined);
+  assert.equal(h.controller.canRetry, false);
+});
+
 test('WorkHub model and thinking selection share versioned saves and reject stale reads', async () => {
   type Session = Awaited<ReturnType<WorkHubServices['getSession']>>;
   const initial = {
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 974578f8e0..22320b7544 100644
--- a/apps/desktop/src/main/runtime-host-workhub-ipc-main.ts
+++ b/apps/desktop/src/main/runtime-host-workhub-ipc-main.ts
@@ -20,12 +20,14 @@
 import { WORKHUB_COORDINATION_SESSION_ID } from '@maka/core/session';
 import { AttachmentIngestBlockedError } from '@maka/core/attachments';
 import { RuntimeHostOperationError, RuntimeHostRequestInterruptedError } from '@maka/runtime-host/client';
+import { WORKHUB_COORDINATION_DEFAULT_MODEL_REQUIRED_MESSAGE } from '@maka/runtime-host/protocol';
 import { prepareIngestItems, resolveAttachmentRefs } from './attachment-ingest.js';
 import type { DesktopRuntimeHostClient } from './runtime-host-client.js';
 import { handleReconciledControl, rethrowReconnectableReadFailure, type ReconnectableReadIpcMain } from './ipc-reconnect-policy.js';
 import type {
   WorkHubAnswerInput,
   WorkHubAnswerResult,
+  WorkHubCoordinationSessionResolution,
   WorkHubPrepareAttachmentsResult,
 } from '../shared/workhub-conversation.js';
 import { toDesktopHostSessionSummary } from './runtime-host-session-catalog-ipc-main.js';
@@ -52,9 +54,21 @@ export function registerRuntimeHostWorkHubIpc(
   options: RuntimeHostWorkHubIpcOptions,
 ): void {
   ipcMain.handle('workhub:getSession', async () => toDesktopHostSessionSummary(await client.getWorkHubSession()));
-  ipcMain.handle('workhub:resolveCoordinationSession', () =>
-    client.resolveWorkHubCoordinationSession(),
-  );
+  ipcMain.handle('workhub:resolveCoordinationSession', async (): Promise<WorkHubCoordinationSessionResolution> => {
+    try {
+      return await client.resolveWorkHubCoordinationSession();
+    } catch (error) {
+      if (
+        error instanceof RuntimeHostOperationError &&
+        error.operation === 'workhub.coordination.resolve' &&
+        error.code === 'operation_conflict' &&
+        error.message === WORKHUB_COORDINATION_DEFAULT_MODEL_REQUIRED_MESSAGE
+      ) {
+        return { kind: 'model_required' };
+      }
+      throw error;
+    }
+  });
   type Attempt = WorkHubAnswerInput & { readonly originHostEpoch: string };
   const unknown = (attempt: Attempt): WorkHubAnswerResult => ({
     kind: 'unknown', originHostEpoch: attempt.originHostEpoch,
diff --git a/apps/desktop/src/main/workhub-presentation.ts b/apps/desktop/src/main/workhub-presentation.ts
index fa1f2160c9..e800dc9d62 100644
--- a/apps/desktop/src/main/workhub-presentation.ts
+++ b/apps/desktop/src/main/workhub-presentation.ts
@@ -648,6 +648,11 @@ export function createWorkHubPresentation(deps: WorkHubPresentationDeps) {
             parseDesktopSessionKey(payload);
             await navigateMain({ kind: 'session', sessionKey: payload }, revision);
             return;
+          case 'settings':
+            if (payload !== 'models') throw new Error('Invalid WorkHub settings section');
+            if (floating && !floating.isDestroyed()) hideFloating();
+            await navigateMain({ kind: 'settings', section: 'models' }, revision);
+            return;
           default: throw new Error('Unknown WorkHub presentation command');
         }
       });
diff --git a/apps/desktop/src/preload/bridge-contract.d.ts b/apps/desktop/src/preload/bridge-contract.d.ts
index cd7b5f4cf1..7702a72994 100644
--- a/apps/desktop/src/preload/bridge-contract.d.ts
+++ b/apps/desktop/src/preload/bridge-contract.d.ts
@@ -1105,7 +1105,7 @@ export interface MakaBridge {
     answer(coordinationSessionId: string, input: WorkHubAnswerInput): Promise<WorkHubAnswerResult>;
     configureModel(coordinationSessionId: string, input: OperationInput<'workhub.coordination.configureModel'>): Promise<OperationOutput<'workhub.coordination.configureModel'>>;
     /** Resolve the active Runtime Host's stable coordination conversation. */
-    resolveCoordinationSession(): Promise<string>;
+    resolveCoordinationSession(): Promise<string | { readonly kind: 'model_required' }>;
 
   };
   sessions: {
diff --git a/apps/desktop/src/preload/preload.ts b/apps/desktop/src/preload/preload.ts
index ca739b3851..35919e1c75 100644
--- a/apps/desktop/src/preload/preload.ts
+++ b/apps/desktop/src/preload/preload.ts
@@ -2102,7 +2102,7 @@ const makaBridge = {
       const scope = await resolveDesktopWorkHubCoordinationCreateScope(coordinationSessionId, runtimeHostSessionRef);
       return ipcRenderer.invoke('workhub:configureModel', scope, input) as Promise<OperationOutput<'workhub.coordination.configureModel'>>;
     },
-    resolveCoordinationSession(): Promise<string> {
+    resolveCoordinationSession(): Promise<string | { readonly kind: 'model_required' }> {
       return resolveDesktopWorkHubCoordinationSession(
         activeRuntimeHostRef,
         (scope) => ipcRenderer.invoke('workhub:resolveCoordinationSession', scope),
diff --git a/apps/desktop/src/preload/workhub-coordination-session.ts b/apps/desktop/src/preload/workhub-coordination-session.ts
index 4daf8ac807..74a819b390 100644
--- a/apps/desktop/src/preload/workhub-coordination-session.ts
+++ b/apps/desktop/src/preload/workhub-coordination-session.ts
@@ -18,6 +18,7 @@
  */
 
 import { WORKHUB_COORDINATION_SESSION_ID } from '@maka/core/session';
+import type { WorkHubCoordinationSessionResolution } from '../shared/workhub-conversation.js';
 import {
   desktopSessionKey,
   type DesktopTargetScope,
@@ -27,10 +28,11 @@ export async function resolveDesktopWorkHubCoordinationSession(
   activeRuntimeHost: () => Promise<DesktopTargetScope>,
   resolveOnHost: (
     scope: DesktopTargetScope,
-  ) => Promise<{ readonly sessionId: string }>,
-): Promise<string> {
+  ) => Promise<WorkHubCoordinationSessionResolution>,
+): Promise<string | { readonly kind: 'model_required' }> {
   const scope = await activeRuntimeHost();
   const result = await resolveOnHost(scope);
+  if ('kind' in result) return result;
   return desktopSessionKey({ hostId: scope.hostId, sessionId: result.sessionId });
 }
 
diff --git a/apps/desktop/src/preload/workhub-presentation.ts b/apps/desktop/src/preload/workhub-presentation.ts
index 1e447d492a..6da71dd2e1 100644
--- a/apps/desktop/src/preload/workhub-presentation.ts
+++ b/apps/desktop/src/preload/workhub-presentation.ts
@@ -40,6 +40,7 @@ export const workHubPresentationBridge: WorkHubPresentationBridge = {
   openUsage: () => ipcRenderer.invoke('workhub-presentation:command', 'usage'),
   toggleWorkbar: () => ipcRenderer.invoke('workhub-presentation:command', 'toggle-workbar'),
   openSession: (sessionKey) => ipcRenderer.invoke('workhub-presentation:command', 'session', sessionKey),
+  openSettings: (section) => ipcRenderer.invoke('workhub-presentation:command', 'settings', section),
   subscribe: (handler) => subscribe('workhub-presentation:changed', handler),
   onViewportInset: (handler) => subscribe('workhub-presentation:viewport-inset', handler),
   onFocusComposer: (handler) => subscribe('workhub-presentation:focus-composer', handler),
diff --git a/apps/desktop/src/renderer/app-shell.tsx b/apps/desktop/src/renderer/app-shell.tsx
index c9b074a387..a17fc5c0d1 100644
--- a/apps/desktop/src/renderer/app-shell.tsx
+++ b/apps/desktop/src/renderer/app-shell.tsx
@@ -2394,7 +2394,8 @@ function AppShellContent({
               <ModuleHub.ModuleHubHost />
               <WorkHubMainNavigation workbarReady={workHubActive && Boolean(workbar.host.activeId)}
                 onOpenUsage={() => commands.toggleTool('inspector')} onToggleWorkbar={commands.toggleRight}
-                onOpenWorkHub={openWorkHub} onOpenSession={(sessionId) => { closeSettings(); openSession(sessionId); }} />
+                onOpenWorkHub={openWorkHub} onOpenSession={(sessionId) => { closeSettings(); openSession(sessionId); }}
+                onOpenSettings={(section) => openSettingsSection(section)} />
               <WorkHubDock workbarCollapsed={selectors.rightCollapsed} enabled={workHubEnabled} visible={workHubActive && sessionsSelected && !shellObscured} />
               <ChatSurfaceLayout
                 // ChatView positions this transcript: switching conversations,
diff --git a/apps/desktop/src/renderer/application/contracts/workhub-workspace/coordination-lifecycle.ts b/apps/desktop/src/renderer/application/contracts/workhub-workspace/coordination-lifecycle.ts
index a100019e85..d02a4c7b4a 100644
--- a/apps/desktop/src/renderer/application/contracts/workhub-workspace/coordination-lifecycle.ts
+++ b/apps/desktop/src/renderer/application/contracts/workhub-workspace/coordination-lifecycle.ts
@@ -28,6 +28,13 @@ export interface WorkHubCoordinationHostChange {
 
 const UNAVAILABLE_DEFAULT_HOST = 'The default Runtime Host is unavailable';
 
+export class WorkHubModelConfigurationRequiredError extends Error {
+  constructor() {
+    super('WorkHub needs a default model before it can start');
+    this.name = 'WorkHubModelConfigurationRequiredError';
+  }
+}
+
 /** Keeps active WorkHub resolution aligned with the current default Runtime Host. */
 export function startWorkHubCoordinationLifecycle(input: {
   readonly resolve: () => Promise<string>;
diff --git a/apps/desktop/src/renderer/features/workhub/controller/use-workhub-controller.ts b/apps/desktop/src/renderer/features/workhub/controller/use-workhub-controller.ts
index 9fa91f96d2..f07d95a95b 100644
--- a/apps/desktop/src/renderer/features/workhub/controller/use-workhub-controller.ts
+++ b/apps/desktop/src/renderer/features/workhub/controller/use-workhub-controller.ts
@@ -36,7 +36,10 @@ import type { WorkHubAnswerInput, WorkHubAnswerResult } from '../../../../shared
 import type { AttachmentRef, FollowUpMode, MessageQueueEntryProjection, MessageQueuePlacement } from '@maka/core/events';
 import type { ThinkingLevel } from '@maka/core/model-thinking';
 import type { ChatModelChoice } from '@maka/core/chat-model-choice';
-import { startWorkHubCoordinationLifecycle } from '../../../application/contracts/workhub-workspace/coordination-lifecycle.js';
+import {
+  startWorkHubCoordinationLifecycle,
+  WorkHubModelConfigurationRequiredError,
+} from '../../../application/contracts/workhub-workspace/coordination-lifecycle.js';
 import { useWorkHubServices } from '../services.js';
 import { workHubLiveCopy } from '../locales/workhub-live-copy.js';
 import type { WorkHubServices, WorkHubTranscript, WorkHubTranscriptSnapshot } from '../ports.js';
@@ -90,6 +93,7 @@ export function useWorkHubController(onSubmit?: () => void) {
   const [sending, setSending] = useState(false);
   const [stopPending, setStopPending] = useState(false);
   const [error, setError] = useState<string>();
+  const [modelSetupRequired, setModelSetupRequired] = useState(false);
   const [readError, setReadError] = useState<string>();
   const [readRevision, setReadRevision] = useState(0);
   const retryResolution = useRef<() => void>(() => undefined);
@@ -216,9 +220,17 @@ export function useWorkHubController(onSubmit?: () => void) {
           setSessionId(undefined);
           setStopPending(false);
           setError(undefined);
+          setModelSetupRequired(false);
         },
         onResolved: setSessionId,
         reportFailure: (reason, action) => {
+          if (reason instanceof WorkHubModelConfigurationRequiredError) {
+            setError(undefined);
+            setModelSetupRequired(true);
+            retryResolution.current = action;
+            return;
+          }
+          setModelSetupRequired(false);
           report(reason);
           retryResolution.current = action;
         },
@@ -616,7 +628,8 @@ export function useWorkHubController(onSubmit?: () => void) {
     sending,
     stopPending,
     error: readError ?? error,
-    canRetry: Boolean(readError || (!sessionId && error) || (error && (pendingSend.current?.admission === 'unknown' || pendingSend.current?.admission === 'rejected'))),
+    modelSetupRequired,
+    canRetry: Boolean(readError || (!modelSetupRequired && !sessionId && error) || (error && (pendingSend.current?.admission === 'unknown' || pendingSend.current?.admission === 'rejected'))),
     send,
     stop,
     changeModel,
diff --git a/apps/desktop/src/renderer/features/workhub/index.ts b/apps/desktop/src/renderer/features/workhub/index.ts
index 4a6d7d3f6e..d62e842c1b 100644
--- a/apps/desktop/src/renderer/features/workhub/index.ts
+++ b/apps/desktop/src/renderer/features/workhub/index.ts
@@ -31,5 +31,9 @@ export { WorkHubDock } from './ui/workhub-dock.js';
 export { WorkHubSurfaceSwitch } from './ui/workhub-surface-switch.js';
 export { WorkHubControlOverlay } from './ui/control-overlay.js';
 export { WorkHubMainNavigation } from './ui/main-navigation.js';
-export { startWorkHubCoordinationLifecycle, type WorkHubCoordinationHostChange } from '../../application/contracts/workhub-workspace/coordination-lifecycle.js';
+export {
+  startWorkHubCoordinationLifecycle,
+  WorkHubModelConfigurationRequiredError,
+  type WorkHubCoordinationHostChange,
+} from '../../application/contracts/workhub-workspace/coordination-lifecycle.js';
 export { WorkHubReturnButton } from './ui/return-button.js';
diff --git a/apps/desktop/src/renderer/features/workhub/locales/workhub-live-copy.ts b/apps/desktop/src/renderer/features/workhub/locales/workhub-live-copy.ts
index b37de22b6e..9c3d50bae6 100644
--- a/apps/desktop/src/renderer/features/workhub/locales/workhub-live-copy.ts
+++ b/apps/desktop/src/renderer/features/workhub/locales/workhub-live-copy.ts
@@ -35,7 +35,7 @@ export const workHubLiveCopy = {
     hide: 'Hide',
     collapseConversation: 'Collapse conversation',
     expandConversation: 'Expand conversation',
-    retry: 'Retry',
+    retry: 'Retry', checkModel: 'Check again', openModelSettings: 'Configure model', modelRequired: 'Set a default model in Settings · Models before WorkHub can start.',
     reloadRequired: 'WorkHub needs to reload',
     welcome: 'What can I help with?',
     hint: 'Ask a question, manage your tasks, or ask me to work in Maka.',
@@ -61,7 +61,7 @@ export const workHubLiveCopy = {
     hide: '隐藏',
     collapseConversation: '收起对话',
     expandConversation: '展开对话',
-    retry: '重试',
+    retry: '重试', checkModel: '检查模型', openModelSettings: '去配置模型', modelRequired: 'WorkHub 需要先在“设置 → 模型”中配置默认模型才能启动。',
     reloadRequired: '工作台需要重新加载',
     welcome: '有什么可以帮你?',
     hint: '问个问题、管理任务,或让我帮你操作 Maka。',
@@ -87,7 +87,7 @@ export const workHubLiveCopy = {
     hide: '隱藏',
     collapseConversation: '收起對話',
     expandConversation: '展開對話',
-    retry: '重試',
+    retry: '重試', checkModel: '重新檢查', openModelSettings: '前往設定模型', modelRequired: 'WorkHub 需要先在「設定 → 模型」中設定預設模型才能啟動。',
     reloadRequired: '工作台需要重新載入',
     welcome: '有什麼可以幫你?',
     hint: '問個問題、管理任務,或讓我幫你操作 Maka。',
diff --git a/apps/desktop/src/renderer/features/workhub/ui/main-navigation.tsx b/apps/desktop/src/renderer/features/workhub/ui/main-navigation.tsx
index 88f164aceb..1930ed7ba6 100644
--- a/apps/desktop/src/renderer/features/workhub/ui/main-navigation.tsx
+++ b/apps/desktop/src/renderer/features/workhub/ui/main-navigation.tsx
@@ -19,7 +19,7 @@
 
 import { useEffect, useRef, useState } from 'react';
 import { useWorkHubServices } from '../services.js';
-export function WorkHubMainNavigation(props: { workbarReady: boolean; onOpenUsage(): void; onToggleWorkbar(): void; onOpenWorkHub(): void; onOpenSession(sessionId: string): void }) {
+export function WorkHubMainNavigation(props: { workbarReady: boolean; onOpenUsage(): void; onToggleWorkbar(): void; onOpenWorkHub(): void; onOpenSession(sessionId: string): void; onOpenSettings(section: 'models'): void }) {
   const [pendingAction, setPendingAction] = useState<'usage' | 'toggle'>();
   const { presentation } = useWorkHubServices();
   const current = useRef(props); current.current = props;
@@ -28,6 +28,7 @@ export function WorkHubMainNavigation(props: { workbarReady: boolean; onOpenUsag
       current.current.onOpenWorkHub();
       setPendingAction(navigation.panelAction);
     }
+    else if (navigation.kind === 'settings') { setPendingAction(undefined); current.current.onOpenSettings(navigation.section); }
     else { setPendingAction(undefined); current.current.onOpenSession(navigation.sessionKey); }
   }), [presentation]);
   useEffect(() => {
diff --git a/apps/desktop/src/renderer/features/workhub/ui/workhub-root.tsx b/apps/desktop/src/renderer/features/workhub/ui/workhub-root.tsx
index f1e1b9bd6a..6796c9e8eb 100644
--- a/apps/desktop/src/renderer/features/workhub/ui/workhub-root.tsx
+++ b/apps/desktop/src/renderer/features/workhub/ui/workhub-root.tsx
@@ -297,7 +297,14 @@ export function WorkHubRoot() {
         style={!showConversation ? { height: expandedLayoutHeight, flex: 'none', position: 'absolute', bottom: 0, width: '100%' } : undefined}
         composer={
           <div className="workHubComposerSurface" ref={composerSurface} onFocusCapture={editProgress} onPointerUpCapture={editProgress}>
-            {(controller.error || control?.error) && (
+            {controller.modelSetupRequired && (
+              <div className="workHubLiveError workHubModelSetup" role="alert">
+                <span>{t.modelRequired}</span>
+                <Button label={t.openModelSettings} variant="primary" onClick={() => call(services.presentation.openSettings('models'))} />
+                <Button label={t.checkModel} variant="ghost" onClick={controller.retry} />
+              </div>
+            )}
+            {!controller.modelSetupRequired && (controller.error || control?.error) && (
               <div className="workHubLiveError" role="alert">
                 {controller.error ?? t.controlFailed}
                 {controller.canRetry && (
diff --git a/apps/desktop/src/renderer/platform/desktop/create-workhub-services.ts b/apps/desktop/src/renderer/platform/desktop/create-workhub-services.ts
index 8a00b6ced9..ef5f7b9ce4 100644
--- a/apps/desktop/src/renderer/platform/desktop/create-workhub-services.ts
+++ b/apps/desktop/src/renderer/platform/desktop/create-workhub-services.ts
@@ -26,6 +26,7 @@ import type { MakaBridge } from '../../../preload/bridge-contract.js';
 import {
   projectWorkHubDelegationState,
   workHubTurnResultPreview,
+  WorkHubModelConfigurationRequiredError,
   type WorkHubDelegationReference,
   type WorkHubServices,
 } from '../../features/workhub/index.js';
@@ -84,7 +85,11 @@ export function createDesktopWorkHubServices(
     },
     presentation: bridge.workHubPresentation,
     control: bridge.workHubControl,
-    resolve: () => bridge.workHub.resolveCoordinationSession(),
+    resolve: async () => {
+      const result = await bridge.workHub.resolveCoordinationSession();
+      if (typeof result !== 'string') throw new WorkHubModelConfigurationRequiredError();
+      return result;
+    },
     getSession: (sessionId) => bridge.workHub.getSession(sessionId),
     subscribeHosts: (handler) => bridge.runtimeHostProfiles.subscribeChanges(handler),
     subscribeAvailability: (handler) => bridge.connections.subscribeEvents(() => handler()),
diff --git a/apps/desktop/src/renderer/styles/workhub.css b/apps/desktop/src/renderer/styles/workhub.css
index 3bd0cd057b..bef64be694 100644
--- a/apps/desktop/src/renderer/styles/workhub.css
+++ b/apps/desktop/src/renderer/styles/workhub.css
@@ -156,6 +156,8 @@
   .workHubDockBackdrop { display: block; width: 100%; height: 100%; object-fit: fill; }
   .workHubLiveWelcome p { color: var(--muted-foreground); }
   .workHubLiveError { padding: var(--space-3); }
+  .workHubModelSetup { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-2); }
+  .workHubModelSetup > span { flex: 1 1 240px; }
   .workHubComposerSurface { position: relative; width: 100%; }
   .workHubComposerContent { position: relative; }
   /* Compact WorkHub keeps its original inline wheel footprint. Other hosts
diff --git a/apps/desktop/src/shared/workhub-conversation.d.ts b/apps/desktop/src/shared/workhub-conversation.d.ts
index b091448427..2628a2396b 100644
--- a/apps/desktop/src/shared/workhub-conversation.d.ts
+++ b/apps/desktop/src/shared/workhub-conversation.d.ts
@@ -31,6 +31,10 @@ export type WorkHubAnswerResult =
   | { readonly kind: 'unknown'; readonly originHostEpoch: string }
   | { readonly kind: 'not_admitted' };
 
+export type WorkHubCoordinationSessionResolution =
+  | { readonly sessionId: string }
+  | { readonly kind: 'model_required' };
+
 export type WorkHubPrepareAttachmentsResult =
   | { readonly ok: true; readonly attachments: AttachmentRef[] }
   | { readonly ok: false; readonly code: AttachmentIngestBlockedCode };
diff --git a/apps/desktop/src/shared/workhub-presentation.d.ts b/apps/desktop/src/shared/workhub-presentation.d.ts
index 4c5f2df215..7a04a9ad38 100644
--- a/apps/desktop/src/shared/workhub-presentation.d.ts
+++ b/apps/desktop/src/shared/workhub-presentation.d.ts
@@ -47,7 +47,8 @@ export interface WorkHubPresentationSnapshot {
 
 export type WorkHubMainNavigation =
   | { readonly kind: 'workhub'; readonly panelAction?: 'usage' | 'toggle' }
-  | { readonly kind: 'session'; readonly sessionKey: string };
+  | { readonly kind: 'session'; readonly sessionKey: string }
+  | { readonly kind: 'settings'; readonly section: 'models' };
 
 export interface WorkHubPresentationBridge {
   ready(): Promise<void>;
@@ -63,6 +64,7 @@ export interface WorkHubPresentationBridge {
   openUsage(): Promise<void>;
   toggleWorkbar(): Promise<void>;
   openSession(sessionKey: string): Promise<void>;
+  openSettings(section: 'models'): Promise<void>;
   subscribe(handler: (snapshot: WorkHubPresentationSnapshot) => void): () => void;
   /** Visible top edge inside the animation canvas, in CSS pixels. */
   onViewportInset(handler: (inset: number) => void): () => void;
diff --git a/apps/desktop/stories/workhub.stories.tsx b/apps/desktop/stories/workhub.stories.tsx
index 3ea802fd81..544ae05ac4 100644
--- a/apps/desktop/stories/workhub.stories.tsx
+++ b/apps/desktop/stories/workhub.stories.tsx
@@ -92,7 +92,7 @@ function makeServices(failFirst: boolean, withHistory: boolean | 'usage', colore
     updateQueueEntry: async () => {}, reorderQueueEntries: async () => {},
     enqueueMessage: async () => 'admitted',
     surface: 'workhub', initialLocale: 'zh-CN', subscribeAppearance: () => () => {},
-    presentation: { ready: async () => {}, progressReady: async () => {}, resizeProgress: async () => {}, expandProgress: async () => {}, getSnapshot: async () => ({ placement: progress ? 'floating' : 'docked', floatingVisible: progress, progressRequest: progress ? 1 : undefined, shortcutRegistered: true, rendererCrashed: false, workbar: { collapsed: true, placement: 'right' } }), setHost: async () => {}, setConversationLayout: async () => {}, detach: async () => {}, dock: async () => {} [...]
+    presentation: { ready: async () => {}, progressReady: async () => {}, resizeProgress: async () => {}, expandProgress: async () => {}, getSnapshot: async () => ({ placement: progress ? 'floating' : 'docked', floatingVisible: progress, progressRequest: progress ? 1 : undefined, shortcutRegistered: true, rendererCrashed: false, workbar: { collapsed: true, placement: 'right' } }), setHost: async () => {}, setConversationLayout: async () => {}, detach: async () => {}, dock: async () => {} [...]
     control: { getSnapshot: async () => ({ revision: 0, phase: 'idle', canUndo: false }), subscribe: () => () => {}, stop: async () => {}, undo: async () => {} },
     bindBrowserSession: () => {},
     resolve: async () => sessionId, subscribeHosts: () => () => {}, subscribeAvailability: () => () => {},
diff --git a/packages/runtime-host/src/protocol/workhub-coordination.ts b/packages/runtime-host/src/protocol/workhub-coordination.ts
index 730520a288..3d0ac4790e 100644
--- a/packages/runtime-host/src/protocol/workhub-coordination.ts
+++ b/packages/runtime-host/src/protocol/workhub-coordination.ts
@@ -85,6 +85,8 @@ export const WORKHUB_COORDINATION_TEXT_MAX_BYTES = 48 * 1024;
 const COORDINATION_TITLE_MAX_BYTES = 512;
 const CANDIDATE_SET_ID_MAX_BYTES = 96;
 export const WORKHUB_COORDINATION_CANDIDATE_MAX_ITEMS = 32;
+export const WORKHUB_COORDINATION_DEFAULT_MODEL_REQUIRED_MESSAGE =
+  'WorkHub Coordination Session requires an available default model';
 
 const RESOLVE_ERRORS = [
   'host_not_ready',
diff --git a/packages/runtime-host/src/server/workhub-coordination-coordinator.ts b/packages/runtime-host/src/server/workhub-coordination-coordinator.ts
index 8992645968..7ae67c183d 100644
--- a/packages/runtime-host/src/server/workhub-coordination-coordinator.ts
+++ b/packages/runtime-host/src/server/workhub-coordination-coordinator.ts
@@ -49,6 +49,10 @@ import type {
   WorkHubCoordinationConfigureModelInput,
 } from '../protocol/index.js';
 import { WORKHUB_COORDINATION_TEXT_MAX_BYTES } from '../protocol/index.js';
+import {
+  WORKHUB_COORDINATION_DEFAULT_MODEL_REQUIRED_MESSAGE,
+  type WorkHubCoordinationSelectAndDelegateInput,
+} from '../protocol/workhub-coordination.js';
 import type {
   ConnectionContext,
   WorkHubCoordinationOperationHandlerMap,
@@ -68,7 +72,6 @@ import {
   RuntimeInteractionFailStopError,
 } from '@maka/runtime/interaction-authority';
 import type { HostInteractionCoordinator } from './interaction-coordinator.js';
-import type { WorkHubCoordinationSelectAndDelegateInput } from '../protocol/workhub-coordination.js';
 import type { SessionContinuityCoordinator } from './session-continuity-coordinator.js';
 import {
   WorkHubActionEffectFailure,
@@ -1098,7 +1101,7 @@ function createTargetFailure(error: unknown): OperationOutcome<'workhub.coordina
   }
   return failure(
     'operation_conflict',
-    'WorkHub Coordination Session requires an available default model',
+    WORKHUB_COORDINATION_DEFAULT_MODEL_REQUIRED_MESSAGE,
   );
 }
 


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