diff --git a/package.json b/package.json index b2f4d33..618fc9c 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "@deepseek-ai/dsh-client-locale", "@deepseek-ai/dsh-client-ui-renderer", "@deepseek-ai/dsh-client-ui-conversation", + "@deepseek-ai/dsh-client-ui-workspace", "@deepseek-ai/dsh-client-modules" ] } diff --git a/plugin.client.js b/plugin.client.js index 5265408..51c239e 100644 --- a/plugin.client.js +++ b/plugin.client.js @@ -408,25 +408,26 @@ function ringFor(versions, sessionId, turn) { * target is activated through the host route before opening. Ghosts (deleted * versions) are never openable. */ -async function openVersionTarget(sessions, v) { - if (!v || v.deleted || !sessions) return; +async function openVersionTarget(navigation, v) { + if (!v || v.deleted || !navigation) return; if (v.archived) { try { await mutate({ action: 'activate', sessionId: v.sessionId }); treeStore.invalidate(); } catch (e) {} } - openWhenListed(sessions, v.sessionId); + openWhenListed(navigation, v.sessionId); } -function openWhenListed(sessions, sessionId) { - const list = sessions.list; - if (!list || typeof list.getSnapshot !== 'function') { sessions.open(sessionId); return; } - if (list.getSnapshot().byId[sessionId] !== undefined) { sessions.open(sessionId); return; } +function openWhenListed(navigation, sessionId) { + const list = navigation.list; + const open = navigation.openVersion; + if (!list || typeof list.getSnapshot !== 'function') { open(sessionId); return; } + if (list.getSnapshot().byId[sessionId] !== undefined) { open(sessionId); return; } const stop = list.subscribe(function () { if (list.getSnapshot().byId[sessionId] !== undefined) { stop(); - sessions.open(sessionId); + open(sessionId); } }); } @@ -853,7 +854,7 @@ const CSS = [ return { // Module dependencies load code; Cordis injection waits for its services. // The session controller becomes ready asynchronously after connection. - inject: ['slots', 'sessions', 'locale'], + inject: ['slots', 'sessions', 'locale', 'uiWorkspace'], apply(ctx) { const slots = ctx.get('slots'); if (slots === undefined) { @@ -864,9 +865,12 @@ return { ctx.effect(function () { syncStyleAttribute(); return styleStore.subscribe(syncStyleAttribute); }); const sessions = ctx.get('sessions'); - if (!sessions || typeof sessions.open !== 'function') { - throw new Error('[dsh-plugin-message-edit] Missing DSH session navigation service. Check client dependencies and restart DSH.'); + const uiWorkspace = ctx.get('uiWorkspace'); + if (!sessions || !sessions.list || !uiWorkspace || typeof uiWorkspace.openSession !== 'function') { + throw new Error('[dsh-plugin-message-edit] Missing DSH sessions or uiWorkspace navigation service. Check client dependencies and restart DSH.'); } + // DSH 0.2 keeps catalog data in sessions and navigation in uiWorkspace. + const navigation = { list: sessions.list, openVersion: id => uiWorkspace.openSession(id) }; ctx.effect(function () { if (sessions && sessions.list && typeof sessions.list.subscribe === 'function') { @@ -1015,7 +1019,7 @@ return { if (!ring) return null; const go = function (delta) { const next = ring.alternatives[ring.index + delta]; - if (next) openVersionTarget(sessions, next); + if (next) openVersionTarget(navigation, next); }; return React.createElement('div', { className: 'mtx-ring' }, React.createElement('button', { @@ -1082,8 +1086,8 @@ return { if (!target || target.deleted) return; restoredFamilies.add(root); pendingRestore.add(root); - openVersionTarget(sessions, target); - }, [versions, sessionId, sessions, prefs.rememberPath]); + openVersionTarget(navigation, target); + }, [versions, sessionId, navigation, prefs.rememberPath]); const [editing, setEditing] = React.useState(false); const [draft, setDraft] = React.useState(''); @@ -1136,7 +1140,7 @@ return { } treeStore.load(result.sessionId); setEditing(false); - if (sessions) openWhenListed(sessions, result.sessionId); + openWhenListed(navigation, result.sessionId); } catch (e) { setError(String(e && e.message || e)); } @@ -1167,7 +1171,7 @@ return { treeStore.setTree(result.sessionId, currentTree.versions.concat([newV])); } treeStore.load(result.sessionId); - if (sessions) openWhenListed(sessions, result.sessionId); + openWhenListed(navigation, result.sessionId); } catch (e) { setError(String(e && e.message || e)); } @@ -1417,7 +1421,7 @@ return { if (!node || node.deleted || !sessions) return; const v = versions.find(function (item) { return item.sessionId === node.sessionId; }); if (!v) return; - openVersionTarget(sessions, v); + openVersionTarget(navigation, v); showChat(); if (typeof node.turn === 'number' && node.turn > 0) flashTurn(node.sessionId, node.turn, 45); } diff --git a/test/client-images.test.mjs b/test/client-images.test.mjs index 8888c5c..24e4648 100644 --- a/test/client-images.test.mjs +++ b/test/client-images.test.mjs @@ -65,10 +65,9 @@ async function mount(t, content, renderMessageImages, options = {}) { }, { filename: 'lib/client.js' }); plugin.apply({ get(name) { return name === 'slots' ? slots : name === 'sessions' ? { - open: options.open ?? (() => {}), list: { subscribe: () => () => {}, getSnapshot: () => ({ byId: Object.fromEntries( ['session-test', ...(options.versions ?? []).map(version => version.sessionId)].map(id => [id, { id }])) }) }, - } : undefined; }, + } : name === 'uiWorkspace' ? { openSession: options.open ?? (() => {}) } : undefined; }, effect(fn) { const dispose = fn(); if (typeof dispose === 'function') disposers.push(dispose); }, }); assert.equal(typeof component, 'function', 'The user-message slot must register'); diff --git a/test/client-registration.test.mjs b/test/client-registration.test.mjs index 3dccaf5..9b58610 100644 --- a/test/client-registration.test.mjs +++ b/test/client-registration.test.mjs @@ -28,7 +28,9 @@ test('declared client dependencies make the UI services available for registrati }; plugin.apply({ get: name => name === 'slots' && pkg.dsh.client.inject.includes('@deepseek-ai/dsh-client-ui-renderer') ? slots - : name === 'sessions' && pkg.dsh.client.inject.includes('@deepseek-ai/dsh-api-session-controller') ? { open() {} } : undefined, + : name === 'sessions' && pkg.dsh.client.inject.includes('@deepseek-ai/dsh-api-session-controller') ? { list: {} } + : name === 'uiWorkspace' && pkg.dsh.client.inject.includes('@deepseek-ai/dsh-client-ui-workspace') ? { openSession() {} } + : undefined, effect: fn => fn(), }); assert.deepEqual(registered, ['settings.section', 'conversation.chat.node', 'conversation.view']); @@ -39,7 +41,17 @@ test('a missing slots service fails visibly instead of silently disabling the mo assert.throws(() => plugin.apply({ get: () => undefined }), /Missing DSH slots service.*inject/); }); -test('Cordis waits for the asynchronous session service before applying the client', async t => { +test('a missing workspace navigation owner fails visibly', () => { + const { plugin } = load(); + const slots = { inject() {} }; + const sessions = { list: {} }; // DSH 0.2 sessions intentionally has no open() method. + assert.throws(() => plugin.apply({ + get: name => name === 'slots' ? slots : name === 'sessions' ? sessions : undefined, + effect: fn => fn(), + }), /Missing DSH sessions or uiWorkspace navigation service/); +}); + +test('Cordis waits for both session data and workspace navigation before applying the client', async t => { const { plugin } = load(); const ctx = new Context(); t.after(() => ctx.fiber.dispose()); @@ -53,7 +65,11 @@ test('Cordis waits for the asynchronous session service before applying the clie await new Promise(resolve => setImmediate(resolve)); assert.equal(fiber.state, 0, 'Client remains pending until the session controller is ready'); assert.deepEqual(registered, []); - ctx.provide('sessions', { open() {} }); + ctx.provide('sessions', { list: {} }); + await new Promise(resolve => setImmediate(resolve)); + assert.equal(fiber.state, 0, 'Client also waits for the workspace navigation owner'); + assert.deepEqual(registered, []); + ctx.provide('uiWorkspace', { openSession() {} }); await fiber; assert.equal(fiber.state, 2); assert.deepEqual(registered, ['settings.section', 'conversation.chat.node', 'conversation.view']); @@ -70,7 +86,9 @@ test('a user-renderer collision is reported while the other UI entries still reg registered.push(spec.name); return () => {}; }, - } : name === 'sessions' ? { open() {} } : undefined, + } : name === 'sessions' ? { list: {} } + : name === 'uiWorkspace' ? { openSession() {} } + : undefined, effect: fn => fn(), }); assert.deepEqual(registered, ['settings.section', 'conversation.view']);