@banana-peeljj12/dsh-trellis 0.3.3 → 0.3.5
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/lib/client.js +445 -222
- package/lib/index.js +10 -4
- package/lib/meta.js +56 -8
- package/lib/settings.js +52 -36
- package/package.json +24 -13
- package/scripts/install.mjs +15 -17
package/lib/client.js
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* trellis-workflow client half —
|
|
2
|
+
* trellis-workflow client half — configuration page on the Plugins list.
|
|
3
3
|
*
|
|
4
|
-
* Contributes
|
|
5
|
-
* `
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* without a restart.
|
|
4
|
+
* Contributes the `trellis-workflow` loader entry's configuration to the
|
|
5
|
+
* Plugins page (slot `plugins.row.config`, keyed
|
|
6
|
+
* `<package name>#<row id>`) so the injection allowlist, injectStep,
|
|
7
|
+
* skipKeywords, inline, and enforceReadonlyPlanning fields are editable there
|
|
8
|
+
* and take effect on the next turn without a restart.
|
|
9
9
|
*
|
|
10
10
|
* This file is a client bundle in the web shell's module format
|
|
11
11
|
* (`window.__ModuleLoader__.load({ id, factory })`); it is served under
|
|
12
12
|
* /plugins by the harness when the package is an enabled Loader entry whose
|
|
13
13
|
* manifest declares `dsh.client` with `platform: web`.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* DSH 0.1.7 settings are Loader-entry driven: the namespace IS the loader entry
|
|
16
|
+
* id (`trellis-workflow`) and the value is the entry's projected Config. The
|
|
17
|
+
* Web client reads and writes it through the shared `configForms` service, and
|
|
18
|
+
* the card registers into the Plugins page's `plugins.row.config` seat.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
window.__ModuleLoader__.load({
|
|
@@ -26,6 +26,7 @@ window.__ModuleLoader__.load({
|
|
|
26
26
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
27
27
|
|
|
28
28
|
let react = require('react');
|
|
29
|
+
let primitives = require('@deepseek-ai/dsh-client-ui-primitives');
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
32
|
* Module-level client root context, captured in apply(ctx). Components (slot
|
|
@@ -35,38 +36,45 @@ window.__ModuleLoader__.load({
|
|
|
35
36
|
*/
|
|
36
37
|
let clientCtx = null;
|
|
37
38
|
|
|
38
|
-
// #region lib/types/client/trellis-
|
|
39
|
+
// #region lib/types/client/trellis-shared.js
|
|
39
40
|
|
|
40
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* The bundle patch row id — also the settings entry id this page edits. A
|
|
43
|
+
* row inserted by a bundle patch is loaded under a namespaced entry id
|
|
44
|
+
* (`include:trellis-workflow`) while a directly-declared row keeps the bare
|
|
45
|
+
* id, so consumers match it as a suffix (see `entryIdOf`).
|
|
46
|
+
*/
|
|
41
47
|
const NS = 'trellis-workflow';
|
|
42
48
|
|
|
43
49
|
/** Locale dictionary namespace owned by this client half. */
|
|
44
50
|
const LOCALE_NS = 'settings.trellisWorkflow';
|
|
45
51
|
|
|
46
52
|
/** Services required by this client half. */
|
|
47
|
-
const inject = ['slots', 'locale', '
|
|
53
|
+
const inject = ['slots', 'locale', 'configForms', 'uiWorkspace', 'sidebarRight'];
|
|
48
54
|
|
|
49
55
|
const zh = {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
'当前 harness 未向 Web 暴露 trellis-workflow 命名空间(dsh-host-apiproxy 的 WEB_SETTINGS_NAMESPACES 白名单未包含它)。请用 scripts/install.mjs 的 --patch-harness 补丁后重启 DSH。',
|
|
56
|
+
unavailable: '该插件当前未加载,暂不可配置(宿主未提供 trellis-workflow 条目)。',
|
|
57
|
+
servedNamespaces: '宿主已服务的命名空间:',
|
|
58
|
+
noneServed: '(无)',
|
|
54
59
|
allowlist: '注入白名单',
|
|
55
60
|
allowlistHint: '命中这些项目根的会话才会收到工作流面包屑。',
|
|
56
|
-
addPlaceholder: '输入项目根路径,如 F:/Projects/FordProject',
|
|
57
|
-
add: '添加',
|
|
58
|
-
remove: '移除',
|
|
59
|
-
empty: '(空)',
|
|
60
61
|
injectStep: '注入步数(injectStep)',
|
|
62
|
+
injectStepHint: '只在该步注入(1 = 每条用户消息的第一步)。',
|
|
61
63
|
skipKeywords: '跳过关键词(逗号分隔)',
|
|
64
|
+
skipKeywordsHint: '出现这些词(独立单词,逗号分隔)时,本轮跳过注入。',
|
|
62
65
|
inline: '按 codex-inline 调度解析阶段',
|
|
63
66
|
enforceReadonlyPlanning: '规划期只读保护(enforceReadonlyPlanning)',
|
|
64
67
|
enforceReadonlyPlanningHint:
|
|
65
68
|
'开启后,命中白名单的项目里:新对话(未建任务)只裁剪 write/edit 与任务写工具(trellis_task_update / trellis_artifact_update / trellis_task_archive / trellis_ui_update),其余工具(含其他插件的工具)保留;规划中的任务只裁剪 write/edit 与创建/跳过/归档类 trellis 工具,其余工具保留;经 trellis_task_skip 跳过任务的会话恢复完整工具。',
|
|
66
|
-
|
|
67
|
-
|
|
69
|
+
save: '保存',
|
|
70
|
+
saving: '保存中…',
|
|
71
|
+
saveFailed: '本部署没有接受这些值,已保留供你修改。',
|
|
72
|
+
readOnly: '本部署的设置为只读。',
|
|
73
|
+
overridden: '已覆盖',
|
|
74
|
+
reset: '恢复默认',
|
|
75
|
+
invalidNumber: '请填数字;留空表示使用默认值。',
|
|
76
|
+
desc: 'Trellis 工作流:按会话项目注入阶段面包屑、提供任务阶段工具与看板。',
|
|
68
77
|
browse: '浏览…',
|
|
69
|
-
browseBusy: '选择中…',
|
|
70
78
|
browseFailed: '调用文件夹选择器失败,请手动输入路径',
|
|
71
79
|
chipTitle: 'Trellis Task',
|
|
72
80
|
chipNoTask: '无活动任务',
|
|
@@ -128,27 +136,30 @@ window.__ModuleLoader__.load({
|
|
|
128
136
|
};
|
|
129
137
|
|
|
130
138
|
const en = {
|
|
131
|
-
tab: 'Trellis Workflow',
|
|
132
|
-
loading: 'Loading configuration…',
|
|
133
139
|
unavailable:
|
|
134
|
-
'
|
|
140
|
+
'This plugin is not loaded, so it cannot be configured right now (the Host does not serve the trellis-workflow entry).',
|
|
141
|
+
servedNamespaces: 'Namespaces the Host serves:',
|
|
142
|
+
noneServed: '(none)',
|
|
135
143
|
allowlist: 'Injection allowlist',
|
|
136
144
|
allowlistHint: 'Sessions whose cwd matches these project roots receive the workflow breadcrumb.',
|
|
137
|
-
addPlaceholder: 'Project root path, e.g. F:/Projects/FordProject',
|
|
138
|
-
add: 'Add',
|
|
139
145
|
browse: 'Browse…',
|
|
140
|
-
browseBusy: 'Picking…',
|
|
141
146
|
browseFailed: 'Could not open the directory picker — enter the path manually',
|
|
142
|
-
remove: 'Remove',
|
|
143
|
-
empty: '(empty)',
|
|
144
147
|
injectStep: 'Inject step (injectStep)',
|
|
148
|
+
injectStepHint: 'Only inject on this step index (1 = the first step of each user message).',
|
|
145
149
|
skipKeywords: 'Skip keywords (comma-separated)',
|
|
150
|
+
skipKeywordsHint: 'Standalone words (comma-separated) that suppress injection for a turn.',
|
|
146
151
|
inline: 'Resolve phases as codex-inline dispatch',
|
|
147
152
|
enforceReadonlyPlanning: 'Read-only planning (enforceReadonlyPlanning)',
|
|
148
153
|
enforceReadonlyPlanningHint:
|
|
149
154
|
'When enabled, in allowlisted projects: a fresh conversation (no task yet) trims only write/edit and task-write trellis tools (trellis_task_update / trellis_artifact_update / trellis_task_archive / trellis_ui_update), keeping all other tools (including other plugins\'); a task in the planning phase trims only write/edit and create/skip/archive trellis tools, keeping all other tools; a session that skipped the task via trellis_task_skip regains the full tool surface.',
|
|
150
|
-
|
|
151
|
-
|
|
155
|
+
save: 'Save',
|
|
156
|
+
saving: 'Saving…',
|
|
157
|
+
saveFailed: 'The deployment did not accept these values; they were left for you to correct.',
|
|
158
|
+
readOnly: 'This deployment stores settings read-only.',
|
|
159
|
+
overridden: 'Overridden',
|
|
160
|
+
reset: 'Reset to default',
|
|
161
|
+
invalidNumber: 'Enter a number, or leave blank to use the default.',
|
|
162
|
+
desc: 'Trellis workflow: injects a per-project phase breadcrumb and provides the task/phase tools and board.',
|
|
152
163
|
chipTitle: 'Trellis Task',
|
|
153
164
|
chipNoTask: 'No active task',
|
|
154
165
|
chipNoSummary: 'No state yet — click to refresh',
|
|
@@ -208,217 +219,426 @@ window.__ModuleLoader__.load({
|
|
|
208
219
|
selectTaskHint: 'Click a task on the left to view details',
|
|
209
220
|
};
|
|
210
221
|
|
|
211
|
-
|
|
212
|
-
const { scope, t } = props;
|
|
213
|
-
const [draftPath, setDraftPath] = react.useState('');
|
|
214
|
-
const [skipDraft, setSkipDraft] = react.useState('');
|
|
215
|
-
const [saved, setSaved] = react.useState(false);
|
|
222
|
+
// #region lib/types/client/trellis-settings-card.js
|
|
216
223
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
224
|
+
/**
|
|
225
|
+
* DSH 0.1.7 settings surface for the `trellis-workflow` loader entry.
|
|
226
|
+
*
|
|
227
|
+
* The old per-plugin settings-scope model is gone: namespaces are loader
|
|
228
|
+
* entry ids, and the Web client reads/writes them through the shared
|
|
229
|
+
* `configForms` service (the same path the official settings cards use).
|
|
230
|
+
* The custom page registers into the Plugins page's `plugins.row.config`
|
|
231
|
+
* seat, keyed `<package name>#<row id>` — the key the plugin list page
|
|
232
|
+
* dispatches for this bundle's `trellis-workflow` row.
|
|
233
|
+
*/
|
|
223
234
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
235
|
+
/** The Plugins-page row key this bundle's configuration registers under. */
|
|
236
|
+
const ROW_CONFIG_KEY = '@banana-peeljj12/dsh-trellis#trellis-workflow';
|
|
237
|
+
|
|
238
|
+
/** Minimal button style for the browse control (the form frame owns the rest). */
|
|
239
|
+
const btnStyle = {
|
|
240
|
+
height: 30,
|
|
241
|
+
padding: '0 12px',
|
|
242
|
+
fontSize: 13,
|
|
243
|
+
font: 'inherit',
|
|
244
|
+
color: 'var(--dsw-alias-label-primary)',
|
|
245
|
+
background: 'transparent',
|
|
246
|
+
border: '1px solid var(--dsw-alias-border-l2)',
|
|
247
|
+
borderRadius: 8,
|
|
248
|
+
cursor: 'pointer',
|
|
249
|
+
};
|
|
229
250
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
251
|
+
/** The label frame the shared settings form renders. */
|
|
252
|
+
function formLabels(t) {
|
|
253
|
+
return {
|
|
254
|
+
unavailable: t('unavailable'),
|
|
255
|
+
readOnly: t('readOnly'),
|
|
256
|
+
saveFailed: t('saveFailed'),
|
|
257
|
+
save: t('save'),
|
|
258
|
+
saving: t('saving'),
|
|
259
|
+
};
|
|
260
|
+
}
|
|
236
261
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
262
|
+
/**
|
|
263
|
+
* A comma-separated list field. The shared form model is text based, so a
|
|
264
|
+
* list stages as one line; an empty draft clears the override.
|
|
265
|
+
* @param {string} field field name inside the entry's config section.
|
|
266
|
+
* @returns {{ field: string, format: (v: unknown) => string, parse: (t: string) => unknown }}
|
|
267
|
+
*/
|
|
268
|
+
function settingsListField(field) {
|
|
269
|
+
return {
|
|
270
|
+
field,
|
|
271
|
+
format: (value) => (Array.isArray(value) ? value.join(', ') : ''),
|
|
272
|
+
parse: (text) => {
|
|
273
|
+
const items = String(text)
|
|
274
|
+
.split(',')
|
|
275
|
+
.map((s) => s.trim())
|
|
276
|
+
.filter(Boolean);
|
|
277
|
+
return items.length === 0 ? { kind: 'clear' } : { kind: 'set', value: items };
|
|
278
|
+
},
|
|
242
279
|
};
|
|
280
|
+
}
|
|
243
281
|
|
|
244
|
-
|
|
245
|
-
|
|
282
|
+
/**
|
|
283
|
+
* A boolean field staged as the text `true`/`false`, so a checkbox can ride
|
|
284
|
+
* the same staged-draft and save semantics as every other control.
|
|
285
|
+
* @param {string} field field name inside the entry's config section.
|
|
286
|
+
* @returns {{ field: string, format: (v: unknown) => string, parse: (t: string) => unknown }}
|
|
287
|
+
*/
|
|
288
|
+
function settingsBoolField(field) {
|
|
289
|
+
return {
|
|
290
|
+
field,
|
|
291
|
+
format: (value) => (value === true ? 'true' : 'false'),
|
|
292
|
+
parse: (text) => ({ kind: 'set', value: String(text).trim() === 'true' }),
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Parse a staged comma-separated draft back into a list. */
|
|
297
|
+
function listFromText(text) {
|
|
298
|
+
return String(text || '')
|
|
299
|
+
.split(',')
|
|
300
|
+
.map((s) => s.trim())
|
|
301
|
+
.filter(Boolean);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* A minimal snapshot store for the card's injected hook. The shared form
|
|
306
|
+
* model owns its own store, but the card must also render before the entry
|
|
307
|
+
* is resolved, so the controller publishes through this one.
|
|
308
|
+
* @param {object} initial the first snapshot.
|
|
309
|
+
* @returns {{ getSnapshot: () => object, subscribe: (fn: () => void) => (() => void), set: (next: object) => void }}
|
|
310
|
+
*/
|
|
311
|
+
function createStore(initial) {
|
|
312
|
+
let snapshot = initial;
|
|
313
|
+
const listeners = new Set();
|
|
314
|
+
return {
|
|
315
|
+
getSnapshot: () => snapshot,
|
|
316
|
+
subscribe(listener) {
|
|
317
|
+
listeners.add(listener);
|
|
318
|
+
return () => listeners.delete(listener);
|
|
319
|
+
},
|
|
320
|
+
set(next) {
|
|
321
|
+
snapshot = next;
|
|
322
|
+
for (const listener of listeners) {
|
|
323
|
+
try {
|
|
324
|
+
listener();
|
|
325
|
+
} catch {
|
|
326
|
+
/* one listener must not break the others */
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
};
|
|
331
|
+
}
|
|
246
332
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
333
|
+
/** The form state before the entry is resolved: nothing to edit yet. */
|
|
334
|
+
function emptyFormState() {
|
|
335
|
+
const empty = { text: '', overridden: false, invalid: false };
|
|
336
|
+
return {
|
|
337
|
+
available: false,
|
|
338
|
+
writable: false,
|
|
339
|
+
dirty: false,
|
|
340
|
+
invalid: false,
|
|
341
|
+
saving: false,
|
|
342
|
+
failed: false,
|
|
343
|
+
served: [],
|
|
344
|
+
allowlist: empty,
|
|
345
|
+
injectStep: empty,
|
|
346
|
+
skipKeywords: empty,
|
|
347
|
+
inline: empty,
|
|
348
|
+
enforceReadonlyPlanning: empty,
|
|
252
349
|
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Resolve this bundle's loader entry id from the served namespaces.
|
|
354
|
+
*
|
|
355
|
+
* A row inserted by a bundle patch is loaded under a namespaced entry id
|
|
356
|
+
* (`include:trellis-workflow`), while the same row declared directly in a
|
|
357
|
+
* profile patch keeps the bare id — so match the row id as a suffix rather
|
|
358
|
+
* than assuming one form.
|
|
359
|
+
* @param {Iterable<string>} served namespaces the Host currently serves.
|
|
360
|
+
* @returns {string | undefined} the entry id, when served.
|
|
361
|
+
*/
|
|
362
|
+
function entryIdOf(served) {
|
|
363
|
+
for (const ns of served) {
|
|
364
|
+
if (ns === NS || ns.endsWith(':' + NS)) return ns;
|
|
365
|
+
}
|
|
366
|
+
return undefined;
|
|
367
|
+
}
|
|
253
368
|
|
|
369
|
+
/**
|
|
370
|
+
* Stage this entry's config edits over the shared `configForms` form and
|
|
371
|
+
* write them on save (the same controller shape the official cards use).
|
|
372
|
+
*
|
|
373
|
+
* The entry id is resolved lazily from the describe mirror: the Web client
|
|
374
|
+
* can mount before the Host serves the entry, and the id a bundle patch
|
|
375
|
+
* produces is namespaced.
|
|
376
|
+
*/
|
|
377
|
+
class TrellisConfigController {
|
|
254
378
|
/**
|
|
255
|
-
*
|
|
256
|
-
* (`ctx.workspaces.pickDirectory()`), normalize the picked path
|
|
257
|
-
* (backslashes → forward slashes) and append it to the allowlist
|
|
258
|
-
* directly (deduped). Cancel (`null`) is a no-op; failures surface the
|
|
259
|
-
* `browseFailed` copy and keep the manual input path untouched.
|
|
379
|
+
* @param {object} configForms the shared `configForms` service.
|
|
260
380
|
*/
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
381
|
+
constructor(configForms) {
|
|
382
|
+
this.configForms = configForms;
|
|
383
|
+
this.mirror = configForms.describe();
|
|
384
|
+
this.model = null;
|
|
385
|
+
this.entryId = undefined;
|
|
386
|
+
this.store = createStore(emptyFormState());
|
|
387
|
+
this.modelStore = null;
|
|
388
|
+
this.modelOff = null;
|
|
389
|
+
this.mirrorOff = this.mirror.subscribe(() => this.settle());
|
|
390
|
+
this.mirror.ensure();
|
|
391
|
+
this.settle();
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* The namespaces the Host currently serves, read from the shared mirror.
|
|
396
|
+
* @returns {string[]} served namespace ids.
|
|
397
|
+
*/
|
|
398
|
+
servedNamespaces() {
|
|
399
|
+
try {
|
|
400
|
+
const view = this.mirror.getSnapshot().view;
|
|
401
|
+
const rows = view && view.namespaces ? view.namespaces : [];
|
|
402
|
+
return rows.map((row) => row && row.ns).filter(Boolean);
|
|
403
|
+
} catch {
|
|
404
|
+
return [];
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** Build the shared form model once the Host serves this entry. */
|
|
409
|
+
settle() {
|
|
410
|
+
if (this.model) return;
|
|
411
|
+
const entryId = entryIdOf(this.servedNamespaces());
|
|
412
|
+
if (entryId === undefined) return;
|
|
413
|
+
this.entryId = entryId;
|
|
414
|
+
this.model = new primitives.SettingsFormModel(this.configForms.get(entryId), [
|
|
415
|
+
settingsListField('allowlist'),
|
|
416
|
+
primitives.settingsNumberField('injectStep'),
|
|
417
|
+
settingsListField('skipKeywords'),
|
|
418
|
+
settingsBoolField('inline'),
|
|
419
|
+
settingsBoolField('enforceReadonlyPlanning'),
|
|
420
|
+
]);
|
|
421
|
+
this.modelStore = this.model.bind(() => this.projection());
|
|
422
|
+
this.modelOff = this.modelStore.subscribe(() => this.store.set(this.projection()));
|
|
423
|
+
this.store.set(this.projection());
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** Build the snapshot the card renders from. */
|
|
427
|
+
projection() {
|
|
428
|
+
const served = this.servedNamespaces();
|
|
429
|
+
if (!this.model) return { ...emptyFormState(), served };
|
|
430
|
+
return {
|
|
431
|
+
...this.model.shell(),
|
|
432
|
+
served,
|
|
433
|
+
allowlist: this.model.field('allowlist'),
|
|
434
|
+
injectStep: this.model.field('injectStep'),
|
|
435
|
+
skipKeywords: this.model.field('skipKeywords'),
|
|
436
|
+
inline: this.model.field('inline'),
|
|
437
|
+
enforceReadonlyPlanning: this.model.field('enforceReadonlyPlanning'),
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Build the face the slot registration injects.
|
|
443
|
+
* @returns the form snapshot hook and the staged-form actions.
|
|
444
|
+
*/
|
|
445
|
+
inject() {
|
|
446
|
+
const call = (name) => (...args) => {
|
|
447
|
+
if (!this.model) return undefined;
|
|
448
|
+
return this.model.actions()[name](...args);
|
|
449
|
+
};
|
|
450
|
+
return {
|
|
451
|
+
hooks: { trellisConfig: this.store },
|
|
452
|
+
edit: call('edit'),
|
|
453
|
+
resetField: call('resetField'),
|
|
454
|
+
save: call('save'),
|
|
455
|
+
discard: call('discard'),
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** Release every subscription. */
|
|
460
|
+
dispose() {
|
|
461
|
+
if (this.mirrorOff) this.mirrorOff();
|
|
462
|
+
if (this.modelOff) this.modelOff();
|
|
463
|
+
if (this.model) this.model.dispose();
|
|
464
|
+
this.mirrorOff = null;
|
|
465
|
+
this.modelOff = null;
|
|
466
|
+
this.model = null;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** One labelled checkbox row for a boolean config field. */
|
|
471
|
+
function BooleanRow(props) {
|
|
472
|
+
const { id, label, hint, checked, disabled, onToggle } = props;
|
|
473
|
+
return react.createElement(
|
|
474
|
+
'div',
|
|
475
|
+
{ style: { margin: '12px 0 2px' } },
|
|
476
|
+
react.createElement(
|
|
477
|
+
'label',
|
|
478
|
+
{ htmlFor: id, style: { display: 'flex', gap: 8, alignItems: 'center', fontSize: 13 } },
|
|
479
|
+
react.createElement('input', {
|
|
480
|
+
id,
|
|
481
|
+
type: 'checkbox',
|
|
482
|
+
checked,
|
|
483
|
+
disabled,
|
|
484
|
+
onChange: (e) => onToggle(e.target.checked),
|
|
485
|
+
}),
|
|
486
|
+
label,
|
|
487
|
+
),
|
|
488
|
+
hint
|
|
489
|
+
? react.createElement(
|
|
490
|
+
'p',
|
|
491
|
+
{ style: { margin: '4px 0 0', color: 'var(--dsw-alias-label-secondary)', fontSize: 12 } },
|
|
492
|
+
hint,
|
|
493
|
+
)
|
|
494
|
+
: null,
|
|
495
|
+
);
|
|
496
|
+
}
|
|
268
497
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
498
|
+
/**
|
|
499
|
+
* Render this entry's configuration: the one-liner when the page asks for a
|
|
500
|
+
* summary, otherwise the shared form with its staged fields and one save.
|
|
501
|
+
* @param {object} props the view asked for, locale copy, the form snapshot,
|
|
502
|
+
* its actions, and the injected slot face.
|
|
503
|
+
* @returns {unknown} the summary line, or the form.
|
|
504
|
+
*/
|
|
505
|
+
function TrellisConfigCard(props) {
|
|
506
|
+
const { t } = props;
|
|
507
|
+
if (props.view === 'summary') return t('desc');
|
|
508
|
+
const state = props.useTrellisConfig((snapshot) => snapshot);
|
|
509
|
+
const disabled = !state.writable;
|
|
510
|
+
const field = (name) => state[name] || { text: '', overridden: false, invalid: false };
|
|
511
|
+
|
|
512
|
+
const onBrowse = () => {
|
|
513
|
+
const ws = clientCtx && clientCtx.uiWorkspace;
|
|
514
|
+
if (!ws || typeof ws.pickDirectory !== 'function') {
|
|
515
|
+
setBrowseError(true);
|
|
274
516
|
return;
|
|
275
517
|
}
|
|
276
|
-
|
|
277
|
-
// Reuse the saved-flag effect's timer slot: browse errors auto-clear
|
|
278
|
-
// via pickError's own effect above; picking ends on any outcome.
|
|
279
|
-
workspaces
|
|
280
|
-
.pickDirectory()
|
|
518
|
+
ws.pickDirectory()
|
|
281
519
|
.then((picked) => {
|
|
282
|
-
|
|
283
|
-
if (picked === null || picked === undefined) return; // user cancelled
|
|
520
|
+
if (picked === null || picked === undefined) return;
|
|
284
521
|
const normalized = String(picked).trim().replace(/\\/g, '/');
|
|
285
|
-
if (!normalized)
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
if (!allowlist.includes(normalized)) write('allowlist', allowlist.concat(normalized));
|
|
290
|
-
else setSaved(true); // already present — still confirm the click landed
|
|
522
|
+
if (!normalized) return;
|
|
523
|
+
const current = listFromText(field('allowlist').text);
|
|
524
|
+
if (current.includes(normalized)) return;
|
|
525
|
+
props.edit('allowlist', current.concat(normalized).join(', '));
|
|
291
526
|
})
|
|
292
|
-
.catch(() =>
|
|
293
|
-
setPicking(false);
|
|
294
|
-
setPickError(true);
|
|
295
|
-
});
|
|
527
|
+
.catch(() => setBrowseError(true));
|
|
296
528
|
};
|
|
297
529
|
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
530
|
+
const [browseError, setBrowseError] = react.useState(false);
|
|
531
|
+
react.useEffect(() => {
|
|
532
|
+
if (!browseError) return undefined;
|
|
533
|
+
const timer = setTimeout(() => setBrowseError(false), 2600);
|
|
534
|
+
return () => clearTimeout(timer);
|
|
535
|
+
}, [browseError]);
|
|
536
|
+
|
|
537
|
+
if (!state.available) {
|
|
538
|
+
// The Host does not (yet) serve this entry. Say so, and name what it
|
|
539
|
+
// does serve, so a mis-resolved entry id is diagnosable from the page
|
|
540
|
+
// itself instead of showing up as an unexplained blank.
|
|
541
|
+
const served = Array.isArray(state.served) ? state.served : [];
|
|
542
|
+
return react.createElement(
|
|
543
|
+
'div',
|
|
544
|
+
{ style: { display: 'flex', flexDirection: 'column', gap: 6 } },
|
|
545
|
+
react.createElement(
|
|
546
|
+
'p',
|
|
547
|
+
{ role: 'status', style: { margin: 0, color: 'var(--dsw-alias-label-tertiary)', fontSize: 13 } },
|
|
548
|
+
t('unavailable'),
|
|
549
|
+
),
|
|
550
|
+
react.createElement(
|
|
551
|
+
'p',
|
|
552
|
+
{
|
|
553
|
+
style: {
|
|
554
|
+
margin: 0,
|
|
555
|
+
color: 'var(--dsw-alias-label-tertiary)',
|
|
556
|
+
fontSize: 12,
|
|
557
|
+
fontFamily: 'var(--ds-font-family-code)',
|
|
558
|
+
overflowWrap: 'anywhere',
|
|
559
|
+
},
|
|
560
|
+
},
|
|
561
|
+
`${t('servedNamespaces')} ${served.length ? served.join(', ') : t('noneServed')}`,
|
|
562
|
+
),
|
|
563
|
+
);
|
|
564
|
+
}
|
|
322
565
|
|
|
323
566
|
return react.createElement(
|
|
324
|
-
|
|
325
|
-
{
|
|
326
|
-
react.createElement(
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
:
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
),
|
|
339
|
-
react.createElement(
|
|
340
|
-
'button',
|
|
341
|
-
{ type: 'button', style: btnStyle, onClick: () => write('allowlist', allowlist.filter((x) => x !== p)) },
|
|
342
|
-
t('remove'),
|
|
343
|
-
),
|
|
344
|
-
),
|
|
345
|
-
),
|
|
567
|
+
primitives.SettingsForm,
|
|
568
|
+
{ labels: formLabels(t), state, onSave: props.save, onDiscard: props.discard },
|
|
569
|
+
react.createElement(primitives.SettingsValueField, {
|
|
570
|
+
id: 'trellis-config-allowlist',
|
|
571
|
+
label: t('allowlist'),
|
|
572
|
+
hint: t('allowlistHint'),
|
|
573
|
+
overriddenLabel: t('overridden'),
|
|
574
|
+
resetLabel: t('reset'),
|
|
575
|
+
invalidLabel: t('invalidNumber'),
|
|
576
|
+
disabled,
|
|
577
|
+
...field('allowlist'),
|
|
578
|
+
onEdit: (text) => props.edit('allowlist', text),
|
|
579
|
+
onReset: () => props.resetField('allowlist'),
|
|
580
|
+
}),
|
|
346
581
|
react.createElement(
|
|
347
582
|
'div',
|
|
348
|
-
{ style:
|
|
349
|
-
react.createElement('input', {
|
|
350
|
-
style: inputStyle,
|
|
351
|
-
placeholder: t('addPlaceholder'),
|
|
352
|
-
value: draftPath,
|
|
353
|
-
onChange: (e) => setDraftPath(e.target.value),
|
|
354
|
-
onKeyDown: (e) => { if (e.key === 'Enter') addPath(); },
|
|
355
|
-
}),
|
|
583
|
+
{ style: { display: 'flex', gap: 8, alignItems: 'center' } },
|
|
356
584
|
react.createElement(
|
|
357
585
|
'button',
|
|
358
|
-
{
|
|
359
|
-
|
|
360
|
-
style: { ...btnStyle, opacity: picking ? 0.6 : 1, cursor: picking ? 'wait' : 'pointer' },
|
|
361
|
-
disabled: picking,
|
|
362
|
-
onClick: browsePath,
|
|
363
|
-
},
|
|
364
|
-
picking ? t('browseBusy') : t('browse'),
|
|
586
|
+
{ type: 'button', style: btnStyle, disabled, onClick: onBrowse },
|
|
587
|
+
t('browse'),
|
|
365
588
|
),
|
|
366
|
-
|
|
589
|
+
browseError
|
|
590
|
+
? react.createElement(
|
|
591
|
+
'p',
|
|
592
|
+
{ style: { margin: 0, color: 'var(--dsw-alias-state-error-primary)', fontSize: 12 }, role: 'alert' },
|
|
593
|
+
t('browseFailed'),
|
|
594
|
+
)
|
|
595
|
+
: null,
|
|
367
596
|
),
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
type: 'number',
|
|
381
|
-
min: 1,
|
|
382
|
-
style: { ...inputStyle, maxWidth: 160 },
|
|
383
|
-
value: value && typeof value.injectStep === 'number' ? value.injectStep : 1,
|
|
384
|
-
onChange: (e) => write('injectStep', Number(e.target.value) || 1),
|
|
597
|
+
react.createElement(primitives.SettingsValueField, {
|
|
598
|
+
id: 'trellis-config-inject-step',
|
|
599
|
+
label: t('injectStep'),
|
|
600
|
+
hint: t('injectStepHint'),
|
|
601
|
+
overriddenLabel: t('overridden'),
|
|
602
|
+
resetLabel: t('reset'),
|
|
603
|
+
invalidLabel: t('invalidNumber'),
|
|
604
|
+
numeric: true,
|
|
605
|
+
disabled,
|
|
606
|
+
...field('injectStep'),
|
|
607
|
+
onEdit: (text) => props.edit('injectStep', text),
|
|
608
|
+
onReset: () => props.resetField('injectStep'),
|
|
385
609
|
}),
|
|
386
|
-
react.createElement(
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
610
|
+
react.createElement(primitives.SettingsValueField, {
|
|
611
|
+
id: 'trellis-config-skip-keywords',
|
|
612
|
+
label: t('skipKeywords'),
|
|
613
|
+
hint: t('skipKeywordsHint'),
|
|
614
|
+
overriddenLabel: t('overridden'),
|
|
615
|
+
resetLabel: t('reset'),
|
|
616
|
+
invalidLabel: t('invalidNumber'),
|
|
617
|
+
disabled,
|
|
618
|
+
...field('skipKeywords'),
|
|
619
|
+
onEdit: (text) => props.edit('skipKeywords', text),
|
|
620
|
+
onReset: () => props.resetField('skipKeywords'),
|
|
621
|
+
}),
|
|
622
|
+
react.createElement(BooleanRow, {
|
|
623
|
+
id: 'trellis-config-inline',
|
|
624
|
+
label: t('inline'),
|
|
625
|
+
checked: field('inline').text === 'true',
|
|
626
|
+
disabled,
|
|
627
|
+
onToggle: (next) => props.edit('inline', next ? 'true' : 'false'),
|
|
628
|
+
}),
|
|
629
|
+
react.createElement(BooleanRow, {
|
|
630
|
+
id: 'trellis-config-readonly',
|
|
631
|
+
label: t('enforceReadonlyPlanning'),
|
|
632
|
+
hint: t('enforceReadonlyPlanningHint'),
|
|
633
|
+
checked: field('enforceReadonlyPlanning').text === 'true',
|
|
634
|
+
disabled,
|
|
635
|
+
onToggle: (next) => props.edit('enforceReadonlyPlanning', next ? 'true' : 'false'),
|
|
394
636
|
}),
|
|
395
|
-
react.createElement(
|
|
396
|
-
'label',
|
|
397
|
-
{ style: { display: 'flex', gap: 8, alignItems: 'center', margin: '14px 0 6px', fontSize: 13 } },
|
|
398
|
-
react.createElement('input', {
|
|
399
|
-
type: 'checkbox',
|
|
400
|
-
checked: !!(value && value.inline),
|
|
401
|
-
onChange: (e) => write('inline', e.target.checked),
|
|
402
|
-
}),
|
|
403
|
-
t('inline'),
|
|
404
|
-
),
|
|
405
|
-
react.createElement(
|
|
406
|
-
'label',
|
|
407
|
-
{ style: { display: 'flex', gap: 8, alignItems: 'center', margin: '14px 0 6px', fontSize: 13 } },
|
|
408
|
-
react.createElement('input', {
|
|
409
|
-
type: 'checkbox',
|
|
410
|
-
checked: !!(value && value.enforceReadonlyPlanning),
|
|
411
|
-
onChange: (e) => write('enforceReadonlyPlanning', e.target.checked),
|
|
412
|
-
}),
|
|
413
|
-
t('enforceReadonlyPlanning'),
|
|
414
|
-
),
|
|
415
|
-
react.createElement('p', { style: { margin: 0, color: 'var(--dsw-alias-label-secondary)', fontSize: 12 } }, t('enforceReadonlyPlanningHint')),
|
|
416
|
-
saved
|
|
417
|
-
? react.createElement('p', { style: { margin: 0, color: 'var(--dsw-alias-state-success-primary)', fontSize: 12 } }, t('saved'))
|
|
418
|
-
: null,
|
|
419
637
|
);
|
|
420
638
|
}
|
|
421
639
|
|
|
640
|
+
// #endregion
|
|
641
|
+
|
|
422
642
|
// #region lib/types/client/trellis-task-chip.js
|
|
423
643
|
|
|
424
644
|
// NOTE: stage lanes are NOT maintained here anymore — the board payload
|
|
@@ -3039,19 +3259,22 @@ window.__ModuleLoader__.load({
|
|
|
3039
3259
|
clientCtx = ctx;
|
|
3040
3260
|
ctx.effect(() => ctx.locale.register(LOCALE_NS, { zh, en }), 'trellis-workflow: dictionaries');
|
|
3041
3261
|
const t = ctx.locale.bind(LOCALE_NS);
|
|
3042
|
-
//
|
|
3043
|
-
|
|
3044
|
-
|
|
3262
|
+
// The entry's configuration on the Plugins page: staged edits over the
|
|
3263
|
+
// shared `configForms` form, registered under the row key that page
|
|
3264
|
+
// dispatches for this bundle's `trellis-workflow` row. The controller
|
|
3265
|
+
// resolves the entry's (possibly namespaced) id from the describe mirror
|
|
3266
|
+
// itself, so the card also works when the Host has not served it yet.
|
|
3267
|
+
const controller = new TrellisConfigController(ctx.configForms);
|
|
3268
|
+
ctx.effect(() => () => controller.dispose(), 'trellis-workflow: config form');
|
|
3269
|
+
ctx.slots.inject('plugins.row.config', () =>
|
|
3045
3270
|
ctx.slots.register(
|
|
3046
3271
|
{
|
|
3047
|
-
name: '
|
|
3048
|
-
|
|
3049
|
-
order: 20,
|
|
3050
|
-
label: () => t('tab'),
|
|
3272
|
+
name: 'plugins.row.config',
|
|
3273
|
+
key: ROW_CONFIG_KEY,
|
|
3051
3274
|
locale: LOCALE_NS,
|
|
3052
|
-
inject: () => (
|
|
3275
|
+
inject: () => controller.inject(),
|
|
3053
3276
|
},
|
|
3054
|
-
|
|
3277
|
+
TrellisConfigCard,
|
|
3055
3278
|
),
|
|
3056
3279
|
);
|
|
3057
3280
|
// Session-header phase chip: additive list seat; the framework injects
|
package/lib/index.js
CHANGED
|
@@ -269,10 +269,14 @@ function lruSet(map, key, value, cap) {
|
|
|
269
269
|
/** The Cordis plugin. */
|
|
270
270
|
export default {
|
|
271
271
|
name: NAME,
|
|
272
|
-
inject: ['fs', 'skills', 'tools'],
|
|
273
|
-
|
|
272
|
+
inject: ['fs', 'skills', 'tools', 'agents'],
|
|
273
|
+
// The schema MUST be declared as `Config` (capital): cordis exposes a plugin's
|
|
274
|
+
// schema at `runtime.Config`, which is exactly what dsh-settings' describe()
|
|
275
|
+
// reads (`entry.fiber.runtime.Config`). With a lowercase `config` the entry is
|
|
276
|
+
// never listed, so its Plugins-page form silently disappears.
|
|
277
|
+
Config: SCHEMA,
|
|
274
278
|
apply(ctx, config) {
|
|
275
|
-
// --- effective config: Web Settings
|
|
279
|
+
// --- effective config: Web Settings form layered over entry config ---
|
|
276
280
|
const effectiveConfig = registerTrellisSettings(ctx, config)
|
|
277
281
|
|
|
278
282
|
// --- skill provisioning (project-level, not bundled registration) -------
|
|
@@ -1058,7 +1062,9 @@ export default {
|
|
|
1058
1062
|
const effective = effectiveConfig.get()
|
|
1059
1063
|
if (!effective.enforceReadonlyPlanning) return assembled
|
|
1060
1064
|
|
|
1061
|
-
|
|
1065
|
+
// AssembleContext carries no `agent` (0.1.7): the assembling agent is the
|
|
1066
|
+
// process-local initiator, so resolve the session cwd from it.
|
|
1067
|
+
const agent = (ctx.agents && typeof ctx.agents.currentInitiator === 'function' && ctx.agents.currentInitiator()) || undefined
|
|
1062
1068
|
const cwd = normalizePath(agent && agent.session && agent.session.header ? agent.session.header.cwd : undefined)
|
|
1063
1069
|
const root = matchAllowlist(cwd, effective.allowlist)
|
|
1064
1070
|
if (!root) return assembled
|
package/lib/meta.js
CHANGED
|
@@ -22,17 +22,26 @@ export const SETTINGS_NAMESPACE = 'trellis-workflow'
|
|
|
22
22
|
*/
|
|
23
23
|
export const API_PREFIX = '/trellis-workflow/api'
|
|
24
24
|
|
|
25
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* Shared config shape: plugin entry config and the user-settings form.
|
|
27
|
+
*
|
|
28
|
+
* DSH 0.1.7 settings are Loader-entry driven: the settings form for this
|
|
29
|
+
* plugin IS the `Config` schema below, keyed by the loader entry id
|
|
30
|
+
* (`trellis-workflow`). Only fields marked `.volatile()` are editable through
|
|
31
|
+
* the Web settings form; at resolve time a volatile field wraps its value in a
|
|
32
|
+
* cosmokit volatile (an object with a `get()` accessor), so consumers must
|
|
33
|
+
* unwrap before use (see `unwrapVolatileConfig`).
|
|
34
|
+
*/
|
|
26
35
|
function configShape() {
|
|
27
36
|
return {
|
|
28
37
|
/** Project roots whose cwd should receive a Trellis breadcrumb. */
|
|
29
|
-
allowlist: z.array(z.string()).default(DEFAULT_ALLOWLIST),
|
|
38
|
+
allowlist: z.array(z.string()).default(DEFAULT_ALLOWLIST).volatile(),
|
|
30
39
|
/** Only inject on this step index (1 = first step of each user message). */
|
|
31
|
-
injectStep: z.number().default(1),
|
|
40
|
+
injectStep: z.number().default(1).volatile(),
|
|
32
41
|
/** Standalone words that suppress injection for a turn. */
|
|
33
|
-
skipKeywords: z.array(z.string()).default(['no-trellis']),
|
|
42
|
+
skipKeywords: z.array(z.string()).default(['no-trellis']).volatile(),
|
|
34
43
|
/** Assume codex-inline dispatch mode when resolving phase names. */
|
|
35
|
-
inline: z.boolean().default(false),
|
|
44
|
+
inline: z.boolean().default(false).volatile(),
|
|
36
45
|
/**
|
|
37
46
|
* Read-only planning enforcement: when a session is in the undecided or
|
|
38
47
|
* planning authorization state, TRIM only the specified tools from the
|
|
@@ -40,13 +49,52 @@ function configShape() {
|
|
|
40
49
|
* tools invalid for that state), keeping every other tool — including
|
|
41
50
|
* tools registered by other plugins — intact.
|
|
42
51
|
*/
|
|
43
|
-
enforceReadonlyPlanning: z.boolean().default(false),
|
|
52
|
+
enforceReadonlyPlanning: z.boolean().default(false).volatile(),
|
|
44
53
|
}
|
|
45
54
|
}
|
|
46
55
|
|
|
47
56
|
export const SCHEMA = z.object(configShape())
|
|
48
57
|
|
|
49
|
-
/**
|
|
58
|
+
/**
|
|
59
|
+
* Resolve a config value produced by a `.volatile()` schema field. schemastery
|
|
60
|
+
* wraps volatile values in a cosmokit volatile — a plain object exposing
|
|
61
|
+
* `get()` — so `.get()` returns the underlying value. Non-volatile (already
|
|
62
|
+
* plain) values pass through unchanged.
|
|
63
|
+
* @param {unknown} value a config field value (possibly volatile-wrapped).
|
|
64
|
+
* @returns {unknown} the unwrapped value.
|
|
65
|
+
*/
|
|
66
|
+
export function unwrapVolatile(value) {
|
|
67
|
+
if (value !== null && typeof value === 'object' && typeof value.get === 'function') {
|
|
68
|
+
try {
|
|
69
|
+
return value.get()
|
|
70
|
+
} catch {
|
|
71
|
+
return value
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return value
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Deep-unwrap a resolved config object whose `.volatile()` fields are wrapped
|
|
79
|
+
* in cosmokit volatiles. Returns a plain config safe for the existing allowlist
|
|
80
|
+
* / keyword / step reads. Non-object input passes through.
|
|
81
|
+
* @param {unknown} config resolved config (e.g. from `SCHEMA(cfg)` or `ctx.config`).
|
|
82
|
+
* @returns {object} the unwrapped config.
|
|
83
|
+
*/
|
|
84
|
+
export function unwrapVolatileConfig(config) {
|
|
85
|
+
if (config === null || typeof config !== 'object') return {}
|
|
86
|
+
const out = {}
|
|
87
|
+
for (const [key, value] of Object.entries(config)) {
|
|
88
|
+
out[key] = unwrapVolatile(value)
|
|
89
|
+
}
|
|
90
|
+
// Defensive: fields consumed as iterables must be arrays even if a volatile
|
|
91
|
+
// wrapper resolved to something unexpected.
|
|
92
|
+
if (!Array.isArray(out.allowlist)) out.allowlist = []
|
|
93
|
+
if (!Array.isArray(out.skipKeywords)) out.skipKeywords = ['no-trellis']
|
|
94
|
+
return out
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** @deprecated DSH 0.1.7 projects the Config schema directly; kept for backward imports. */
|
|
50
98
|
export function settingsSchema() {
|
|
51
|
-
return
|
|
99
|
+
return SCHEMA
|
|
52
100
|
}
|
package/lib/settings.js
CHANGED
|
@@ -1,53 +1,69 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Settings integration for the trellis trigger (DSH 0.1.7 model).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Since 0.1.7 the settings service is Loader-entry driven: each plugin's
|
|
5
|
+
* `Config` schema is projected into a settings form keyed by the loader entry
|
|
6
|
+
* id (`trellis-workflow`), and the Web client reads/writes it through the
|
|
7
|
+
* shared `configForms` service. Plugins no longer register a namespace —
|
|
8
|
+
* `ctx.settings.register()` is gone.
|
|
7
9
|
*
|
|
8
|
-
*
|
|
10
|
+
* Two properties of that model shape this module:
|
|
9
11
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* 1. The entry's form is generated by default: dsh-settings resolves
|
|
13
|
+
* `autoGenerate` as `presentations.get(fiber)?.auto ?? true`, so a plugin
|
|
14
|
+
* does NOT have to opt in. Calling `ctx.settings.configure()` from an
|
|
15
|
+
* `inject` callback would in fact fail with "cannot create effect on
|
|
16
|
+
* inactive context", so this module deliberately does not call it.
|
|
17
|
+
* 2. The live effective value must be read from the plugin's own Cordis fiber.
|
|
18
|
+
* `ctx.config` is guard-blocked ("cannot get property \"config\" without
|
|
19
|
+
* inject"); `ctx.fiber.config` carries the resolved config the Loader keeps
|
|
20
|
+
* current as Web-form edits apply. The mount-time argument stays the
|
|
21
|
+
* fallback, so a composition without a settings provider still works.
|
|
14
22
|
*/
|
|
15
23
|
|
|
16
|
-
import {
|
|
24
|
+
import { SCHEMA, unwrapVolatileConfig } from './meta.js'
|
|
17
25
|
|
|
18
26
|
/**
|
|
19
|
-
*
|
|
27
|
+
* Return a live getter for this plugin's effective config.
|
|
20
28
|
*
|
|
21
|
-
* Registration goes through `ctx.inject(['settings'], …)` rather than a
|
|
22
|
-
* one-shot `ctx.get('settings')` probe: plugin activation order is
|
|
23
|
-
* service-availability driven, so the settings service may not be mounted yet
|
|
24
|
-
* when this plugin's `apply` runs. The inject callback fires once settings is
|
|
25
|
-
* available (or never, when a composition mounts no settings provider), so the
|
|
26
|
-
* namespace is registered whenever the host can serve it. Without a provider,
|
|
27
|
-
* resolution falls back to the entry config unchanged.
|
|
28
29
|
* @param {import('@deepseek-ai/cordis').Context} ctx plugin context.
|
|
29
30
|
* @param {object} config the plugin's entry config (composition `base`).
|
|
30
31
|
* @returns {{ get: () => object }} a live getter for the effective config.
|
|
31
32
|
*/
|
|
32
33
|
export function registerTrellisSettings(ctx, config) {
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
// `apply` receives the config cordis already resolved against `Config`:
|
|
35
|
+
// defaults are applied and every `.volatile()` field arrives as a cosmokit
|
|
36
|
+
// wrapper. So normalize by unwrapping (never by re-validating through the
|
|
37
|
+
// schema, which would resolve the wrappers a second time) and layering it
|
|
38
|
+
// over the schema defaults for anything the entry omitted entirely.
|
|
39
|
+
const defaults = unwrapVolatileConfig(SCHEMA({}))
|
|
40
|
+
const base = { ...defaults }
|
|
41
|
+
for (const [key, value] of Object.entries(unwrapVolatileConfig(config ?? {}))) {
|
|
42
|
+
if (value !== undefined) base[key] = value
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Resolve the effective config, preferring the fiber's live resolved config
|
|
47
|
+
* (Web edits land there) and falling back to the mount-time config. Never
|
|
48
|
+
* throws: a throwing getter would fail every tool call and the per-turn
|
|
49
|
+
* breadcrumb with it.
|
|
50
|
+
* @returns {object} plain config values.
|
|
51
|
+
*/
|
|
52
|
+
function get() {
|
|
40
53
|
try {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
54
|
+
const live = ctx.fiber && ctx.fiber.config
|
|
55
|
+
if (live !== null && typeof live === 'object') {
|
|
56
|
+
const out = { ...base }
|
|
57
|
+
for (const [key, value] of Object.entries(unwrapVolatileConfig(live))) {
|
|
58
|
+
if (value !== undefined) out[key] = value
|
|
59
|
+
}
|
|
60
|
+
return out
|
|
61
|
+
}
|
|
62
|
+
} catch {
|
|
63
|
+
/* unreachable fiber, or a build whose config is unreadable — use base */
|
|
50
64
|
}
|
|
51
|
-
|
|
52
|
-
|
|
65
|
+
return base
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { get }
|
|
53
69
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@banana-peeljj12/dsh-trellis",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"description": "Self-contained Trellis workflow trigger for DeepSeek Harness: per-turn breadcrumb injection via agent/pre-step, a bundled skill provider, and DSH-native task/phase state resolution. No Trellis AGPL code is vendored; workflow semantics are rewritten and licensed MIT.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -12,9 +12,11 @@
|
|
|
12
12
|
"client": {
|
|
13
13
|
"inject": [
|
|
14
14
|
"@deepseek-ai/dsh-api-remotes",
|
|
15
|
-
"@deepseek-ai/dsh-client-
|
|
15
|
+
"@deepseek-ai/dsh-client-locale",
|
|
16
|
+
"@deepseek-ai/dsh-client-ui-plugin-manager",
|
|
16
17
|
"@deepseek-ai/dsh-client-ui-settings",
|
|
17
|
-
"@deepseek-ai/dsh-client-
|
|
18
|
+
"@deepseek-ai/dsh-client-ui-sidebar-right",
|
|
19
|
+
"@deepseek-ai/dsh-client-ui-workspace"
|
|
18
20
|
],
|
|
19
21
|
"platform": "web"
|
|
20
22
|
}
|
|
@@ -25,8 +27,7 @@
|
|
|
25
27
|
"scripts": {
|
|
26
28
|
"test": "node --test",
|
|
27
29
|
"install:trellis": "node ./scripts/install.mjs"
|
|
28
|
-
},
|
|
29
|
-
"exports": {
|
|
30
|
+
}, "exports": {
|
|
30
31
|
".": {
|
|
31
32
|
"types": "./lib/types/index.d.ts",
|
|
32
33
|
"default": "./lib/index.js"
|
|
@@ -54,14 +55,24 @@
|
|
|
54
55
|
"url": "git+https://github.com/1264459640/dsh-trellis.git"
|
|
55
56
|
},
|
|
56
57
|
"peerDependencies": {
|
|
57
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
58
|
-
"@deepseek-ai/dsh-agent": "^0.1.
|
|
59
|
-
"@deepseek-ai/dsh-fs": "^0.1.
|
|
60
|
-
"@deepseek-ai/dsh-llm": "^0.1.
|
|
61
|
-
"@deepseek-ai/dsh-session": "^0.1.
|
|
62
|
-
"@deepseek-ai/dsh-settings": "^0.1.
|
|
63
|
-
"@deepseek-ai/dsh-tools": "^0.1.
|
|
64
|
-
"@deepseek-ai/schemastery": "^3.18.
|
|
58
|
+
"@deepseek-ai/cordis": "^4.0.4",
|
|
59
|
+
"@deepseek-ai/dsh-agent": "^0.1.7-rc.1",
|
|
60
|
+
"@deepseek-ai/dsh-fs": "^0.1.7-rc.1",
|
|
61
|
+
"@deepseek-ai/dsh-llm": "^0.1.7-rc.1",
|
|
62
|
+
"@deepseek-ai/dsh-session": "^0.1.7-rc.1",
|
|
63
|
+
"@deepseek-ai/dsh-settings": "^0.1.7-rc.1",
|
|
64
|
+
"@deepseek-ai/dsh-tools": "^0.1.7-rc.1",
|
|
65
|
+
"@deepseek-ai/schemastery": "^3.18.4"
|
|
66
|
+
},
|
|
67
|
+
"devDependencies": {
|
|
68
|
+
"@deepseek-ai/cordis": "4.0.4",
|
|
69
|
+
"@deepseek-ai/dsh-agent": "0.1.7-rc.1",
|
|
70
|
+
"@deepseek-ai/dsh-fs": "0.1.7-rc.1",
|
|
71
|
+
"@deepseek-ai/dsh-llm": "0.1.7-rc.1",
|
|
72
|
+
"@deepseek-ai/dsh-session": "0.1.7-rc.1",
|
|
73
|
+
"@deepseek-ai/dsh-settings": "0.1.7-rc.1",
|
|
74
|
+
"@deepseek-ai/dsh-tools": "0.1.7-rc.1",
|
|
75
|
+
"@deepseek-ai/schemastery": "3.18.4"
|
|
65
76
|
},
|
|
66
77
|
"peerDependenciesMeta": {
|
|
67
78
|
"@deepseek-ai/dsh-settings": {
|
package/scripts/install.mjs
CHANGED
|
@@ -13,9 +13,12 @@
|
|
|
13
13
|
* 3. (optional) — `--fix-deps` removes stale `link:` deps whose target
|
|
14
14
|
* path does not exist; `--uninstall` removes the config row, the
|
|
15
15
|
* dependency junction, and the package.json dep entries in one step.
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
*
|
|
17
|
+
* DSH 0.1.7 note: settings are Loader-entry driven — the Web settings form is
|
|
18
|
+
* the entry's Config schema keyed by `trellis-workflow`, reached by the client
|
|
19
|
+
* through the generated `remote.settings` namespace. No harness
|
|
20
|
+
* `WEB_SETTINGS_NAMESPACES` patch is needed anymore, so this installer no
|
|
21
|
+
* longer patches dsh-host-apiproxy.
|
|
19
22
|
*
|
|
20
23
|
* After a successful run, restart the DSH process for the profile. The package
|
|
21
24
|
* `postinstall` hook invokes this script with `--auto` (safe, idempotent, and
|
|
@@ -114,18 +117,16 @@ trellis-workflow 注入层一键安装器
|
|
|
114
117
|
--inline 按 codex-inline 调度模式解析阶段
|
|
115
118
|
--auto 幂等自动模式(postinstall 使用;无法识别时静默退出)
|
|
116
119
|
--dry-run 只预览将发生的改动,不写盘
|
|
117
|
-
--uninstall 一步卸载:配置行 + 依赖 junction + package.json 依赖项
|
|
120
|
+
--uninstall 一步卸载:配置行 + 依赖 junction + package.json 依赖项
|
|
118
121
|
--fix-deps 清理 package.json 中指向不存在路径的 trellis link 依赖
|
|
119
|
-
--patch-harness 只对 dsh-host-apiproxy 的 WEB_SETTINGS_NAMESPACES 白名单做幂等补丁
|
|
120
|
-
(新增 trellis-workflow,无需 profile;DSH 升级覆盖后可重跑补回)
|
|
121
122
|
-h, --help 显示本帮助
|
|
122
123
|
|
|
123
|
-
|
|
124
|
-
|
|
124
|
+
DSH 0.1.7 起 settings 由 Loader 条目驱动:Web 设置表单即条目 Config(命名空间为
|
|
125
|
+
loader 条目 id trellis-workflow),客户端经生成的 remote.settings 命名空间读写,
|
|
126
|
+
不再需要打 harness 白名单补丁。
|
|
125
127
|
|
|
126
128
|
示例:
|
|
127
129
|
node scripts/install.mjs --profile web --allowlist "F:/Projects/FordProject"
|
|
128
|
-
node scripts/install.mjs --patch-harness
|
|
129
130
|
node scripts/install.mjs --dry-run
|
|
130
131
|
node scripts/install.mjs --uninstall --dry-run
|
|
131
132
|
node scripts/install.mjs --uninstall
|
|
@@ -532,10 +533,9 @@ if (process.env.TRELLIS_SKIP_AUTO === '1' && args.auto) {
|
|
|
532
533
|
process.exit(0)
|
|
533
534
|
}
|
|
534
535
|
if (args.patchHarness) {
|
|
535
|
-
//
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
console.log('\n补丁完成。重启 DSH(对应 profile 的进程)后,Web 设置页签生效。')
|
|
536
|
+
// DSH 0.1.7: settings are Loader-entry driven and reached by the client via
|
|
537
|
+
// the generated remote.settings namespace — no harness allowlist patch.
|
|
538
|
+
console.log('DSH 0.1.7 起 Web 设置由 Loader 条目驱动(remote.settings),无需再补丁 harness 白名单。')
|
|
539
539
|
process.exit(0)
|
|
540
540
|
}
|
|
541
541
|
|
|
@@ -578,8 +578,6 @@ if (args.uninstall) {
|
|
|
578
578
|
console.log(removeLink(profileDir, args.dryRun).message)
|
|
579
579
|
// 3. package.json dep entries
|
|
580
580
|
for (const message of removeTrellisDeps(profileDir, args.dryRun)) console.log(message)
|
|
581
|
-
// 4. harness settings-exposure allowlist (path A reversal)
|
|
582
|
-
for (const message of unpatchHarnessAllowlist(args.dryRun)) console.log(message)
|
|
583
581
|
console.log('\n卸载完成。重启 DSH(对应 profile 的进程)后,插件完全移除。')
|
|
584
582
|
process.exit(0)
|
|
585
583
|
}
|
|
@@ -614,8 +612,8 @@ if (args.fixDeps) {
|
|
|
614
612
|
if (!bad.length) console.log('依赖检查:无失效 link 依赖')
|
|
615
613
|
}
|
|
616
614
|
|
|
617
|
-
//
|
|
618
|
-
|
|
615
|
+
// DSH 0.1.7: the Web settings form is the entry's Config reached via
|
|
616
|
+
// remote.settings — no harness WEB_SETTINGS_NAMESPACES patch is required.
|
|
619
617
|
|
|
620
618
|
if (ok) {
|
|
621
619
|
console.log('\n完成。重启 DSH(对应 profile 的进程)后,注入层与 Web 设置页签生效。')
|