pi-ui-extend 1.0.40 → 1.0.42

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.
Files changed (186) hide show
  1. package/README.md +10 -2
  2. package/dist/app/app.d.ts +3 -0
  3. package/dist/app/app.js +26 -0
  4. package/dist/app/commands/command-host.d.ts +1 -0
  5. package/dist/app/commands/command-model-actions.js +6 -1
  6. package/dist/app/commands/command-registry.js +3 -3
  7. package/dist/app/commands/command-session-actions.d.ts +0 -1
  8. package/dist/app/commands/command-session-actions.js +30 -15
  9. package/dist/app/commands/reload-context-inventory.d.ts +11 -0
  10. package/dist/app/commands/reload-context-inventory.js +59 -0
  11. package/dist/app/extensions/extension-actions-controller.d.ts +1 -0
  12. package/dist/app/extensions/extension-actions-controller.js +6 -0
  13. package/dist/app/extensions/subagent-catalog-state.d.ts +9 -0
  14. package/dist/app/extensions/subagent-catalog-state.js +23 -0
  15. package/dist/app/icons.d.ts +14 -0
  16. package/dist/app/icons.js +33 -0
  17. package/dist/app/rendering/conversation-tool-renderer.js +2 -2
  18. package/dist/app/rendering/dcp-stats.d.ts +6 -1
  19. package/dist/app/rendering/dcp-stats.js +214 -46
  20. package/dist/app/rendering/editor-panels.js +17 -8
  21. package/dist/app/session/lazy-session-manager.js +12 -1
  22. package/dist/app/session/session-lifecycle-controller.d.ts +1 -0
  23. package/dist/app/session/session-lifecycle-controller.js +6 -0
  24. package/dist/app/session/tabs-controller.d.ts +3 -5
  25. package/dist/app/session/tabs-controller.js +31 -22
  26. package/dist/app/subagents/subagents-files.js +60 -1
  27. package/dist/app/subagents/subagents-model.d.ts +15 -1
  28. package/dist/app/subagents/subagents-model.js +52 -17
  29. package/dist/app/types.d.ts +7 -0
  30. package/dist/bundled-extensions/session-title/config.js +1 -1
  31. package/dist/default-pix-config.js +10 -0
  32. package/dist/markdown-format.js +27 -9
  33. package/dist/schemas/index.d.ts +1 -0
  34. package/dist/schemas/index.js +1 -0
  35. package/dist/schemas/pi-tools-suite-schema.d.ts +106 -71
  36. package/dist/schemas/pi-tools-suite-schema.js +68 -88
  37. package/dist/schemas/pix-schema.d.ts +7 -0
  38. package/dist/schemas/pix-schema.js +14 -0
  39. package/dist/schemas/tasks-schema.d.ts +18 -0
  40. package/dist/schemas/tasks-schema.js +41 -0
  41. package/docs/concurrency.md +6 -0
  42. package/docs/desktop-mvp.md +12 -1
  43. package/docs/desktop-task-manager.md +93 -82
  44. package/external/pi-tools-suite/README.md +192 -72
  45. package/external/pi-tools-suite/docs/browser-qa-subagent.md +50 -21
  46. package/external/pi-tools-suite/docs/context-gateway-p00-adr.md +216 -0
  47. package/external/pi-tools-suite/docs/context-gateway-p01n-gate-review.md +122 -0
  48. package/external/pi-tools-suite/docs/context-gateway-p01n-measurement.md +133 -0
  49. package/external/pi-tools-suite/docs/context-gateway-p01r-ra-evidence.md +111 -0
  50. package/external/pi-tools-suite/docs/context-gateway-p01r-rb-evidence.md +100 -0
  51. package/external/pi-tools-suite/docs/context-gateway-p01r-rc-evidence.md +69 -0
  52. package/external/pi-tools-suite/docs/context-gateway-p01r-rd-evidence.md +100 -0
  53. package/external/pi-tools-suite/docs/context-gateway-p01r-re-evidence.md +74 -0
  54. package/external/pi-tools-suite/docs/context-gateway-p01r-rf-evidence.md +153 -0
  55. package/external/pi-tools-suite/docs/context-gateway-p01r-rg-evidence.md +235 -0
  56. package/external/pi-tools-suite/docs/evals.md +1 -1
  57. package/external/pi-tools-suite/docs/session-recovery.md +40 -14
  58. package/external/pi-tools-suite/docs/subagent-model-pools.md +120 -0
  59. package/external/pi-tools-suite/package.json +3 -0
  60. package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/scripts/browser-qa-runner.mjs +82 -1
  61. package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/SKILL.md → agents/browser-qa.md} +261 -12
  62. package/external/pi-tools-suite/src/async-subagents/agents/frontier-review.md +23 -0
  63. package/external/pi-tools-suite/src/async-subagents/agents/implement.md +20 -0
  64. package/external/pi-tools-suite/src/async-subagents/agents/oracle.md +16 -0
  65. package/external/pi-tools-suite/src/async-subagents/agents/presets.jsonc +16 -0
  66. package/external/pi-tools-suite/src/async-subagents/agents/research.md +20 -0
  67. package/external/pi-tools-suite/src/async-subagents/agents/verify.md +18 -0
  68. package/external/pi-tools-suite/src/async-subagents/commands.ts +20 -72
  69. package/external/pi-tools-suite/src/async-subagents/core/activity.ts +33 -0
  70. package/external/pi-tools-suite/src/async-subagents/core/agent-catalog.ts +42 -0
  71. package/external/pi-tools-suite/src/async-subagents/core/agent-strategy.ts +13 -93
  72. package/external/pi-tools-suite/src/async-subagents/core/agents-dir.ts +499 -0
  73. package/external/pi-tools-suite/src/async-subagents/core/browser-qa.ts +9 -0
  74. package/external/pi-tools-suite/src/async-subagents/core/config.ts +284 -417
  75. package/external/pi-tools-suite/src/async-subagents/core/model-fallback.ts +1 -1
  76. package/external/pi-tools-suite/src/async-subagents/core/model-selection.ts +54 -0
  77. package/external/pi-tools-suite/src/async-subagents/core/prompt.ts +7 -6
  78. package/external/pi-tools-suite/src/async-subagents/core/routing.ts +65 -45
  79. package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +18 -6
  80. package/external/pi-tools-suite/src/async-subagents/core/state.ts +40 -0
  81. package/external/pi-tools-suite/src/async-subagents/core/types.ts +7 -0
  82. package/external/pi-tools-suite/src/async-subagents/index.ts +77 -3
  83. package/external/pi-tools-suite/src/async-subagents/lib.ts +12 -9
  84. package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +54 -22
  85. package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +6 -4
  86. package/external/pi-tools-suite/src/async-subagents/types.ts +3 -0
  87. package/external/pi-tools-suite/src/config.ts +42 -2
  88. package/external/pi-tools-suite/src/context-gateway/accounting.ts +151 -0
  89. package/external/pi-tools-suite/src/context-gateway/config.ts +120 -0
  90. package/external/pi-tools-suite/src/context-gateway/enforcement.ts +102 -0
  91. package/external/pi-tools-suite/src/context-gateway/index.ts +214 -0
  92. package/external/pi-tools-suite/src/context-gateway/metadata-normalization.ts +88 -0
  93. package/external/pi-tools-suite/src/context-gateway/storeless-capabilities.ts +89 -0
  94. package/external/pi-tools-suite/src/context-gateway/telemetry.ts +502 -0
  95. package/external/pi-tools-suite/src/context-gateway/test-output-parser.ts +326 -0
  96. package/external/pi-tools-suite/src/context-gateway/types.ts +158 -0
  97. package/external/pi-tools-suite/src/context-inventory.ts +99 -0
  98. package/external/pi-tools-suite/src/dcp/auto-compress-budget.ts +106 -0
  99. package/external/pi-tools-suite/src/dcp/auto-compress.ts +892 -136
  100. package/external/pi-tools-suite/src/dcp/commands.ts +82 -139
  101. package/external/pi-tools-suite/src/dcp/compress-tool.ts +533 -74
  102. package/external/pi-tools-suite/src/dcp/compression-blocks.ts +488 -94
  103. package/external/pi-tools-suite/src/dcp/compression-preview.ts +122 -0
  104. package/external/pi-tools-suite/src/dcp/compression-progress.ts +70 -0
  105. package/external/pi-tools-suite/src/dcp/config.ts +86 -68
  106. package/external/pi-tools-suite/src/dcp/conversation-index.ts +421 -0
  107. package/external/pi-tools-suite/src/dcp/debug-log.ts +7 -5
  108. package/external/pi-tools-suite/src/dcp/index.ts +617 -203
  109. package/external/pi-tools-suite/src/dcp/journal.ts +566 -0
  110. package/external/pi-tools-suite/src/dcp/progress-controller.ts +244 -0
  111. package/external/pi-tools-suite/src/dcp/prompts.ts +83 -189
  112. package/external/pi-tools-suite/src/dcp/protected-continuity.ts +175 -0
  113. package/external/pi-tools-suite/src/dcp/provider-tool-results.ts +189 -0
  114. package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +299 -79
  115. package/external/pi-tools-suite/src/dcp/pruner-compression-blocks.ts +173 -281
  116. package/external/pi-tools-suite/src/dcp/pruner-emergency.ts +2 -4
  117. package/external/pi-tools-suite/src/dcp/pruner-message-ids.ts +23 -15
  118. package/external/pi-tools-suite/src/dcp/pruner-metadata.ts +11 -1
  119. package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +30 -82
  120. package/external/pi-tools-suite/src/dcp/pruner-tools.ts +28 -136
  121. package/external/pi-tools-suite/src/dcp/pruner.ts +18 -33
  122. package/external/pi-tools-suite/src/dcp/recovery.ts +129 -0
  123. package/external/pi-tools-suite/src/dcp/shadow-plan.ts +127 -0
  124. package/external/pi-tools-suite/src/dcp/state-transaction.ts +102 -0
  125. package/external/pi-tools-suite/src/dcp/state.ts +158 -580
  126. package/external/pi-tools-suite/src/dcp/ui.ts +1 -0
  127. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +36 -214
  128. package/external/pi-tools-suite/src/index.ts +16 -1
  129. package/external/pi-tools-suite/src/model-tools/index.ts +76 -42
  130. package/external/pi-tools-suite/src/repo-discovery/index.ts +84 -18
  131. package/external/pi-tools-suite/src/repo-discovery/native-compact.ts +458 -0
  132. package/external/pi-tools-suite/src/resource-registry/index.ts +2290 -0
  133. package/external/pi-tools-suite/src/session-recovery/index.ts +189 -43
  134. package/external/pi-tools-suite/src/shell-command-policy.ts +219 -0
  135. package/external/pi-tools-suite/src/tool-descriptions.ts +41 -38
  136. package/external/pi-tools-suite/src/truncation-metadata-normalizer/index.ts +17 -0
  137. package/package.json +6 -3
  138. package/schemas/pi-tools-suite.json +540 -312
  139. package/schemas/pix.json +24 -0
  140. package/schemas/tasks.json +132 -0
  141. package/skills/spec-wiki/SKILL.md +55 -0
  142. package/skills/spec-wiki/evals/evals.json +132 -0
  143. package/skills/spec-wiki/evals/files/bootstrap/architecture/rfc-007.md +14 -0
  144. package/skills/spec-wiki/evals/files/bootstrap/docs/how-to-pay.md +4 -0
  145. package/skills/spec-wiki/evals/files/bootstrap/docs/overview.md +9 -0
  146. package/skills/spec-wiki/evals/files/bootstrap/requirements/payment-behavior.md +17 -0
  147. package/skills/spec-wiki/evals/files/bootstrap/src/payments.ts +12 -0
  148. package/skills/spec-wiki/evals/files/bootstrap/tests/payments.test.ts +11 -0
  149. package/skills/spec-wiki/evals/files/focused/src/session.ts +5 -0
  150. package/skills/spec-wiki/evals/files/focused/tests/session.test.ts +17 -0
  151. package/skills/spec-wiki/evals/files/maintenance/low-signal/incoming/capture.txt +2 -0
  152. package/skills/spec-wiki/evals/files/maintenance/low-signal/src/capture.ts +3 -0
  153. package/skills/spec-wiki/evals/files/maintenance/move/docs/old-job-contract.md +18 -0
  154. package/skills/spec-wiki/evals/files/maintenance/move/src/job.ts +5 -0
  155. package/skills/spec-wiki/evals/files/maintenance/new-relation/docs/session-refresh.md +24 -0
  156. package/skills/spec-wiki/evals/files/maintenance/new-relation/incoming/refresh-worker.ts +7 -0
  157. package/skills/spec-wiki/evals/files/maintenance/new-relation/src/session.ts +7 -0
  158. package/skills/spec-wiki/evals/files/maintenance/no-impact/docs/auth-contract.md +17 -0
  159. package/skills/spec-wiki/evals/files/maintenance/no-impact/incoming/telemetry-format.ts +3 -0
  160. package/skills/spec-wiki/evals/files/maintenance/no-impact/src/auth.ts +3 -0
  161. package/skills/spec-wiki/evals/files/query/docs/how-to-run-jobs.md +4 -0
  162. package/skills/spec-wiki/evals/files/query/docs/spec-overview.md +9 -0
  163. package/skills/spec-wiki/evals/files/query/features/jobs/cancellation-contract.md +15 -0
  164. package/skills/spec-wiki/evals/files/query/features/jobs/retry-spec.md +6 -0
  165. package/skills/spec-wiki/evals/maintenance_regression.py +311 -0
  166. package/skills/spec-wiki/evals/retrieval-eval.md +37 -0
  167. package/skills/spec-wiki/evals/retrieval-evals.json +37 -0
  168. package/skills/spec-wiki/evals/trigger-evals.json +94 -0
  169. package/skills/spec-wiki/references/bootstrap.md +100 -0
  170. package/skills/spec-wiki/references/focused-spec.md +117 -0
  171. package/skills/spec-wiki/references/index-schema.md +330 -0
  172. package/skills/spec-wiki/references/maintenance.md +134 -0
  173. package/skills/spec-wiki/references/query.md +74 -0
  174. package/skills/spec-wiki/scripts/spec_wiki.py +2076 -0
  175. package/docs/desktop-markdown-media.md +0 -77
  176. package/external/pi-tools-suite/docs/dcp-emergency-current-turn.md +0 -102
  177. package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +0 -270
  178. package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/references/auth-scaffold-spec.md +0 -78
  179. package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/references/qa-design.md +0 -223
  180. package/external/pi-tools-suite/src/dcp/state-persistence.ts +0 -195
  181. package/external/pi-tools-suite/src/skill-installer/index.ts +0 -333
  182. package/skills/spec-lite/SKILL.md +0 -140
  183. /package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/references → agents/browser-qa/examples}/qa-auth.example.jsonc +0 -0
  184. /package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/references → agents/browser-qa/examples}/qa-flow.example.jsonc +0 -0
  185. /package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/vendor/fflate.LICENSE +0 -0
  186. /package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/vendor/fflate.mjs +0 -0
@@ -1,14 +1,18 @@
1
- import { readFileSync } from "node:fs";
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
2
3
  import { join } from "node:path";
4
+ import { parse as parseJsonc } from "jsonc-parser";
3
5
  import { normalizeToolName, parseArgsText } from "../../tool-renderers/utils.js";
4
6
  const NUDGE_TYPES = ["turn", "iteration", "context-soft", "context-strong"];
5
- export function formatDcpStatsToast(session) {
6
- const latestState = resolveLatestDcpState(session);
7
- const stats = collectDcpSessionStats(session, latestState);
8
- const nudgeStats = collectDcpNudgeStats(session, latestState?.data);
7
+ export function formatDcpStatsToast(session, options = {}) {
8
+ const branch = dcpStatsBranch(session);
9
+ const latestState = resolveJournalDcpState(branch);
10
+ const manualModeBaseline = options.manualModeBaseline ?? readDcpManualModeBaseline(session, options.configPath);
11
+ const stats = collectDcpSessionStats(session, latestState, branch, manualModeBaseline);
12
+ const nudgeStats = collectDcpNudgeStats(branch, latestState?.data);
9
13
  const activeBlocks = stats.activeBlocks ?? 0;
10
14
  const totalBlocks = stats.totalBlocks ?? stats.activeBlocks ?? 0;
11
- const totalNudgeEvents = nudgeStats.emitted + nudgeStats.upgraded;
15
+ const totalNudgeEvents = nudgeStats.emitted + nudgeStats.reapplied + nudgeStats.upgraded;
12
16
  const activeAnchors = NUDGE_TYPES.reduce((sum, type) => sum + nudgeStats.activeByType[type], 0);
13
17
  const lines = [
14
18
  "DCP Session Statistics:",
@@ -19,7 +23,7 @@ export function formatDcpStatsToast(session) {
19
23
  ` State source: ${formatStateSource(stats.stateSource)}`,
20
24
  "",
21
25
  "Nudge telemetry:",
22
- ` Sent: ${fmt(nudgeStats.emitted)} emitted, ${fmt(nudgeStats.upgraded)} upgraded`,
26
+ ` Sent: ${fmt(nudgeStats.emitted)} emitted, ${fmt(nudgeStats.reapplied)} reapplied, ${fmt(nudgeStats.upgraded)} upgraded`,
23
27
  ` By type: ${NUDGE_TYPES.map((type) => `${type}=${fmt(nudgeStats.byType[type])}`).join(", ")}`,
24
28
  ` Active anchors: ${fmt(activeAnchors)}${activeAnchors > 0 ? ` (${NUDGE_TYPES.map((type) => `${type}=${fmt(nudgeStats.activeByType[type])}`).join(", ")})` : ""}`,
25
29
  ` Cleared after compress: ${fmt(nudgeStats.clearedEvents)} time${nudgeStats.clearedEvents === 1 ? "" : "s"} (${fmt(nudgeStats.clearedAnchors)} anchor${nudgeStats.clearedAnchors === 1 ? "" : "s"})`,
@@ -32,7 +36,7 @@ export function formatDcpStatsToast(session) {
32
36
  ];
33
37
  return lines.join("\n");
34
38
  }
35
- function collectDcpSessionStats(session, latestState) {
39
+ function collectDcpSessionStats(session, latestState, branch, manualModeBaseline) {
36
40
  const usage = session.getContextUsage();
37
41
  const stats = {
38
42
  runs: 0,
@@ -42,18 +46,16 @@ function collectDcpSessionStats(session, latestState) {
42
46
  summaryTokens: 0,
43
47
  prunedTools: 0,
44
48
  stateSource: latestState?.source ?? "tool-results",
49
+ ...(latestState ? { manualMode: manualModeBaseline } : {}),
45
50
  ...(usage?.tokens != null ? { contextTokens: usage.tokens } : {}),
46
51
  ...(usage?.contextWindow != null ? { contextWindow: usage.contextWindow } : {}),
47
52
  ...(usage?.percent != null ? { contextPercent: usage.percent } : {}),
48
53
  };
49
- const branch = session.sessionManager.getBranch();
50
54
  if (latestState)
51
55
  applyDcpStateStats(stats, latestState.data);
52
56
  for (const entry of branch) {
53
57
  if (entry.type !== "message")
54
58
  continue;
55
- if (latestState)
56
- continue;
57
59
  const message = entry.message;
58
60
  if (message.role !== "toolResult")
59
61
  continue;
@@ -64,18 +66,24 @@ function collectDcpSessionStats(session, latestState) {
64
66
  const result = parseToolResultText(message.content);
65
67
  if (!result)
66
68
  continue;
67
- stats.runs += 1;
68
- stats.tokensSaved += numberValue(result.tokensSaved) ?? 0;
69
- stats.totalPruneCount = Math.max(stats.totalPruneCount, numberValue(result.totalPruneCount) ?? 0);
70
- stats.items += numberValue(result.itemCount) ?? sumDefined(numberValue(result.ranges), numberValue(result.messages)) ?? 0;
71
- stats.summaryTokens += numberValue(result.totalSummaryTokens) ?? 0;
72
- stats.prunedTools += numberValue(result.prunedTools) ?? 0;
73
- const activeBlocks = numberValue(result.activeBlocks);
74
- const totalBlocks = numberValue(result.totalBlocks);
75
- if (activeBlocks != null)
76
- stats.activeBlocks = activeBlocks;
77
- if (totalBlocks != null)
78
- stats.totalBlocks = totalBlocks;
69
+ if (result.idempotentReplay !== true) {
70
+ stats.runs += 1;
71
+ stats.tokensSaved += numberValue(result.tokensSaved) ?? 0;
72
+ stats.items += numberValue(result.itemCount) ?? sumDefined(numberValue(result.ranges), numberValue(result.messages)) ?? 0;
73
+ stats.summaryTokens += numberValue(result.totalSummaryTokens) ?? 0;
74
+ }
75
+ if (!latestState) {
76
+ stats.totalPruneCount = Math.max(stats.totalPruneCount, numberValue(result.totalPruneCount) ?? 0);
77
+ const prunedTools = numberValue(result.prunedTools);
78
+ if (prunedTools != null)
79
+ stats.prunedTools = prunedTools;
80
+ const activeBlocks = numberValue(result.activeBlocks);
81
+ const totalBlocks = numberValue(result.totalBlocks);
82
+ if (activeBlocks != null)
83
+ stats.activeBlocks = activeBlocks;
84
+ if (totalBlocks != null)
85
+ stats.totalBlocks = totalBlocks;
86
+ }
79
87
  const contextTokens = numberValue(result.contextTokens);
80
88
  const contextWindow = numberValue(result.contextWindow);
81
89
  const contextPercent = numberValue(result.contextPercent);
@@ -86,10 +94,17 @@ function collectDcpSessionStats(session, latestState) {
86
94
  if (stats.contextPercent == null && contextPercent != null)
87
95
  stats.contextPercent = contextPercent;
88
96
  }
97
+ if (latestState) {
98
+ // Journal state owns durable block/prune counts; successful compress tool
99
+ // results own operation gain because the journal intentionally stores only
100
+ // projection decisions rather than derived accounting snapshots.
101
+ stats.totalPruneCount = Math.max(stats.totalPruneCount, (stats.totalBlocks ?? 0) + stats.prunedTools);
102
+ }
89
103
  return stats;
90
104
  }
91
105
  function applyDcpStateStats(stats, data) {
92
106
  stats.tokensSaved = numberValue(data.tokensSaved) ?? stats.tokensSaved;
107
+ stats.tokensSaved += numberValue(data.prunedTokensSaved) ?? 0;
93
108
  stats.totalPruneCount = numberValue(data.totalPruneCount) ?? stats.totalPruneCount;
94
109
  const blocks = Array.isArray(data.compressionBlocks) ? data.compressionBlocks : undefined;
95
110
  if (blocks) {
@@ -101,16 +116,16 @@ function applyDcpStateStats(stats, data) {
101
116
  if (typeof data.manualMode === "boolean")
102
117
  stats.manualMode = data.manualMode;
103
118
  }
104
- function collectDcpNudgeStats(session, latestState) {
119
+ function collectDcpNudgeStats(branch, latestState) {
105
120
  const stats = {
106
121
  emitted: 0,
122
+ reapplied: 0,
107
123
  upgraded: 0,
108
124
  clearedEvents: 0,
109
125
  clearedAnchors: 0,
110
126
  byType: { "turn": 0, "iteration": 0, "context-soft": 0, "context-strong": 0 },
111
127
  activeByType: { "turn": 0, "iteration": 0, "context-soft": 0, "context-strong": 0 },
112
128
  };
113
- const branch = session.sessionManager.getBranch();
114
129
  if (latestState)
115
130
  applyActiveAnchorStats(stats, latestState);
116
131
  for (const entry of branch) {
@@ -118,9 +133,11 @@ function collectDcpNudgeStats(session, latestState) {
118
133
  if (!data)
119
134
  continue;
120
135
  const event = data.event;
121
- if ((event === "emitted" || event === "upgraded") && isNudgeType(data.type)) {
136
+ if ((event === "emitted" || event === "reapplied" || event === "upgraded") && isNudgeType(data.type)) {
122
137
  if (event === "emitted")
123
138
  stats.emitted += 1;
139
+ else if (event === "reapplied")
140
+ stats.reapplied += 1;
124
141
  else
125
142
  stats.upgraded += 1;
126
143
  stats.byType[data.type] += 1;
@@ -142,6 +159,95 @@ function collectDcpNudgeStats(session, latestState) {
142
159
  }
143
160
  return stats;
144
161
  }
162
+ function readDcpManualModeBaseline(session, configPath = join(homedir(), ".config", "pi", "pi-tools-suite.jsonc")) {
163
+ let enabled = false;
164
+ if (!existsSync(configPath))
165
+ return enabled;
166
+ try {
167
+ const parsed = parseJsonc(readFileSync(configPath, "utf8"));
168
+ if (!isRecord(parsed))
169
+ return enabled;
170
+ const dcp = parsed.dcp;
171
+ if (!isRecord(dcp))
172
+ return enabled;
173
+ const manualMode = dcp.manualMode;
174
+ if (isRecord(manualMode) && typeof manualMode.enabled === "boolean")
175
+ enabled = manualMode.enabled;
176
+ const overrides = dcp.modelOverrides;
177
+ if (!isRecord(overrides))
178
+ return enabled;
179
+ for (const override of matchingModelOverrides(overrides, dcpModelKeys(session))) {
180
+ const overrideManualMode = override.manualMode;
181
+ if (isRecord(overrideManualMode) && typeof overrideManualMode.enabled === "boolean") {
182
+ enabled = overrideManualMode.enabled;
183
+ }
184
+ }
185
+ }
186
+ catch {
187
+ return false;
188
+ }
189
+ return enabled;
190
+ }
191
+ function dcpModelKeys(session) {
192
+ const model = session.model;
193
+ const provider = normalizeModelKey(model?.provider);
194
+ const id = normalizeModelKey(model?.id);
195
+ return [provider && id ? `${provider}/${id}` : undefined, id].filter((key) => key !== undefined);
196
+ }
197
+ function matchingModelOverrides(overrides, modelKeys) {
198
+ const candidates = modelKeys.map(normalizeModelKey).filter((key, index, all) => key !== undefined && all.indexOf(key) === index);
199
+ const exact = new Map();
200
+ const wildcard = [];
201
+ for (const [rawKey, value] of Object.entries(overrides)) {
202
+ const key = normalizeModelKey(rawKey);
203
+ if (!key || !isRecord(value))
204
+ continue;
205
+ if (key.includes("*") || key.includes("?"))
206
+ wildcard.push([key, value]);
207
+ else
208
+ exact.set(key, value);
209
+ }
210
+ const bare = candidates.filter((candidate) => !candidate.includes("/"));
211
+ const full = candidates.filter((candidate) => candidate.includes("/"));
212
+ const matches = [];
213
+ for (const candidate of bare)
214
+ for (const [pattern, value] of wildcard)
215
+ if (!pattern.includes("/") && globMatches(pattern, candidate))
216
+ matches.push(value);
217
+ for (const candidate of bare) {
218
+ const value = exact.get(candidate);
219
+ if (value)
220
+ matches.push(value);
221
+ }
222
+ for (const candidate of full)
223
+ for (const [pattern, value] of wildcard)
224
+ if (pattern.includes("/") && globMatches(pattern, candidate))
225
+ matches.push(value);
226
+ for (const candidate of full) {
227
+ const value = exact.get(candidate);
228
+ if (value)
229
+ matches.push(value);
230
+ }
231
+ return matches;
232
+ }
233
+ function normalizeModelKey(value) {
234
+ if (typeof value !== "string")
235
+ return undefined;
236
+ const trimmed = value.trim();
237
+ return trimmed || undefined;
238
+ }
239
+ function globMatches(pattern, candidate) {
240
+ let source = "^";
241
+ for (const char of pattern) {
242
+ if (char === "*")
243
+ source += ".*";
244
+ else if (char === "?")
245
+ source += ".";
246
+ else
247
+ source += char.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
248
+ }
249
+ return new RegExp(`${source}$`).test(candidate);
250
+ }
145
251
  function applyActiveAnchorStats(stats, data) {
146
252
  stats.activeByType = { "turn": 0, "iteration": 0, "context-soft": 0, "context-strong": 0 };
147
253
  const anchors = Array.isArray(data.nudgeAnchors) ? data.nudgeAnchors : [];
@@ -161,33 +267,95 @@ function applyActiveAnchorStats(stats, data) {
161
267
  };
162
268
  }
163
269
  }
164
- function resolveLatestDcpState(session) {
165
- const sidecar = loadSidecarDcpState(session);
166
- if (sidecar)
167
- return { data: sidecar, source: "sidecar" };
168
- return undefined;
169
- }
170
- function loadSidecarDcpState(session) {
171
- const sessionManager = session.sessionManager;
172
- const sessionDir = sessionManager.getSessionDir?.();
173
- const sessionId = sessionManager.getSessionId?.();
174
- if (!sessionDir || !sessionId)
270
+ function resolveJournalDcpState(branch) {
271
+ const blocks = new Map();
272
+ const pruned = new Map();
273
+ let initialized = false;
274
+ let manualMode;
275
+ let nudgeAnchors = [];
276
+ let lastNudge;
277
+ for (const entry of branch) {
278
+ const operation = customEntryData(entry, "dcp-journal");
279
+ if (!operation || operation.schemaVersion !== 1)
280
+ continue;
281
+ if (operation.kind === "init") {
282
+ initialized = true;
283
+ continue;
284
+ }
285
+ if (!initialized || operation.kind !== "delta")
286
+ continue;
287
+ if (Array.isArray(operation.blocks)) {
288
+ for (const raw of operation.blocks) {
289
+ if (!isRecord(raw) || typeof raw.id !== "number")
290
+ continue;
291
+ blocks.set(raw.id, { ...raw });
292
+ }
293
+ }
294
+ if (Array.isArray(operation.blockStates)) {
295
+ for (const raw of operation.blockStates) {
296
+ if (!isRecord(raw) || typeof raw.id !== "number")
297
+ continue;
298
+ const block = blocks.get(raw.id);
299
+ if (!block)
300
+ continue;
301
+ if (typeof raw.active === "boolean")
302
+ block.active = raw.active;
303
+ if (typeof raw.deactivatedReason === "string")
304
+ block.deactivatedReason = raw.deactivatedReason;
305
+ }
306
+ }
307
+ if (Array.isArray(operation.prunedTools)) {
308
+ for (const raw of operation.prunedTools) {
309
+ if (!isRecord(raw) || typeof raw.toolCallId !== "string")
310
+ continue;
311
+ pruned.set(raw.toolCallId, {
312
+ ...(typeof raw.reason === "string" ? { reason: raw.reason } : {}),
313
+ tokenEstimate: Math.max(0, numberValue(raw.tokenEstimate) ?? 0),
314
+ });
315
+ }
316
+ }
317
+ if (typeof operation.manualMode === "boolean")
318
+ manualMode = operation.manualMode;
319
+ if (Array.isArray(operation.nudgeAnchors))
320
+ nudgeAnchors = operation.nudgeAnchors;
321
+ if (Object.prototype.hasOwnProperty.call(operation, "lastNudge"))
322
+ lastNudge = operation.lastNudge ?? undefined;
323
+ }
324
+ if (!initialized)
175
325
  return undefined;
326
+ return {
327
+ source: "journal",
328
+ data: {
329
+ compressionBlocks: [...blocks.values()],
330
+ prunedToolIds: [...pruned.keys()],
331
+ prunedTokensSaved: [...pruned.values()].reduce((sum, item) => sum + item.tokenEstimate, 0),
332
+ manualMode,
333
+ nudgeAnchors,
334
+ lastNudge,
335
+ },
336
+ };
337
+ }
338
+ function dcpStatsBranch(session) {
339
+ const manager = session.sessionManager;
176
340
  try {
177
- const statePath = join(sessionDir, "dcp-state", safeSessionFileName(sessionId));
178
- const parsed = JSON.parse(readFileSync(statePath, "utf8"));
179
- return isRecord(parsed) ? parsed : undefined;
341
+ const full = manager.readFullBranchEntriesSync?.();
342
+ if (Array.isArray(full))
343
+ return full;
180
344
  }
181
345
  catch {
182
- return undefined;
346
+ // Fall back to the manager's currently materialized branch below.
347
+ }
348
+ try {
349
+ const branch = manager.getBranch();
350
+ return Array.isArray(branch) ? branch : [];
351
+ }
352
+ catch {
353
+ return [];
183
354
  }
184
- }
185
- function safeSessionFileName(sessionId) {
186
- return `${sessionId.replace(/[^a-zA-Z0-9._-]/g, "_")}.json`;
187
355
  }
188
356
  function formatStateSource(source) {
189
- if (source === "sidecar")
190
- return "dcp-state sidecar";
357
+ if (source === "journal")
358
+ return "session journal";
191
359
  return "compress tool results";
192
360
  }
193
361
  function customEntryData(entry, customType) {
@@ -1,8 +1,8 @@
1
1
  import { stringDisplayWidth } from "../../terminal-width.js";
2
2
  import { SUBAGENTS_WIDGET_MAX_ROWS } from "../constants.js";
3
- import { ellipsizeDisplay, padOrTrimPlain, wrapLine } from "./render-text.js";
3
+ import { ellipsizeDisplay, padOrTrimPlain, sanitizeText, wrapLine } from "./render-text.js";
4
4
  import { thinkingLevelThemeColor } from "./status-line-renderer.js";
5
- import { activeSubagentStates, formatElapsedSince, formatSubagentsPanelStats, subagentModelThinkingLabel, subagentRunName, subagentStatusIcon, taskPreviewMap, } from "../subagents/subagents-model.js";
5
+ import { activeSubagentStates, formatElapsedSince, formatSubagentLastActivity, formatSubagentsPanelStats, subagentIcon, subagentModelThinkingLabel, subagentRunName, taskPreviewMap, } from "../subagents/subagents-model.js";
6
6
  import { formatTodoPanelStats, formatTodoTaskLine, hasOpenTodoTasks, shiftSegmentsToSlice, todoTaskLineSegments, visibleTodoTaskRows, visibleTodoTasks, } from "../todo/todo-model.js";
7
7
  export function renderTodoPanel(details, expanded, width, colors) {
8
8
  if (!details)
@@ -106,18 +106,20 @@ export function renderSubagentsPanel(state, expanded, width, colors) {
106
106
  for (const visibleRun of visibleAgents) {
107
107
  const { agent, preview } = visibleRun;
108
108
  const model = subagentModelThinkingLabel(preview);
109
- const task = preview?.task?.trim() || preview?.scope?.trim() || "task unavailable";
110
- const icon = subagentStatusIcon(agent.status);
109
+ const task = singleLinePreview(preview?.task) || singleLinePreview(preview?.scope) || "task unavailable";
110
+ const icon = subagentIcon(preview);
111
111
  const runLabel = visibleRun.showRunLabel ? `${subagentRunName(visibleRun.runDir)} ` : "";
112
112
  const prefix = `${runLabel}${icon} ${agent.id} ${model} `;
113
- const suffix = ` ${formatElapsedSince(agent.startedAt, now)}`;
113
+ const activity = formatSubagentLastActivity(agent.lastActivity);
114
+ const elapsed = formatElapsedSince(agent.startedAt, now);
115
+ const suffix = activity ? ` ${activity} ${elapsed}` : ` ${elapsed}`;
114
116
  const taskWidth = Math.max(8, rowWidth - stringDisplayWidth(prefix) - stringDisplayWidth(suffix));
115
117
  const taskText = ellipsizeDisplay(task, taskWidth);
116
118
  const text = `${prefix}${taskText}${suffix}`;
117
119
  lines.push({
118
120
  text: padOrTrimPlain(text, width),
119
121
  colorOverride: colors.muted,
120
- segments: subagentPanelLineSegments({ text, icon, agentId: agent.id, model, taskText, prefix, runLabel, status: agent.status }, colors),
122
+ segments: subagentPanelLineSegments({ text, icon, agentId: agent.id, model, taskText, prefix, runLabel, activity, elapsed, status: agent.status }, colors),
121
123
  target,
122
124
  });
123
125
  }
@@ -126,20 +128,27 @@ export function renderSubagentsPanel(state, expanded, width, colors) {
126
128
  lines.push({ text: padOrTrimPlain(`+${hidden} more`, width), variant: "muted", target });
127
129
  return lines;
128
130
  }
131
+ function singleLinePreview(value) {
132
+ return value ? sanitizeText(value).replace(/\s+/gu, " ").trim() : "";
133
+ }
129
134
  function subagentPanelLineSegments(input, colors) {
130
- const iconStart = input.text.indexOf(input.icon);
131
135
  const runLabelStart = input.runLabel ? input.text.indexOf(input.runLabel) : -1;
136
+ const iconStart = input.runLabel.length;
132
137
  const nameStart = input.text.indexOf(input.agentId, iconStart + input.icon.length);
133
138
  const modelStart = input.text.indexOf(input.model, nameStart + input.agentId.length);
134
139
  const taskStart = input.prefix.length;
135
140
  const suffixStart = taskStart + input.taskText.length;
141
+ const elapsedStart = input.text.length - input.elapsed.length;
142
+ const activityStart = input.activity ? elapsedStart - input.activity.length - 1 : -1;
136
143
  return [
137
144
  ...(runLabelStart >= 0 ? [{ start: runLabelStart, end: runLabelStart + input.runLabel.length, foreground: colors.warning }] : []),
138
145
  { start: iconStart, end: iconStart + input.icon.length, foreground: subagentStatusColor(input.status, colors), bold: true },
139
146
  { start: nameStart, end: nameStart + input.agentId.length, foreground: colors.accent, bold: true },
140
147
  { start: modelStart, end: modelStart + input.model.length, foreground: colors.info },
141
148
  { start: taskStart, end: suffixStart, foreground: colors.muted },
142
- { start: suffixStart, end: input.text.length, foreground: colors.muted },
149
+ { start: suffixStart, end: input.activity ? activityStart : input.text.length, foreground: colors.muted },
150
+ ...(input.activity ? [{ start: activityStart, end: activityStart + input.activity.length, foreground: colors.info }] : []),
151
+ ...(input.activity ? [{ start: activityStart + input.activity.length, end: input.text.length, foreground: colors.muted }] : []),
143
152
  ].filter((segment) => segment.start >= 0 && segment.end > segment.start);
144
153
  }
145
154
  function subagentStatusColor(status, colors) {
@@ -145,6 +145,14 @@ class LazySessionManager {
145
145
  const entries = await readAllSessionEntries(this.sessionFilePath);
146
146
  return branchEntries(entries, this.leafId ?? entries.at(-1)?.id);
147
147
  }
148
+ /**
149
+ * Explicit synchronous full-branch escape hatch for user-triggered diagnostics
150
+ * such as DCP statistics. Normal transcript rendering stays lazy; callers that
151
+ * opt into this method accept one full session hydration for exact branch data.
152
+ */
153
+ readFullBranchEntriesSync() {
154
+ return this.hydrate().getBranch();
155
+ }
148
156
  async readFullSessionEntries() {
149
157
  if (this.hydrated)
150
158
  return this.hydrated.getEntries();
@@ -320,10 +328,13 @@ class LazySessionManager {
320
328
  }
321
329
  }
322
330
  appendEntry(entry) {
331
+ // Persist first. If the append fails, the in-memory branch must remain
332
+ // unchanged; otherwise extensions that treat append success as a commit
333
+ // boundary can observe a phantom entry that never reached the JSONL file.
334
+ appendFileSync(this.sessionFilePath, `${JSON.stringify(entry)}\n`, "utf8");
323
335
  this.entries.push(entry);
324
336
  this.byId.set(entry.id, entry);
325
337
  this.leafId = entry.id;
326
- appendFileSync(this.sessionFilePath, `${JSON.stringify(entry)}\n`, "utf8");
327
338
  return entry.id;
328
339
  }
329
340
  newEntry(type, payload) {
@@ -27,6 +27,7 @@ export type AppSessionLifecycleHost = {
27
27
  handleExtensionError(error: ExtensionError): void;
28
28
  handleSessionEvent(event: AgentSessionEvent): void;
29
29
  bindAgentPause(session: AgentSession): void;
30
+ contextInventoryText?(runtime: AgentSessionRuntime, heading: string): string;
30
31
  addEntry(entry: Entry): void;
31
32
  setStatus(status: string): void;
32
33
  showToast(message: string, kind: "success" | "error" | "warning" | "info"): void;
@@ -44,6 +44,12 @@ export class AppSessionLifecycleController {
44
44
  await this.bindCurrentSession({ awaitExtensions: false });
45
45
  });
46
46
  await this.bindCurrentSession({ awaitExtensions: false });
47
+ if (runtime.session.messages.length === 0) {
48
+ await this.awaitCurrentSessionExtensions(runtime);
49
+ const text = this.host.contextInventoryText?.(runtime, `Empty tab ready. cwd=${runtime.cwd}`);
50
+ if (text)
51
+ this.host.addEntry({ id: createId("system"), kind: "system", text });
52
+ }
47
53
  if (runtime.modelFallbackMessage) {
48
54
  this.host.addEntry({ id: createId("system"), kind: "system", text: runtime.modelFallbackMessage });
49
55
  }
@@ -43,6 +43,7 @@ export type AppTabsControllerHost = {
43
43
  restoreAutoUserMessages?(messages: readonly SubmittedUserMessage[]): void;
44
44
  captureDeferredUserMessages?(): readonly SubmittedUserMessage[];
45
45
  restoreDeferredUserMessages?(messages: readonly SubmittedUserMessage[]): void;
46
+ contextInventoryText?(runtime: AgentSessionRuntime, heading: string): string;
46
47
  addEntry(entry: Entry): void;
47
48
  showToast(message: string, kind: "success" | "error" | "warning" | "info"): void;
48
49
  render(): void;
@@ -184,13 +185,10 @@ export declare class AppTabsController {
184
185
  private prewarmTabs;
185
186
  private cleanupOldProjectSessions;
186
187
  /**
187
- * Unlink a project session file and, best-effort, its DCP sidecar state.
188
- * The sidecar path is derived from the session id in the first line of the
189
- * `.jsonl` (mirrors the DCP module's `safeSessionFileName`), so retention
190
- * never leaves orphan sidecars behind. Everything here is best-effort:
188
+ * Unlink a project session file. Everything here is best-effort:
191
189
  * session retention must never interrupt the terminal UI.
192
190
  */
193
- private unlinkSessionAndDcpSidecar;
191
+ private unlinkSessionFile;
194
192
  private preservedSessionPaths;
195
193
  private maxProjectSessions;
196
194
  }
@@ -350,6 +350,12 @@ export class AppTabsController {
350
350
  this.scheduleProjectSessionRetention();
351
351
  this.scheduleTabPrewarm();
352
352
  await this.loadActiveSessionHistory(restoredRuntime, generation);
353
+ if (restoredRuntime.session.messages.length === 0) {
354
+ await this.host.awaitCurrentSessionExtensions?.(restoredRuntime);
355
+ const text = this.host.contextInventoryText?.(restoredRuntime, `Opened an empty tab. cwd=${restoredRuntime.cwd}`);
356
+ if (text)
357
+ this.host.addEntry({ id: createId("system"), kind: "system", text });
358
+ }
353
359
  this.scheduleRestoredTabTitleRefresh(restoredSessionPaths, generation);
354
360
  }
355
361
  openNewTab() {
@@ -436,6 +442,7 @@ export class AppTabsController {
436
442
  this.host.render();
437
443
  try {
438
444
  await this.host.activateRuntime(newRuntime, { awaitExtensions: false });
445
+ await this.host.awaitCurrentSessionExtensions?.(newRuntime);
439
446
  }
440
447
  catch (error) {
441
448
  if (this.pendingActiveTabId === targetTab.id)
@@ -477,7 +484,12 @@ export class AppTabsController {
477
484
  this.scheduleProjectSessionRetention();
478
485
  this.host.resetSessionView();
479
486
  this.restoreDeferredUserMessages(targetTab.id);
480
- this.host.addEntry({ id: createId("system"), kind: "system", text: `Opened a new tab. cwd=${newRuntime.cwd}` });
487
+ this.host.addEntry({
488
+ id: createId("system"),
489
+ kind: "system",
490
+ text: this.host.contextInventoryText?.(newRuntime, `Opened a new tab. cwd=${newRuntime.cwd}`)
491
+ ?? `Opened a new tab. cwd=${newRuntime.cwd}`,
492
+ });
481
493
  if (newRuntime.modelFallbackMessage)
482
494
  this.host.addEntry({ id: createId("system"), kind: "system", text: newRuntime.modelFallbackMessage });
483
495
  for (const diag of newRuntime.diagnostics ?? []) {
@@ -571,6 +583,7 @@ export class AppTabsController {
571
583
  this.host.render();
572
584
  try {
573
585
  await this.host.activateRuntime(newRuntime, { awaitExtensions: false });
586
+ await this.host.awaitCurrentSessionExtensions?.(newRuntime);
574
587
  }
575
588
  catch {
576
589
  this.pendingActiveTabId = undefined;
@@ -615,6 +628,11 @@ export class AppTabsController {
615
628
  void this.saveTabs();
616
629
  this.scheduleTabPrewarm();
617
630
  await this.loadActiveSessionHistory(newRuntime, generation);
631
+ if (newRuntime.session.messages.length === 0) {
632
+ const text = this.host.contextInventoryText?.(newRuntime, `Opened an empty tab. cwd=${newRuntime.cwd}`);
633
+ if (text)
634
+ this.host.addEntry({ id: createId("system"), kind: "system", text });
635
+ }
618
636
  return true;
619
637
  }
620
638
  forkSessionEntryInNewTab(entryId) {
@@ -1953,7 +1971,12 @@ export class AppTabsController {
1953
1971
  for (const session of sessions) {
1954
1972
  if (keep.has(session.path))
1955
1973
  continue;
1956
- await this.unlinkSessionAndDcpSidecar(session.path);
1974
+ // Lifecycle state can change while retention is scanning. Re-check the
1975
+ // current runtime/open tabs immediately before destructive work so a
1976
+ // session that became live after the initial snapshot is not removed.
1977
+ if (this.preservedSessionPaths().has(resolve(session.path)))
1978
+ continue;
1979
+ await this.unlinkSessionFile(session.path);
1957
1980
  }
1958
1981
  }
1959
1982
  catch {
@@ -1964,31 +1987,17 @@ export class AppTabsController {
1964
1987
  }
1965
1988
  }
1966
1989
  /**
1967
- * Unlink a project session file and, best-effort, its DCP sidecar state.
1968
- * The sidecar path is derived from the session id in the first line of the
1969
- * `.jsonl` (mirrors the DCP module's `safeSessionFileName`), so retention
1970
- * never leaves orphan sidecars behind. Everything here is best-effort:
1990
+ * Unlink a project session file. Everything here is best-effort:
1971
1991
  * session retention must never interrupt the terminal UI.
1972
1992
  */
1973
- async unlinkSessionAndDcpSidecar(sessionPath) {
1974
- let sidecarPath;
1975
- const header = await readSessionHeader(sessionPath);
1976
- if (header?.type === "session" && typeof header.id === "string" && header.id) {
1977
- sidecarPath = join(dirname(sessionPath), "dcp-state", header.id.replace(/[^a-zA-Z0-9._-]/g, "_") + ".json");
1978
- }
1993
+ async unlinkSessionFile(sessionPath, unlinkFile = unlink) {
1994
+ if (this.preservedSessionPaths().has(resolve(sessionPath)))
1995
+ return;
1979
1996
  try {
1980
- await unlink(sessionPath);
1997
+ await unlinkFile(sessionPath);
1981
1998
  }
1982
1999
  catch {
1983
- // Session retention must never interrupt the terminal UI.
1984
- }
1985
- if (sidecarPath) {
1986
- try {
1987
- await unlink(sidecarPath);
1988
- }
1989
- catch {
1990
- // Sidecar removal is best-effort; never interrupt the terminal UI.
1991
- }
2000
+ return;
1992
2001
  }
1993
2002
  }
1994
2003
  preservedSessionPaths() {