Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
}
Expand Down
38 changes: 21 additions & 17 deletions plugin.client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
});
}
Expand Down Expand Up @@ -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) {
Expand All @@ -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') {
Expand Down Expand Up @@ -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', {
Expand Down Expand Up @@ -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('');
Expand Down Expand Up @@ -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));
}
Expand Down Expand Up @@ -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));
}
Expand Down Expand Up @@ -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);
}
Expand Down
3 changes: 1 addition & 2 deletions test/client-images.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
26 changes: 22 additions & 4 deletions test/client-registration.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
Expand All @@ -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());
Expand All @@ -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']);
Expand All @@ -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']);
Expand Down
Loading