@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/README.en.md +176 -0
- package/README.md +99 -65
- package/docs/images/capsules.webp +0 -0
- package/docs/images/image-diff.webp +0 -0
- package/docs/images/pill-tip.webp +0 -0
- package/lib/client.js +585 -10
- package/lib/index.js +166 -2
- package/package.json +11 -2
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
|
-
|
|
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 =
|
|
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.
|
|
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",
|