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