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/19 16:40:51 UTC

(maka) branch feat/runtime-host-rust updated: feat(workhub): resolve every desktop coordinator through its client

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

M4n5ter pushed a commit to branch feat/runtime-host-rust
in repository https://gitbox.apache.org/repos/asf/maka.git


The following commit(s) were added to refs/heads/feat/runtime-host-rust by this push:
     new 8e483e922c feat(workhub): resolve every desktop coordinator through its client
8e483e922c is described below

commit 8e483e922cd48a899b9517f75eb2dab072118a1a
Author: Wang <m4...@gmail.com>
AuthorDate: Sun Sep 20 00:40:37 2026 +0800

    feat(workhub): resolve every desktop coordinator through its client
    
    Remove the legacy resolver IPC and lifecycle. Share supported renderer UI with Client bundles and preserve exact Host bindings across refresh, retry and retirement.
    
    Generated-by: OpenAI Codex
---
 .../main/__tests__/client-plugin-session.test.ts   |   9 +-
 .../runtime-host-client-operations.test.ts         |  19 --
 .../workhub-coordination-lifecycle.test.ts         | 230 ---------------------
 .../workhub-coordination-session-preload.test.ts   |  33 ---
 .../main/__tests__/workhub-send-visibility.test.ts |  28 +--
 apps/desktop/src/main/runtime-host-client.ts       |   4 -
 .../src/main/runtime-host-workhub-ipc-main.ts      |   4 -
 apps/desktop/src/preload/bridge-contract.d.ts      |   2 -
 apps/desktop/src/preload/preload.ts                |   7 -
 .../src/preload/workhub-coordination-session.ts    |  16 +-
 .../workhub-workspace/coordination-lifecycle.ts    | 110 ----------
 .../renderer/composition/legacy-desktop-region.tsx |   8 +-
 .../src/renderer/features/client-plugins/index.tsx |   3 +-
 .../workhub/controller/use-workhub-controller.ts   |  33 +--
 .../desktop/src/renderer/features/workhub/index.ts |   1 -
 .../desktop/src/renderer/features/workhub/ports.ts |   3 -
 .../renderer/features/workhub/ui/workhub-root.tsx  |   6 +-
 .../platform/desktop/create-workhub-services.ts    |   3 -
 apps/desktop/stories/workhub.stories.tsx           |   4 +-
 crates/runtime-host/src/plugins/workhub/client.tsx |  22 +-
 docs/rust-parity.md                                |   2 +-
 docs/rust-parity.zh-CN.md                          |   2 +-
 packages/plugin-sdk/README.md                      |   2 +
 packages/plugin-sdk/README.zh-CN.md                |   2 +
 packages/plugin-sdk/src/build.ts                   |   8 +-
 packages/ui/package.json                           |   1 +
 .../ui/src/plugin.ts                               |   8 +-
 27 files changed, 78 insertions(+), 492 deletions(-)

diff --git a/apps/desktop/src/main/__tests__/client-plugin-session.test.ts b/apps/desktop/src/main/__tests__/client-plugin-session.test.ts
index 7c86b95d26..17e77d1103 100644
--- a/apps/desktop/src/main/__tests__/client-plugin-session.test.ts
+++ b/apps/desktop/src/main/__tests__/client-plugin-session.test.ts
@@ -76,6 +76,7 @@ test('the WorkHub Client resolves main panels through its origin and withdraws s
   let subscriptions = 0;
   const projections: string[] = [];
   const calls: string[] = [];
+  let unavailable = true;
   const closed: string[] = [];
   const lateProjection = deferred<string>();
   const services: ClientPluginServices = {
@@ -94,6 +95,7 @@ test('the WorkHub Client resolves main panels through its origin and withdraws s
           const method = (() => async () => {
             signal.throwIfAborted();
             calls.push(host.hostId);
+            if (unavailable) return { ok: false, error: { code: 'operation_conflict', message: 'Choose a model' } };
             await refreshing?.promise;
             return { ok: true, result: { sessionId: 'coordinator' } };
           }) as ClientRemote['method'];
@@ -127,6 +129,11 @@ test('the WorkHub Client resolves main panels through its origin and withdraws s
     await act(async () => render(false));
     assert.equal(defaultCalls, 0);
     await act(async () => render(true));
+    await until(() => document.body.textContent!.includes('Choose a model'));
+    const retry = document.querySelector('button');
+    assert.equal(retry?.textContent, 'Retry');
+    unavailable = false;
+    await act(async () => retry!.dispatchEvent(new window.Event('click', { bubbles: true })));
     await until(() => projections.includes('first'));
     assert.equal(latest!.sessionId, undefined);
     selected = second;
@@ -150,7 +157,7 @@ test('the WorkHub Client resolves main panels through its origin and withdraws s
     enabled = true; revision++;
     await act(async () => changedCatalog());
     await until(() => latest!.sessionId === expected);
-    assert.deepEqual(calls, ['first', 'second', 'second', 'second']);
+    assert.deepEqual(calls, ['first', 'first', 'second', 'second', 'second']);
     await act(async () => render(false));
     await until(() => subscriptions === 0);
     assert.equal(latest!.sessionId, undefined);
diff --git a/apps/desktop/src/main/__tests__/runtime-host-client-operations.test.ts b/apps/desktop/src/main/__tests__/runtime-host-client-operations.test.ts
index 8f16a512c7..5fd20d8f1d 100644
--- a/apps/desktop/src/main/__tests__/runtime-host-client-operations.test.ts
+++ b/apps/desktop/src/main/__tests__/runtime-host-client-operations.test.ts
@@ -95,25 +95,6 @@ test('restarts a paginated catalog read instead of mixing revisions', async () =
   ]);
 });
 
-test('resolves WorkHub coordination through the dedicated Host operation', async () => {
-  const { client, requests } = clientWithResponses([
-    { sessionId: 'maka_workhub_coordination' },
-    { candidateSetId: `sha256:${'a'.repeat(64)}`, candidates: [] },
-  ]);
-
-  assert.deepEqual(await client.resolveWorkHubCoordinationSession(), {
-    sessionId: 'maka_workhub_coordination',
-  });
-  assert.deepEqual(await client.listWorkHubCoordinationCandidates(), {
-    candidateSetId: `sha256:${'a'.repeat(64)}`,
-    candidates: [],
-  });
-  assert.deepEqual(requests, [
-    { operation: 'workhub.coordination.resolve', input: {} },
-    { operation: 'workhub.coordination.candidates', input: {} },
-  ]);
-});
-
 test('re-reads the Session revision before retrying a product update', async () => {
   const { client, requests } = clientWithResponses([
     { kind: 'session', session: session('session-1', 4) },
diff --git a/apps/desktop/src/main/__tests__/workhub-coordination-lifecycle.test.ts b/apps/desktop/src/main/__tests__/workhub-coordination-lifecycle.test.ts
deleted file mode 100644
index 13ec4ebcc0..0000000000
--- a/apps/desktop/src/main/__tests__/workhub-coordination-lifecycle.test.ts
+++ /dev/null
@@ -1,230 +0,0 @@
-/*
- * 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 test from 'node:test';
-import { desktopSessionKey } from '../../shared/runtime-host-identity.js';
-import {
-  startWorkHubCoordinationLifecycle,
-  type WorkHubCoordinationHostChange,
-} from '../../renderer/features/workhub/index.js';
-
-const coordinationSessionId = (hostId: string) => desktopSessionKey({
-  hostId,
-  sessionId: 'maka_workhub_coordination',
-});
-
-test('WorkHub resolves on open and when the default Host authority changes', async () => {
-  let hostChange: ((event: WorkHubCoordinationHostChange) => void) | undefined;
-  let availabilityChange: (() => void) | undefined;
-  const calls: string[] = [];
-  let activeHostId = 'host-a';
-  const stop = startWorkHubCoordinationLifecycle({
-    resolve: () => {
-      calls.push('resolve');
-      return Promise.resolve(coordinationSessionId(activeHostId));
-    },
-    subscribeHostChanges(handler) {
-      hostChange = handler;
-      return () => calls.push('unsubscribe-hosts');
-    },
-    subscribeAvailabilityChanges(handler) {
-      availabilityChange = handler;
-      return () => calls.push('unsubscribe-availability');
-    },
-    onResolving: () => calls.push('resolving'),
-    onResolved: (sessionId) => calls.push(`resolved:${sessionId}`),
-    reportFailure: () => calls.push('failure'),
-  });
-
-  assert.deepEqual(calls, ['resolving', 'resolve']);
-  await Promise.resolve();
-  hostChange?.({ isDefault: false, readiness: 'ready' });
-  hostChange?.({ isDefault: true, readiness: 'reconnecting', hostId: 'host-a' });
-  assert.deepEqual(calls, [
-    'resolving',
-    'resolve',
-    `resolved:${coordinationSessionId('host-a')}`,
-  ]);
-
-  availabilityChange?.();
-  assert.equal(calls.at(-1), `resolved:${coordinationSessionId('host-a')}`);
-
-  activeHostId = 'host-b';
-  hostChange?.({ isDefault: true, readiness: 'ready', hostId: 'host-b' });
-  assert.deepEqual(calls.slice(-2), ['resolving', 'resolve']);
-  await Promise.resolve();
-  assert.equal(calls.at(-1), `resolved:${coordinationSessionId('host-b')}`);
-
-  stop();
-  assert.deepEqual(calls.slice(-2), ['unsubscribe-hosts', 'unsubscribe-availability']);
-  hostChange?.({ isDefault: true, readiness: 'ready', hostId: 'host-c' });
-  availabilityChange?.();
-  assert.equal(calls.at(-1), 'unsubscribe-availability');
-});
-
-test('WorkHub reports an unavailable default Host instead of holding the loading state', () => {
-  let hostChange: ((event: WorkHubCoordinationHostChange) => void) | undefined;
-  let availabilityChange: (() => void) | undefined;
-  const calls: string[] = [];
-  let manualRetry: (() => void) | undefined;
-  const stop = startWorkHubCoordinationLifecycle({
-    resolve: () => {
-      calls.push('resolve');
-      return new Promise<string>(() => undefined);
-    },
-    subscribeHostChanges(handler) {
-      hostChange = handler;
-      return () => undefined;
-    },
-    subscribeAvailabilityChanges(handler) {
-      availabilityChange = handler;
-      return () => undefined;
-    },
-    onResolving: () => calls.push('resolving'),
-    onResolved: () => calls.push('resolved'),
-    reportFailure: (error, retry) => {
-      calls.push(`failure:${error instanceof Error ? error.message : String(error)}`);
-      manualRetry = retry;
-    },
-  });
-
-  // Connecting is still on its way to an answer and keeps the loading state.
-  hostChange?.({ isDefault: true, readiness: 'connecting' });
-  assert.deepEqual(calls, ['resolving', 'resolve', 'resolving']);
-
-  hostChange?.({ isDefault: true, readiness: 'unavailable' });
-  assert.deepEqual(calls, [
-    'resolving',
-    'resolve',
-    'resolving',
-    'resolving',
-    'failure:The default Runtime Host is unavailable',
-  ]);
-
-  // A revoked generation that reported a failure stays reopenable, by the
-  // Retry control and by an availability change alike.
-  availabilityChange?.();
-  assert.equal(calls.at(-1), 'resolve');
-  hostChange?.({ isDefault: true, readiness: 'unavailable', removed: true });
-  assert.equal(calls.at(-1), 'failure:The default Runtime Host is unavailable');
-  manualRetry?.();
-  assert.equal(calls.at(-1), 'resolve');
-  stop();
-});
-
-test('WorkHub exposes a retry and automatically retries when model availability changes', async () => {
-  let hostChange: ((event: WorkHubCoordinationHostChange) => void) | undefined;
-  let availabilityChange: (() => void) | undefined;
-  const failures: unknown[] = [];
-  let manualRetry: (() => void) | undefined;
-  let resolveAttempts = 0;
-  const failure = new Error('offline');
-  const stop = startWorkHubCoordinationLifecycle({
-    resolve: () => {
-      resolveAttempts += 1;
-      return resolveAttempts < 3
-        ? Promise.reject(failure)
-        : Promise.resolve(coordinationSessionId('host-a'));
-    },
-    subscribeHostChanges(handler) {
-      hostChange = handler;
-      return () => undefined;
-    },
-    subscribeAvailabilityChanges(handler) {
-      availabilityChange = handler;
-      return () => undefined;
-    },
-    onResolving: () => undefined,
-    onResolved: (sessionId) => failures.push(sessionId),
-    reportFailure: (error, retry) => {
-      failures.push(error);
-      manualRetry = retry;
-    },
-  });
-
-  await Promise.resolve();
-  await Promise.resolve();
-  assert.deepEqual(failures, [failure]);
-
-  manualRetry?.();
-  await Promise.resolve();
-  await Promise.resolve();
-  assert.deepEqual(failures, [failure, failure]);
-
-  availabilityChange?.();
-  await Promise.resolve();
-  await Promise.resolve();
-  assert.deepEqual(failures, [failure, failure, coordinationSessionId('host-a')]);
-
-  availabilityChange?.();
-  hostChange?.({ isDefault: false, readiness: 'ready' });
-  assert.equal(resolveAttempts, 3);
-  stop();
-});
-
-test('WorkHub ignores a stale Host resolution that settles after a newer Host', async () => {
-  let hostChange: ((event: WorkHubCoordinationHostChange) => void) | undefined;
-  const pending: Array<(sessionId: string) => void> = [];
-  const resolved: string[] = [];
-  const stop = startWorkHubCoordinationLifecycle({
-    resolve: () => new Promise<string>((resolve) => pending.push(resolve)),
-    subscribeHostChanges(handler) {
-      hostChange = handler;
-      return () => undefined;
-    },
-    subscribeAvailabilityChanges: () => () => undefined,
-    onResolving: () => undefined,
-    onResolved: (sessionId) => resolved.push(sessionId),
-    reportFailure: (error) => assert.fail(error instanceof Error ? error.message : String(error)),
-  });
-
-  hostChange?.({ isDefault: true, readiness: 'ready' });
-  pending[1]?.(coordinationSessionId('host-b'));
-  await Promise.resolve();
-  pending[0]?.(coordinationSessionId('host-a'));
-  await Promise.resolve();
-
-  assert.deepEqual(resolved, [coordinationSessionId('host-b')]);
-  stop();
-});
-
-test('selecting an unready default Host revokes the old scope and invalidates its resolve', async () => {
-  let hostChange: ((event: WorkHubCoordinationHostChange) => void) | undefined;
-  const pending: Array<(sessionId: string) => void> = [];
-  const calls: string[] = [];
-  const stop = startWorkHubCoordinationLifecycle({
-    resolve: () => new Promise<string>((resolve) => pending.push(resolve)),
-    subscribeHostChanges(handler) {
-      hostChange = handler;
-      return () => undefined;
-    },
-    subscribeAvailabilityChanges: () => () => undefined,
-    onResolving: () => calls.push('revoked'),
-    onResolved: (sessionId) => calls.push(`resolved:${sessionId}`),
-    reportFailure: (error) => assert.fail(error instanceof Error ? error.message : String(error)),
-  });
-
-  hostChange?.({ isDefault: true, readiness: 'reconnecting' });
-  pending[0]?.('old-host-coordination');
-  await Promise.resolve();
-
-  assert.deepEqual(calls, ['revoked', 'revoked']);
-  stop();
-});
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..d5dfa23560 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
@@ -19,43 +19,10 @@
 
 import assert from 'node:assert/strict';
 import test from 'node:test';
-import { parseDesktopSessionKey } from '../../shared/runtime-host-identity.js';
 import {
   resolveDesktopWorkHubCoordinationCreateScope,
-  resolveDesktopWorkHubCoordinationSession,
 } from '../../preload/workhub-coordination-session.js';
 
-test('resolves the Coordination Session through the currently active Runtime Host scope', async () => {
-  const scopes = [
-    { hostId: 'host-a', targetEpoch: 'epoch-a' },
-    { hostId: 'host-b', targetEpoch: 'epoch-b' },
-  ];
-  let active = 0;
-  const seen: typeof scopes = [];
-  const resolve = () =>
-    resolveDesktopWorkHubCoordinationSession(
-      async () => scopes[active]!,
-      async (scope) => {
-        seen.push(scope);
-        return { sessionId: 'maka_workhub_coordination' };
-      },
-    );
-
-  const first = await resolve();
-  active = 1;
-  const second = await resolve();
-
-  assert.deepEqual(seen, scopes);
-  assert.deepEqual(parseDesktopSessionKey(first), {
-    hostId: 'host-a',
-    sessionId: 'maka_workhub_coordination',
-  });
-  assert.deepEqual(parseDesktopSessionKey(second), {
-    hostId: 'host-b',
-    sessionId: 'maka_workhub_coordination',
-  });
-});
-
 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..3a802c1fd0 100644
--- a/apps/desktop/src/main/__tests__/workhub-send-visibility.test.ts
+++ b/apps/desktop/src/main/__tests__/workhub-send-visibility.test.ts
@@ -87,11 +87,9 @@ async function mountController(failFirstRead = false, overrides: Partial<WorkHub
         ...(rootTurn.status === 'running' ? { status: 'running' as const } : { status: rootTurn.status, terminalEventId: 'terminal', abortSource: 'user_stop' }) } : null });
   }
   const services = {
-    resolve: async () => sessionId,
     getSession: async () => ({ id: sessionId, runningTurnIds: [] }),
     listSessions: async () => [],
     modelChoices: async () => [],
-    subscribeHosts: () => () => {},
     subscribeAvailability: () => () => {},
     subscribeSessions: () => () => {},
     observe: (_id: string, handler: typeof observe, _onError: unknown, phase: typeof onPhase, execution: typeof onExecution) => { observe = handler; onPhase = phase; onExecution = execution; return () => {}; },
@@ -123,7 +121,7 @@ async function mountController(failFirstRead = false, overrides: Partial<WorkHub
     ...overrides,
   } as unknown as WorkHubServices;
   let submissions = 0;
-  function Probe() { controller = useWorkHubController(() => { submissions++; }); return null; }
+  function Probe() { controller = useWorkHubController(sessionId, () => { submissions++; }); return null; }
   await act(async () => {
     root.render(createElement(LocaleProvider, { locale: 'en', children:
       createElement(WorkHubServicesProvider, { services }, createElement(Probe)),
@@ -321,7 +319,7 @@ test('WorkHub carries Stop through deferred or uncertain admission for the origi
     await act(async () => { sent = h.controller.send('stop this attempt', []); });
     const turnId = h.requests[0]!.turnId;
     if (order === 'stop-after-response') {
-      h.admit(turnId);
+      await act(async () => h.admit(turnId));
       await act(async () => { h.admission.resolve({ turnId }); await sent; });
     } else if (order === 'lost-response') {
       await act(async () => {
@@ -348,7 +346,7 @@ test('WorkHub carries Stop through deferred or uncertain admission for the origi
       let retried!: Promise<boolean>;
       await act(async () => { retried = h.controller.send('stop this attempt', []); });
       assert.equal(h.requests[1]!.turnId, turnId);
-      h.admit(turnId);
+      await act(async () => h.admit(turnId));
       await act(async () => {
         h.emit({ type: 'text_delta', id: 'retry-output', turnId, messageId: 'retry-answer', ts: 2, text: 'Retrying' });
         h.admission.resolve({ turnId });
@@ -368,10 +366,10 @@ test('WorkHub carries Stop through deferred or uncertain admission for the origi
       }
       if (order === 'stop-before-response' || order === 'lost-response' || order === 'response-before-observation') {
         assert.deepEqual(h.interrupts, []);
-        h.admit('different-turn');
+        await act(async () => h.admit('different-turn'));
         await act(async () => h.emit({ type: 'text_delta', id: 'other', turnId: 'different-turn', messageId: 'other-answer', ts: 1, text: 'Other work' }));
         assert.deepEqual(h.interrupts, []);
-        h.admit(turnId);
+        await act(async () => h.admit(turnId));
         if (order !== 'stop-before-response')
           await act(async () => h.emit({ type: 'text_delta', id: 'first-output', turnId, messageId: 'answer', ts: 2, text: 'Working' }));
       }
@@ -380,7 +378,7 @@ test('WorkHub carries Stop through deferred or uncertain admission for the origi
       }
       assert.deepEqual(h.interrupts, [{ sessionId: h.sessionId, turnId, runId: `run:${turnId}` }], order);
       assert.equal(h.controller.stopPending, false);
-      h.admit('later-turn');
+      await act(async () => h.admit('later-turn'));
       await act(async () => h.emit({ type: 'text_delta', id: 'later', turnId: 'later-turn', messageId: 'later-answer', ts: 3, text: 'Later work' }));
       assert.equal(h.interrupts.length, 1, 'the intent cannot transfer to a later Turn');
     }
@@ -412,8 +410,10 @@ test('an unknown WorkHub submission converges through the original Host admissio
     assert.equal(h.controller.canRetry, true);
     const submitted = h.requests.length;
     const submissions = h.submissions;
-    if (outcome === 'running') h.admit(original.turnId);
-    if (outcome === 'completed') h.complete(original.turnId);
+    await act(async () => {
+      if (outcome === 'running') h.admit(original.turnId);
+      if (outcome === 'completed') h.complete(original.turnId);
+    });
     if (outcome === 'replay') h.resetAdmission();
     await act(async () => h.reconnect(outcome === 'replay' ? undefined : 'host-epoch-2'));
     if (outcome === 'replay') {
@@ -479,7 +479,7 @@ test('WorkHub steering keeps the current Turn and Stop authority and reconciles
   let sent!: Promise<boolean>;
   await act(async () => { sent = h.controller.send('original request', []); });
   const turnId = h.requests[0]!.turnId;
-  h.admit(turnId);
+  await act(async () => h.admit(turnId));
   await act(async () => { h.admission.resolve({ turnId }); await sent; });
   const attachments: AttachmentRef[] = [{ kind: 'doc', name: 'brief.txt', mimeType: 'text/plain', bytes: 4, ref: { kind: 'workspace_file', relativePath: 'brief.txt' } }];
   await act(async () => { assert.equal(await h.controller.send('change direction', attachments, 'steer'), true); });
@@ -617,7 +617,7 @@ test('WorkHub defaults to follow-up and moves each message into its admitted suc
   const first = h.steers[0]![1];
   const second = h.steers[1]![1];
   assert.deepEqual(h.controller.transientMessages.map((message) => message.id), [first, second]);
-  await act(() => h.reconnect());
+  await act(async () => h.reconnect());
   assert.deepEqual(h.controller.transientMessages.map((message) => message.id), [first, second],
     'disconnecting before canonical evidence cannot hide accepted messages');
   const entries = h.steers.map(([, messageId, text, attachments]) => ({
@@ -717,10 +717,10 @@ test('Stop retires only Host-confirmed queued messages even without retraction e
       let sent!: Promise<boolean>;
       await act(async () => { sent = h.controller.send('original request', []); });
       turnId = h.requests[0]!.turnId;
-      h.admit(turnId);
+      await act(async () => h.admit(turnId));
       await act(async () => { h.admission.resolve({ turnId }); await sent; });
     } else {
-      h.admit(turnId);
+      await act(async () => h.admit(turnId));
       await act(() => h.emit({ type: 'text_delta', id: 'live', turnId, messageId: 'answer', ts: 1, text: 'Working' }));
     }
     const entries = ['steering', 'followup', 'retained'].map((messageId) => ({
diff --git a/apps/desktop/src/main/runtime-host-client.ts b/apps/desktop/src/main/runtime-host-client.ts
index 56dea39613..f76a31f86b 100644
--- a/apps/desktop/src/main/runtime-host-client.ts
+++ b/apps/desktop/src/main/runtime-host-client.ts
@@ -927,10 +927,6 @@ export class DesktopRuntimeHostClient {
     );
   }
 
-  resolveWorkHubCoordinationSession() {
-    return this.request("workhub.coordination.resolve", {});
-  }
-
   async getWorkHubSession(): Promise<SessionCatalogProjection> {
     return requireSessionProjection(await this.request('workhub.coordination.query', {}));
   }
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..df4ae34594 100644
--- a/apps/desktop/src/main/runtime-host-workhub-ipc-main.ts
+++ b/apps/desktop/src/main/runtime-host-workhub-ipc-main.ts
@@ -35,7 +35,6 @@ type RuntimeHostWorkHubClient = Pick<
   | 'ingestAttachment'
   | 'answerWorkHubCoordination'
   | 'configureWorkHubModel'
-  | 'resolveWorkHubCoordinationSession'
   | 'getWorkHubSession'
   | 'queryTurn'
   | 'hostEpoch'
@@ -52,9 +51,6 @@ export function registerRuntimeHostWorkHubIpc(
   options: RuntimeHostWorkHubIpcOptions,
 ): void {
   ipcMain.handle('workhub:getSession', async () => toDesktopHostSessionSummary(await client.getWorkHubSession()));
-  ipcMain.handle('workhub:resolveCoordinationSession', () =>
-    client.resolveWorkHubCoordinationSession(),
-  );
   type Attempt = WorkHubAnswerInput & { readonly originHostEpoch: string };
   const unknown = (attempt: Attempt): WorkHubAnswerResult => ({
     kind: 'unknown', originHostEpoch: attempt.originHostEpoch,
diff --git a/apps/desktop/src/preload/bridge-contract.d.ts b/apps/desktop/src/preload/bridge-contract.d.ts
index 6fd1f6d6ad..cc3e1ed1fb 100644
--- a/apps/desktop/src/preload/bridge-contract.d.ts
+++ b/apps/desktop/src/preload/bridge-contract.d.ts
@@ -1111,8 +1111,6 @@ export interface MakaBridge {
     prepareAttachments(coordinationSessionId: string, items: RendererIngestInput[]): Promise<WorkHubPrepareAttachmentsResult>;
     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>;
 
   };
   sessions: {
diff --git a/apps/desktop/src/preload/preload.ts b/apps/desktop/src/preload/preload.ts
index 950e9979c4..e911120462 100644
--- a/apps/desktop/src/preload/preload.ts
+++ b/apps/desktop/src/preload/preload.ts
@@ -42,7 +42,6 @@ import { createThreadSearchClient } from './multi-host-thread-search.js';
 import { releaseSessionObservation } from './session-observation-release.js';
 import {
   resolveDesktopWorkHubCoordinationCreateScope,
-  resolveDesktopWorkHubCoordinationSession,
 } from './workhub-coordination-session.js';
 import type {
   MakaBridge,
@@ -2144,12 +2143,6 @@ const makaBridge = {
       const scope = await resolveDesktopWorkHubCoordinationCreateScope(coordinationSessionId, runtimeHostSessionRef);
       return ipcRenderer.invoke('workhub:configureModel', scope, input) as Promise<OperationOutput<'workhub.coordination.configureModel'>>;
     },
-    resolveCoordinationSession(): Promise<string> {
-      return resolveDesktopWorkHubCoordinationSession(
-        activeRuntimeHostRef,
-        (scope) => ipcRenderer.invoke('workhub:resolveCoordinationSession', scope),
-      );
-    },
 
   },
   sessionLocal: {
diff --git a/apps/desktop/src/preload/workhub-coordination-session.ts b/apps/desktop/src/preload/workhub-coordination-session.ts
index 4daf8ac807..78b58ad0cb 100644
--- a/apps/desktop/src/preload/workhub-coordination-session.ts
+++ b/apps/desktop/src/preload/workhub-coordination-session.ts
@@ -18,21 +18,7 @@
  */
 
 import { WORKHUB_COORDINATION_SESSION_ID } from '@maka/core/session';
-import {
-  desktopSessionKey,
-  type DesktopTargetScope,
-} from '../shared/runtime-host-identity.js';
-
-export async function resolveDesktopWorkHubCoordinationSession(
-  activeRuntimeHost: () => Promise<DesktopTargetScope>,
-  resolveOnHost: (
-    scope: DesktopTargetScope,
-  ) => Promise<{ readonly sessionId: string }>,
-): Promise<string> {
-  const scope = await activeRuntimeHost();
-  const result = await resolveOnHost(scope);
-  return desktopSessionKey({ hostId: scope.hostId, sessionId: result.sessionId });
-}
+import type { DesktopTargetScope } from '../shared/runtime-host-identity.js';
 
 /** Resolves an exact Host scope from the durable Coordination identity, never from UI focus. */
 export async function resolveDesktopWorkHubCoordinationCreateScope(
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
deleted file mode 100644
index a100019e85..0000000000
--- a/apps/desktop/src/renderer/application/contracts/workhub-workspace/coordination-lifecycle.ts
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * 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 { parseDesktopSessionKey } from '../../../../shared/runtime-host-identity.js';
-
-export interface WorkHubCoordinationHostChange {
-  readonly hostId?: string;
-  readonly isDefault: boolean;
-  readonly readiness: string;
-  readonly removed?: boolean;
-}
-
-const UNAVAILABLE_DEFAULT_HOST = 'The default Runtime Host is unavailable';
-
-/** Keeps active WorkHub resolution aligned with the current default Runtime Host. */
-export function startWorkHubCoordinationLifecycle(input: {
-  readonly resolve: () => Promise<string>;
-  readonly subscribeHostChanges: (
-    handler: (event: WorkHubCoordinationHostChange) => void,
-  ) => () => void;
-  readonly subscribeAvailabilityChanges: (handler: () => void) => () => void;
-  readonly onResolving: () => void;
-  readonly onResolved: (sessionId: string) => void;
-  readonly reportFailure: (error: unknown, retry: () => void) => void;
-}): () => void {
-  let stopped = false;
-  let generation = 0;
-  let failedGeneration: number | undefined;
-  let resolvedHostId: string | undefined;
-
-  const revoke = () => {
-    const currentGeneration = ++generation;
-    failedGeneration = undefined;
-    resolvedHostId = undefined;
-    input.onResolving();
-    return currentGeneration;
-  };
-  const reportGenerationFailure = (currentGeneration: number, error: unknown) => {
-    failedGeneration = currentGeneration;
-    input.reportFailure(error, () => {
-      if (!stopped && failedGeneration === currentGeneration) beginResolve();
-    });
-  };
-  const resolveGeneration = (currentGeneration: number) => {
-    void input
-      .resolve()
-      .then((sessionId) => {
-        if (stopped || currentGeneration !== generation) return;
-        resolvedHostId = parseDesktopSessionKey(sessionId).hostId;
-        failedGeneration = undefined;
-        input.onResolved(sessionId);
-      })
-      .catch((error) => {
-        if (stopped || currentGeneration !== generation) return;
-        reportGenerationFailure(currentGeneration, error);
-      });
-  };
-  const beginResolve = () => resolveGeneration(revoke());
-
-  const unsubscribeHosts = input.subscribeHostChanges((event) => {
-    if (stopped || !event.isDefault) return;
-    if (
-      resolvedHostId !== undefined &&
-      event.hostId === resolvedHostId &&
-      event.readiness !== 'unavailable' &&
-      !event.removed
-    ) {
-      return;
-    }
-    const currentGeneration = revoke();
-    if (event.readiness === 'ready') {
-      resolveGeneration(currentGeneration);
-      return;
-    }
-    // Connecting and reconnecting are still on their way to an answer, so the
-    // loading state stays honest. An unavailable default Host is not: without a
-    // reported failure the surface would hold that spinner forever, and the
-    // availability subscription only reopens a generation that failed.
-    if (event.readiness === 'unavailable' || event.removed) {
-      reportGenerationFailure(currentGeneration, new Error(UNAVAILABLE_DEFAULT_HOST));
-    }
-  });
-  const unsubscribeAvailability = input.subscribeAvailabilityChanges(() => {
-    if (!stopped && failedGeneration === generation) beginResolve();
-  });
-  beginResolve();
-  return () => {
-    stopped = true;
-    generation += 1;
-    failedGeneration = undefined;
-    unsubscribeHosts();
-    unsubscribeAvailability();
-  };
-}
diff --git a/apps/desktop/src/renderer/composition/legacy-desktop-region.tsx b/apps/desktop/src/renderer/composition/legacy-desktop-region.tsx
index 2baeb0fc86..42f52e6956 100644
--- a/apps/desktop/src/renderer/composition/legacy-desktop-region.tsx
+++ b/apps/desktop/src/renderer/composition/legacy-desktop-region.tsx
@@ -18,8 +18,14 @@
  */
 
 import type { ComponentProps } from 'react';
+import { useUiLocale } from '@maka/ui';
+import { usePluginSession } from '../features/client-plugins/index.js';
 import { AppShell as LegacyAppShell } from '../app-shell';
 import { WorkHubRoot, WorkHubSurfaceSwitch } from '../features/workhub';
 export function AppShell(props: ComponentProps<typeof LegacyAppShell>) {
-  return <WorkHubSurfaceSwitch main={<LegacyAppShell {...props} />} workhub={<WorkHubRoot />} />;
+  return <WorkHubSurfaceSwitch main={<LegacyAppShell {...props} />} workhub={<WorkHubConversation />} />;
+}
+function WorkHubConversation() {
+  const binding = usePluginSession('maka.workhub.ui', true, useUiLocale());
+  return <>{binding.resolver}<WorkHubRoot key={binding.sessionId} sessionId={binding.sessionId} /></>;
 }
diff --git a/apps/desktop/src/renderer/features/client-plugins/index.tsx b/apps/desktop/src/renderer/features/client-plugins/index.tsx
index 094d3c26bf..8424082347 100644
--- a/apps/desktop/src/renderer/features/client-plugins/index.tsx
+++ b/apps/desktop/src/renderer/features/client-plugins/index.tsx
@@ -20,6 +20,7 @@
 import * as React from 'react';
 import * as JsxRuntime from 'react/jsx-runtime';
 import * as ClientSdk from '@maka-agent/plugin-sdk/client';
+import * as ClientUi from '@maka/ui/plugin';
 import { ClientSlot } from '@maka/ui/client-plugins';
 import { createServicesContext } from '../../application/contracts/feature-services.js';
 import type { ClientHostRef, ClientPluginServices } from './ports.js';
@@ -37,7 +38,7 @@ export function ClientPluginServicesProvider(props: { services: ClientPluginServ
   const value = React.useMemo(() => ({ services: props.services, hosts: new Map<string, ClientHostRuntime>() }), [props.services]);
   return <Provider services={value}>{props.children}</Provider>;
 }
-const modules = { react: React, 'react/jsx-runtime': JsxRuntime, '@maka-agent/plugin-sdk/client': ClientSdk };
+const modules = { react: React, 'react/jsx-runtime': JsxRuntime, '@maka-agent/plugin-sdk/client': ClientSdk, '@maka/ui/plugin': ClientUi };
 
 /** Bound to an originating Host, never the currently selected default Host. */
 export function ClientPluginSlot<K extends keyof ClientSdk.ClientSlots>(props: {
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..dbde35c098 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,6 @@ 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 { useWorkHubServices } from '../services.js';
 import { workHubLiveCopy } from '../locales/workhub-live-copy.js';
 import type { WorkHubServices, WorkHubTranscript, WorkHubTranscriptSnapshot } from '../ports.js';
@@ -57,12 +56,11 @@ interface MessagePresentation {
   transientMessages: TransientUserMessageProjection[];
   messageQueue: { entries: MessageQueueEntryProjection[]; revision?: number };
 }
-export function useWorkHubController(onSubmit?: () => void) {
+export function useWorkHubController(sessionId: string | undefined, onSubmit?: () => void) {
   const services = useWorkHubServices();
   const locale = useUiLocale();
   const localeRef = useRef(locale);
   localeRef.current = locale;
-  const [sessionId, setSessionId] = useState<string>();
   const [sessions, setSessions] = useState<Awaited<ReturnType<WorkHubServices['listSessions']>>>(
     [],
   );
@@ -92,7 +90,6 @@ export function useWorkHubController(onSubmit?: () => void) {
   const [error, setError] = useState<string>();
   const [readError, setReadError] = useState<string>();
   const [readRevision, setReadRevision] = useState(0);
-  const retryResolution = useRef<() => void>(() => undefined);
   const refreshSessions = useRef<() => void>(() => undefined);
   const range = useRef<WorkHubTranscript | undefined>(undefined);
   const currentSessionId = useRef(sessionId);
@@ -205,26 +202,10 @@ export function useWorkHubController(onSubmit?: () => void) {
     }
   }
 
-  useEffect(
-    () =>
-      startWorkHubCoordinationLifecycle({
-        resolve: services.resolve,
-        subscribeHostChanges: services.subscribeHosts,
-        subscribeAvailabilityChanges: services.subscribeAvailability,
-        onResolving: () => {
-          currentSessionId.current = undefined;
-          setSessionId(undefined);
-          setStopPending(false);
-          setError(undefined);
-        },
-        onResolved: setSessionId,
-        reportFailure: (reason, action) => {
-          report(reason);
-          retryResolution.current = action;
-        },
-      }),
-    [services],
-  );
+  useEffect(() => {
+    setStopPending(false);
+    setError(undefined);
+  }, [sessionId]);
 
   useEffect(() => {
     let disposed = false;
@@ -616,7 +597,7 @@ export function useWorkHubController(onSubmit?: () => void) {
     sending,
     stopPending,
     error: readError ?? error,
-    canRetry: Boolean(readError || (!sessionId && error) || (error && (pendingSend.current?.admission === 'unknown' || pendingSend.current?.admission === 'rejected'))),
+    canRetry: Boolean(readError || (error && (pendingSend.current?.admission === 'unknown' || pendingSend.current?.admission === 'rejected'))),
     send,
     stop,
     changeModel,
@@ -634,7 +615,7 @@ export function useWorkHubController(onSubmit?: () => void) {
         void recoverSend();
       } else if (attempt && attempt.sessionId === sessionId && attempt.admission === 'rejected') {
         void send(attempt.input.text, attempt.input.attachments ?? []);
-      } else retryResolution.current();
+      }
     },
     loadEarlier: () => range.current?.loadEarlier(),
     report,
diff --git a/apps/desktop/src/renderer/features/workhub/index.ts b/apps/desktop/src/renderer/features/workhub/index.ts
index 4a6d7d3f6e..b05b79edde 100644
--- a/apps/desktop/src/renderer/features/workhub/index.ts
+++ b/apps/desktop/src/renderer/features/workhub/index.ts
@@ -31,5 +31,4 @@ 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 { WorkHubReturnButton } from './ui/return-button.js';
diff --git a/apps/desktop/src/renderer/features/workhub/ports.ts b/apps/desktop/src/renderer/features/workhub/ports.ts
index 163fcc4b2b..c54bdca20c 100644
--- a/apps/desktop/src/renderer/features/workhub/ports.ts
+++ b/apps/desktop/src/renderer/features/workhub/ports.ts
@@ -27,7 +27,6 @@ import type { OperationInput, OperationOutput } from '@maka/runtime-host/protoco
 import type { WorkHubAnswerInput, WorkHubAnswerResult } from '../../../shared/workhub-conversation.js';
 import type { WorkHubControlBridge } from '../../../shared/workhub-control.js';
 import type { WorkHubPresentationBridge } from '../../../shared/workhub-presentation.js';
-import type { WorkHubCoordinationHostChange } from '../../application/contracts/workhub-workspace/coordination-lifecycle.js';
 import type {
   WorkHubDelegationFeedback,
   WorkHubDelegationReference,
@@ -44,8 +43,6 @@ export interface WorkHubTranscript {
   close(): Promise<void>;
 }
 export interface WorkHubServices {
-  resolve(): Promise<string>;
-  subscribeHosts(handler: (event: WorkHubCoordinationHostChange) => void): () => void;
   subscribeAvailability(handler: () => void): () => void;
   readonly inspector: import('../../application/contracts/session-inspector/service.js').SessionInspectorService;
   readonly surface: 'main' | 'workhub';
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..131177ad84 100644
--- a/apps/desktop/src/renderer/features/workhub/ui/workhub-root.tsx
+++ b/apps/desktop/src/renderer/features/workhub/ui/workhub-root.tsx
@@ -70,9 +70,10 @@ function revealWordmark(element: HTMLDivElement | null, content: HTMLDivElement
   }
 }
 
-export function WorkHubRoot() {
+export function WorkHubRoot({ sessionId }: { sessionId: string | undefined }) {
   const highlight = useWorkHubHighlightState();
-  const controller = useWorkHubController(() => highlight.selectWork(undefined));
+  const locale = useUiLocale();
+  const controller = useWorkHubController(sessionId, () => highlight.selectWork(undefined));
   const { services, session, transcript, busy } = controller;
   useEffect(() => {
     services.bindBrowserSession(controller.sessionId ?? null);
@@ -84,7 +85,6 @@ export function WorkHubRoot() {
   const thinkingLevels = modelChoice?.thinkingLevels ?? [];
   const liveContextUsage = useLiveContextUsage({ inspector: services.inspector, sessionId: controller.sessionId, model: session?.model, providerType: modelChoice?.providerType });
   const thinkingLevel = session?.thinkingLevel && thinkingLevels.includes(session.thinkingLevel) ? session.thinkingLevel : undefined;
-  const locale = useUiLocale();
   const t = workHubLiveCopy[locale];
   const shortcutLabel = navigator.platform.toLowerCase().includes('mac') ? '⌘⇧K' : 'Ctrl+Shift+K';
   const composer = useRef<ComposerHandle>(null);
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..6045f53b1c 100644
--- a/apps/desktop/src/renderer/platform/desktop/create-workhub-services.ts
+++ b/apps/desktop/src/renderer/platform/desktop/create-workhub-services.ts
@@ -50,7 +50,6 @@ export function createDesktopWorkHubServices(
     | 'sessions'
     | 'transcripts'
     | 'connections'
-    | 'runtimeHostProfiles'
     | 'settings'
     | 'attachments'
   > = window.maka,
@@ -84,9 +83,7 @@ export function createDesktopWorkHubServices(
     },
     presentation: bridge.workHubPresentation,
     control: bridge.workHubControl,
-    resolve: () => bridge.workHub.resolveCoordinationSession(),
     getSession: (sessionId) => bridge.workHub.getSession(sessionId),
-    subscribeHosts: (handler) => bridge.runtimeHostProfiles.subscribeChanges(handler),
     subscribeAvailability: (handler) => bridge.connections.subscribeEvents(() => handler()),
     listSessions: () => bridge.sessions.list(),
     subscribeSessions: (handler) => bridge.sessions.subscribeChanges(handler),
diff --git a/apps/desktop/stories/workhub.stories.tsx b/apps/desktop/stories/workhub.stories.tsx
index 3ea802fd81..6cb2cc3061 100644
--- a/apps/desktop/stories/workhub.stories.tsx
+++ b/apps/desktop/stories/workhub.stories.tsx
@@ -95,7 +95,7 @@ function makeServices(failFirst: boolean, withHistory: boolean | 'usage', colore
     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: () => () => {},
+    subscribeAvailability: () => () => {},
     getSession: async () => session,
     listSessions: async () => coloredHistory ? [target, secondTarget] : [target], subscribeSessions: (handler) => { updateSessions = handler; return () => { updateSessions = undefined; }; }, modelChoices: async () => choices,
     delegationFeedback: async (references) => references.map(({ id }) => ({
@@ -168,7 +168,7 @@ function Surface({ failFirst = false, history = false, colors = false, selectTar
     if (progress) services.presentation.resizeProgress = async (_request, height) => { setProgressHeight(height); };
     return services;
   });
-  return <LocaleProvider locale="zh-CN"><AstryxLocaleProvider><ToastProvider><WorkHubServicesProvider services={services}><div style={{ height: progress ? progressHeight : '100dvh', width: progress ? 360 : undefined, maxWidth: '100%' }}><WorkHubRoot /></div></WorkHubServicesProvider></ToastProvider></AstryxLocaleProvider></LocaleProvider>;
+  return <LocaleProvider locale="zh-CN"><AstryxLocaleProvider><ToastProvider><WorkHubServicesProvider services={services}><div style={{ height: progress ? progressHeight : '100dvh', width: progress ? 360 : undefined, maxWidth: '100%' }}><WorkHubRoot sessionId={sessionId} /></div></WorkHubServicesProvider></ToastProvider></AstryxLocaleProvider></LocaleProvider>;
 }
 const meta = { title: 'Product/WorkHub', parameters: { layout: 'fullscreen' }, beforeEach: () => {
   Object.values(writes).forEach((spy) => spy.mockClear());
diff --git a/crates/runtime-host/src/plugins/workhub/client.tsx b/crates/runtime-host/src/plugins/workhub/client.tsx
index e46694b5c0..023b556df7 100644
--- a/crates/runtime-host/src/plugins/workhub/client.tsx
+++ b/crates/runtime-host/src/plugins/workhub/client.tsx
@@ -17,7 +17,8 @@
  * under the License.
  */
 
-import { useEffect } from 'react';
+import { useEffect, useState } from 'react';
+import { Button } from '@maka/ui/plugin';
 import type { ClientPlugin, ClientSlots } from '@maka-agent/plugin-sdk/client';
 
 type Resolution =
@@ -31,12 +32,15 @@ const plugin: ClientPlugin = {
       'session.resolve',
       'coordinator',
       function Coordinator(props: ClientSlots['session.resolve']) {
+        const [failed, setFailed] = useState(false);
+        const [retry, setRetry] = useState(0);
         useEffect(() => {
           props.onResolving();
           return props.onResolving;
         }, [props.onResolving]);
         useEffect(() => {
           const observation = new AbortController();
+          setFailed(false);
           void resolve(null)
             .then((outcome) => {
               if (observation.signal.aborted) return;
@@ -44,14 +48,24 @@ const plugin: ClientPlugin = {
               props.onResolved(outcome.result.sessionId, observation.signal);
             })
             .catch((error: unknown) => {
-              if (!observation.signal.aborted)
+              if (!observation.signal.aborted) {
+                setFailed(true);
                 props.onError(error instanceof Error ? error.message : String(error));
+              }
             });
           return () => {
             observation.abort();
           };
-        }, [props.contextRevision, props.onResolved, props.onError]);
-        return null;
+        }, [props.contextRevision, props.onResolved, props.onError, retry]);
+        return failed ? (
+          <Button
+            label={props.locale === 'en' ? 'Retry' : props.locale === 'zh-TW' ? '重試' : '重试'}
+            onClick={() => {
+              props.onResolving();
+              setRetry((current) => current + 1);
+            }}
+          />
+        ) : null;
       },
     );
   },
diff --git a/docs/rust-parity.md b/docs/rust-parity.md
index 79ca089200..2a88ef8a0f 100644
--- a/docs/rust-parity.md
+++ b/docs/rust-parity.md
@@ -57,7 +57,7 @@ Client changes are allowed when they remove an obsolete path, as with Graph.
 | Domain / current coupling | Target and completion condition |
 | --- | --- |
 | **Skills:** `maka.skills` owns discovery, input preparation, per-step tool/context snapshots, governance, preference CAS, preview, import and workspace/user publication. Published Client Contributions own Session/new-workspace pickers, management and draft suggestions. | Desktop supplies target-bound Slots, generic Remote transport and authorized native file actions. The old scanner, importer, controller and Skills IPC/preload facade are removed. Host retains thin external protocol ada [...]
-| **WorkHub:** `maka.workhub` owns coordinator configuration, answer composition, native `workhub_tasks`, routing/selection/correction/Stop/Resume workflows and recovery policy. Its Client resolves main-window Session bindings through Remote; backend retirement withdraws the Client and rejects stale calls. Desktop supplies window control and workspace/preferences, not task orchestration. | Migrate the full conversation UI and feedback; replace private Session-management rules and complet [...]
+| **WorkHub:** `maka.workhub` owns coordinator configuration, answer composition, native `workhub_tasks`, routing/selection/correction/Stop/Resume workflows and recovery policy. Its Client resolves main and floating-window Session bindings through Remote; backend retirement withdraws the Client and rejects stale calls. Desktop supplies window control and workspace/preferences, not task orchestration. | Migrate the full conversation UI and feedback; replace private Session-management rule [...]
 | **Default assistant behavior:** `maka.assistant` publishes the default behavior, persona, personalization and workspace instructions. | Sources are frozen per logical model step; disabling the plugin removes its persona. Explicit Session/child instructions remain independent of replaceable prompt Contributions. Execution/compaction invariants remain Host-owned. |
 | **Graph/Swarm and Scheduler:** already built-in plugins; behavior selection uses an open typed identity. Graph uses atomic activate/stop/retire-idle commands and read-only preferences, without `Executions`, configuration writers or Host locks. | Preserve existing orchestration and wakeup behavior. Reuse these narrow commands where semantics match; review Scheduler against the same ownership rule. Typed domain repositories remain legitimate. |
 | **Code Mode:** mode selection, nested dispatch and history projection cross several crates. | After the first domain migrations, move its user-facing tool and mode policy where a real contribution boundary helps. Keep V8 ownership, nested-call permissions, dispatch/settlement and canonical history in the runtime. Do not invent a universal executor hook merely to move `exec`. |
diff --git a/docs/rust-parity.zh-CN.md b/docs/rust-parity.zh-CN.md
index 7ad69162a7..1906334c64 100644
--- a/docs/rust-parity.zh-CN.md
+++ b/docs/rust-parity.zh-CN.md
@@ -50,7 +50,7 @@
 | 领域/当前耦合 | 目标与完成条件 |
 | --- | --- |
 | **Skills:**`maka.skills` 拥有发现、输入准备、每步工具/上下文快照、治理、偏好 CAS、预览、导入及 workspace/user 发布;已发布的 Client Contribution 拥有 Session/新工作区选择器、管理页与草稿建议。 | Desktop 提供目标绑定的 Slot、通用 Remote 传输及授权的原生文件操作;旧扫描、导入、控制器和 Skills IPC/preload 门面已删除。Host 仅保留薄外部协议适配、准入与不可变回执,不再解析 Skill。 |
-| **WorkHub:**`maka.workhub` 拥有协调会话配置、回答组合、原生 `workhub_tasks`、路由/选择/纠正/Stop/Resume 流程和恢复策略。Client 通过 Remote 解析主窗口 Session 绑定;后端退休撤下 Client 并拒绝旧代调用。Desktop 提供窗口控制与工作区/偏好,不再编排任务。 | 迁移完整会话 UI 和反馈,替换私有 Session 管理规则并补齐协调会话 steering/followup。保留精确消息定位、原子准入/取消/回执,以及 Host 对已接受操作的结算。 |
+| **WorkHub:**`maka.workhub` 拥有协调会话配置、回答组合、原生 `workhub_tasks`、路由/选择/纠正/Stop/Resume 流程和恢复策略。Client 通过 Remote 解析主窗口与浮动窗口 Session 绑定;后端退休撤下 Client 并拒绝旧代调用。Desktop 提供窗口控制与工作区/偏好,不再编排任务。 | 迁移完整会话 UI 和反馈,替换私有 Session 管理规则并补齐协调会话 steering/followup。保留精确消息定位、原子准入/取消/回执,以及 Host 对已接受操作的结算。 |
 | **默认助手行为:**`maka.assistant` 发布默认 behavior、persona、个性化和工作区指令。 | 每逻辑模型步骤冻结来源;停用后不保留隐藏 persona。显式 Session/子任务指令独立于可替换的提示词 Contribution;执行/压缩不变量仍由 Host 维护。 |
 | **Graph/Swarm 与 Scheduler:**已经是内置插件,behavior 按开放的类型化身份选择。Graph 使用原子激活/停止/空闲退休命令及只读偏好,不接收 `Executions`、配置写入器或 Host 锁。 | 保持已有编排与唤醒行为;语义相同时复用这些窄命令,按同一归属规则复核 Scheduler。类型化领域 repository 可以保留。 |
 | **Code Mode:**模式选择、嵌套派发及历史投影跨越多个 crate。 | 首批领域迁移后,在有实际 Contribution 边界收益时迁移面向用户的工具和模式策略;V8 所有权、嵌套调用权限、派发/结算及规范历史保留 runtime。不为搬迁 `exec` 发明万能执行 hook。 |
diff --git a/packages/plugin-sdk/README.md b/packages/plugin-sdk/README.md
index 84888a5204..0d7315d807 100644
--- a/packages/plugin-sdk/README.md
+++ b/packages/plugin-sdk/README.md
@@ -77,6 +77,8 @@ Slots include `session.composer.before`, `workspace.composer.before` and `worksp
 
 Optional `ctx.localFiles.pick()` / `open(path)` use Desktop-local paths only. They are unavailable for remote Host files. Desktop validates the published Client identity before native actions and discards picker results after navigation or retirement.
 
+Desktop supplies `@maka/ui/plugin` as a shared UI module (currently `Button`). Import supported components from this entry instead of bundling another component-library instance. It is not the internal UI package's complete API.
+
 Host plugins publish `ctx.remote.method(name, callback)` or `ctx.remote.stream(name, open)`. Client plugins obtain a callable with `ctx.remote.method<Input, Output>(name, sessionId?)` or an async-iterable factory with `ctx.remote.stream<Input, Output>(name, sessionId?)`. Calls start only after UI publication. Handles retain their original Host connection and backend registration; replacement never redirects them. Breaking iteration closes its stream; retiring UI or navigating closes its  [...]
 
 Native endpoints accepting caller-supplied Host paths declare `Endpoint::requiring_host_paths()`. Host checks that grant at both bind and call, even for a borrowed registration target. Project-ID and existing-Session queries do not require raw-path authority; plugins receive explicitly injected read-only views.
diff --git a/packages/plugin-sdk/README.zh-CN.md b/packages/plugin-sdk/README.zh-CN.md
index 305629a226..fee65e281a 100644
--- a/packages/plugin-sdk/README.zh-CN.md
+++ b/packages/plugin-sdk/README.zh-CN.md
@@ -78,6 +78,8 @@ Slot 包括 `session.composer.before`、`workspace.composer.before` 和 `workspa
 
 可选的 `ctx.localFiles.pick()` / `open(path)` 仅处理 Desktop 本地路径,不用于远程 Host 文件。Desktop 在原生操作前校验 Client 发布身份,导航或退休后返回的文件选择结果会被丢弃。
 
+Desktop 通过 `@maka/ui/plugin` 提供共享 UI 模块(目前为 `Button`)。使用该入口支持的组件,不再打包一份组件库实例;它不暴露内部 UI 包的完整 API。
+
 Host 插件通过 `ctx.remote.method(name, callback)` 或 `ctx.remote.stream(name, open)` 发布接口。Client 插件通过 `ctx.remote.method<Input, Output>(name, sessionId?)` 获取调用函数,或通过 `ctx.remote.stream<Input, Output>(name, sessionId?)` 获取异步迭代器工厂。UI 发布后才能调用;句柄固定到原 Host 连接和后端注册,不随替换重定向。退出迭代会关闭流,UI 卸载或页面导航会关闭所属文档。Remote 调用不是 Agent 调用,不隐含进程权限。
 
 接受调用者 Host 路径的 Rust endpoint 声明 `Endpoint::requiring_host_paths()`。Host 在绑定和调用时都检查路径授权,借用其他连接的注册目标也不能绕过。项目 ID 和已有 Session 查询不要求原始路径权限;插件通过显式注入的只读视图访问它们。
diff --git a/packages/plugin-sdk/src/build.ts b/packages/plugin-sdk/src/build.ts
index 78c40ce0aa..2a5978f1e7 100644
--- a/packages/plugin-sdk/src/build.ts
+++ b/packages/plugin-sdk/src/build.ts
@@ -44,7 +44,13 @@ export async function buildClient(
     target: 'es2022',
     jsx: 'automatic',
     legalComments: 'inline',
-    external: ['react', 'react/jsx-runtime', '@maka-agent/plugin-sdk/client', ...dependencies],
+    external: [
+      'react',
+      'react/jsx-runtime',
+      '@maka-agent/plugin-sdk/client',
+      '@maka/ui/plugin',
+      ...dependencies,
+    ],
     banner: {
       js:
         'window.__MakaClientBundle__({id:' +
diff --git a/packages/ui/package.json b/packages/ui/package.json
index 0343b94fa5..e35dab8eeb 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -7,6 +7,7 @@
   "main": "./dist/index.js",
   "types": "./dist/index.d.ts",
   "exports": {
+    "./plugin": "./dist/plugin.js",
     "./client-plugins": "./dist/client-plugins/index.js",
     ".": "./dist/index.js",
     "./artifact-preview-registry": "./dist/artifact-preview-registry.js",
diff --git a/apps/desktop/src/renderer/composition/legacy-desktop-region.tsx b/packages/ui/src/plugin.ts
similarity index 69%
copy from apps/desktop/src/renderer/composition/legacy-desktop-region.tsx
copy to packages/ui/src/plugin.ts
index 2baeb0fc86..e9d091c853 100644
--- a/apps/desktop/src/renderer/composition/legacy-desktop-region.tsx
+++ b/packages/ui/src/plugin.ts
@@ -17,9 +17,5 @@
  * under the License.
  */
 
-import type { ComponentProps } from 'react';
-import { AppShell as LegacyAppShell } from '../app-shell';
-import { WorkHubRoot, WorkHubSurfaceSwitch } from '../features/workhub';
-export function AppShell(props: ComponentProps<typeof LegacyAppShell>) {
-  return <WorkHubSurfaceSwitch main={<LegacyAppShell {...props} />} workhub={<WorkHubRoot />} />;
-}
+/** Shared Renderer components supported for trusted Client bundles. */
+export { Button } from '@astryxdesign/core/Button';


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