@banana-peeljj12/dsh-trellis 0.3.1 → 0.3.3
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 +2 -1
- package/README_EN.md +1 -1
- package/lib/client.js +164 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -67,7 +67,8 @@ dsh plugin --profile web add @banana-peeljj12/dsh-trellis@latest
|
|
|
67
67
|
为避免非预期介入,插件默认不会拦截未授权的目录。请在 Web 端或配置文件中添加生效路径:
|
|
68
68
|
|
|
69
69
|
1. 打开 DSH Web 客户端,进入左下角 **设置 → 插件 → Trellis 工作流**;
|
|
70
|
-
2. 在 **白名单项目 (allowlist)**
|
|
70
|
+
2. 在 **白名单项目 (allowlist)** 中添加需要启用该工作流的项目根目录绝对路径——既可手动输入,
|
|
71
|
+
也可点击 **浏览…** 按钮弹出系统文件夹选择器直接选取,保存后即时生效。
|
|
71
72
|
|
|
72
73
|
> **提示**:若希望在方案定稿前限制模型修改源码,可在该设置页同时开启 **规划期只读保护 (enforceReadonlyPlanning)** 开关。
|
|
73
74
|
|
package/README_EN.md
CHANGED
|
@@ -67,7 +67,7 @@ After installation, **restart the DSH service**.
|
|
|
67
67
|
By default, the plugin will not intercept any unconfigured workspaces. Add your target workspace directory to the allowlist:
|
|
68
68
|
|
|
69
69
|
1. Open the DSH Web UI and navigate to bottom-left **Settings → Plugins → Trellis Workflow**;
|
|
70
|
-
2. Under **Allowed Projects (allowlist)**, add the absolute path of your workspace root directory
|
|
70
|
+
2. Under **Allowed Projects (allowlist)**, add the absolute path of your workspace root directory — either type it manually or click **Browse…** to pick a folder via the system folder picker, then click Save (hot-reloaded).
|
|
71
71
|
|
|
72
72
|
> **Note**: To prevent the model from editing source code prior to plan approval, enable **Enforce Read-Only Planning (`enforceReadonlyPlanning`)** in the same settings panel.
|
|
73
73
|
|
package/lib/client.js
CHANGED
|
@@ -27,6 +27,14 @@ window.__ModuleLoader__.load({
|
|
|
27
27
|
|
|
28
28
|
let react = require('react');
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Module-level client root context, captured in apply(ctx). Components (slot
|
|
32
|
+
* registrations) do not receive ctx directly, but need host services such as
|
|
33
|
+
* `sidebarRight` (native document preview) at click time — this is the
|
|
34
|
+
* bridge. Null until apply runs, which happens before any component mounts.
|
|
35
|
+
*/
|
|
36
|
+
let clientCtx = null;
|
|
37
|
+
|
|
30
38
|
// #region lib/types/client/trellis-settings-tab.js
|
|
31
39
|
|
|
32
40
|
/** Settings namespace registered by the Host half (see lib/meta.js). */
|
|
@@ -36,7 +44,7 @@ window.__ModuleLoader__.load({
|
|
|
36
44
|
const LOCALE_NS = 'settings.trellisWorkflow';
|
|
37
45
|
|
|
38
46
|
/** Services required by this client half. */
|
|
39
|
-
const inject = ['slots', 'locale', 'settingsScope'];
|
|
47
|
+
const inject = ['slots', 'locale', 'settingsScope', 'sidebarRight', 'workspaces'];
|
|
40
48
|
|
|
41
49
|
const zh = {
|
|
42
50
|
tab: 'Trellis 工作流',
|
|
@@ -57,6 +65,9 @@ window.__ModuleLoader__.load({
|
|
|
57
65
|
'开启后,命中白名单的项目里:新对话(未建任务)只裁剪 write/edit 与任务写工具(trellis_task_update / trellis_artifact_update / trellis_task_archive / trellis_ui_update),其余工具(含其他插件的工具)保留;规划中的任务只裁剪 write/edit 与创建/跳过/归档类 trellis 工具,其余工具保留;经 trellis_task_skip 跳过任务的会话恢复完整工具。',
|
|
58
66
|
saved: '已保存',
|
|
59
67
|
writeFailed: '保存失败',
|
|
68
|
+
browse: '浏览…',
|
|
69
|
+
browseBusy: '选择中…',
|
|
70
|
+
browseFailed: '调用文件夹选择器失败,请手动输入路径',
|
|
60
71
|
chipTitle: 'Trellis Task',
|
|
61
72
|
chipNoTask: '无活动任务',
|
|
62
73
|
chipNoSummary: '尚无状态,点击刷新',
|
|
@@ -94,6 +105,7 @@ window.__ModuleLoader__.load({
|
|
|
94
105
|
sendToChat: '推进任务',
|
|
95
106
|
sendToChatSuccess: '已填入输入框,回车即可发送',
|
|
96
107
|
sendToChatFailed: '自动填入失败,已复制到剪贴板',
|
|
108
|
+
openFailed: '打开失败,文件引用已复制到剪贴板',
|
|
97
109
|
pendingVerification: '待验证',
|
|
98
110
|
blocked: '阻塞',
|
|
99
111
|
open: '打开',
|
|
@@ -124,6 +136,9 @@ window.__ModuleLoader__.load({
|
|
|
124
136
|
allowlistHint: 'Sessions whose cwd matches these project roots receive the workflow breadcrumb.',
|
|
125
137
|
addPlaceholder: 'Project root path, e.g. F:/Projects/FordProject',
|
|
126
138
|
add: 'Add',
|
|
139
|
+
browse: 'Browse…',
|
|
140
|
+
browseBusy: 'Picking…',
|
|
141
|
+
browseFailed: 'Could not open the directory picker — enter the path manually',
|
|
127
142
|
remove: 'Remove',
|
|
128
143
|
empty: '(empty)',
|
|
129
144
|
injectStep: 'Inject step (injectStep)',
|
|
@@ -171,6 +186,7 @@ window.__ModuleLoader__.load({
|
|
|
171
186
|
sendToChat: 'Push to chat',
|
|
172
187
|
sendToChatSuccess: 'Filled into the input — press Enter to send',
|
|
173
188
|
sendToChatFailed: 'Auto-fill failed, copied to clipboard',
|
|
189
|
+
openFailed: 'Failed to open — file reference copied to clipboard',
|
|
174
190
|
pendingVerification: 'Verifying',
|
|
175
191
|
blocked: 'Blocked',
|
|
176
192
|
open: 'Open',
|
|
@@ -235,6 +251,50 @@ window.__ModuleLoader__.load({
|
|
|
235
251
|
setDraftPath('');
|
|
236
252
|
};
|
|
237
253
|
|
|
254
|
+
/**
|
|
255
|
+
* Folder picker browse button: open the Host's native directory picker
|
|
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.
|
|
260
|
+
*/
|
|
261
|
+
const [picking, setPicking] = react.useState(false);
|
|
262
|
+
const [pickError, setPickError] = react.useState(false);
|
|
263
|
+
react.useEffect(() => {
|
|
264
|
+
if (!pickError) return undefined;
|
|
265
|
+
const timer = setTimeout(() => setPickError(false), 2600);
|
|
266
|
+
return () => clearTimeout(timer);
|
|
267
|
+
}, [pickError]);
|
|
268
|
+
|
|
269
|
+
const browsePath = () => {
|
|
270
|
+
if (picking) return;
|
|
271
|
+
const workspaces = clientCtx && clientCtx.workspaces;
|
|
272
|
+
if (!workspaces || typeof workspaces.pickDirectory !== 'function') {
|
|
273
|
+
setPickError(true);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
setPicking(true);
|
|
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()
|
|
281
|
+
.then((picked) => {
|
|
282
|
+
setPicking(false);
|
|
283
|
+
if (picked === null || picked === undefined) return; // user cancelled
|
|
284
|
+
const normalized = String(picked).trim().replace(/\\/g, '/');
|
|
285
|
+
if (!normalized) {
|
|
286
|
+
setPickError(true);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (!allowlist.includes(normalized)) write('allowlist', allowlist.concat(normalized));
|
|
290
|
+
else setSaved(true); // already present — still confirm the click landed
|
|
291
|
+
})
|
|
292
|
+
.catch(() => {
|
|
293
|
+
setPicking(false);
|
|
294
|
+
setPickError(true);
|
|
295
|
+
});
|
|
296
|
+
};
|
|
297
|
+
|
|
238
298
|
const rowStyle = { display: 'flex', gap: 8, alignItems: 'center', marginBottom: 6 };
|
|
239
299
|
const inputStyle = {
|
|
240
300
|
flex: 1,
|
|
@@ -293,8 +353,28 @@ window.__ModuleLoader__.load({
|
|
|
293
353
|
onChange: (e) => setDraftPath(e.target.value),
|
|
294
354
|
onKeyDown: (e) => { if (e.key === 'Enter') addPath(); },
|
|
295
355
|
}),
|
|
356
|
+
react.createElement(
|
|
357
|
+
'button',
|
|
358
|
+
{
|
|
359
|
+
type: 'button',
|
|
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'),
|
|
365
|
+
),
|
|
296
366
|
react.createElement('button', { type: 'button', style: btnStyle, onClick: addPath }, t('add')),
|
|
297
367
|
),
|
|
368
|
+
pickError
|
|
369
|
+
? react.createElement(
|
|
370
|
+
'p',
|
|
371
|
+
{
|
|
372
|
+
style: { margin: 0, color: 'var(--dsw-alias-state-error-primary)', fontSize: 12 },
|
|
373
|
+
role: 'alert',
|
|
374
|
+
},
|
|
375
|
+
t('browseFailed'),
|
|
376
|
+
)
|
|
377
|
+
: null,
|
|
298
378
|
react.createElement('label', { style: labelStyle }, t('injectStep')),
|
|
299
379
|
react.createElement('input', {
|
|
300
380
|
type: 'number',
|
|
@@ -942,8 +1022,72 @@ window.__ModuleLoader__.load({
|
|
|
942
1022
|
return '@.trellis/tasks/' + task.slug + '/' + name;
|
|
943
1023
|
}
|
|
944
1024
|
|
|
1025
|
+
/**
|
|
1026
|
+
* DSH-native file address for an artifact token. Mirrors the official
|
|
1027
|
+
* `fileAddressFor` / `sessionFileAddress` contract
|
|
1028
|
+
* (@deepseek-ai/dsh-client-ui-sidebar-files): the token's path (minus the
|
|
1029
|
+
* leading `@`) is a workspace-relative path, so it maps to a session-scoped
|
|
1030
|
+
* `dsh-resource://file/session/<sessionId>/<path>` address the right-sidebar
|
|
1031
|
+
* document preview resolves against the session's workspace. Path segments
|
|
1032
|
+
* are component-encoded, keeping `:` literal for drive letters.
|
|
1033
|
+
* @param {string} sessionId the session whose workspace resolves the path.
|
|
1034
|
+
* @param {string} token artifact token from {@link artifactToken}.
|
|
1035
|
+
* @returns {string} `dsh-resource://file/session/...` address.
|
|
1036
|
+
*/
|
|
1037
|
+
function artifactResourceUrl(sessionId, token) {
|
|
1038
|
+
const path = (token || '').replace(/^@/, '');
|
|
1039
|
+
const encodeSegment = (segment) => encodeURIComponent(segment).replace(/%3A/gi, ':');
|
|
1040
|
+
const encodedPath = path
|
|
1041
|
+
.replace(/\\/g, '/')
|
|
1042
|
+
.split('/')
|
|
1043
|
+
.map(encodeSegment)
|
|
1044
|
+
.join('/');
|
|
1045
|
+
return 'dsh-resource://file/session/' + encodeSegment(sessionId) + '/' + encodedPath;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/**
|
|
1049
|
+
* Open an artifact file in the DSH-native document preview (right sidebar),
|
|
1050
|
+
* mirroring how the official chat client opens `@file` references
|
|
1051
|
+
* (`ctx.sidebarRight.openResource(...)`). Never touches the composer input.
|
|
1052
|
+
* Falls back to copying the token to the clipboard when the right-sidebar
|
|
1053
|
+
* service is unavailable or the open fails, so the click still yields
|
|
1054
|
+
* something useful without polluting the input box.
|
|
1055
|
+
* @param {object} ctx client root context (with `sidebarRight` injected).
|
|
1056
|
+
* @param {string} sessionId the session owning the artifact's workspace.
|
|
1057
|
+
* @param {string} token artifact token from {@link artifactToken}.
|
|
1058
|
+
* @returns {boolean} true when opened natively; false when only copied.
|
|
1059
|
+
*/
|
|
1060
|
+
function openArtifactFile(ctx, sessionId, token) {
|
|
1061
|
+
try {
|
|
1062
|
+
const sidebar = ctx && ctx.sidebarRight;
|
|
1063
|
+
if (sidebar && typeof sidebar.openResource === 'function') {
|
|
1064
|
+
sidebar.openResource(artifactResourceUrl(sessionId, token));
|
|
1065
|
+
return true;
|
|
1066
|
+
}
|
|
1067
|
+
} catch {
|
|
1068
|
+
/* fall through to clipboard copy */
|
|
1069
|
+
}
|
|
1070
|
+
try {
|
|
1071
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
1072
|
+
navigator.clipboard.writeText(token);
|
|
1073
|
+
} else {
|
|
1074
|
+
const ta = document.createElement('textarea');
|
|
1075
|
+
ta.value = token;
|
|
1076
|
+
ta.style.position = 'fixed';
|
|
1077
|
+
ta.style.opacity = '0';
|
|
1078
|
+
document.body.appendChild(ta);
|
|
1079
|
+
ta.select();
|
|
1080
|
+
document.execCommand('copy');
|
|
1081
|
+
document.body.removeChild(ta);
|
|
1082
|
+
}
|
|
1083
|
+
} catch {
|
|
1084
|
+
/* nothing more we can do */
|
|
1085
|
+
}
|
|
1086
|
+
return false;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
945
1089
|
function KanbanDetails(props) {
|
|
946
|
-
const { task, t, active, busy, onActivate, onDeactivate, tracks } = props;
|
|
1090
|
+
const { task, t, active, busy, onActivate, onDeactivate, tracks, onOpenArtifact } = props;
|
|
947
1091
|
// One-shot injection notice: 'ok' | 'fail' | null, auto-clears.
|
|
948
1092
|
const [notice, setNotice] = react.useState(null);
|
|
949
1093
|
react.useEffect(() => {
|
|
@@ -1276,7 +1420,11 @@ window.__ModuleLoader__.load({
|
|
|
1276
1420
|
textAlign: 'center',
|
|
1277
1421
|
},
|
|
1278
1422
|
},
|
|
1279
|
-
notice === 'ok'
|
|
1423
|
+
notice === 'ok'
|
|
1424
|
+
? t('sendToChatSuccess')
|
|
1425
|
+
: notice === 'open-fail'
|
|
1426
|
+
? t('openFailed')
|
|
1427
|
+
: t('sendToChatFailed'),
|
|
1280
1428
|
)
|
|
1281
1429
|
: null,
|
|
1282
1430
|
),
|
|
@@ -1336,7 +1484,13 @@ window.__ModuleLoader__.load({
|
|
|
1336
1484
|
'button',
|
|
1337
1485
|
{
|
|
1338
1486
|
type: 'button',
|
|
1339
|
-
onClick: () =>
|
|
1487
|
+
onClick: () => {
|
|
1488
|
+
const token = artifactToken(task, name);
|
|
1489
|
+
const opened = onOpenArtifact ? onOpenArtifact(token) : false;
|
|
1490
|
+
// Notice only on failure: a successful native open is
|
|
1491
|
+
// visible in the right sidebar already, so no toast.
|
|
1492
|
+
setNotice(opened ? null : 'open-fail');
|
|
1493
|
+
},
|
|
1340
1494
|
title: t('open') + ' ' + artifactToken(task, name),
|
|
1341
1495
|
style: {
|
|
1342
1496
|
display: 'inline-flex',
|
|
@@ -1814,6 +1968,7 @@ window.__ModuleLoader__.load({
|
|
|
1814
1968
|
const {
|
|
1815
1969
|
board,
|
|
1816
1970
|
t,
|
|
1971
|
+
sessionId,
|
|
1817
1972
|
selected,
|
|
1818
1973
|
onSelect,
|
|
1819
1974
|
busy,
|
|
@@ -1823,6 +1978,7 @@ window.__ModuleLoader__.load({
|
|
|
1823
1978
|
onFilterChange,
|
|
1824
1979
|
onClose,
|
|
1825
1980
|
initialViewMode,
|
|
1981
|
+
onOpenArtifact,
|
|
1826
1982
|
} = props;
|
|
1827
1983
|
const [query, setQuery] = react.useState('');
|
|
1828
1984
|
const [viewMode, setViewMode] = react.useState(initialViewMode === 'list' ? 'list' : 'lanes'); // 'lanes' | 'list'
|
|
@@ -2424,6 +2580,7 @@ window.__ModuleLoader__.load({
|
|
|
2424
2580
|
onActivate: () => onActivate(selectedTask.slug),
|
|
2425
2581
|
onDeactivate,
|
|
2426
2582
|
tracks,
|
|
2583
|
+
onOpenArtifact: (token) => openArtifactFile(clientCtx, sessionId, token),
|
|
2427
2584
|
}),
|
|
2428
2585
|
),
|
|
2429
2586
|
),
|
|
@@ -2812,6 +2969,7 @@ window.__ModuleLoader__.load({
|
|
|
2812
2969
|
? react.createElement(KanbanExpandedModal, {
|
|
2813
2970
|
board,
|
|
2814
2971
|
t,
|
|
2972
|
+
sessionId,
|
|
2815
2973
|
selected,
|
|
2816
2974
|
onSelect: setSelected,
|
|
2817
2975
|
busy,
|
|
@@ -2820,6 +2978,7 @@ window.__ModuleLoader__.load({
|
|
|
2820
2978
|
filter,
|
|
2821
2979
|
onFilterChange: setFilter,
|
|
2822
2980
|
onClose: closeExpanded,
|
|
2981
|
+
onOpenArtifact: (token) => openArtifactFile(clientCtx, sessionId, token),
|
|
2823
2982
|
})
|
|
2824
2983
|
: null;
|
|
2825
2984
|
|
|
@@ -2877,6 +3036,7 @@ window.__ModuleLoader__.load({
|
|
|
2877
3036
|
// #endregion
|
|
2878
3037
|
|
|
2879
3038
|
function apply(ctx) {
|
|
3039
|
+
clientCtx = ctx;
|
|
2880
3040
|
ctx.effect(() => ctx.locale.register(LOCALE_NS, { zh, en }), 'trellis-workflow: dictionaries');
|
|
2881
3041
|
const t = ctx.locale.bind(LOCALE_NS);
|
|
2882
3042
|
// Bound once on this plugin's fiber: the scope's disposer follows the fiber.
|
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.3",
|
|
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",
|