dsh-context 0.40.1 → 0.41.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/client.js CHANGED
@@ -37,10 +37,11 @@ window.__ModuleLoader__.load({
37
37
  "timing.title": "耗时统计",
38
38
  "timing.hint": "整个会话的活跃时长分布",
39
39
  "timing.total": "总活跃时长",
40
- "timing.lm": "模型调用",
40
+ "timing.ttft": "模型等待",
41
+ "timing.gen": "模型生成",
41
42
  "timing.tools": "工具执行",
42
43
  "timing.other": "其他开销",
43
- "timing.lmTimes": "{n}次",
44
+ "timing.callTimes": "{n}次",
44
45
  "timing.toolTimes": "{n}次",
45
46
  "timing.empty": "暂无耗时数据 · 对话开始后这里会显示时长分布",
46
47
  "tokens.title": "Token 统计",
@@ -312,10 +313,11 @@ window.__ModuleLoader__.load({
312
313
  "timing.title": "Timing Stats",
313
314
  "timing.hint": "Whole-session active-time split",
314
315
  "timing.total": "Active Time",
315
- "timing.lm": "Model calls",
316
+ "timing.ttft": "TTFT",
317
+ "timing.gen": "LLM Gen",
316
318
  "timing.tools": "Tool runs",
317
319
  "timing.other": "Overhead",
318
- "timing.lmTimes": "{n} calls",
320
+ "timing.callTimes": "{n} calls",
319
321
  "timing.toolTimes": "{n} runs",
320
322
  "timing.empty": "No timing data yet — durations appear as the conversation runs",
321
323
  "tokens.title": "Token Stats",
@@ -997,7 +999,8 @@ window.__ModuleLoader__.load({
997
999
  const t = value;
998
1000
  for (const k of [
999
1001
  "wallMs",
1000
- "lmMs",
1002
+ "ttftMs",
1003
+ "genMs",
1001
1004
  "calls",
1002
1005
  "toolsMs",
1003
1006
  "toolCalls"
@@ -1038,7 +1041,8 @@ window.__ModuleLoader__.load({
1038
1041
  }
1039
1042
  return {
1040
1043
  wallMs: msNumOf(data.wallMs),
1041
- lmMs: msNumOf(data.lmMs),
1044
+ ttftMs: msNumOf(data.ttftMs),
1045
+ genMs: msNumOf(data.genMs),
1042
1046
  calls: msNumOf(data.calls),
1043
1047
  toolsMs: msNumOf(data.toolsMs),
1044
1048
  toolCalls: msNumOf(data.toolCalls),
@@ -1268,6 +1272,63 @@ window.__ModuleLoader__.load({
1268
1272
  return;
1269
1273
  }
1270
1274
  }
1275
+ /**
1276
+ * Walk a (possibly traced/proxied) service value down a plain-property path,
1277
+ * degrading at the FIRST throw. cordis's undeclared-service proxies are one
1278
+ * hostile object class — any accessor backed by host state can throw too —
1279
+ * and the no-white-screen contract needs the whole chain guarded, not just
1280
+ * the `ctx.get` call.
1281
+ */
1282
+ function readKeysOf(value, ...keys) {
1283
+ let current = value;
1284
+ for (const key of keys) {
1285
+ if (current === null || typeof current !== "object") return void 0;
1286
+ try {
1287
+ current = current[key];
1288
+ } catch {
1289
+ return;
1290
+ }
1291
+ }
1292
+ return current;
1293
+ }
1294
+ /** The `page` verb of a history face, re-proved and bound to its owner. */
1295
+ function readPageOf(face) {
1296
+ const fn = readKeysOf(face, "page");
1297
+ return typeof fn === "function" ? fn.bind(face) : void 0;
1298
+ }
1299
+ /** The `history` verb of the legacy api client face, re-proved and bound. */
1300
+ function readHistoryOf(face) {
1301
+ const sessions = readKeysOf(face, "api", "sessions");
1302
+ const fn = readKeysOf(sessions, "history");
1303
+ return typeof fn === "function" ? fn.bind(sessions) : void 0;
1304
+ }
1305
+ /**
1306
+ * The 0.1.2+ gateway history page verb, resolved through the DECLARED inject
1307
+ * (see {@link watchHistoryFaces}) and bound up front: a method extracted
1308
+ * unbound loses `this`, and the traced `remote` proxy that hands it out
1309
+ * requires the inject to resolve at all.
1310
+ */
1311
+ let declaredPage;
1312
+ /**
1313
+ * Register the plugin's 0.1.2+ history faces with the harness through the
1314
+ * DECLARED inject — both `remote` AND `remote.session` (the ui-chat idiom)
1315
+ * must be in one fiber's requirement list, because the traced `remote`
1316
+ * proxy resolves `.session` through the context and each name needs the
1317
+ * other's declaration. Pre-0.1.2 hosts never provide either name, so the
1318
+ * callback simply never fires and the legacy `connection` face carries the
1319
+ * reads. The callback re-runs on every unload/remount, so it owns the
1320
+ * slot's lifetime. The face itself is re-proven (a never-fired or hostile
1321
+ * invocation leaves the slot unset — nothing here can throw).
1322
+ */
1323
+ function watchHistoryFaces(ctx) {
1324
+ ctx.inject(["remote", "remote.session"], (c) => {
1325
+ const session = c.remote?.session;
1326
+ declaredPage = session !== void 0 ? readPageOf(session) : void 0;
1327
+ return () => {
1328
+ declaredPage = void 0;
1329
+ };
1330
+ });
1331
+ }
1271
1332
  /** The rows array of a history/page response, under every served envelope. */
1272
1333
  function rowsOf(response) {
1273
1334
  let payload = response;
@@ -1291,15 +1352,17 @@ window.__ModuleLoader__.load({
1291
1352
  * pre-0.1.2 api client verb (`connection.api.sessions.history`) beneath it.
1292
1353
  * Both cut message-aligned pages, so the returned read covers `seq` whenever
1293
1354
  * the durable log still holds it: the newer face pins the inclusive cut to
1294
- * the seq itself, the legacy reader uses the exclusive bound one past it.
1295
- * Undefined when no face exists (older hosts) callers keep their static
1296
- * degradation.
1355
+ * the seq itself, the legacy reader uses the exclusive bound one past it
1356
+ * and because a non-declared read of the traced remote proxy throws
1357
+ * ("cannot get property … without inject"), the page face prefers the
1358
+ * injection-resolved slot and only then tries the reflect read. Every
1359
+ * service property access degrades at its own guard instead of taking the
1360
+ * view down. Undefined when no face exists (older hosts) — callers keep
1361
+ * their static degradation.
1297
1362
  */
1298
1363
  function pageReadersOf(ctx, sessionId) {
1299
- const pageFace = serviceOf(ctx, "remote.session");
1300
- const page = pageFace !== void 0 && typeof pageFace.page === "function" ? pageFace.page.bind(pageFace) : void 0;
1301
- const historyFace = serviceOf(ctx, "connection")?.api?.sessions;
1302
- const history = historyFace !== void 0 && typeof historyFace.history === "function" ? historyFace.history.bind(historyFace) : void 0;
1364
+ const page = declaredPage ?? readPageOf(serviceOf(ctx, "remote.session"));
1365
+ const history = readHistoryOf(serviceOf(ctx, "connection"));
1303
1366
  if (page === void 0 && history === void 0) return void 0;
1304
1367
  return (seq) => {
1305
1368
  if (page !== void 0) return page({
@@ -5059,7 +5122,7 @@ window.__ModuleLoader__.load({
5059
5122
  return function PluginInfo() {
5060
5123
  const [latest, setLatest] = React.useState(null);
5061
5124
  React.useEffect(() => {
5062
- if ("0.40.1".includes("-dev")) return;
5125
+ if ("0.41.1".includes("-dev")) return;
5063
5126
  let on = true;
5064
5127
  fetchLatestVersion().then((v) => {
5065
5128
  if (on && v) setLatest(v);
@@ -5068,8 +5131,8 @@ window.__ModuleLoader__.load({
5068
5131
  on = false;
5069
5132
  };
5070
5133
  }, []);
5071
- const update = latest !== null && isNewerVersion(latest, "0.40.1") ? latest : null;
5072
- const nameText = "dsh-context (v0.40.1)";
5134
+ const update = latest !== null && isNewerVersion(latest, "0.41.1") ? latest : null;
5135
+ const nameText = "dsh-context (v0.41.1)";
5073
5136
  const nameValue = [nameText];
5074
5137
  if (update) nameValue.push(/* @__PURE__ */ React.createElement("span", {
5075
5138
  key: "update",
@@ -5477,20 +5540,27 @@ window.__ModuleLoader__.load({
5477
5540
  let segments = [];
5478
5541
  let rows = [];
5479
5542
  if (timing !== null && (wall > 0 || timing.calls > 0 || timing.toolCalls > 0)) {
5480
- const lm = Math.min(timing.lmMs, wall);
5481
- const toolRing = Math.max(0, Math.min(timing.toolsMs, wall - lm));
5482
- const other = Math.max(0, wall - lm - toolRing);
5543
+ const ttft = Math.min(timing.ttftMs, wall);
5544
+ const gen = Math.max(0, Math.min(timing.genMs, wall - ttft));
5545
+ const toolRing = Math.max(0, Math.min(timing.toolsMs, wall - ttft - gen));
5546
+ const other = Math.max(0, wall - ttft - gen - toolRing);
5483
5547
  const share = (ms) => wall > 0 ? ms / wall : 0;
5484
5548
  const countOf = (ms, times) => {
5485
5549
  const dur = fmtDuration(ms, lang);
5486
5550
  if (times === void 0) return dur;
5487
5551
  return ms > 0 ? `${dur} · ${times}` : times;
5488
5552
  };
5553
+ const callTimes = t("timing.callTimes", { n: fmt(timing.calls) });
5489
5554
  segments = [
5490
5555
  {
5491
- key: "lm",
5556
+ key: "ttft",
5492
5557
  color: "#3b82f6",
5493
- value: share(lm)
5558
+ value: share(ttft)
5559
+ },
5560
+ {
5561
+ key: "gen",
5562
+ color: "#8b5cf6",
5563
+ value: share(gen)
5494
5564
  },
5495
5565
  {
5496
5566
  key: "tools",
@@ -5505,12 +5575,20 @@ window.__ModuleLoader__.load({
5505
5575
  ];
5506
5576
  rows = [
5507
5577
  {
5508
- key: "lm",
5578
+ key: "ttft",
5509
5579
  color: "#3b82f6",
5510
- label: t("timing.lm"),
5511
- dim: timing.lmMs === 0,
5512
- pct: fmtShare(timing.lmMs, wall),
5513
- count: countOf(timing.lmMs, t("timing.lmTimes", { n: fmt(timing.calls) }))
5580
+ label: t("timing.ttft"),
5581
+ dim: timing.ttftMs === 0,
5582
+ pct: fmtShare(timing.ttftMs, wall),
5583
+ count: countOf(timing.ttftMs, callTimes)
5584
+ },
5585
+ {
5586
+ key: "gen",
5587
+ color: "#8b5cf6",
5588
+ label: t("timing.gen"),
5589
+ dim: timing.genMs === 0,
5590
+ pct: fmtShare(timing.genMs, wall),
5591
+ count: countOf(timing.genMs, callTimes)
5514
5592
  },
5515
5593
  {
5516
5594
  key: "tools",
@@ -6431,7 +6509,7 @@ window.__ModuleLoader__.load({
6431
6509
  }
6432
6510
  //#endregion
6433
6511
  //#region \0dsh-global-css:/home/runner/work/dsh-context/dsh-context/src/client/styles/base.css.mjs
6434
- const css$13 = ".lc-root{box-sizing:border-box;height:100%;color:var(--dsw-alias-label-primary);padding:16px 20px 32px;font-size:13px;overflow-y:auto}.lc-card{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);border-radius:10px;margin-bottom:14px;padding:14px 16px}.lc-card-title{flex-wrap:wrap;align-items:baseline;gap:8px;margin-bottom:10px;font-weight:600;display:flex}.lc-card-title-text{white-space:nowrap;flex:none}.lc-gran,.lc-kinds{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:6px;gap:2px;margin-left:auto;padding:1px;display:flex}.lc-trend-ctl{align-items:center;gap:6px;margin-left:auto;display:flex}.lc-trend-ctl .lc-gran{margin-left:0}.lc-gran-btn{color:var(--dsw-alias-label-secondary);cursor:pointer;transition:color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);background:0 0;border:0;border-radius:5px;padding:3px 8px;font-family:inherit;font-size:11px;line-height:1}.lc-gran-btn:hover{color:var(--dsw-alias-label-primary)}.lc-gran-on,.lc-gran-on:hover{background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground)}.lc-tip{z-index:6;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);width:max-content;color:var(--dsw-alias-label-primary);box-shadow:var(--dsw-shadow-lv3,0 2px 8px #0000002e);pointer-events:none;opacity:0;transition:opacity var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);border-radius:6px;padding:6px 10px;font-size:12px;position:absolute}.lc-events,.lc-fa-list{flex-direction:column;gap:2px;height:320px;display:flex;overflow-y:auto}.lc-cols{flex-wrap:wrap;gap:14px;margin-bottom:14px;display:flex}.lc-col{flex:1;min-width:280px}.lc-cols>.lc-card,.lc-col>.lc-card:last-child{margin-bottom:0}.lc-col-browser{flex-direction:column;display:flex}.lc-col-browser>.lc-card{flex:1}.lc-head>.lc-card{flex:1 1 0;min-width:0;container:lc-head-card/inline-size}.lc-head>.lc-card:first-child{flex-direction:column;display:flex}.lc-head>.lc-card:first-child .lc-stats{flex:1;align-content:stretch}.lc-head>.lc-card:first-child .lc-stat{justify-content:center}.lc-empty{color:var(--dsw-alias-label-secondary);text-align:center;padding:18px 0}.lc-error{flex-direction:column;align-items:center;gap:8px;padding:40px 16px;display:flex}.lc-error-msg{font-family:var(--ds-font-family-code,ui-monospace, SFMono-Regular, Menlo, monospace);color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);overflow-wrap:anywhere;border-radius:6px;max-width:100%;padding:4px 8px;font-size:12px}.lc-error-retry{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font:inherit;cursor:pointer;transition:border-color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);border-radius:6px;padding:4px 14px;font-size:12px}.lc-error-retry:hover{border-color:var(--dsw-alias-label-primary)}.lc-foot{color:var(--dsw-alias-label-secondary);margin-top:4px;font-size:12px}[data-conversation-scroll]:has(.lc-root)>[data-composer-seat]:not(:has([data-approval-key],[data-question-key],[data-plan-review-key])){display:none}";
6512
+ const css$13 = ".lc-root{box-sizing:border-box;height:100%;color:var(--dsw-alias-label-primary);padding:16px 20px 32px;font-size:13px;overflow-y:auto}.lc-card{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);border-radius:10px;margin-bottom:14px;padding:14px 16px}.lc-root .lc-card{margin-bottom:7px;padding:7px 8px}.lc-root .lc-cols{gap:7px;margin-bottom:7px}.lc-card-title{flex-wrap:wrap;align-items:baseline;gap:8px;margin-bottom:10px;font-weight:600;display:flex}.lc-card-title-text{white-space:nowrap;flex:none}.lc-gran,.lc-kinds{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:6px;gap:2px;margin-left:auto;padding:1px;display:flex}.lc-trend-ctl{align-items:center;gap:6px;margin-left:auto;display:flex}.lc-trend-ctl .lc-gran{margin-left:0}.lc-gran-btn{color:var(--dsw-alias-label-secondary);cursor:pointer;transition:color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);background:0 0;border:0;border-radius:5px;padding:3px 8px;font-family:inherit;font-size:11px;line-height:1}.lc-gran-btn:hover{color:var(--dsw-alias-label-primary)}.lc-gran-on,.lc-gran-on:hover{background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground)}.lc-tip{z-index:6;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);width:max-content;color:var(--dsw-alias-label-primary);box-shadow:var(--dsw-shadow-lv3,0 2px 8px #0000002e);pointer-events:none;opacity:0;transition:opacity var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);border-radius:6px;padding:6px 10px;font-size:12px;position:absolute}.lc-events,.lc-fa-list{flex-direction:column;gap:2px;height:320px;display:flex;overflow-y:auto}.lc-cols{flex-wrap:wrap;gap:14px;margin-bottom:14px;display:flex}.lc-col{flex:1;min-width:280px}.lc-cols>.lc-card,.lc-col>.lc-card:last-child{margin-bottom:0}.lc-col-browser{flex-direction:column;display:flex}.lc-col-browser>.lc-card{flex:1}.lc-head>.lc-card{flex:1 1 0;min-width:0;container:lc-head-card/inline-size}.lc-head>.lc-card:first-child{flex-direction:column;display:flex}.lc-head>.lc-card:first-child .lc-stats{flex:1;align-content:stretch}.lc-head>.lc-card:first-child .lc-stat{justify-content:center}.lc-empty{color:var(--dsw-alias-label-secondary);text-align:center;padding:18px 0}.lc-error{flex-direction:column;align-items:center;gap:8px;padding:40px 16px;display:flex}.lc-error-msg{font-family:var(--ds-font-family-code,ui-monospace, SFMono-Regular, Menlo, monospace);color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);overflow-wrap:anywhere;border-radius:6px;max-width:100%;padding:4px 8px;font-size:12px}.lc-error-retry{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font:inherit;cursor:pointer;transition:border-color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);border-radius:6px;padding:4px 14px;font-size:12px}.lc-error-retry:hover{border-color:var(--dsw-alias-label-primary)}.lc-foot{color:var(--dsw-alias-label-secondary);margin-top:4px;font-size:12px}[data-conversation-scroll]:has(.lc-root)>[data-composer-seat]:not(:has([data-approval-key],[data-question-key],[data-plan-review-key])){display:none}";
6435
6513
  const tagId$13 = "dsh-context/base.css";
6436
6514
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$13) + "]") === null) {
6437
6515
  const tag = document.createElement("style");
@@ -6617,6 +6695,7 @@ window.__ModuleLoader__.load({
6617
6695
  }, "dsh-context: dictionaries");
6618
6696
  const t = ctx.locale.bind(NS);
6619
6697
  const kit = makeViewKit(t);
6698
+ watchHistoryFaces(ctx);
6620
6699
  const settings = createContextSettings();
6621
6700
  const ContextView = makeContextView(ctx, kit, settings);
6622
6701
  ctx.slots.inject("conversation.view", () => {
package/lib/index.d.ts CHANGED
@@ -112,14 +112,18 @@ interface TimelineState {
112
112
  timing?: TimingTotals;
113
113
  /**
114
114
  * The open step's start instant, armed by `step/start` and consumed by the
115
- * `assistant/message` (LM-call time) and `step/end` (wall time) that follow
116
- * it. One slot, not a map: steps are sequential in the log, so the newest
117
- * `step/start` is the one those events close a hostile interleaved log
118
- * degrades to skipped durations, never to unbounded state. Same
119
- * arm/remove lifecycle as `pendingShadowedSeqs`.
115
+ * `assistant/message` (TTFT/generation split) and `step/end` (wall time)
116
+ * that follow it; `assistant/chunk` stamps `firstToken` on the step's first
117
+ * token delta absent when the stream carried none (legacy or aborted
118
+ * steps), which leaves that call's model time unattributed. One slot, not a
119
+ * map: steps are sequential in the log, so the newest `step/start` is the
120
+ * one those events close — a hostile interleaved log degrades to skipped
121
+ * durations, never to unbounded state. Same arm/remove lifecycle as
122
+ * `pendingShadowedSeqs`.
120
123
  */
121
124
  stepStart?: {
122
125
  time: number;
126
+ firstToken?: number;
123
127
  };
124
128
  /**
125
129
  * Tool callId → the call's name and start instant, armed by `tool/call` and
@@ -288,18 +292,22 @@ interface ToolTimingTotals {
288
292
  }
289
293
  /**
290
294
  * Whole-session timing totals, host-folded from the durable `step/start` /
291
- `step/end` / `tool/call` / `tool/result` lifecycle (running totals over the
292
- * COMPLETE session log — the same never-trimmed framing as `cost`). Durations
293
- * are wall-clock milliseconds: `wallMs` sums whole steps, `lmMs` the
294
- * step-start → assistant-message slice (the model call), `toolsMs` the sum of
295
- * per-call tool durations (parallel calls each count, so it can overlap).
295
+ `step/end` / `assistant/chunk` / `tool/call` / `tool/result` lifecycle
296
+ * (running totals over the COMPLETE session log — the same never-trimmed
297
+ * framing as `cost`). Durations are wall-clock milliseconds: `wallMs` sums
298
+ * whole steps, `ttftMs` the step-start → first-token slice (the model wait)
299
+ * and `genMs` the first-token assistant-message slice (the generation)
300
+ * both only over calls whose stream carried a token delta, `toolsMs` the sum
301
+ * of per-call tool durations (parallel calls each count, so it can overlap).
296
302
  * Absent until the first step lifecycle completes in the log.
297
303
  */
298
304
  interface TimingTotals {
299
305
  /** Summed wall time of completed steps (the session's active time). */
300
306
  wallMs: number;
301
- /** Summed step-start → assistant-message time (model-call time). */
302
- lmMs: number;
307
+ /** Summed step-start → first-token time (the model wait, TTFT). */
308
+ ttftMs: number;
309
+ /** Summed first-token → assistant-message time (the generation). */
310
+ genMs: number;
303
311
  /** Completed model calls (assistant messages folded). */
304
312
  calls: number;
305
313
  /** Summed per-call durations of completed tool calls. */
package/lib/index.js CHANGED
@@ -993,6 +993,21 @@ function durOf(from, to) {
993
993
  return Math.max(0, to - from);
994
994
  }
995
995
  /**
996
+ * Whether a stream chunk carries a non-empty token delta — the first-token
997
+ * marker the TTFT fold waits for (the same rule as the harness's own
998
+ * session-stats fold). Shape-guarded: a malformed chunk is just not a token.
999
+ */
1000
+ function isTokenDelta(chunk) {
1001
+ if (chunk === null || typeof chunk !== "object") return false;
1002
+ const c = chunk;
1003
+ switch (c.type) {
1004
+ case "text-delta":
1005
+ case "reasoning-delta": return typeof c.text === "string" && c.text !== "";
1006
+ case "tool-call-delta": return typeof c.argumentsDelta === "string" && c.argumentsDelta !== "" || c.name !== void 0;
1007
+ default: return false;
1008
+ }
1009
+ }
1010
+ /**
996
1011
  * The fold's private timing accumulator: created on first use, and CLONED on
997
1012
  * every later ensure() (see `applyTimeline`) — the object left in the
998
1013
  * persisted previous state is never written into in place.
@@ -1000,7 +1015,8 @@ function durOf(from, to) {
1000
1015
  function ensureTiming(st) {
1001
1016
  if (st.timing === void 0) st.timing = {
1002
1017
  wallMs: 0,
1003
- lmMs: 0,
1018
+ ttftMs: 0,
1019
+ genMs: 0,
1004
1020
  calls: 0,
1005
1021
  toolsMs: 0,
1006
1022
  toolCalls: 0,
@@ -1091,6 +1107,17 @@ function applyTimeline(state, event, bounds) {
1091
1107
  };
1092
1108
  }
1093
1109
  break;
1110
+ case "assistant/chunk": {
1111
+ const start = state.stepStart;
1112
+ if (start === void 0 || start.firstToken !== void 0) return state;
1113
+ if (!isTokenDelta(data?.chunk)) return state;
1114
+ const s = ensure();
1115
+ s.stepStart = {
1116
+ time: start.time,
1117
+ firstToken: event.time
1118
+ };
1119
+ break;
1120
+ }
1094
1121
  case "step/start": {
1095
1122
  const s = ensure();
1096
1123
  s.stepStart = { time: event.time };
@@ -1177,7 +1204,10 @@ function applyTimeline(state, event, bounds) {
1177
1204
  const timing = ensureTiming(s);
1178
1205
  timing.calls += 1;
1179
1206
  const stepStart = state.stepStart;
1180
- if (stepStart !== void 0) timing.lmMs += durOf(stepStart.time, event.time);
1207
+ if (stepStart !== void 0 && stepStart.firstToken !== void 0) {
1208
+ timing.ttftMs += durOf(stepStart.time, stepStart.firstToken);
1209
+ timing.genMs += durOf(stepStart.firstToken, event.time);
1210
+ }
1181
1211
  const asstMsg = deriveEventMessage(event);
1182
1212
  applySurface(s, event, event.type, data, asstMsg);
1183
1213
  break;
@@ -1404,7 +1434,8 @@ const toolTimingSchema = z.object({
1404
1434
  }).strict();
1405
1435
  const timingTotalsSchema = z.object({
1406
1436
  wallMs: z.number().nonnegative(),
1407
- lmMs: z.number().nonnegative(),
1437
+ ttftMs: z.number().nonnegative(),
1438
+ genMs: z.number().nonnegative(),
1408
1439
  calls: z.number().int().nonnegative(),
1409
1440
  toolsMs: z.number().nonnegative(),
1410
1441
  toolCalls: z.number().int().nonnegative(),
@@ -1460,7 +1491,10 @@ const timelineStateSchema = z.object({
1460
1491
  }).strict().optional(),
1461
1492
  archiveFloor: z.number().optional(),
1462
1493
  timing: timingTotalsSchema.optional(),
1463
- stepStart: z.object({ time: z.number() }).strict().optional(),
1494
+ stepStart: z.object({
1495
+ time: z.number(),
1496
+ firstToken: z.number().optional()
1497
+ }).strict().optional(),
1464
1498
  callNames: z.record(z.string(), z.object({
1465
1499
  name: z.string(),
1466
1500
  start: z.number()
@@ -1499,7 +1533,7 @@ function createContextTimelineDefinition(config) {
1499
1533
  },
1500
1534
  init: () => createTimelineState(),
1501
1535
  apply: (state, event) => applyTimeline(state, event, bounds),
1502
- stateVersion: 11
1536
+ stateVersion: 12
1503
1537
  };
1504
1538
  }
1505
1539
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-context",
3
- "version": "0.40.1",
3
+ "version": "0.41.1",
4
4
  "description": "A DeepSeek Harness plugin for context insight and management, with context dashboard and context command, for understanding how the context is made of, and how it evolves.",
5
5
  "author": "bowenliang123",
6
6
  "repository": {
@@ -49,6 +49,7 @@
49
49
  },
50
50
  "client": {
51
51
  "inject": [
52
+ "@deepseek-ai/dsh-api-remotes",
52
53
  "@deepseek-ai/dsh-client-connection",
53
54
  "@deepseek-ai/dsh-client-locale",
54
55
  "@deepseek-ai/dsh-client-ui-conversation",