@mars.liu/dsh-cybernetics 0.2.0 → 0.7.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +133 -2
- package/cordis.patch.yml +18 -1
- package/docs/issue-browser-connection-pool.md +79 -0
- package/docs/listBundles-hang-investigation-20261103.md +93 -0
- package/docs/protocol.md +253 -3
- package/lib/client.js +517 -15
- package/package.json +18 -2
- package/src/controller.ts +193 -3
- package/src/executor.ts +414 -0
- package/src/index.ts +776 -49
- package/src/projection.ts +763 -0
- package/src/upstream.d.ts +22 -0
- package/workflows/round.js +101 -1
- package/test/v03-offline.mjs +0 -203
- package/tsconfig.json +0 -16
package/lib/client.js
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
|
-
// dsh-cybernetics client module: the「目标 / Goal」conversation tab
|
|
1
|
+
// dsh-cybernetics client module: the「目标 / Goal」conversation tab, the
|
|
2
|
+
//「控制」tab, and (v0.7.2+, fixed in v0.7.3) the allGoalsMode settings card
|
|
3
|
+
// seated on the official Plugins page (plugins.item / plugins.row.config /
|
|
4
|
+
// plugins.bundle.config, bound to the row's ConfigForm in either of the two
|
|
5
|
+
// shapes the seats deliver — volatile writes stay live without a restart).
|
|
2
6
|
// Rendered from the session's `goal` projection (dsh-goal), pushed live by the
|
|
3
7
|
// host — no plugin HTTP route needed. Hand-written ModuleLoader wrapper, same
|
|
4
8
|
// format the host bundler emits (see dsh-context / dsh-maze client.js).
|
|
5
9
|
/* global window */
|
|
10
|
+
// Registration id = the package name in package.json, NOT the short plugin row
|
|
11
|
+
// id: the client-modules boot graph keys every row by the package manifest name
|
|
12
|
+
// (`@mars.liu/dsh-cybernetics`) and only materializes a row whose bundle
|
|
13
|
+
// registered under exactly that id — a stale `dsh-cybernetics` here executes
|
|
14
|
+
// the batch script fine, then reports "loaded without registering".
|
|
6
15
|
window.__ModuleLoader__.load({
|
|
7
|
-
id: 'dsh-cybernetics',
|
|
16
|
+
id: '@mars.liu/dsh-cybernetics',
|
|
8
17
|
factory: (require) => {
|
|
9
18
|
var module = { exports: {} };
|
|
10
19
|
var exports = module.exports;
|
|
@@ -35,6 +44,44 @@ window.__ModuleLoader__.load({
|
|
|
35
44
|
'agent.noData': '目标数据未同步',
|
|
36
45
|
'agents.empty': '本会话当前没有子 agent。',
|
|
37
46
|
'agents.count': '{running}/{total} 个 agent 运行中',
|
|
47
|
+
// v0.6 multi-goal projection (`cyberneticsGoals`) — every goal of the
|
|
48
|
+
// session in one tab, updated in place.
|
|
49
|
+
'goals.count': '{count} 个目标',
|
|
50
|
+
'goal.round': '第 {round} 轮',
|
|
51
|
+
'goal.subgoal': '当前子目标',
|
|
52
|
+
'goal.depth.quick': '轻量',
|
|
53
|
+
'goal.depth.full': '完整',
|
|
54
|
+
'goal.executor.ephemeral': '临时执行者',
|
|
55
|
+
'goal.executor.persistent': '持久执行者',
|
|
56
|
+
'goalStatus.pending': '待启动',
|
|
57
|
+
'goalStatus.running': '进行中',
|
|
58
|
+
'goalStatus.paused': '已暂停',
|
|
59
|
+
'goalStatus.met': '已完成',
|
|
60
|
+
'goalStatus.cancelled': '已取消',
|
|
61
|
+
'goalStatus.superseded': '已被取代',
|
|
62
|
+
'goalStatus.failed': '受阻',
|
|
63
|
+
// v0.6 append-only goal lineage lines (server sets these fields)
|
|
64
|
+
'goal.supersededBy': '已被「{title}」取代',
|
|
65
|
+
'goal.revisedFrom': '修订自「{title}」',
|
|
66
|
+
// v0.6 executor line: the durable teammate's name (persistent only)
|
|
67
|
+
'goal.member': '成员 {name}',
|
|
68
|
+
'control.goals': '目标',
|
|
69
|
+
// v0.7.2 plugins-page settings card (plugins.item / plugins.row.config /
|
|
70
|
+
// plugins.bundle.config). cardLabel titles the 内置插件配置 group card.
|
|
71
|
+
'settings.cardLabel': 'Cybernetics 编排循环',
|
|
72
|
+
'settings.title': '全目标模式',
|
|
73
|
+
'settings.description': '所有用户请求(含只读问题)必须先经 cybernetics goal。保存后即时生效,无需重载。',
|
|
74
|
+
'settings.on': '开',
|
|
75
|
+
'settings.off': '关',
|
|
76
|
+
'settings.summaryOn': '全目标模式:开',
|
|
77
|
+
'settings.summaryOff': '全目标模式:关',
|
|
78
|
+
'settings.unavailable': '配置不可用(设置文档未开放或插件未运行)。',
|
|
79
|
+
'settings.saving': '保存中…',
|
|
80
|
+
'settings.saveFailed': '保存未生效(宿主拒绝了写入)。',
|
|
81
|
+
// v0.7.6 path-(b) diagnostics: the direct settings-transport retry
|
|
82
|
+
// surfaces the host's exact RemoteError message (the shared
|
|
83
|
+
// ConfigFormController swallows it into a bare `false`).
|
|
84
|
+
'settings.saveFailedDetail': '保存未生效,宿主拒绝原因:{message}',
|
|
38
85
|
};
|
|
39
86
|
const en = {
|
|
40
87
|
'view.goal': 'Goal',
|
|
@@ -57,6 +104,35 @@ window.__ModuleLoader__.load({
|
|
|
57
104
|
'agent.noData': 'Goal data not synced',
|
|
58
105
|
'agents.empty': 'No child agents in this session yet.',
|
|
59
106
|
'agents.count': '{running}/{total} agents running',
|
|
107
|
+
'goals.count': '{count} goals',
|
|
108
|
+
'goal.round': 'round {round}',
|
|
109
|
+
'goal.subgoal': 'Current subgoal',
|
|
110
|
+
'goal.depth.quick': 'Quick',
|
|
111
|
+
'goal.depth.full': 'Full',
|
|
112
|
+
'goal.executor.ephemeral': 'Ephemeral executor',
|
|
113
|
+
'goal.executor.persistent': 'Persistent executor',
|
|
114
|
+
'goalStatus.pending': 'Pending',
|
|
115
|
+
'goalStatus.running': 'Running',
|
|
116
|
+
'goalStatus.paused': 'Paused',
|
|
117
|
+
'goalStatus.met': 'Complete',
|
|
118
|
+
'goalStatus.cancelled': 'Cancelled',
|
|
119
|
+
'goalStatus.superseded': 'Superseded',
|
|
120
|
+
'goalStatus.failed': 'Blocked',
|
|
121
|
+
'goal.supersededBy': 'Superseded by "{title}"',
|
|
122
|
+
'goal.revisedFrom': 'Revised from "{title}"',
|
|
123
|
+
'goal.member': 'Member {name}',
|
|
124
|
+
'control.goals': 'Goals',
|
|
125
|
+
'settings.cardLabel': 'Cybernetics goal loop',
|
|
126
|
+
'settings.title': 'All-goals mode',
|
|
127
|
+
'settings.description': 'Every user request (including read-only questions) must go through a cybernetics goal first. Takes effect immediately on save; no reload needed.',
|
|
128
|
+
'settings.on': 'On',
|
|
129
|
+
'settings.off': 'Off',
|
|
130
|
+
'settings.summaryOn': 'All-goals mode: on',
|
|
131
|
+
'settings.summaryOff': 'All-goals mode: off',
|
|
132
|
+
'settings.unavailable': 'Configuration unavailable (settings document not served or plugin not running).',
|
|
133
|
+
'settings.saving': 'Saving…',
|
|
134
|
+
'settings.saveFailed': 'Save did not take effect (the host refused the write).',
|
|
135
|
+
'settings.saveFailedDetail': 'Save did not take effect — host refusal: {message}',
|
|
60
136
|
};
|
|
61
137
|
//#endregion
|
|
62
138
|
|
|
@@ -68,12 +144,104 @@ window.__ModuleLoader__.load({
|
|
|
68
144
|
};
|
|
69
145
|
|
|
70
146
|
//#region goal view component
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
|
|
147
|
+
// v0.6: the tab is driven by the plugin's own `cyberneticsGoals`
|
|
148
|
+
// projection — the WHOLE multi-goal board of this session, pushed live
|
|
149
|
+
// by the host (one whole-value `cybernetics/goals` event per change, no
|
|
150
|
+
// polling). When that key is absent (older plugin/host) or carries an
|
|
151
|
+
// empty board, fall back to dsh-goal's single-slot `goal` projection,
|
|
152
|
+
// which is exactly the pre-v0.6 rendering.
|
|
153
|
+
const GOAL_STATUS_KEY = {
|
|
154
|
+
pending: 'goalStatus.pending',
|
|
155
|
+
running: 'goalStatus.running',
|
|
156
|
+
paused: 'goalStatus.paused',
|
|
157
|
+
met: 'goalStatus.met',
|
|
158
|
+
cancelled: 'goalStatus.cancelled',
|
|
159
|
+
superseded: 'goalStatus.superseded',
|
|
160
|
+
failed: 'goalStatus.failed',
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
function goalStatusStyle(status) {
|
|
164
|
+
if (status === 'running') return { color: '#3fb950' };
|
|
165
|
+
if (status === 'failed') return { color: '#d29922' };
|
|
166
|
+
if (status === 'met') return { color: '#58a6ff' };
|
|
167
|
+
return { color: '#8b949e' };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// End-of-life statuses: the same two-segment rule the goal tab uses
|
|
171
|
+
// (unfinished first, finished after). The server already sorts that way;
|
|
172
|
+
// this stable partition keeps the invariant even if a view arrives with a
|
|
173
|
+
// different order, without re-sorting anything inside a segment.
|
|
174
|
+
const COMPLETED_GOAL_STATUSES = ['met', 'cancelled', 'superseded'];
|
|
175
|
+
function groupGoals(goals) {
|
|
176
|
+
const open = [];
|
|
177
|
+
const done = [];
|
|
178
|
+
for (const g of goals) {
|
|
179
|
+
if (COMPLETED_GOAL_STATUSES.indexOf(g.status) >= 0) done.push(g);
|
|
180
|
+
else open.push(g);
|
|
181
|
+
}
|
|
182
|
+
return open.concat(done);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// One goal row: title + status badge + round + current subgoal + tags.
|
|
186
|
+
// Append-only lineage (v0.6): a superseded goal names its successor, a
|
|
187
|
+
// revised goal names where it came from — titles resolved from the board
|
|
188
|
+
// snapshot, falling back to the goalId when the successor is not listed.
|
|
189
|
+
function GoalRow(props) {
|
|
75
190
|
const t = props.t;
|
|
76
|
-
const
|
|
191
|
+
const g = props.goal;
|
|
192
|
+
const titles = props.titles || {};
|
|
193
|
+
const statusKey = GOAL_STATUS_KEY[g.status] ?? 'goalStatus.pending';
|
|
194
|
+
const depthKey = g.depth === 'quick' ? 'goal.depth.quick' : 'goal.depth.full';
|
|
195
|
+
const executorKey = g.executor === 'persistent' ? 'goal.executor.persistent' : 'goal.executor.ephemeral';
|
|
196
|
+
return jsx.jsxs('div', { style: styles.goalCard, children: [
|
|
197
|
+
jsx.jsxs('div', { style: styles.goalHead, children: [
|
|
198
|
+
jsx.jsx('span', { style: styles.goalTitle, children: g.title || g.goalId }),
|
|
199
|
+
jsx.jsx('span', { style: { ...styles.badge, ...goalStatusStyle(g.status) }, children: t(statusKey) }),
|
|
200
|
+
] }),
|
|
201
|
+
jsx.jsxs('div', { style: styles.goalMetaRow, children: [
|
|
202
|
+
jsx.jsx('span', { style: styles.meta, children: t('goal.round', { round: g.round ?? 0 }) }),
|
|
203
|
+
jsx.jsx('span', { style: styles.tag, children: t(depthKey) }),
|
|
204
|
+
jsx.jsx('span', { style: styles.tag, children: t(executorKey) }),
|
|
205
|
+
g.memberName
|
|
206
|
+
? jsx.jsx('span', { style: styles.tag, children: t('goal.member', { name: g.memberName }) })
|
|
207
|
+
: null,
|
|
208
|
+
] }),
|
|
209
|
+
g.supersededBy
|
|
210
|
+
? jsx.jsx('div', { style: styles.lineage, children: t('goal.supersededBy', { title: titles[g.supersededBy] || g.supersededBy }) })
|
|
211
|
+
: null,
|
|
212
|
+
g.parentGoalId
|
|
213
|
+
? jsx.jsx('div', { style: styles.lineage, children: t('goal.revisedFrom', { title: titles[g.parentGoalId] || g.parentGoalId }) })
|
|
214
|
+
: null,
|
|
215
|
+
g.subgoal
|
|
216
|
+
? jsx.jsxs('div', { style: styles.goalSubgoal, children: [t('goal.subgoal') + ':', g.subgoal] })
|
|
217
|
+
: null,
|
|
218
|
+
] });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Multi-goal list. Order is the projection's (server-side two segments:
|
|
222
|
+
// unfinished goals first, finished after; newest change first inside each),
|
|
223
|
+
// so the client renders verbatim and never re-sorts.
|
|
224
|
+
function GoalsList(props) {
|
|
225
|
+
const t = props.t;
|
|
226
|
+
const goals = props.goals;
|
|
227
|
+
const titles = {};
|
|
228
|
+
for (const g of goals) titles[g.goalId] = g.title || g.goalId;
|
|
229
|
+
return jsx.jsxs('div', { style: styles.root, children: [
|
|
230
|
+
jsx.jsx('div', { style: styles.badgeRow, children:
|
|
231
|
+
jsx.jsx('span', { style: styles.badge, children: t('goals.count', { count: goals.length }) }) }),
|
|
232
|
+
...goals.map(function (g) {
|
|
233
|
+
return jsx.jsx(GoalRow, { goal: g, t: t, titles: titles }, g.goalId + ':' + (g.updatedAt ?? 0));
|
|
234
|
+
}),
|
|
235
|
+
jsx.jsx('div', { style: styles.hint, children: t('loopHint') }),
|
|
236
|
+
] });
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// props.raw: the session `goal` projection snapshot value. It may be the
|
|
240
|
+
// raw state ({current: GoalProjection | null, ...}) or the projection
|
|
241
|
+
// value itself — normalize both; undefined = not yet arrived.
|
|
242
|
+
function SingleGoalView(props) {
|
|
243
|
+
const t = props.t;
|
|
244
|
+
const raw = props.raw;
|
|
77
245
|
if (raw === undefined) return null;
|
|
78
246
|
const goal = raw !== null && typeof raw === 'object' && 'current' in raw ? raw.current : raw;
|
|
79
247
|
if (goal === null || goal === undefined) {
|
|
@@ -93,6 +261,16 @@ window.__ModuleLoader__.load({
|
|
|
93
261
|
] });
|
|
94
262
|
}
|
|
95
263
|
|
|
264
|
+
function GoalView(props) {
|
|
265
|
+
const goalsRaw = react.useSyncExternalStore(props.goals.subscribe, props.goals.getSnapshot);
|
|
266
|
+
const singleRaw = react.useSyncExternalStore(props.goal.subscribe, props.goal.getSnapshot);
|
|
267
|
+
const goals = goalsRaw !== null && typeof goalsRaw === 'object' && Array.isArray(goalsRaw.goals)
|
|
268
|
+
? goalsRaw.goals
|
|
269
|
+
: [];
|
|
270
|
+
if (goals.length > 0) return jsx.jsx(GoalsList, { t: props.t, goals: goals });
|
|
271
|
+
return jsx.jsx(SingleGoalView, { t: props.t, raw: singleRaw });
|
|
272
|
+
}
|
|
273
|
+
|
|
96
274
|
var styles = {
|
|
97
275
|
root: { display: 'flex', flexDirection: 'column', gap: 8, padding: '12px 16px', overflow: 'auto' },
|
|
98
276
|
empty: { padding: '12px 16px', opacity: 0.6 },
|
|
@@ -102,6 +280,17 @@ window.__ModuleLoader__.load({
|
|
|
102
280
|
blocked: { color: '#d29922', fontSize: 12 },
|
|
103
281
|
meta: { opacity: 0.65, fontSize: 12 },
|
|
104
282
|
hint: { opacity: 0.45, fontSize: 11 },
|
|
283
|
+
// v0.6 multi-goal rows
|
|
284
|
+
goalCard: { display: 'flex', flexDirection: 'column', gap: 4, padding: '8px 10px', border: '1px solid rgba(127,127,127,0.25)', borderRadius: 6 },
|
|
285
|
+
goalHead: { display: 'flex', alignItems: 'center', gap: 6, minWidth: 0 },
|
|
286
|
+
goalTitle: { fontSize: 13, fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
|
|
287
|
+
goalMetaRow: { display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' },
|
|
288
|
+
tag: { fontSize: 10, padding: '0 6px', borderRadius: 999, border: '1px solid rgba(127,127,127,0.35)', opacity: 0.75 },
|
|
289
|
+
goalSubgoal: { fontSize: 12, opacity: 0.85, whiteSpace: 'pre-wrap', lineHeight: 1.45 },
|
|
290
|
+
lineage: { fontSize: 11, opacity: 0.6, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
|
|
291
|
+
// v0.6 control tab executor rows
|
|
292
|
+
controlGoalRow: { display: 'flex', alignItems: 'center', gap: 6, minWidth: 0, flexWrap: 'wrap', padding: '2px 0' },
|
|
293
|
+
goalCards: { display: 'flex', flexDirection: 'column', gap: 4, marginBottom: 4 },
|
|
105
294
|
agentRow: { borderLeft: '2px solid transparent', borderRadius: 4 },
|
|
106
295
|
agentMain: { display: 'flex', flexDirection: 'column', gap: 2 },
|
|
107
296
|
agentHead: { display: 'flex', alignItems: 'center', gap: 6, minWidth: 0 },
|
|
@@ -122,13 +311,39 @@ window.__ModuleLoader__.load({
|
|
|
122
311
|
//#endregion
|
|
123
312
|
|
|
124
313
|
//#region control view component
|
|
125
|
-
// The「控制 / Control」tab: every
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
314
|
+
// The「控制 / Control」tab: every goal of the session with its EXECUTOR
|
|
315
|
+
// line (substrate + the durable teammate's name when it runs on one),
|
|
316
|
+
// plus every agent of the current session — the lead itself and the
|
|
317
|
+
// subagent catalog (recursively) — with its run state and its own durable
|
|
318
|
+
// goal (dsh-goal projection values). Goal data comes from the same
|
|
319
|
+
// `cyberneticsGoals` whole-value projection the goal tab reads; the agent
|
|
320
|
+
// tree comes from the session list store, updated live by the Session
|
|
321
|
+
// Controller via the control stream (running flips, goal/control frames).
|
|
130
322
|
const MAX_TREE_DEPTH = 3;
|
|
131
323
|
|
|
324
|
+
// No goal face (older host/plugin): keep the agent tree rendering exactly
|
|
325
|
+
// as before instead of throwing.
|
|
326
|
+
const NOOP_GOALS_FACE = { subscribe: () => () => {}, getSnapshot: () => undefined };
|
|
327
|
+
|
|
328
|
+
// One goal row of the control tab: status + title + executor + durable
|
|
329
|
+
// member name + round. Same fields as the goal tab, restated as the
|
|
330
|
+
// operator's line — who is executing this goal and how far it got.
|
|
331
|
+
function ControlGoalRow(props) {
|
|
332
|
+
const t = props.t;
|
|
333
|
+
const g = props.goal;
|
|
334
|
+
const statusKey = GOAL_STATUS_KEY[g.status] ?? 'goalStatus.pending';
|
|
335
|
+
const executorKey = g.executor === 'persistent' ? 'goal.executor.persistent' : 'goal.executor.ephemeral';
|
|
336
|
+
return jsx.jsxs('div', { style: styles.controlGoalRow, children: [
|
|
337
|
+
jsx.jsx('span', { style: { ...styles.badge, ...goalStatusStyle(g.status) }, children: t(statusKey) }),
|
|
338
|
+
jsx.jsx('span', { style: styles.agentName, children: g.title || g.goalId }),
|
|
339
|
+
jsx.jsx('span', { style: styles.tag, children: t(executorKey) }),
|
|
340
|
+
g.memberName
|
|
341
|
+
? jsx.jsx('span', { style: styles.tag, children: t('goal.member', { name: g.memberName }) })
|
|
342
|
+
: null,
|
|
343
|
+
jsx.jsx('span', { style: styles.meta, children: t('goal.round', { round: g.round ?? 0 }) }),
|
|
344
|
+
] });
|
|
345
|
+
}
|
|
346
|
+
|
|
132
347
|
/** Depth-first agent rows rooted at `rootId`: lead + subagentCatalog, recursed. */
|
|
133
348
|
function collectAgents(list, rootId) {
|
|
134
349
|
const rows = [];
|
|
@@ -184,9 +399,23 @@ window.__ModuleLoader__.load({
|
|
|
184
399
|
const t = props.t;
|
|
185
400
|
const sessions = props.sessions;
|
|
186
401
|
const list = react.useSyncExternalStore(sessions.list.subscribe, sessions.list.getSnapshot);
|
|
402
|
+
const goalsFace = props.goals ?? NOOP_GOALS_FACE;
|
|
403
|
+
const goalsRaw = react.useSyncExternalStore(goalsFace.subscribe, goalsFace.getSnapshot);
|
|
404
|
+
const goals = goalsRaw !== null && typeof goalsRaw === 'object' && Array.isArray(goalsRaw.goals)
|
|
405
|
+
? groupGoals(goalsRaw.goals)
|
|
406
|
+
: [];
|
|
187
407
|
const agents = collectAgents(list, props.sessionId);
|
|
188
408
|
const runningCount = agents.filter((a) => a.running).length;
|
|
189
409
|
return jsx.jsxs('div', { style: styles.root, children: [
|
|
410
|
+
goals.length > 0
|
|
411
|
+
? jsx.jsxs('div', { style: styles.goalCards, children: [
|
|
412
|
+
jsx.jsxs('div', { style: styles.badgeRow, children: [
|
|
413
|
+
jsx.jsx('span', { style: styles.meta, children: t('control.goals') }),
|
|
414
|
+
jsx.jsx('span', { style: styles.badge, children: t('goals.count', { count: goals.length }) }),
|
|
415
|
+
] }),
|
|
416
|
+
...goals.map((g) => jsx.jsx(ControlGoalRow, { goal: g, t }, 'goal:' + g.goalId + ':' + (g.updatedAt ?? 0))),
|
|
417
|
+
] })
|
|
418
|
+
: null,
|
|
190
419
|
jsx.jsxs('div', { style: styles.badgeRow, children: [
|
|
191
420
|
jsx.jsx('span', { style: styles.badge, children: t('agents.count', { total: agents.length, running: runningCount }) }),
|
|
192
421
|
] }),
|
|
@@ -196,14 +425,267 @@ window.__ModuleLoader__.load({
|
|
|
196
425
|
}
|
|
197
426
|
//#endregion
|
|
198
427
|
|
|
428
|
+
//#region plugins-page settings card (v0.7.2)
|
|
429
|
+
// The official Plugins page (@deepseek-ai/dsh-client-ui-plugin-manager, the
|
|
430
|
+
// runtime copy that owns the 内置插件配置/实验性插件/已安装插件 groups) renders a
|
|
431
|
+
// bundle's own configuration ONLY through the slots it declares:
|
|
432
|
+
// plugins.bundle.config keyed by the bundle name (profile dep name)
|
|
433
|
+
// plugins.row.config keyed by `<bundle name>#<row id>`
|
|
434
|
+
// (rowConfigKey in that page's client.js; slot contract in
|
|
435
|
+
// dsh-cordis-client-runner's slot catalog). A server-side Config schema
|
|
436
|
+
// alone surfaces nowhere once the ui-settings-plugins section is disabled
|
|
437
|
+
// by the family plugin-manager — exactly the observed gap.
|
|
438
|
+
// Both entries bind the row's own ConfigForm from ctx.configForms (the
|
|
439
|
+
// namespace id is the loader row id `dsh-cybernetics`, same convention
|
|
440
|
+
// @deepseek-ai/dsh-experimental-client-ui-cot-translation uses). A toggle
|
|
441
|
+
// write goes settings mutate → loader volatile commit
|
|
442
|
+
// (`loader/volatile-update`), so allGoalsMode stays live without a
|
|
443
|
+
// restart — the v0.7.1 volatile semantics, unchanged.
|
|
444
|
+
const SETTINGS_ENTRY_ID = 'dsh-cybernetics';
|
|
445
|
+
const NO_FORM_SNAP = { status: 'unavailable', value: undefined, writable: false };
|
|
446
|
+
const noopSubscribe = () => () => {};
|
|
447
|
+
|
|
448
|
+
var settingsStyles = {
|
|
449
|
+
card: { display: 'flex', flexDirection: 'column', gap: 6, padding: '12px 0' },
|
|
450
|
+
row: { display: 'flex', alignItems: 'center', gap: 12 },
|
|
451
|
+
main: { display: 'flex', flexDirection: 'column', gap: 2, flex: 1, minWidth: 0 },
|
|
452
|
+
title: { fontSize: 13, fontWeight: 600 },
|
|
453
|
+
desc: { fontSize: 12, opacity: 0.65, lineHeight: 1.5 },
|
|
454
|
+
toggle: {
|
|
455
|
+
cursor: 'pointer', borderRadius: 999, width: 40, height: 22,
|
|
456
|
+
border: '1px solid rgba(127,127,127,0.45)', position: 'relative', flex: 'none',
|
|
457
|
+
background: 'transparent', padding: 0,
|
|
458
|
+
},
|
|
459
|
+
knob: {
|
|
460
|
+
position: 'absolute', top: 2, width: 16, height: 16, borderRadius: 999,
|
|
461
|
+
transition: 'left .15s ease',
|
|
462
|
+
},
|
|
463
|
+
trackOn: { background: '#2da44e', borderColor: '#2da44e' },
|
|
464
|
+
trackOff: { background: 'rgba(127,127,127,0.25)' },
|
|
465
|
+
knobOn: { left: 20, background: '#fff' },
|
|
466
|
+
knobOff: { left: 2, background: 'currentColor' },
|
|
467
|
+
unavailable: { fontSize: 12, opacity: 0.6 },
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* One boolean settings row; shared by all three seats. Two form shapes
|
|
472
|
+
* reach it, and the card must speak both:
|
|
473
|
+
* - the LIVE ConfigForm this module injects (ctx.configForms.get):
|
|
474
|
+
* getSnapshot / subscribe / set — the only shape on the
|
|
475
|
+
* plugins.bundle.config page and in summary rows, whose owners pass
|
|
476
|
+
* no form of their own;
|
|
477
|
+
* - the HOST's ConfigPageForm { state, mutate }: the plugins.item and
|
|
478
|
+
* plugins.row.config detail pages compute it from the same shared
|
|
479
|
+
* mirror (configForms.get(id).getSnapshot()/mutate) and pass it as
|
|
480
|
+
* an OWNER prop, which spreads AFTER inject at the render seam
|
|
481
|
+
* (@deepseek-ai/dsh-client-ui-renderer renderEntry: ...injected,
|
|
482
|
+
* ...ownerProps) — on those pages this shape WINS over the injected
|
|
483
|
+
* form. Reading `.subscribe` on it crashed the first v0.7.2 cut.
|
|
484
|
+
* The host shape carries no subscribe; its `state` is re-computed by
|
|
485
|
+
* the owning page on every mirror update, so reading it per render is
|
|
486
|
+
* already live.
|
|
487
|
+
*/
|
|
488
|
+
function SettingsCard(props) {
|
|
489
|
+
const t = props.t;
|
|
490
|
+
const form = props.form;
|
|
491
|
+
// v0.7.6 path-(b) fallback write face: the plugin's own bound
|
|
492
|
+
// remote.settings.mutate. SAME host write path as the ConfigForm
|
|
493
|
+
// (remote.settings.mutate → @deepseek-ai/dsh-settings SettingsForms
|
|
494
|
+
// → @deepseek-ai/dsh-config-editor edit → the USER patch row
|
|
495
|
+
// `{id, name, config}` appended to the profile cordis.patch.yml,
|
|
496
|
+
// reconciled by reconcileProfilePatches) — the only落点 the host
|
|
497
|
+
// accepts. Used ONLY when the primary write refuses, without an
|
|
498
|
+
// expectedRevision (the controller documents undefined as an
|
|
499
|
+
// unconditional write), so a stale-revision refusal heals in place
|
|
500
|
+
// and a hard refusal surfaces its exact host message instead of a
|
|
501
|
+
// bare「宿主拒绝了写入」with no reason.
|
|
502
|
+
const directMutate = props.directMutate;
|
|
503
|
+
const view = props.view === 'summary' ? 'summary' : 'page';
|
|
504
|
+
const live = form !== null && typeof form === 'object'
|
|
505
|
+
&& typeof form.subscribe === 'function' && typeof form.getSnapshot === 'function' && typeof form.set === 'function'
|
|
506
|
+
? form : undefined;
|
|
507
|
+
const host = live === undefined && form !== null && typeof form === 'object'
|
|
508
|
+
&& typeof form.mutate === 'function' && form.state !== null && typeof form.state === 'object'
|
|
509
|
+
? form : undefined;
|
|
510
|
+
const liveSnap = react.useSyncExternalStore(
|
|
511
|
+
live ? (cb) => live.subscribe(cb) : noopSubscribe,
|
|
512
|
+
live ? () => live.getSnapshot() : () => NO_FORM_SNAP,
|
|
513
|
+
);
|
|
514
|
+
const snap = host !== undefined ? host.state : liveSnap;
|
|
515
|
+
const [saving, setSaving] = react.useState(false);
|
|
516
|
+
// `false` = no failure; `true` = failure without a host message;
|
|
517
|
+
// a string = the host's exact refusal (RemoteError.message).
|
|
518
|
+
const [failed, setFailed] = react.useState(false);
|
|
519
|
+
const on = snap.status === 'ready' && snap.value != null && snap.value.allGoalsMode === true;
|
|
520
|
+
const disabled = (live === undefined && host === undefined) || snap.status !== 'ready' || snap.writable === false || saving;
|
|
521
|
+
|
|
522
|
+
const toggle = () => {
|
|
523
|
+
if (disabled) return;
|
|
524
|
+
setSaving(true);
|
|
525
|
+
setFailed(false);
|
|
526
|
+
// Both write paths land in the same host mutation: ConfigForm.set
|
|
527
|
+
// is one-field sugar over ConfigForm.mutate, and the host's
|
|
528
|
+
// ConfigPageForm.mutate IS that form's mutate (the expected
|
|
529
|
+
// revision carried explicitly). The host commits the write into
|
|
530
|
+
// the running plugin instance (volatile) and the next snapshot /
|
|
531
|
+
// page render pushes the accepted value back here.
|
|
532
|
+
// Failure policy (v0.7.4): the transport can REFUSE (settles
|
|
533
|
+
// false) or stall (the observed "保存中" hang). Neither is
|
|
534
|
+
// silent: a refused write shows saveFailed for a few seconds,
|
|
535
|
+
// and a stalled write releases the switch after 15 s with the
|
|
536
|
+
// same hint — the switch always returns to the snapshot value,
|
|
537
|
+
// which is the only state the host confirmed.
|
|
538
|
+
// v0.7.6: a refused write is retried ONCE through the direct
|
|
539
|
+
// settings transport (props.directMutate, no expectedRevision).
|
|
540
|
+
// Same落点 (user patch row via configEditor), unconditional
|
|
541
|
+
// revision — a stale-revision conflict therefore heals, and any
|
|
542
|
+
// remaining refusal reports its host message verbatim.
|
|
543
|
+
let settled = false;
|
|
544
|
+
const finish = (ok, detail) => {
|
|
545
|
+
if (settled) return;
|
|
546
|
+
settled = true;
|
|
547
|
+
setSaving(false);
|
|
548
|
+
if (!ok) {
|
|
549
|
+
setFailed(detail || true);
|
|
550
|
+
setTimeout(() => setFailed(false), 6000);
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
const guard = setTimeout(() => finish(false), 15000);
|
|
554
|
+
const op = { op: 'set', path: ['allGoalsMode'], value: !on };
|
|
555
|
+
const write = live !== undefined
|
|
556
|
+
? live.set('allGoalsMode', !on)
|
|
557
|
+
: host.mutate([op], snap.revision);
|
|
558
|
+
Promise.resolve(write)
|
|
559
|
+
.then(async (accepted) => {
|
|
560
|
+
if (accepted !== false) { clearTimeout(guard); finish(true); return; }
|
|
561
|
+
if (typeof directMutate !== 'function') { clearTimeout(guard); finish(false); return; }
|
|
562
|
+
try {
|
|
563
|
+
const answer = await directMutate([op]);
|
|
564
|
+
clearTimeout(guard);
|
|
565
|
+
if (answer && typeof answer === 'object' && answer.ok === false) {
|
|
566
|
+
finish(false, answer.error && answer.error.message);
|
|
567
|
+
} else finish(true);
|
|
568
|
+
} catch (error) {
|
|
569
|
+
clearTimeout(guard);
|
|
570
|
+
finish(false, error && error.message);
|
|
571
|
+
}
|
|
572
|
+
})
|
|
573
|
+
.catch((error) => { clearTimeout(guard); finish(false, error && error.message); });
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
if (view === 'summary') {
|
|
577
|
+
return jsx.jsx('span', { children: snap.status === 'ready' ? t(on ? 'settings.summaryOn' : 'settings.summaryOff') : t('settings.unavailable') });
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
return jsx.jsxs('div', { style: settingsStyles.card, children: [
|
|
581
|
+
jsx.jsxs('div', { style: settingsStyles.row, children: [
|
|
582
|
+
jsx.jsxs('div', { style: settingsStyles.main, children: [
|
|
583
|
+
jsx.jsx('div', { style: settingsStyles.title, children: t('settings.title') }),
|
|
584
|
+
jsx.jsx('div', { style: settingsStyles.desc, children: t('settings.description') }),
|
|
585
|
+
] }),
|
|
586
|
+
snap.status === 'ready'
|
|
587
|
+
? jsx.jsx('button', {
|
|
588
|
+
type: 'button', role: 'switch', 'aria-checked': on, 'aria-label': t('settings.title'),
|
|
589
|
+
disabled, onClick: toggle,
|
|
590
|
+
style: { ...settingsStyles.toggle, ...(on ? settingsStyles.trackOn : settingsStyles.trackOff) },
|
|
591
|
+
children: jsx.jsx('span', { style: { ...settingsStyles.knob, ...(on ? settingsStyles.knobOn : settingsStyles.knobOff) } }),
|
|
592
|
+
})
|
|
593
|
+
: null,
|
|
594
|
+
] }),
|
|
595
|
+
snap.status !== 'ready' ? jsx.jsx('div', { style: settingsStyles.unavailable, children: t('settings.unavailable') }) : null,
|
|
596
|
+
saving ? jsx.jsx('div', { style: settingsStyles.unavailable, children: t('settings.saving') }) : null,
|
|
597
|
+
!saving && failed ? jsx.jsx('div', { style: { ...settingsStyles.unavailable, color: '#d29922' }, children: failed === true ? t('settings.saveFailed') : t('settings.saveFailedDetail', { message: failed }) }) : null,
|
|
598
|
+
] });
|
|
599
|
+
}
|
|
600
|
+
//#endregion
|
|
601
|
+
|
|
199
602
|
//#region plugin apply
|
|
200
603
|
const name = 'dsh-cybernetics';
|
|
201
|
-
|
|
604
|
+
// configForms: the per-entry ConfigForm registry (@deepseek-ai/dsh-client-ui-settings)
|
|
605
|
+
// that backs the plugins-page settings card below.
|
|
606
|
+
const inject = ['slots', 'sessions', 'locale', 'configForms',
|
|
607
|
+
// v0.7.6: the settings card's diagnostic fallback write rides the
|
|
608
|
+
// same namespace remote the official ui-settings declares
|
|
609
|
+
// (["remote", "remote.settings"]). Both are required: ctx access is
|
|
610
|
+
// gated by the inject declaration (client-runner guard), so an
|
|
611
|
+
// undeclared `remote` would throw on read and silently disable the
|
|
612
|
+
// fallback. Optional at runtime (guarded in apply): a host without
|
|
613
|
+
// the namespace parks this client module exactly as it would
|
|
614
|
+
// ui-settings itself — the web profile always serves it.
|
|
615
|
+
'remote', 'remote.settings'];
|
|
202
616
|
|
|
203
617
|
function apply(ctx) {
|
|
204
618
|
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-cybernetics: dictionaries');
|
|
205
619
|
const t = ctx.locale.bind(NS);
|
|
206
620
|
|
|
621
|
+
// v0.7.2: bind the row's ConfigForm once per apply body. The namespace
|
|
622
|
+
// is the loader row id (`dsh-cybernetics`), the same convention the
|
|
623
|
+
// official cot-translation bundle uses (`ctx.configForms.get(ns)`). If
|
|
624
|
+
// the host does not serve this namespace yet, the card renders its
|
|
625
|
+
// unavailable state instead of throwing.
|
|
626
|
+
let settingsForm;
|
|
627
|
+
try {
|
|
628
|
+
settingsForm = ctx.configForms.get(SETTINGS_ENTRY_ID);
|
|
629
|
+
} catch {
|
|
630
|
+
settingsForm = undefined;
|
|
631
|
+
}
|
|
632
|
+
// v0.7.6 path-(b) diagnostics face: the direct settings transport,
|
|
633
|
+
// bound from this module's own context (`remote.settings` inject).
|
|
634
|
+
// Guarded: a deployment without the namespace leaves it undefined
|
|
635
|
+
// and the card falls back to the v0.7.4 hint-only failure state.
|
|
636
|
+
let settingsDirectMutate;
|
|
637
|
+
try {
|
|
638
|
+
if (ctx.remote && ctx.remote.settings && typeof ctx.remote.settings.mutate === 'function') {
|
|
639
|
+
settingsDirectMutate = (ops, expectedRevision) => ctx.remote.settings.mutate(SETTINGS_ENTRY_ID, ops, expectedRevision);
|
|
640
|
+
}
|
|
641
|
+
} catch {
|
|
642
|
+
settingsDirectMutate = undefined;
|
|
643
|
+
}
|
|
644
|
+
const settingsInject = () => ({ form: settingsForm, directMutate: settingsDirectMutate });
|
|
645
|
+
// v0.7.2 (the actual gap): the official Plugins page renders the
|
|
646
|
+
// 内置插件配置 group cards ONLY from the `plugins.item` slot
|
|
647
|
+
// (ItemCard in @deepseek-ai/dsh-client-ui-plugin-manager client.js:
|
|
648
|
+
// basicCards = ledger.items from slot "plugins.item"; ItemDetail
|
|
649
|
+
// renders view "page" with the host form for item.id). The keyed
|
|
650
|
+
// seats below surface only on an installed bundle's own page, which
|
|
651
|
+
// this linked bundle never reaches in the 已安装插件 group — that was
|
|
652
|
+
// why v0.7.1's schema alone (and the first v0.7.2 cut) showed no card.
|
|
653
|
+
// Register the list card UNCONDITIONALLY (v0.7.5 hardening): the earlier
|
|
654
|
+
// cut gated it on configForms.whileServed, so a deployment where the
|
|
655
|
+
// host does not (yet) serve the `dsh-cybernetics` namespace showed NO
|
|
656
|
+
// card at all — indistinguishable from the registration never running.
|
|
657
|
+
// ctx.configForms.get() binds a live ConfigFormController that follows
|
|
658
|
+
// the shared describe mirror (ConfigForms.get → new ConfigFormController
|
|
659
|
+
// + mirror.ensure), so a card registered before the namespace appears
|
|
660
|
+
// flips to the toggle the moment the host starts serving it; while it
|
|
661
|
+
// does not, the card renders its `settings.unavailable` line — visible
|
|
662
|
+
// and diagnosable instead of absent.
|
|
663
|
+
ctx.effect(() => ctx.slots.inject('plugins.item', () => ctx.slots.register({
|
|
664
|
+
name: 'plugins.item',
|
|
665
|
+
id: SETTINGS_ENTRY_ID,
|
|
666
|
+
order: 80,
|
|
667
|
+
label: () => t('settings.cardLabel'),
|
|
668
|
+
locale: NS,
|
|
669
|
+
inject: settingsInject,
|
|
670
|
+
}, SettingsCard)), 'dsh-cybernetics: plugins-page settings card');
|
|
671
|
+
// The dispatch key bundles use the profile dependency name (the value
|
|
672
|
+
// in dsh.profile.bundles = `dsh-cybernetics`); register the scoped
|
|
673
|
+
// package manifest name too, so either resolution finds the card.
|
|
674
|
+
for (const bundleKey of ['dsh-cybernetics', '@mars.liu/dsh-cybernetics']) {
|
|
675
|
+
ctx.slots.inject('plugins.row.config', () => ctx.slots.register({
|
|
676
|
+
name: 'plugins.row.config',
|
|
677
|
+
key: bundleKey + '#' + SETTINGS_ENTRY_ID,
|
|
678
|
+
locale: NS,
|
|
679
|
+
inject: settingsInject,
|
|
680
|
+
}, SettingsCard));
|
|
681
|
+
ctx.slots.inject('plugins.bundle.config', () => ctx.slots.register({
|
|
682
|
+
name: 'plugins.bundle.config',
|
|
683
|
+
key: bundleKey,
|
|
684
|
+
locale: NS,
|
|
685
|
+
inject: settingsInject,
|
|
686
|
+
}, SettingsCard));
|
|
687
|
+
}
|
|
688
|
+
|
|
207
689
|
// Conversation-view tab, same slot the trajectory/context tabs use.
|
|
208
690
|
// `t` reaches the view as a slot-provided prop from `locale: NS`
|
|
209
691
|
// (PropsLocale) — do NOT also pass a hand-bound t through inject:
|
|
@@ -218,19 +700,39 @@ window.__ModuleLoader__.load({
|
|
|
218
700
|
inject: (sessionId) => {
|
|
219
701
|
const binding = ctx.sessions.binding(sessionId);
|
|
220
702
|
if (binding === undefined) throw new Error('dsh-cybernetics: session "' + sessionId + '" unavailable');
|
|
221
|
-
|
|
703
|
+
// v0.6: both faces. `cyberneticsGoals` carries the whole goal
|
|
704
|
+
// board (live, pushed by the host); `goal` stays as the
|
|
705
|
+
// backward-compatible fallback for older plugin/host builds.
|
|
706
|
+
return {
|
|
707
|
+
goals: binding.session.projections.faceOf('cyberneticsGoals'),
|
|
708
|
+
goal: binding.session.projections.faceOf('goal'),
|
|
709
|
+
};
|
|
222
710
|
},
|
|
223
711
|
}, GoalView));
|
|
224
712
|
|
|
225
713
|
// Control tab: goals + run states of every agent in this session,
|
|
226
714
|
// driven by the session list store (live via the control stream).
|
|
715
|
+
// The goal face is optional (older host/plugin): the agent tree then
|
|
716
|
+
// renders exactly as before.
|
|
227
717
|
ctx.slots.inject('conversation.view', () => ctx.slots.register({
|
|
228
718
|
name: 'conversation.view',
|
|
229
719
|
id: 'cybernetics-control',
|
|
230
720
|
order: 16,
|
|
231
721
|
locale: NS,
|
|
232
722
|
label: () => t('view.control'),
|
|
233
|
-
inject: (sessionId) =>
|
|
723
|
+
inject: (sessionId) => {
|
|
724
|
+
let goals = NOOP_GOALS_FACE;
|
|
725
|
+
try {
|
|
726
|
+
const binding = ctx.sessions.binding(sessionId);
|
|
727
|
+
const face = binding && binding.session && binding.session.projections
|
|
728
|
+
? binding.session.projections.faceOf('cyberneticsGoals')
|
|
729
|
+
: undefined;
|
|
730
|
+
if (face) goals = face;
|
|
731
|
+
} catch {
|
|
732
|
+
// No multi-goal projection: keep the control tab's agent tree.
|
|
733
|
+
}
|
|
734
|
+
return { sessionId, sessions: ctx.sessions, goals };
|
|
735
|
+
},
|
|
234
736
|
}, ControlView));
|
|
235
737
|
}
|
|
236
738
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mars.liu/dsh-cybernetics",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.8",
|
|
4
4
|
"private": false,
|
|
5
|
+
"files": [
|
|
6
|
+
"src",
|
|
7
|
+
"lib",
|
|
8
|
+
"workflows",
|
|
9
|
+
"docs",
|
|
10
|
+
"locale",
|
|
11
|
+
"cordis.patch.yml",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
5
14
|
"description": "Cybernetic work protocol for DSH: the session goal (dsh-goal) driven loop of task -> verify -> triage agents, implicitly advanced via one workflow run per round. Server plugin + client goal tab.",
|
|
6
15
|
"type": "module",
|
|
7
16
|
"main": "src/index.ts",
|
|
@@ -24,10 +33,17 @@
|
|
|
24
33
|
"@deepseek-ai/dsh-client-locale",
|
|
25
34
|
"@deepseek-ai/dsh-client-ui-conversation",
|
|
26
35
|
"@deepseek-ai/dsh-client-ui-renderer",
|
|
27
|
-
"@deepseek-ai/dsh-client-ui-session"
|
|
36
|
+
"@deepseek-ai/dsh-client-ui-session",
|
|
37
|
+
"@deepseek-ai/dsh-client-ui-settings"
|
|
28
38
|
],
|
|
29
39
|
"platform": "web",
|
|
30
40
|
"immediately": true
|
|
31
41
|
}
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@deepseek-ai/schemastery": "^3.18.5"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"@deepseek-ai/dsh-session": "*"
|
|
32
48
|
}
|
|
33
49
|
}
|