dsh-context 0.54.4 → 0.55.0

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
@@ -2,7 +2,7 @@ import { createRequire } from "node:module";
2
2
  import { z } from "zod";
3
3
  import { SessionLogOffset, deriveEventMessage, interruptedTurnClosers } from "@deepseek-ai/dsh-session";
4
4
  import { existsSync, readFileSync } from "node:fs";
5
- import { dirname, join, normalize, relative } from "node:path";
5
+ import { dirname, isAbsolute, join, normalize, relative } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import z$1 from "@deepseek-ai/schemastery";
8
8
  import { randomUUID } from "node:crypto";
@@ -372,21 +372,30 @@ function decodeKindOfBlock(blockType) {
372
372
  if (blockType === "tool-call") return "toolarg";
373
373
  }
374
374
  /**
375
- * Per-kind decode spans inside one embedded assistant stream, tiling
376
- * [first block-start, endTime]: each `block-start` record owns the interval up
377
- * to the next one, the last one up to `endTime`. This is the V2+ shape, whose
378
- * timed stream rides the settlement (`assistant/message.data.stream`) instead
379
- * of separate `assistant/chunk` events. Total over untrusted input — a
380
- * malformed record is skipped, a non-finite boundary yields zero, and a stream
381
- * with no marker (or not an array) yields all zeros.
382
- */
383
- function decodeSpansOfStream(stream, endTime) {
384
- const spans = {
385
- reasoning: 0,
386
- text: 0,
387
- toolarg: 0
375
+ * Per-kind decode spans AND counted `block-start` markers inside one embedded
376
+ * assistant stream, tiling [first block-start, endTime]: each `block-start`
377
+ * record owns the interval up to the next one, the last one up to `endTime`.
378
+ * This is the V2+ shape, whose timed stream rides the settlement
379
+ * (`assistant/message.data.stream`) instead of separate `assistant/chunk`
380
+ * events. Total over untrusted input — a malformed record is skipped whole (a
381
+ * marker with an unusable time anchors no span and counts nothing), a
382
+ * non-finite boundary yields zero, and a stream with no marker (or not an
383
+ * array) yields all zeros.
384
+ */
385
+ function decodeTallyOfStream(stream, endTime) {
386
+ const tally = {
387
+ spans: {
388
+ reasoning: 0,
389
+ text: 0,
390
+ toolarg: 0
391
+ },
392
+ blocks: {
393
+ reasoning: 0,
394
+ text: 0,
395
+ toolarg: 0
396
+ }
388
397
  };
389
- if (!Array.isArray(stream) || !Number.isFinite(endTime)) return spans;
398
+ if (!Array.isArray(stream) || !Number.isFinite(endTime)) return tally;
390
399
  let kind;
391
400
  let since = 0;
392
401
  for (const record of stream) {
@@ -397,12 +406,13 @@ function decodeSpansOfStream(stream, endTime) {
397
406
  if (chunk.type !== "block-start") continue;
398
407
  const time = r.time;
399
408
  if (typeof time !== "number" || !Number.isFinite(time)) continue;
400
- if (kind !== void 0) spans[kind] += Math.max(0, time - since);
409
+ if (kind !== void 0) tally.spans[kind] += Math.max(0, time - since);
401
410
  kind = decodeKindOfBlock(chunk.blockType);
411
+ if (kind !== void 0) tally.blocks[kind] += 1;
402
412
  since = time;
403
413
  }
404
- if (kind !== void 0) spans[kind] += Math.max(0, endTime - since);
405
- return spans;
414
+ if (kind !== void 0) tally.spans[kind] += Math.max(0, endTime - since);
415
+ return tally;
406
416
  }
407
417
  /**
408
418
  * The first token's instant inside one PACKED delta run (`text-chunks` /
@@ -507,6 +517,24 @@ function kindOfCall(tool, args) {
507
517
  return kindOfTool(tool);
508
518
  }
509
519
  /**
520
+ * Whether a settled call's arguments can yield an op at all: the file tools
521
+ * plus `str_replace_editor`, whose purpose follows its `command`. The public
522
+ * face of the gate {@link opsOfCall} short-circuits on — callers that decide
523
+ * whether to parse or to KEEP a call's raw arguments ask here instead of
524
+ * restating the tool list.
525
+ */
526
+ function opBearingTool(tool) {
527
+ return tool === "str_replace_editor" || kindOfTool(tool) !== null;
528
+ }
529
+ /**
530
+ * Whether a call's raw arguments must ride the fold state
531
+ * (TimelineState.callNames): every op-bearing tool, plus `run_code`, whose
532
+ * `description` labels the ops its nested dispatches book at flush time.
533
+ */
534
+ function rawArgsNeeded(tool) {
535
+ return opBearingTool(tool) || tool === "run_code";
536
+ }
537
+ /**
510
538
  * The operation's target path: the path-ish argument of read/write tools;
511
539
  * for searches the narrowing `path`, else the pattern itself (a pathless
512
540
  * grep/glob's target IS the pattern — the workspace-wide search text).
@@ -645,7 +673,7 @@ function searchFilesOf(meta) {
645
673
  * resolve no target) rows nothing.
646
674
  */
647
675
  function opsOfCall(input) {
648
- const args = parseCallArgs(input.argsRaw);
676
+ const args = opBearingTool(input.tool) ? parseCallArgs(input.argsRaw) : null;
649
677
  const kind = kindOfCall(input.tool, args);
650
678
  if (kind === null) return [];
651
679
  const stamp = {
@@ -724,8 +752,15 @@ function countTurnRuns(requests) {
724
752
  return runs;
725
753
  }
726
754
  function trimState(st, bounds) {
727
- if (countTurnRuns(st.requests) > bounds.maxKeptTurns) st.requests = trimToLastTurns(st.requests, bounds.maxKeptTurns);
728
- if (st.requests.length > bounds.maxRequestSteps) st.requests = st.requests.slice(-bounds.maxRequestSteps);
755
+ st.turnRuns = st.turnRuns ?? countTurnRuns(st.requests);
756
+ if (st.turnRuns > bounds.maxKeptTurns) {
757
+ st.requests = trimToLastTurns(st.requests, bounds.maxKeptTurns);
758
+ st.turnRuns = countTurnRuns(st.requests);
759
+ }
760
+ if (st.requests.length > bounds.maxRequestSteps) {
761
+ st.requests = st.requests.slice(-bounds.maxRequestSteps);
762
+ st.turnRuns = countTurnRuns(st.requests);
763
+ }
729
764
  if (st.events.length > bounds.maxEvents) st.events = st.events.slice(-bounds.maxEvents);
730
765
  if (st.fileOps.length > bounds.maxFileOps) {
731
766
  const drop = st.fileOps.length - bounds.maxFileOps;
@@ -757,6 +792,7 @@ function createTimelineState() {
757
792
  systemTokens: 0,
758
793
  toolsTokens: 0,
759
794
  requests: [],
795
+ turnRuns: 0,
760
796
  events: [],
761
797
  archived: [],
762
798
  callNames: {},
@@ -1118,17 +1154,32 @@ function ensureTiming(st) {
1118
1154
  };
1119
1155
  return st.timing;
1120
1156
  }
1157
+ /** The TimingTotals fields each decode bucket's span and count land in. */
1158
+ const DECODE_FIELDS = {
1159
+ reasoning: {
1160
+ ms: "reasoningMs",
1161
+ n: "reasoningBlocks"
1162
+ },
1163
+ text: {
1164
+ ms: "textMs",
1165
+ n: "textBlocks"
1166
+ },
1167
+ toolarg: {
1168
+ ms: "toolArgMs",
1169
+ n: "toolArgBlocks"
1170
+ }
1171
+ };
1121
1172
  /**
1122
- * Fold one block's decode span into the totals' generation split (see
1123
- * TimingTotals). A zero span stays ABSENT — the field then carries the
1124
- * "no time was decoded in this bucket" fact without adding dead properties to
1125
- * every pre-split-shaped state, and the card reads absence as 0.
1173
+ * Fold one block's decode span and count into the totals' generation split
1174
+ * (see TimingTotals). A zero span or count stays ABSENT — the field then
1175
+ * carries the "nothing was decoded/counted in this bucket" fact without adding
1176
+ * dead properties to every pre-split-shaped state, and the card reads absence
1177
+ * as 0.
1126
1178
  */
1127
- function addDecode(timing, kind, ms) {
1128
- if (!(ms > 0)) return;
1129
- if (kind === "reasoning") timing.reasoningMs = (timing.reasoningMs ?? 0) + ms;
1130
- else if (kind === "text") timing.textMs = (timing.textMs ?? 0) + ms;
1131
- else timing.toolArgMs = (timing.toolArgMs ?? 0) + ms;
1179
+ function addDecode(timing, kind, ms, blocks) {
1180
+ const field = DECODE_FIELDS[kind];
1181
+ if (ms > 0) timing[field.ms] = (timing[field.ms] ?? 0) + ms;
1182
+ if (blocks > 0) timing[field.n] = (timing[field.n] ?? 0) + blocks;
1132
1183
  }
1133
1184
  /**
1134
1185
  * Tally one completed tool call into the per-name ranking, bounded to
@@ -1161,22 +1212,42 @@ function bumpToolTotals(timing, name, ms) {
1161
1212
  ms: cur.ms + ms
1162
1213
  };
1163
1214
  }
1215
+ const CLONE_ALL = [
1216
+ "surface",
1217
+ "sums",
1218
+ "requests",
1219
+ "events",
1220
+ "archived",
1221
+ "callNames",
1222
+ "fileOps",
1223
+ "timing"
1224
+ ];
1164
1225
  function applyTimeline(state, event, bounds) {
1165
1226
  let st;
1166
- const ensure = () => st ??= {
1167
- ...state,
1168
- surface: [...state.surface],
1169
- sums: { ...state.sums },
1170
- requests: [...state.requests],
1171
- events: [...state.events],
1172
- archived: [...state.archived],
1173
- callNames: { ...state.callNames },
1174
- fileOps: [...state.fileOps],
1175
- ...state.pendingCodeOps !== void 0 ? { pendingCodeOps: { ...state.pendingCodeOps } } : {},
1176
- ...state.timing !== void 0 ? { timing: {
1227
+ /**
1228
+ * The event's private working state: a shallow object clone up front, with
1229
+ * only the named collections cloned into private copies. Everything else
1230
+ * stays shared with the persisted previous state — the apply contract is
1231
+ * "never mutate the previous state in place". Called EXACTLY ONCE per event
1232
+ * (each case names the collections its own path may write into; helpers
1233
+ * never call it), so building afresh per call is safe. The default is the
1234
+ * full clone — a future branch that omits its list degrades to the old
1235
+ * behavior instead of sharing a collection it mutates.
1236
+ */
1237
+ const ensure = (keys = CLONE_ALL) => {
1238
+ st = { ...state };
1239
+ for (const key of keys) if (key === "surface") st.surface = [...state.surface];
1240
+ else if (key === "sums") st.sums = { ...state.sums };
1241
+ else if (key === "requests") st.requests = [...state.requests];
1242
+ else if (key === "events") st.events = [...state.events];
1243
+ else if (key === "archived") st.archived = [...state.archived];
1244
+ else if (key === "callNames") st.callNames = { ...state.callNames };
1245
+ else if (key === "fileOps") st.fileOps = [...state.fileOps];
1246
+ else if (state.timing !== void 0) st.timing = {
1177
1247
  ...state.timing,
1178
1248
  tools: { ...state.timing.tools }
1179
- } } : {}
1249
+ };
1250
+ return st;
1180
1251
  };
1181
1252
  const data = event.data;
1182
1253
  try {
@@ -1184,7 +1255,7 @@ function applyTimeline(state, event, bounds) {
1184
1255
  case "request/header": {
1185
1256
  const header = data?.header ?? {};
1186
1257
  const tools = Array.isArray(header.tools) ? header.tools : [];
1187
- const s = ensure();
1258
+ const s = ensure(["events"]);
1188
1259
  s.toolsTokens = estimateToolsTotal(tools);
1189
1260
  const systemText = header.system;
1190
1261
  if (typeof systemText === "string" && systemText !== "") {
@@ -1216,7 +1287,11 @@ function applyTimeline(state, event, bounds) {
1216
1287
  break;
1217
1288
  }
1218
1289
  case "system/message": {
1219
- const s = ensure();
1290
+ const s = ensure([
1291
+ "surface",
1292
+ "sums",
1293
+ "archived"
1294
+ ]);
1220
1295
  delete s.pendingShadowedSeqs;
1221
1296
  delete s.pendingShadowEventSeq;
1222
1297
  const op = replaceRangeOf(event.surfaceOp);
@@ -1235,7 +1310,7 @@ function applyTimeline(state, event, bounds) {
1235
1310
  break;
1236
1311
  }
1237
1312
  case "request/context": {
1238
- const s = ensure();
1313
+ const s = ensure([]);
1239
1314
  if (data && typeof data.contextWindow === "number") s.contextWindow = data.contextWindow;
1240
1315
  if (data && typeof data.model === "string") s.model = data.model;
1241
1316
  if (data && typeof data.provider === "string") s.provider = data.provider;
@@ -1243,8 +1318,8 @@ function applyTimeline(state, event, bounds) {
1243
1318
  }
1244
1319
  case "tool/call":
1245
1320
  if (data && typeof data.callId === "string" && typeof data.name === "string") {
1246
- const s = ensure();
1247
- const argsRaw = argsRawOf(data.arguments);
1321
+ const s = ensure(["callNames"]);
1322
+ const argsRaw = rawArgsNeeded(data.name) ? argsRawOf(data.arguments) : void 0;
1248
1323
  s.callNames[data.callId] = {
1249
1324
  name: data.name,
1250
1325
  start: event.time,
@@ -1257,14 +1332,15 @@ function applyTimeline(state, event, bounds) {
1257
1332
  const rootCallId = data?.rootCallId;
1258
1333
  const name = data?.name;
1259
1334
  if (typeof rootCallId === "string" && typeof name === "string") {
1335
+ const argsRaw = opBearingTool(name) ? argsRawOf(data?.arguments) : void 0;
1260
1336
  const ops = opsOfCall({
1261
1337
  seq: event.seq,
1262
1338
  time: event.time,
1263
1339
  tool: name,
1264
- argsRaw: argsRawOf(data?.arguments),
1340
+ argsRaw,
1265
1341
  err: data?.isError === true
1266
1342
  });
1267
- if (ops.length > 0) bufferCodeOps(ensure(), rootCallId, ops);
1343
+ if (ops.length > 0) bufferCodeOps(ensure([]), rootCallId, ops);
1268
1344
  }
1269
1345
  break;
1270
1346
  }
@@ -1275,17 +1351,24 @@ function applyTimeline(state, event, bounds) {
1275
1351
  if (chunk !== null && typeof chunk === "object" && chunk.type === "block-start") {
1276
1352
  const kind = decodeKindOfBlock(chunk.blockType);
1277
1353
  if (start.block === void 0 && kind === void 0) return state;
1278
- const s = ensure();
1354
+ const s = ensure([]);
1279
1355
  const decode = { ...start.decode ?? {
1280
1356
  reasoning: 0,
1281
1357
  text: 0,
1282
1358
  toolarg: 0
1283
1359
  } };
1284
1360
  if (start.block !== void 0) decode[start.block.kind] += durOf(start.block.since, event.time);
1361
+ const blocks = { ...start.blocks ?? {
1362
+ reasoning: 0,
1363
+ text: 0,
1364
+ toolarg: 0
1365
+ } };
1366
+ if (kind !== void 0) blocks[kind] += 1;
1285
1367
  s.stepStart = {
1286
1368
  time: start.time,
1287
1369
  ...start.firstToken !== void 0 ? { firstToken: start.firstToken } : {},
1288
1370
  decode,
1371
+ blocks,
1289
1372
  ...kind !== void 0 ? { block: {
1290
1373
  kind,
1291
1374
  since: event.time
@@ -1295,7 +1378,7 @@ function applyTimeline(state, event, bounds) {
1295
1378
  }
1296
1379
  if (start.firstToken !== void 0) return state;
1297
1380
  if (!isTokenChunk(data?.chunk)) return state;
1298
- const s = ensure();
1381
+ const s = ensure([]);
1299
1382
  s.stepStart = {
1300
1383
  ...start,
1301
1384
  firstToken: event.time
@@ -1307,7 +1390,7 @@ function applyTimeline(state, event, bounds) {
1307
1390
  if (start === void 0 || start.firstToken !== void 0) return state;
1308
1391
  const first = firstTokenTimeOfStream(data?.stream);
1309
1392
  if (first === void 0) return state;
1310
- const s = ensure();
1393
+ const s = ensure([]);
1311
1394
  s.stepStart = {
1312
1395
  time: start.time,
1313
1396
  firstToken: first
@@ -1315,21 +1398,26 @@ function applyTimeline(state, event, bounds) {
1315
1398
  break;
1316
1399
  }
1317
1400
  case "step/start": {
1318
- const s = ensure();
1401
+ const s = ensure([]);
1319
1402
  s.stepStart = { time: event.time };
1320
1403
  break;
1321
1404
  }
1322
1405
  case "step/end": {
1323
1406
  const start = state.stepStart;
1324
1407
  if (start === void 0) return state;
1325
- const s = ensure();
1408
+ const s = ensure(["timing"]);
1326
1409
  ensureTiming(s).wallMs += durOf(start.time, event.time);
1327
1410
  delete s.stepStart;
1328
1411
  break;
1329
1412
  }
1330
1413
  case "user/message": {
1331
1414
  const msg = deriveEventMessage(event);
1332
- const s = ensure();
1415
+ const s = ensure([
1416
+ "surface",
1417
+ "sums",
1418
+ "archived",
1419
+ "events"
1420
+ ]);
1333
1421
  bumpDetailRev(s);
1334
1422
  const node = applySurface(s, event, event.type, data, msg);
1335
1423
  const source = msg?.source;
@@ -1366,7 +1454,14 @@ function applyTimeline(state, event, bounds) {
1366
1454
  const blockId = firstBlock?.toolCallId;
1367
1455
  const pendingEntry = (typeof srcId === "string" ? state.callNames[srcId] : void 0) ?? (typeof blockId === "string" ? state.callNames[blockId] : void 0);
1368
1456
  const buffered = (typeof srcId === "string" ? state.pendingCodeOps?.[srcId] : void 0) ?? (typeof blockId === "string" ? state.pendingCodeOps?.[blockId] : void 0);
1369
- const s = ensure();
1457
+ const s = ensure([
1458
+ "surface",
1459
+ "sums",
1460
+ "archived",
1461
+ "events",
1462
+ "fileOps",
1463
+ "timing"
1464
+ ]);
1370
1465
  bumpDetailRev(s);
1371
1466
  const node = applySurface(s, event, event.type, data, toolMsg);
1372
1467
  if (node.tool === ASK_USER_TOOL) s.humanInputs = (s.humanInputs ?? 0) + 1;
@@ -1413,7 +1508,14 @@ function applyTimeline(state, event, bounds) {
1413
1508
  }
1414
1509
  case "assistant/message": {
1415
1510
  const usage = data?.usage;
1416
- const s = ensure();
1511
+ const s = ensure([
1512
+ "surface",
1513
+ "sums",
1514
+ "archived",
1515
+ "events",
1516
+ "requests",
1517
+ "timing"
1518
+ ]);
1417
1519
  bumpDetailRev(s);
1418
1520
  const total = s.systemTokens + s.toolsTokens + s.sums.user + s.sums.inject + s.sums.skill + s.sums.assistant + s.sums.tool;
1419
1521
  const record = {
@@ -1449,6 +1551,8 @@ function applyTimeline(state, event, bounds) {
1449
1551
  }
1450
1552
  }
1451
1553
  s.requests.push(record);
1554
+ const prevLast = state.requests.at(-1);
1555
+ s.turnRuns = (state.turnRuns ?? countTurnRuns(state.requests)) + (prevLast === void 0 ? record.turn !== void 0 ? 1 : 0 : record.turn === prevLast.turn ? 0 : 1);
1452
1556
  const timing = ensureTiming(s);
1453
1557
  timing.calls += 1;
1454
1558
  const stepStart = state.stepStart;
@@ -1464,10 +1568,10 @@ function applyTimeline(state, event, bounds) {
1464
1568
  if (stepStart.decode !== void 0) {
1465
1569
  const decode = { ...stepStart.decode };
1466
1570
  if (stepStart.block !== void 0) decode[stepStart.block.kind] += durOf(stepStart.block.since, event.time);
1467
- for (const kind of DECODE_KINDS) addDecode(timing, kind, decode[kind]);
1571
+ for (const kind of DECODE_KINDS) addDecode(timing, kind, decode[kind], stepStart.blocks?.[kind] ?? 0);
1468
1572
  } else {
1469
- const spans = decodeSpansOfStream(data?.stream, event.time);
1470
- for (const kind of DECODE_KINDS) addDecode(timing, kind, spans[kind]);
1573
+ const tally = decodeTallyOfStream(data?.stream, event.time);
1574
+ for (const kind of DECODE_KINDS) addDecode(timing, kind, tally.spans[kind], tally.blocks[kind]);
1471
1575
  }
1472
1576
  }
1473
1577
  }
@@ -1477,7 +1581,7 @@ function applyTimeline(state, event, bounds) {
1477
1581
  }
1478
1582
  case "plan/mode":
1479
1583
  if (data && typeof data.active === "boolean") {
1480
- const s = ensure();
1584
+ const s = ensure(["events"]);
1481
1585
  s.events.push({
1482
1586
  seq: event.seq,
1483
1587
  time: event.time,
@@ -1489,7 +1593,7 @@ function applyTimeline(state, event, bounds) {
1489
1593
  break;
1490
1594
  case "compaction/summary":
1491
1595
  case "compaction/prune": {
1492
- const s = ensure();
1596
+ const s = ensure(["events"]);
1493
1597
  bumpDetailRev(s);
1494
1598
  if (data && Array.isArray(data.shadowedSeqs)) {
1495
1599
  s.pendingShadowedSeqs = data.shadowedSeqs.filter((x) => typeof x === "number");
@@ -2399,18 +2503,21 @@ function watchActivityBackfill(ctx) {
2399
2503
  //#endregion
2400
2504
  //#region src/host/config.ts
2401
2505
  /**
2402
- * dsh-context host configuration — the `config:` block of the `dsh-context`
2403
- * loader row in cordis.yml.
2404
- *
2405
- * Cordis validates the entry config against this exported `Config` schema
2406
- * (any Standard Schema v1 validator — zod is ours) before `apply` runs, fills
2407
- * per-field defaults, and fails the load loudly on invalid or unknown keys
2408
- * (`.strict()`). The official plugin-config principle this answers: "anything
2409
- * that two deployments may want to set differently is a configuration field".
2506
+ * dsh-context entry configuration — the `config:` block of the `dsh-context`
2507
+ * loader row in cordis.yml, plus the per-user display preferences.
2410
2508
  *
2411
- * The persisted projection state shape is independent of these bounds — they
2412
- * only tune the fold's retention / presentation slice, so changing them never
2413
- * requires a projection `stateVersion` bump.
2509
+ * The schema is schemastery so both consumers it must serve accept it: cordis
2510
+ * validates the entry config through the schema's Standard Schema face before
2511
+ * `apply` runs (defaults per field, unknown keys merged through), and the
2512
+ * harness's Config-form generation (dsh 0.1.7+) derives the Plugins page's
2513
+ * live form from this same schema — there the namespace is the entry id
2514
+ * (`dsh-context`) and only the `.volatile()` preference fields are served and
2515
+ * editable. Bounds edits remount the entry (the folds read them at apply);
2516
+ * preference edits commit volatile-only and never remount. On the older lines
2517
+ * the preferences are inert here — their surface is the registered settings
2518
+ * namespace (settings.ts). The `Config` type describes the RESOLVED shape
2519
+ * cordis hands to `apply`; the raw patch values are partial and the schema
2520
+ * fills every default.
2414
2521
  */
2415
2522
  const DEFAULT_BOUNDS = {
2416
2523
  maxRequestSteps: 1500,
@@ -2421,19 +2528,57 @@ const DEFAULT_BOUNDS = {
2421
2528
  maxFileOps: 400
2422
2529
  };
2423
2530
  /**
2424
- * The cordis `Config` validator: strict on keys, defaults on the schema fields; tolerates `undefined` (a patch row without a `config:`
2425
- * block — defaults win).
2426
- */
2427
- const Config = z.preprocess((v) => v ?? {}, z.object({
2428
- maxRequestSteps: z.number().int().min(1).default(DEFAULT_BOUNDS.maxRequestSteps),
2429
- maxKeptTurns: z.number().int().min(1).default(DEFAULT_BOUNDS.maxKeptTurns),
2430
- maxEvents: z.number().int().min(1).default(DEFAULT_BOUNDS.maxEvents),
2431
- maxNodes: z.number().int().min(1).default(DEFAULT_BOUNDS.maxNodes),
2432
- maxArchiveNodes: z.number().int().min(1).default(DEFAULT_BOUNDS.maxArchiveNodes),
2433
- maxFileOps: z.number().int().min(1).default(DEFAULT_BOUNDS.maxFileOps)
2434
- }).strict());
2531
+ * Mark a field live-editable where the harness's schemastery ships the
2532
+ * `.volatile()` modifier (the Config-form generation reads the mark to serve
2533
+ * the field on the Plugins page); a plain field on the older lines whose
2534
+ * schemastery predates the modifier.
2535
+ */
2536
+ function volatileField(field) {
2537
+ const volatile = field.volatile;
2538
+ return typeof volatile === "function" ? volatile.call(field) : field;
2539
+ }
2540
+ /** One bounded positive-integer count. */
2541
+ function count(defaultValue) {
2542
+ return z$1.number().min(1).step(1).default(defaultValue);
2543
+ }
2544
+ /** The cordis `Config` validator and the Config-form generation's served schema. */
2545
+ const Config = z$1.object({
2546
+ maxRequestSteps: count(DEFAULT_BOUNDS.maxRequestSteps),
2547
+ maxKeptTurns: count(DEFAULT_BOUNDS.maxKeptTurns),
2548
+ maxEvents: count(DEFAULT_BOUNDS.maxEvents),
2549
+ maxNodes: count(DEFAULT_BOUNDS.maxNodes),
2550
+ maxArchiveNodes: count(DEFAULT_BOUNDS.maxArchiveNodes),
2551
+ maxFileOps: count(DEFAULT_BOUNDS.maxFileOps),
2552
+ defaultPlacement: volatileField(z$1.union([
2553
+ "all",
2554
+ "tab",
2555
+ "sidebar"
2556
+ ]).default("all").loose()),
2557
+ defaultGranularity: volatileField(z$1.union(["step", "turn"]).default("step").loose()),
2558
+ defaultTrendMode: volatileField(z$1.union(["total", "delta"]).default("total").loose()),
2559
+ defaultToolSort: volatileField(z$1.union([
2560
+ "size",
2561
+ "count",
2562
+ "name"
2563
+ ]).default("count").loose()),
2564
+ defaultFileSort: volatileField(z$1.union([
2565
+ "count",
2566
+ "latest",
2567
+ "path"
2568
+ ]).default("count").loose()),
2569
+ insightsEntry: volatileField(z$1.union(["show", "hide"]).default("show").loose())
2570
+ });
2571
+ /** Resolve the fold's retention bounds (the schema fills every default). */
2435
2572
  function resolveBounds(config) {
2436
- return Config.parse(config ?? {});
2573
+ const resolved = Config(config ?? {});
2574
+ return {
2575
+ maxRequestSteps: resolved.maxRequestSteps,
2576
+ maxKeptTurns: resolved.maxKeptTurns,
2577
+ maxEvents: resolved.maxEvents,
2578
+ maxNodes: resolved.maxNodes,
2579
+ maxArchiveNodes: resolved.maxArchiveNodes,
2580
+ maxFileOps: resolved.maxFileOps
2581
+ };
2437
2582
  }
2438
2583
  //#endregion
2439
2584
  //#region src/host/detail.ts
@@ -2887,8 +3032,11 @@ const timingTotalsSchema = z.object({
2887
3032
  ttftMs: z.number().nonnegative(),
2888
3033
  genMs: z.number().nonnegative(),
2889
3034
  reasoningMs: z.number().nonnegative().optional(),
3035
+ reasoningBlocks: z.number().int().nonnegative().optional(),
2890
3036
  textMs: z.number().nonnegative().optional(),
3037
+ textBlocks: z.number().int().nonnegative().optional(),
2891
3038
  toolArgMs: z.number().nonnegative().optional(),
3039
+ toolArgBlocks: z.number().int().nonnegative().optional(),
2892
3040
  speedTokens: z.number().nonnegative().optional(),
2893
3041
  speedMs: z.number().nonnegative().optional(),
2894
3042
  calls: z.number().int().nonnegative(),
@@ -2974,6 +3122,7 @@ const timelineStateSchema = z.object({
2974
3122
  lastModel: z.string().optional(),
2975
3123
  contextWindow: z.number().optional(),
2976
3124
  requests: z.array(requestRecordSchema),
3125
+ turnRuns: z.number().int().nonnegative().optional(),
2977
3126
  events: z.array(contextEventSchema),
2978
3127
  archived: z.array(surfaceNodeSchema),
2979
3128
  cost: costUsageSchema.optional(),
@@ -2989,6 +3138,11 @@ const timelineStateSchema = z.object({
2989
3138
  text: z.number(),
2990
3139
  toolarg: z.number()
2991
3140
  }).strict().optional(),
3141
+ blocks: z.object({
3142
+ reasoning: z.number().int(),
3143
+ text: z.number().int(),
3144
+ toolarg: z.number().int()
3145
+ }).strict().optional(),
2992
3146
  block: z.object({
2993
3147
  kind: z.enum([
2994
3148
  "reasoning",
@@ -3390,11 +3544,15 @@ function probeAnchor(resolve, packageNames) {
3390
3544
  /**
3391
3545
  * Whether `candidate` lies inside `root`'s tree. A `..`-prefixed relative path
3392
3546
  * escapes it; a sibling such as `dsh-context-extra` is reached through `..` too,
3393
- * so it never counts as a child. A pathological child literally named `..foo`
3394
- * would degrade to the home anchor, which is the safe direction.
3547
+ * so it never counts as a child. On Windows, a cross-drive `relative()` returns
3548
+ * the candidate's ABSOLUTE path (no `..` prefix) — a different drive is never
3549
+ * inside this tree, so the absolute answer is rejected the same way. A
3550
+ * pathological child literally named `..foo` would degrade to the home anchor,
3551
+ * which is the safe direction.
3395
3552
  */
3396
3553
  function isInside(root, candidate) {
3397
- return !relative(root, candidate).startsWith("..");
3554
+ const rel = relative(root, candidate);
3555
+ return !rel.startsWith("..") && !isAbsolute(rel);
3398
3556
  }
3399
3557
  /**
3400
3558
  * The running module tree's answer, or undefined when it cannot be trusted.
package/locale/en.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Context (dsh-context)",
4
+ "description": "Context insights and management, with dashboard, browser, dynamics and commands, for understanding how the context composites, and how it evolves."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "上下文 (dsh-context)",
4
+ "description": "上下文洞察与管理插件:提供跨会话的上下文仪表盘、上下文浏览器、上下文动态、/context 命令,看清上下文的构成与演变。"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "dsh-context",
3
- "version": "0.54.4",
3
+ "version": "0.55.0",
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
+ "icon": "icon.svg",
5
6
  "author": "bowenliang123",
6
7
  "repository": {
7
8
  "type": "git",
@@ -23,9 +24,12 @@
23
24
  "default": "./lib/index.js"
24
25
  },
25
26
  "./client": "./lib/client.js",
26
- "./package.json": "./package.json"
27
+ "./package.json": "./package.json",
28
+ "./locale/*.json": "./locale/*.json"
27
29
  },
28
30
  "files": [
31
+ "icon.svg",
32
+ "locale/*.json",
29
33
  "lib/client.js",
30
34
  "lib/index.js",
31
35
  "lib/index.d.ts",
@@ -67,7 +71,7 @@
67
71
  "0.1.2-rc.1": "compatible",
68
72
  "0.1.3-alpha.2": "compatible",
69
73
  "0.1.5-rc.1": "compatible",
70
- "0.1.7-alpha.1": "compatible"
74
+ "0.1.7-alpha.2": "compatible"
71
75
  }
72
76
  }
73
77
  },
@@ -109,7 +113,7 @@
109
113
  "@deepseek-ai/dsh-session-projection": "0.1.2-rc.1",
110
114
  "@deepseek-ai/dsh-settings": "0.1.2-rc.1",
111
115
  "@deepseek-ai/dsh-util-workspace-path": "0.1.5-rc.1",
112
- "@deepseek-ai/schemastery": "^3.18.2",
116
+ "@deepseek-ai/schemastery": "^3.18.4",
113
117
  "@opencode-ai/models": "^0.0.72",
114
118
  "@stylistic/eslint-plugin": "^5.10.0",
115
119
  "@tailwindcss/node": "4.3.3",