@konnga/dsh-hud 0.9.0 → 0.9.1

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/index.js CHANGED
@@ -20,6 +20,10 @@
20
20
  * - `image { cwd, path, origPath?, side?, sha? }` → 图片改动**两侧**的字节与像素尺寸
21
21
  * (`kind` 为 `image` / `too-large` / `not-image` / `missing`),给二进制预览用;
22
22
  * 单侧上限 `MAX_IMAGE_BYTES`,超限只回报字节数。
23
+ * - `session-time { sessionId }` → 该会话的创建时刻与累计墙钟(`llmMs` / `toolMs` /
24
+ * `turns` / `steps`,来自内置 `sessionStats` 投影;服务缺失时 `available: false`)。
25
+ * - `prompt-history { sessionId, limit? }` → 该会话的用户 prompt(**新的在前**,来自内置
26
+ * `turnOutline` 投影;相邻重复只留一条,默认最多 100 条)。
23
27
  * - `log { cwd, count? }` → 最近提交(短 sha、作者、日期、refs、标题),条数钳制 1..50。
24
28
  * - `commit { cwd, sha }` → 某次提交的元信息与改动清单(相对**第一个父提交**,
25
29
  * 合并提交也按第一父提交看),清单形状与 `status.changes` 一致。
@@ -276,6 +280,90 @@ export async function readStatus(cwd) {
276
280
  /** 一次最多看多少个工作区(多个会话可能共用一个目录,按目录去重之后再算)。 */
277
281
  const ROW_STATUS_MAX_WORKTREES = 8
278
282
 
283
+ /**
284
+ * 一个会话的用户 prompt 历史,**新的在前**(给输入框的 `↑` 召回用)。
285
+ *
286
+ * 来源是内置 `turnOutline` 投影的每条 `prompt` —— 也就是会话日志里真正的用户消息,
287
+ * 不是我们自己记的一份。截断到 `limit` 条,相邻重复(同一句连发两次)只留一条。
288
+ * 投影没注册、会话不在(还没恢复进存储 / 已归档)时回 `available: false` 与空列表。
289
+ *
290
+ * @param sessionId - 会话 id。
291
+ * @param limit - 最多返回多少条(1..200,默认 {@link PROMPT_HISTORY_MAX})。
292
+ */
293
+ export function readPromptHistory(sessionId, limit = PROMPT_HISTORY_MAX) {
294
+ const empty = { available: false, sessionId, prompts: [] }
295
+ if (typeof sessionId !== 'string' || sessionId.length === 0) return empty
296
+ const max = typeof limit === 'number' && Number.isFinite(limit) ? Math.min(200, Math.max(1, Math.trunc(limit))) : PROMPT_HISTORY_MAX
297
+
298
+ const sessionStore = sessionsService()
299
+ if (sessionStore === null) return empty
300
+ let live = []
301
+ try {
302
+ live = sessionStore.list()
303
+ } catch {
304
+ return empty
305
+ }
306
+ const session = live.find(item => item && item.header && item.header.id === sessionId)
307
+ if (session === undefined) return empty
308
+
309
+ const outline = sessionOutlineReader === null ? null : sessionOutlineReader(session)
310
+ const turns = outline !== null && Array.isArray(outline.turns) ? outline.turns : null
311
+ if (turns === null) return empty
312
+
313
+ const prompts = []
314
+ for (let index = turns.length - 1; index >= 0 && prompts.length < max; index -= 1) {
315
+ const turn = turns[index]
316
+ const raw = turn && typeof turn.prompt === 'string' ? turn.prompt.trim() : ''
317
+ if (raw.length === 0) continue
318
+ if (prompts.length > 0 && prompts[prompts.length - 1] === raw) continue
319
+ prompts.push(raw)
320
+ }
321
+ return { available: true, sessionId, prompts }
322
+ }
323
+
324
+ /**
325
+ * 一个会话的"运行时长"事实:创建时刻 + 内置 `sessionStats` 的累计数字。
326
+ *
327
+ * 与 codex-hud 的会话时长口径一致:**挂钟时长 = 现在 − 会话创建时刻**(含等待与空闲),
328
+ * 另外把"真的在干活"的两段(模型墙钟 `llmMs`、工具墙钟 `toolMs`)一并给出,让胶囊的悬停
329
+ * 能说清"74 小时里其实只有 1 小时 52 分在跑"。
330
+ *
331
+ * 只读;`sessions` 服务没到齐或会话已不在(归档、子会话已回收)时回 `available: false`,
332
+ * 客户端据此不画这枚胶囊。
333
+ *
334
+ * @param sessionId - 会话 id。
335
+ * @returns `{ available, sessionId, createdAt, turns, steps, llmMs, toolMs, now }`。
336
+ */
337
+ export function readSessionTime(sessionId) {
338
+ const now = Date.now()
339
+ const empty = { available: false, sessionId, createdAt: null, turns: null, steps: null, llmMs: null, toolMs: null, now }
340
+ if (typeof sessionId !== 'string' || sessionId.length === 0) return empty
341
+
342
+ const sessionStore = sessionsService()
343
+ if (sessionStore === null) return empty
344
+ let live = []
345
+ try {
346
+ live = sessionStore.list()
347
+ } catch {
348
+ return empty
349
+ }
350
+ const session = live.find(item => item && item.header && item.header.id === sessionId)
351
+ if (session === undefined) return empty
352
+
353
+ const createdAt = typeof session.header.createdAt === 'number' ? session.header.createdAt : null
354
+ const stats = sessionStatsReader === null ? null : sessionStatsReader(session)
355
+ return {
356
+ available: true,
357
+ sessionId,
358
+ createdAt,
359
+ turns: stats === null ? null : stats.turns,
360
+ steps: stats === null ? null : stats.steps,
361
+ llmMs: stats === null ? null : stats.llmMs,
362
+ toolMs: stats === null ? null : stats.toolMs,
363
+ now,
364
+ }
365
+ }
366
+
279
367
  /**
280
368
  * 会话一览:每个活着的会话 → (它那个工作区的 git 摘要)+(这个会话自己的改动计数)。
281
369
  *
@@ -284,10 +372,11 @@ const ROW_STATUS_MAX_WORKTREES = 8
284
372
  * 不用再问 git)。**不返回 `changes`**:这一行只要摘要,别把清单也拖过来。
285
373
  */
286
374
  export async function readSessionOverview() {
287
- if (hostSessions === null) return { sessions: [], unavailable: true, truncated: false, worktrees: 0 }
375
+ const sessionStore = sessionsService()
376
+ if (sessionStore === null) return { sessions: [], unavailable: true, truncated: false, worktrees: 0 }
288
377
  let live = []
289
378
  try {
290
- live = hostSessions.list()
379
+ live = sessionStore.list()
291
380
  } catch {
292
381
  return { sessions: [], unavailable: true, truncated: false, worktrees: 0 }
293
382
  }
@@ -1386,11 +1475,46 @@ const pendingChangeIntents = new Map()
1386
1475
  * 归因照常工作 —— 绝不因为少一个服务把整块降级掉。
1387
1476
  */
1388
1477
  let sessionTurnReader = null
1478
+ /**
1479
+ * `sessionStats` 投影的读数器(内置 `@deepseek-ai/dsh-session-stats` 提供)。
1480
+ *
1481
+ * 会话计时胶囊要的是"这个会话跑了多久、模型/工具各占多少"—— 这些正是它的字段,
1482
+ * 而且是**从完整持久日志折叠**出来的,分页/压实都不会让数字跳变。
1483
+ * 同样是软依赖:这个键没注册(插件没装/被组合掉)就回 null,胶囊只显示挂钟时长。
1484
+ */
1485
+ let sessionStatsReader = null
1486
+ /**
1487
+ * `turnOutline` 投影的读数器:每条 turn 都带着原始 `prompt`。
1488
+ *
1489
+ * 这是 prompt 历史最好的来源 —— 比在客户端自己记一份可靠:分页、压实、重启、换设备都不影响,
1490
+ * 因为它就是会话日志折叠出来的。同样是软依赖(投影没注册就回 null,功能整个不启用)。
1491
+ */
1492
+ let sessionOutlineReader = null
1493
+
1494
+ /** prompt 历史的条数上限(与客户端的 `↑` 召回共用这一个数)。 */
1495
+ export const PROMPT_HISTORY_MAX = 100
1389
1496
  /**
1390
1497
  * 宿主 `sessions` 服务(枚举所有活着的会话)—— 同样是软依赖:拿不到时
1391
1498
  * `session-statuses` 端点报"不可用",侧栏那枚 chip 就不画,其余功能照旧。
1392
1499
  */
1393
1500
  let hostSessions = null
1501
+ /**
1502
+ * 宿主 ctx 的引用。用于**兜底**取 `sessions`:注入回调没到齐、或者被别的挂载实例的
1503
+ * 卸载清掉时(模块级状态 + 每个挂载各自 return 清理,本来就有这个缝),
1504
+ * 按内置契约用 `ctx.get("sessions")` 再问一次(Service 目录里写明的可选取法)。
1505
+ */
1506
+ let hostContext = null
1507
+
1508
+ /** 会话服务:先信注入给的那份,再按契约兜底问 ctx。两个调用方共用这一处。 */
1509
+ function sessionsService() {
1510
+ if (hostSessions !== null) return hostSessions
1511
+ try {
1512
+ const fallback = hostContext !== null && typeof hostContext.get === 'function' ? hostContext.get('sessions') : undefined
1513
+ return fallback === undefined ? null : fallback
1514
+ } catch {
1515
+ return null
1516
+ }
1517
+ }
1394
1518
 
1395
1519
  /** 这次调用发生在第几回合(拿不到就是 null,界面据此不显示回合信息)。 */
1396
1520
  function currentTurnOf(exec) {
@@ -1854,6 +1978,29 @@ function trackSessionChanges(ctx) {
1854
1978
  }))
1855
1979
  }
1856
1980
  stops.push(ctx.inject(['sessionProjections'], (scope) => {
1981
+ sessionOutlineReader = (session) => {
1982
+ try {
1983
+ const state = scope.sessionProjections.stateOf(session, 'turnOutline')
1984
+ return state === undefined || state === null ? null : state
1985
+ } catch {
1986
+ return null
1987
+ }
1988
+ }
1989
+ sessionStatsReader = (session) => {
1990
+ try {
1991
+ const state = scope.sessionProjections.stateOf(session, 'sessionStats')
1992
+ if (state === undefined || state === null) return null
1993
+ const num = value => (typeof value === 'number' && Number.isFinite(value) ? value : 0)
1994
+ return {
1995
+ turns: num(state.turns),
1996
+ steps: num(state.steps),
1997
+ llmMs: num(state.llmMs),
1998
+ toolMs: num(state.toolMs),
1999
+ }
2000
+ } catch {
2001
+ return null
2002
+ }
2003
+ }
1857
2004
  sessionTurnReader = (session) => {
1858
2005
  try {
1859
2006
  const state = scope.sessionProjections.stateOf(session, 'turnBoundary')
@@ -1865,6 +2012,8 @@ function trackSessionChanges(ctx) {
1865
2012
  }
1866
2013
  return () => {
1867
2014
  sessionTurnReader = null
2015
+ sessionStatsReader = null
2016
+ sessionOutlineReader = null
1868
2017
  }
1869
2018
  }))
1870
2019
  }
@@ -1905,6 +2054,20 @@ const ENDPOINTS = {
1905
2054
  return readSessionOverview()
1906
2055
  },
1907
2056
 
2057
+ /** 一个会话的 prompt 历史(新的在前)—— 给输入框的 `↑` 召回用。 */
2058
+ async 'prompt-history'(args) {
2059
+ const input = args && typeof args === 'object' ? args : {}
2060
+ const sessionId = typeof input.sessionId === 'string' && input.sessionId.length > 0 ? input.sessionId : null
2061
+ return readPromptHistory(sessionId, input.limit)
2062
+ },
2063
+
2064
+ /** 一个会话的运行时长(挂钟 + 模型/工具墙钟)—— 给会话计时胶囊用。 */
2065
+ async 'session-time'(args) {
2066
+ const input = args && typeof args === 'object' ? args : {}
2067
+ const sessionId = typeof input.sessionId === 'string' && input.sessionId.length > 0 ? input.sessionId : null
2068
+ return readSessionTime(sessionId)
2069
+ },
2070
+
1908
2071
  async diff(args) {
1909
2072
  const input = args && typeof args === 'object' ? args : {}
1910
2073
  const cwd = validateCwd(input.cwd)
@@ -2157,6 +2320,7 @@ async function handleRequest(ctx, req, res) {
2157
2320
  * @param ctx - 已注入 webServer 与 connection 的 Cordis 上下文。
2158
2321
  */
2159
2322
  export function apply(ctx) {
2323
+ hostContext = ctx
2160
2324
  trackSessionChanges(ctx)
2161
2325
  ctx.effect(() => ctx.webServer.register({
2162
2326
  kind: 'prefix',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@konnga/dsh-hud",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "DeepSeek Harness 的只读工作区 Git 面板:分支、ahead/behind、改动清单与差异,外加「这个会话改过哪些文件、审到哪了、跑了什么工具」。A read-only workspace Git panel for DeepSeek Harness — branch, changed files, diffs, plus per-session change attribution, review progress, and tool activity.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -13,6 +13,7 @@
13
13
  "lib",
14
14
  "cordis.patch.yml",
15
15
  "README.md",
16
+ "README.en.md",
16
17
  "LICENSE",
17
18
  "docs/images"
18
19
  ],
@@ -33,7 +34,15 @@
33
34
  "review",
34
35
  "sidebar",
35
36
  "floating-panel",
36
- "web-gui"
37
+ "web-gui",
38
+ "deepseek",
39
+ "diff",
40
+ "image-diff",
41
+ "session-timer",
42
+ "prompt-history",
43
+ "review-progress",
44
+ "tool-activity",
45
+ "dock"
37
46
  ],
38
47
  "license": "MIT",
39
48
  "author": "konnga",