dsh-context 0.36.0 → 0.38.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
@@ -786,10 +786,18 @@ function applySurface(st, ev, type, data, message) {
786
786
  }
787
787
  } else if (type === "tool/result") {
788
788
  const srcId = source?.callId;
789
- const srcName = typeof srcId === "string" ? st.callNames[srcId] : void 0;
790
789
  const blockId = (message?.content?.[0])?.toolCallId;
791
- if (srcName !== void 0) node.tool = srcName;
792
- else if (typeof blockId === "string" && Object.hasOwn(st.callNames, blockId)) node.tool = st.callNames[blockId];
790
+ const srcEntry = typeof srcId === "string" ? st.callNames[srcId] : void 0;
791
+ const blockEntry = srcEntry === void 0 && typeof blockId === "string" ? st.callNames[blockId] : void 0;
792
+ const toolEntry = srcEntry ?? blockEntry;
793
+ if (toolEntry !== void 0) {
794
+ node.tool = toolEntry.name;
795
+ const timing = ensureTiming(st);
796
+ const dur = durOf(toolEntry.start, ev.time);
797
+ timing.toolsMs += dur;
798
+ timing.toolCalls += 1;
799
+ bumpToolTotals(timing, toolEntry.name, dur);
800
+ }
793
801
  if (typeof srcId === "string" || typeof blockId === "string") {
794
802
  const kept = {};
795
803
  for (const k in st.callNames) if (k !== srcId && k !== blockId) kept[k] = st.callNames[k];
@@ -922,6 +930,60 @@ function accumulateCost(st, time, usage) {
922
930
  * `bounds` come from the plugin config (config.ts) — retention only, they
923
931
  * never change the state shape.
924
932
  */
933
+ /** The timing card's per-tool ranking cap: the busiest 16 names are kept. */
934
+ const TOOL_TIMING_CAP = 16;
935
+ /** Non-negative, NaN-proof duration between two instants (hostile times degrade to 0). */
936
+ function durOf(from, to) {
937
+ if (!Number.isFinite(from) || !Number.isFinite(to)) return 0;
938
+ return Math.max(0, to - from);
939
+ }
940
+ /**
941
+ * The fold's private timing accumulator: created on first use, and CLONED on
942
+ * every later ensure() (see `applyTimeline`) — the object left in the
943
+ * persisted previous state is never written into in place.
944
+ */
945
+ function ensureTiming(st) {
946
+ if (st.timing === void 0) st.timing = {
947
+ wallMs: 0,
948
+ lmMs: 0,
949
+ calls: 0,
950
+ toolsMs: 0,
951
+ toolCalls: 0,
952
+ tools: {}
953
+ };
954
+ return st.timing;
955
+ }
956
+ /**
957
+ * Tally one completed tool call into the per-name ranking, bounded to
958
+ * TOOL_TIMING_CAP names: repeated names update in place, a new name beyond
959
+ * the cap evicts the smallest tally first (the ranking's tail), so state
960
+ * stays bounded even over a hostile log of unique names.
961
+ */
962
+ function bumpToolTotals(timing, name, ms) {
963
+ if (!Object.hasOwn(timing.tools, name)) {
964
+ if (Object.keys(timing.tools).length >= TOOL_TIMING_CAP) {
965
+ let minKey = "";
966
+ let minMs = Infinity;
967
+ for (const k in timing.tools) if (timing.tools[k].ms < minMs) {
968
+ minMs = timing.tools[k].ms;
969
+ minKey = k;
970
+ }
971
+ const kept = {};
972
+ for (const k in timing.tools) if (k !== minKey) kept[k] = timing.tools[k];
973
+ timing.tools = kept;
974
+ }
975
+ timing.tools[name] = {
976
+ calls: 1,
977
+ ms
978
+ };
979
+ return;
980
+ }
981
+ const cur = timing.tools[name];
982
+ timing.tools[name] = {
983
+ calls: cur.calls + 1,
984
+ ms: cur.ms + ms
985
+ };
986
+ }
925
987
  function applyTimeline(state, event, bounds) {
926
988
  let st;
927
989
  const ensure = () => st ??= {
@@ -931,7 +993,11 @@ function applyTimeline(state, event, bounds) {
931
993
  requests: [...state.requests],
932
994
  events: [...state.events],
933
995
  archived: [...state.archived],
934
- callNames: { ...state.callNames }
996
+ callNames: { ...state.callNames },
997
+ ...state.timing !== void 0 ? { timing: {
998
+ ...state.timing,
999
+ tools: { ...state.timing.tools }
1000
+ } } : {}
935
1001
  };
936
1002
  const data = event.data;
937
1003
  try {
@@ -964,9 +1030,25 @@ function applyTimeline(state, event, bounds) {
964
1030
  case "tool/call":
965
1031
  if (data && typeof data.callId === "string" && typeof data.name === "string") {
966
1032
  const s = ensure();
967
- s.callNames[data.callId] = data.name;
1033
+ s.callNames[data.callId] = {
1034
+ name: data.name,
1035
+ start: event.time
1036
+ };
968
1037
  }
969
1038
  break;
1039
+ case "step/start": {
1040
+ const s = ensure();
1041
+ s.stepStart = { time: event.time };
1042
+ break;
1043
+ }
1044
+ case "step/end": {
1045
+ const start = state.stepStart;
1046
+ if (start === void 0) return state;
1047
+ const s = ensure();
1048
+ ensureTiming(s).wallMs += durOf(start.time, event.time);
1049
+ delete s.stepStart;
1050
+ break;
1051
+ }
970
1052
  case "user/message": {
971
1053
  const msg = deriveEventMessage(event);
972
1054
  const s = ensure();
@@ -1037,6 +1119,10 @@ function applyTimeline(state, event, bounds) {
1037
1119
  accumulateCost(s, event.time, usage);
1038
1120
  }
1039
1121
  s.requests.push(record);
1122
+ const timing = ensureTiming(s);
1123
+ timing.calls += 1;
1124
+ const stepStart = state.stepStart;
1125
+ if (stepStart !== void 0) timing.lmMs += durOf(stepStart.time, event.time);
1040
1126
  const asstMsg = deriveEventMessage(event);
1041
1127
  applySurface(s, event, event.type, data, asstMsg);
1042
1128
  break;
@@ -1085,9 +1171,9 @@ function buildTimelineView(state, bounds) {
1085
1171
  const surfaceTotal = state.sums.user + state.sums.inject + state.sums.assistant + state.sums.tool;
1086
1172
  const result = {
1087
1173
  ok: true,
1088
- model: state.model,
1089
- provider: state.provider,
1090
- contextWindow: state.contextWindow,
1174
+ ...state.model !== void 0 ? { model: state.model } : {},
1175
+ ...state.provider !== void 0 ? { provider: state.provider } : {},
1176
+ ...state.contextWindow !== void 0 ? { contextWindow: state.contextWindow } : {},
1091
1177
  current: {
1092
1178
  system: state.systemTokens,
1093
1179
  tools: state.toolsTokens,
@@ -1120,6 +1206,14 @@ function buildTimelineView(state, bounds) {
1120
1206
  if (pro !== void 0) cost.pro = pro;
1121
1207
  result.cost = cost;
1122
1208
  }
1209
+ if (state.timing !== void 0) {
1210
+ const tools = {};
1211
+ for (const k in state.timing.tools) tools[k] = { ...state.timing.tools[k] };
1212
+ result.timing = {
1213
+ ...state.timing,
1214
+ tools
1215
+ };
1216
+ }
1123
1217
  const overflowCount = Math.max(0, state.surface.length - bounds.maxNodes);
1124
1218
  const overflow = state.surface.slice(0, overflowCount);
1125
1219
  const tail = state.surface.slice(overflowCount);
@@ -1249,6 +1343,18 @@ const costFamilySchema = z.object({
1249
1343
  peak: costBucketsSchema.optional(),
1250
1344
  off: costBucketsSchema.optional()
1251
1345
  }).strict();
1346
+ const toolTimingSchema = z.object({
1347
+ calls: z.number().int().nonnegative(),
1348
+ ms: z.number().nonnegative()
1349
+ }).strict();
1350
+ const timingTotalsSchema = z.object({
1351
+ wallMs: z.number().nonnegative(),
1352
+ lmMs: z.number().nonnegative(),
1353
+ calls: z.number().int().nonnegative(),
1354
+ toolsMs: z.number().nonnegative(),
1355
+ toolCalls: z.number().int().nonnegative(),
1356
+ tools: z.record(z.string(), toolTimingSchema)
1357
+ }).strict();
1252
1358
  const contextTimelineSchema = z.object({
1253
1359
  ok: z.literal(true),
1254
1360
  model: z.string().optional(),
@@ -1263,6 +1369,7 @@ const contextTimelineSchema = z.object({
1263
1369
  flash: costFamilySchema.optional(),
1264
1370
  pro: costFamilySchema.optional()
1265
1371
  }).strict().optional(),
1372
+ timing: timingTotalsSchema.optional(),
1266
1373
  nodes: z.array(surfaceNodeSchema),
1267
1374
  droppedNodes: z.number().int().nonnegative(),
1268
1375
  archive: z.array(surfaceNodeSchema),
@@ -1297,7 +1404,12 @@ const timelineStateSchema = z.object({
1297
1404
  pro: costFamilySchema.optional()
1298
1405
  }).strict().optional(),
1299
1406
  archiveFloor: z.number().optional(),
1300
- callNames: z.record(z.string(), z.string()),
1407
+ timing: timingTotalsSchema.optional(),
1408
+ stepStart: z.object({ time: z.number() }).strict().optional(),
1409
+ callNames: z.record(z.string(), z.object({
1410
+ name: z.string(),
1411
+ start: z.number()
1412
+ }).strict()),
1301
1413
  pendingShadowedSeqs: z.array(z.number()).optional(),
1302
1414
  pendingShadowEventSeq: z.number().optional()
1303
1415
  });
@@ -1314,6 +1426,9 @@ const timelineStateSchema = z.object({
1314
1426
  * The definition carries BOTH session-projection contracts (see compat.ts):
1315
1427
  * `schema`/`view` for dsh <= 0.1.0-rc.8, `stateSchema`/`wire` for
1316
1428
  * dsh >= 0.1.1-rc.1 — each registry reads its own fields off the same unit.
1429
+ * (The return type is the mirrored dual contract, not the installed dts
1430
+ * `ProjectionDefinition`: the 0.1.1+ registry's wired-register overload
1431
+ * demands `wire` PRESENT, which the dts's optional `wire?` fails.)
1317
1432
  * Without the `wire` block the 0.1.1-rc.1+ registry treats the unit as
1318
1433
  * host-only and never delivers `contextTimeline` to the browser (the Context
1319
1434
  * tab would stay on its loading screen forever).
@@ -1332,7 +1447,7 @@ function createContextTimelineDefinition(config) {
1332
1447
  },
1333
1448
  init: () => createTimelineState(),
1334
1449
  apply: (state, event) => applyTimeline(state, event, bounds),
1335
- stateVersion: 10
1450
+ stateVersion: 11
1336
1451
  };
1337
1452
  }
1338
1453
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-context",
3
- "version": "0.36.0",
3
+ "version": "0.38.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
5
  "author": "bowenliang123",
6
6
  "repository": {
@@ -34,7 +34,7 @@
34
34
  "build": "tsdown",
35
35
  "lint": "oxlint",
36
36
  "lint:fix": "oxlint --fix",
37
- "watch": "tsdown --watch",
37
+ "watch": "bash scripts/watch.sh",
38
38
  "register": "bash scripts/register.sh",
39
39
  "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.tests.json",
40
40
  "test": "pnpm run typecheck && vitest run --coverage",
@@ -52,7 +52,6 @@
52
52
  "inject": [
53
53
  "@deepseek-ai/dsh-client-connection",
54
54
  "@deepseek-ai/dsh-client-locale",
55
- "@deepseek-ai/dsh-client-runtime",
56
55
  "@deepseek-ai/dsh-client-ui-conversation",
57
56
  "@deepseek-ai/dsh-client-ui-settings"
58
57
  ],
@@ -71,9 +70,9 @@
71
70
  "license": "Apache-2.0",
72
71
  "peerDependencies": {
73
72
  "@deepseek-ai/cordis": "^4.0.1",
74
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.7",
75
- "@deepseek-ai/dsh-session": "^0.1.0-rc.7",
76
- "@deepseek-ai/dsh-settings": "^0.1.0-rc.7",
73
+ "@deepseek-ai/dsh-client-ui-primitives": ">=0.1.0-rc.7",
74
+ "@deepseek-ai/dsh-session": ">=0.1.0-rc.7",
75
+ "@deepseek-ai/dsh-settings": ">=0.1.0-rc.7",
77
76
  "@deepseek-ai/schemastery": "^3.18.1",
78
77
  "react": "^18.3.1",
79
78
  "zod": "^4.4.3"
@@ -88,10 +87,10 @@
88
87
  },
89
88
  "devDependencies": {
90
89
  "@deepseek-ai/cordis": "^4.0.1",
91
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.7",
92
- "@deepseek-ai/dsh-session": "^0.1.0-rc.7",
93
- "@deepseek-ai/dsh-session-projection": "^0.1.0-rc.7",
94
- "@deepseek-ai/dsh-settings": "^0.1.0-rc.7",
90
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.1.1-rc.2",
91
+ "@deepseek-ai/dsh-session": "^0.1.1-rc.2",
92
+ "@deepseek-ai/dsh-session-projection": "^0.1.1-rc.2",
93
+ "@deepseek-ai/dsh-settings": "^0.1.1-rc.2",
95
94
  "@deepseek-ai/schemastery": "^3.18.1",
96
95
  "@stylistic/eslint-plugin": "^5.10.0",
97
96
  "@types/node": "^26.2.0",