@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 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 and click Save (hot-reloaded).
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' ? t('sendToChatSuccess') : t('sendToChatFailed'),
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: () => push(artifactToken(task, name)),
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.1",
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",