dsh-workspace-api-key 0.2.6 → 0.2.7

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
@@ -33,6 +33,7 @@ Host half (`lib/index.js`):
33
33
  - rewrites only refs a provider actually reads: the default ref, every adapter's configured `apiKeyEnv`, and its own generated refs. Every other ref passes through untouched;
34
34
  - builds the session list from the `sessionController` service (`list()`, unwrapping its `{items: […]}` envelope) and falls back to the live `sessions` service (`ctx.get('sessions').list()`), so the panel still has sessions when the controller is missing, throws, or returns an empty list. It hides the two kinds of session the sidebar hides: subagent sessions (`origin: 'subagent'`, or a `parentSession`) and *blank* sessions (created but never prompted — the sidebar shows only the one currently open). Without those two filters one workspace with a single real session listed thirteen;
35
35
  - reports its own version and a session probe (`available`, `shape`, `source`, `count`, `withoutCwd`, `subagents`, `samples`, `error`, plus sample workspace paths) in `/wsk-api/state`, so the panel can say *why* no sessions are listed: a stale host half (only a page reload instead of an app restart), a missing `sessionController`, a throwing `list()`, an unexpected envelope shape, or a `cwd` that matches no workspace path;
36
+ - lists a directory that is not a registered workspace only if the sidebar would show it. It cannot decide that itself, because "only the currently open blank session counts" is knowledge the browser side holds (`retainedBy.mainView`), so it sends every such directory plus a `blank` flag per session and a `hiddenBlankDirs` list naming the ones whose sessions are all blank — the browser half applies the same verdict the sidebar does;
36
37
  - keeps its own bookkeeping (which level maps to which ref, invalid flags, the provider a key was recorded for) in `$DSH_HOME/storages/dsh-workspace-api-key.json`;
37
38
  - observes failures through the `agent/request-error` waterfall without ever returning `{kind: 'retry'}` — it only records the verdict and calls `next()`.
38
39
 
@@ -51,7 +52,7 @@ The browser half (`lib/client.js`) registers into the `sidebar.panellist` slot (
51
52
 
52
53
  Only loopback callers are accepted (`127.0.0.1`, `::1`, `::ffff:127.0.0.1`) with a same-origin or absent `Origin`. No response ever contains a plaintext key, only a mask.
53
54
 
54
- Workspace targets may be directories that are not registered workspaces; such entries appear as ungrouped directories next to the workspaces they belong to.
55
+ Workspace targets may be directories that are not registered workspaces; such entries appear as ungrouped directories, and only the ones the sidebar would actually show — a directory whose only sessions are brand-new and still empty (the sidebar shows just the currently open one) is not listed.
55
56
 
56
57
  ## Install
57
58
 
@@ -71,8 +72,8 @@ Then add `"dsh-workspace-api-key"` to `dsh.profile.bundles` in that profile's `p
71
72
  ## Tests
72
73
 
73
74
  ```sh
74
- node test/host.test.mjs # 130 assertions
75
- node test/client.test.mjs # 139 assertions
75
+ node test/host.test.mjs # 136 assertions
76
+ node test/client.test.mjs # 145 assertions
76
77
  ```
77
78
 
78
79
  Neither suite needs a running DSH process. The host suite drives `apply()` against a mock context (fake `credentials`, `workspaceRegistry`, `agents`, `sessionController`, `sessions`, `webServer`, stubbed `fetch`) and covers ref derivation, the session → workspace → default cascade, workspace matching, redirecting a second adapter's `apiKeyEnv`, clearing back to the default, 401/402/429/5xx classification, the loopback and origin checks, state persistence, and the session probe including the `{items}` envelope, the live-session fallback, and subagent and blank sessions being hidden. The client suite loads `lib/client.js` through a minimal `window.__ModuleLoader__` plus a small React runtime, renders the panel and the overlay, and asserts slot registration, the first render, session expand/collapse, per-session saving and testing, sessions recovered from the browser-side store when the host has none (with `displayTitle` winning and subagent sessions hidden), the ungrouped-directory card, the stale and mismatch prompts, the "go configure" hand-off, graceful degradation without a session list, and back-navigation when `ctx.get('layout')` only becomes available after the plugin was applied.
package/README.zh.md CHANGED
@@ -32,6 +32,7 @@ DSH 只从一个全局凭据 ref(默认 `DEEPSEEK_API_KEY`)解析 DeepSeek k
32
32
  - 由每一层派生出稳定的 ref —— 工作区是 `DEEPSEEK_API_KEY_WS_<sha1(规范化路径) 前 16 位十六进制大写>`,会话是 `DEEPSEEK_API_KEY_SS_<sha1(会话 id) 前 16 位十六进制大写>`(会话 id 含 `-`,不能直接当 ref 名)——通过凭据服务写入 key,并返回这个 ref 的值;
33
33
  - 只改写 provider 真正会读的 ref:默认 ref、每个适配器配置的 `apiKeyEnv`,以及插件自己生成的 ref;其它 ref 原样透传;
34
34
  - 会话列表来自 `sessionController` 服务(`list()`,会拆开它 `{items: […]}` 的信封),控件缺失 / 抛错 / 列表为空时再退到宿主 live 会话服务(`ctx.get('sessions').list()`),所以那种情况下面板仍能列出会话;侧栏藏起来的两种会话这里也藏:子代理会话(`origin: 'subagent'` 或带 `parentSession`)和**空会话**(建了但一句话都没发过,侧栏只显示当前开着的那个)。两种都不过滤的话,一个只有 1 个真实会话的工作区会列出 13 个;
35
+ - 不是注册工作区的目录,只有侧栏真的会显示它才会被列出来。这件事宿主判不了:「空会话里只有当前开着的那个算数」是浏览器端才知道的事(`retainedBy.mainView`),所以宿主把这种目录连同每个会话的 `blank` 标记一起发出去,并用 `hiddenBlankDirs` 点名「里面全是空会话」的目录,由浏览器半边套用与侧栏一致的判据;
35
36
  - `/wsk-api/state` 会带上插件自身版本与会话探针(`available`、`shape`、`source`、`count`、`withoutCwd`、`subagents`、`samples`、`error`,以及工作区路径样本),面板据此说明**为什么**没有列出会话:宿主半边是旧版本(只刷新了页面而没重启应用)、没有 `sessionController`、`list()` 抛错、信封形状不认识,还是会话的 `cwd` 与任何工作区路径都对不上;
36
37
  - 自己的账本(哪一层对应哪个 ref、失效标记、key 是给哪个服务商记的)落在 `$DSH_HOME/storages/dsh-workspace-api-key.json`;
37
38
  - 通过 `agent/request-error` waterfall 观察失败,但**从不**返回 `{kind: 'retry'}`——只记录判定结果并 `next()` 放行。
@@ -51,7 +52,7 @@ DSH 只从一个全局凭据 ref(默认 `DEEPSEEK_API_KEY`)解析 DeepSeek k
51
52
 
52
53
  只接受本机回环调用(`127.0.0.1`、`::1`、`::ffff:127.0.0.1`),且 `Origin` 必须同源或缺失。任何响应都不会包含明文 key,只有掩码。
53
54
 
54
- 工作区目标也接受不是注册工作区的目录;这类条目会作为「未分组目录」和它们所属的工作区一起列在面板里。
55
+ 工作区目标也接受不是注册工作区的目录;这类条目会作为「未分组目录」列在面板里,且只列侧栏真的会显示的目录——一个目录里若只有「刚建好、一句话都没发过」的空会话,侧栏不会显示它(空会话里只有当前打开的那个算数),面板也不会列。
55
56
 
56
57
  ## 安装
57
58
 
@@ -71,8 +72,8 @@ pnpm add "dsh-workspace-api-key@github:COH2357/dsh-workspace-api-key"
71
72
  ## 测试
72
73
 
73
74
  ```sh
74
- node test/host.test.mjs # 130 条断言
75
- node test/client.test.mjs # 139 条断言
75
+ node test/host.test.mjs # 136 条断言
76
+ node test/client.test.mjs # 145 条断言
76
77
  ```
77
78
 
78
79
  两个套件都不需要启动 DSH。宿主套件用一个假 ctx(假的 `credentials`、`workspaceRegistry`、`agents`、`sessionController`、`sessions`、`webServer`,以及打桩的 `fetch`)驱动 `apply()`,覆盖 ref 派生、会话→工作区→默认的级联、工作区匹配、把第二个适配器的 `apiKeyEnv` 一起重定向、清回默认、401/402/429/5xx 的判定、回环与来源校验、状态落盘,以及会话探针(含 `{items}` 信封与 live 兜底)。浏览器套件通过一个极小的 `window.__ModuleLoader__` 与自制 React 运行时加载 `lib/client.js`,渲染出面板与覆盖层,断言槽注册、首屏渲染、会话折叠/展开、会话级保存与测试、宿主列不出会话时从浏览器端存储恢复会话(标题取 `displayTitle`、隐藏子代理会话与空会话、浏览器端只认识一部分会话时不挤掉宿主行)、会话列表服务晚于插件提供时的重试、未分组目录卡片、失效与不匹配提醒、「去配置」的交接、没有会话列表时的降级,以及 `ctx.get('layout')` 在插件 apply 之后才可用时返回键仍然有效。
package/lib/client.js CHANGED
@@ -279,12 +279,12 @@ window.__ModuleLoader__.load({
279
279
  if (entries.length === 0) {
280
280
  // 没有浏览器端列表时也把子代理/空会话滤掉(宿主新版本已经过滤,这里兜旧宿主)。
281
281
  const clean = (rows) => (Array.isArray(rows) ? rows : []).filter(isVisible)
282
- return {
283
- ...state,
284
- workspaces: (state.workspaces ?? []).map((ws) => ({ ...ws, sessions: clean(ws.sessions) })),
285
- ungrouped: (state.ungrouped ?? []).map((group) => ({ ...group, sessions: clean(group.sessions) })),
286
- sessionBlanks: blankSkipped,
287
- }
282
+ const workspaces = (state.workspaces ?? []).map((ws) => ({ ...ws, sessions: clean(ws.sessions) }))
283
+ // 未分组目录:过滤后一个会话都不剩的就不列(侧栏对空目录同样什么都不显示)。
284
+ const ungrouped = (state.ungrouped ?? [])
285
+ .map((group) => ({ ...group, sessions: clean(group.sessions) }))
286
+ .filter((group) => (group.sessions?.length ?? 0) > 0)
287
+ return { ...state, workspaces, ungrouped, sessionBlanks: blankSkipped }
288
288
  }
289
289
 
290
290
  const hostRows = new Map()
@@ -381,6 +381,8 @@ window.__ModuleLoader__.load({
381
381
  const rows = fill(group.sessions)
382
382
  for (const row of (extras.get(key) ?? [])) rows.push(row)
383
383
  extras.delete(key)
384
+ // 过滤后没有会话的目录不再列(宿主新版本已经不产生这种组,这里兜旧宿主)。
385
+ if (rows.length === 0) continue
384
386
  ungrouped.push({ ...group, sessions: sortByUpdated(rows) })
385
387
  }
386
388
  for (const [key, rows] of extras) {
@@ -703,9 +705,14 @@ window.__ModuleLoader__.load({
703
705
  const noCwd = probe?.withoutCwd > 0 ? ` · ${probe.withoutCwd} 个没有 cwd` : ''
704
706
  const subs = probe?.subagents > 0 ? ` · 已跳过 ${probe.subagents} 个子代理会话` : ''
705
707
  const blanks = (view?.sessionBlanks ?? 0) > 0 ? ` · 已跳过 ${view.sessionBlanks} 个空会话` : ''
708
+ // 宿主只标注「这个目录里只有还没说过话的空会话」,列不列由本文件按侧栏语义决定
709
+ // (空会话里只有当前打开的那一个算数)——所以这里说的是「宿主标了几个」。
710
+ const blankDirs = Array.isArray(state.hiddenBlankDirs) && state.hiddenBlankDirs.length > 0
711
+ ? ` · ${state.hiddenBlankDirs.length} 个目录只有空会话(未分组里不列)`
712
+ : ''
706
713
  return `插件 v${hostVersion} · 状态存储 v${state.storeVersion ?? '—'}`
707
714
  + ` · 宿主会话列表:${sessions}${listError}${noCwd}${subs}`
708
- + ` · 浏览器会话列表:${clientCount} 个${blanks}`
715
+ + ` · 浏览器会话列表:${clientCount} 个${blanks}${blankDirs}`
709
716
  + ` · 工作区 ${grouped.length} + 未分组 ${ungrouped.length} · 挂在目录下的会话 ${sessionTotal}`
710
717
  }
711
718
 
@@ -1063,7 +1070,8 @@ window.__ModuleLoader__.load({
1063
1070
  ? h('div', { className: 'wsk-card' },
1064
1071
  h('h4', null, `未分组目录(${ungrouped.length})`),
1065
1072
  h('div', { className: 'wsk-hint', style: { marginBottom: 6 } },
1066
- '这些目录里的会话没有挂在工作区下(侧栏的「未分组」),同样可以单独配置。'),
1073
+ '这些目录里的会话没有挂在工作区下(侧栏的「未分组」),同样可以单独配置。'
1074
+ + '只有侧栏真的会显示的目录才列在这里(刚建好、还没发过消息的空会话不算)。'),
1067
1075
  h('div', { className: 'wsk-list' }, ungrouped.map((ws) => renderWorkspace(ws))))
1068
1076
  : null,
1069
1077
 
package/lib/index.js CHANGED
@@ -1048,6 +1048,21 @@ async function workspaceRow(ctx, state, { id, path: dir, title, kind }, rows, cl
1048
1048
  }
1049
1049
  }
1050
1050
 
1051
+ /**
1052
+ * 侧栏会不会真的显示这一行。
1053
+ * 侧栏对「还没发过消息的空会话」(sessionController 的 blank 标记)只显示当前打开的那一个,
1054
+ * 其余全部藏起来(另见 lib/client.js 同名判据与 @deepseek-ai/dsh-client-ui-workspace 的
1055
+ * sessionVisible)。空目录之所以要按这个判据过滤:见 snapshot() 里 ungrouped 的注释。
1056
+ */
1057
+ function sidebarVisibleRow(row) {
1058
+ return row?.blank !== true
1059
+ }
1060
+
1061
+ /** 目录里至少有一个「侧栏会显示」的会话(没有会话的目录不算)。 */
1062
+ function hasVisibleSession(rows) {
1063
+ return Array.isArray(rows) && rows.some((row) => sidebarVisibleRow(row))
1064
+ }
1065
+
1051
1066
  async function snapshot(ctx, state) {
1052
1067
  const credentials = ctx.get('credentials')
1053
1068
  const defaultRefName = 'DEEPSEEK_API_KEY'
@@ -1105,10 +1120,18 @@ async function snapshot(ctx, state) {
1105
1120
  }
1106
1121
 
1107
1122
  // 未注册成工作区、但确实有会话的目录(侧栏的「未分组」):也能单独给会话配 key。
1123
+ // 注意:**不在这里过滤**。侧栏对一个目录显不显示,取决于「目录里有没有它真的显示出来的
1124
+ // 会话」,而「空会话只显示当前打开的那一个」这条规则只有浏览器端知道(会话 store 的
1125
+ // retainedBy.mainView)。宿主这边只把目录和 blank 标记原样发出去,由 lib/client.js 按
1126
+ // 同一个判据决定列不列 —— 以前面板会冒出侧栏上并不存在的「未分组目录」(用户实测:
1127
+ // E:\web 与插件自己的目录,各自只有一个还没说过话的空会话)。
1108
1128
  const ungrouped = []
1129
+ const hiddenBlankDirs = []
1109
1130
  for (const [norm, list] of byCwd) {
1110
1131
  if (claimed.has(norm)) continue
1111
1132
  const dir = typeof list[0]?.cwd === 'string' ? list[0].cwd : norm
1133
+ // 诊断用:这个目录里一个「真实会话」都没有(界面把它藏起来,但用户能看到原因)。
1134
+ if (!hasVisibleSession(list)) hiddenBlankDirs.push(dir)
1112
1135
  ungrouped.push(await workspaceRow(ctx, state, { id: undefined, path: dir, title: basename(dir) || dir, kind: 'directory' }, list))
1113
1136
  }
1114
1137
 
@@ -1166,6 +1189,9 @@ async function snapshot(ctx, state) {
1166
1189
  statePath: stateFilePath(),
1167
1190
  workspaces,
1168
1191
  ungrouped,
1192
+ // 诊断:这些未分组目录里只有空会话,界面按侧栏语义会把它藏起来(不直接从这里删,
1193
+ // 因为「当前打开的空会话」要显示,而这件事只有浏览器端知道)。
1194
+ hiddenBlankDirs,
1169
1195
  sessionOverrides,
1170
1196
  }
1171
1197
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-workspace-api-key",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "A DeepSeek Harness plugin that binds a separate DeepSeek API key to each workspace, or to a single session, so the spend of one key stays with the sessions you choose.",
5
5
  "type": "module",
6
6
  "license": "MIT",