pi-ui-extend 1.0.39 → 1.0.41

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 (113) hide show
  1. package/README.md +1 -1
  2. package/dist/app/commands/command-registry.js +2 -2
  3. package/dist/app/commands/command-session-actions.d.ts +0 -1
  4. package/dist/app/commands/command-session-actions.js +22 -13
  5. package/dist/app/icons.d.ts +14 -0
  6. package/dist/app/icons.js +33 -0
  7. package/dist/app/rendering/conversation-tool-renderer.js +2 -2
  8. package/dist/app/rendering/dcp-stats.d.ts +6 -1
  9. package/dist/app/rendering/dcp-stats.js +214 -46
  10. package/dist/app/rendering/editor-panels.js +8 -5
  11. package/dist/app/session/lazy-session-manager.js +12 -1
  12. package/dist/app/session/tabs-controller.d.ts +2 -5
  13. package/dist/app/session/tabs-controller.js +12 -21
  14. package/dist/app/subagents/subagents-model.d.ts +14 -1
  15. package/dist/app/subagents/subagents-model.js +34 -15
  16. package/dist/app/types.d.ts +2 -0
  17. package/dist/bundled-extensions/session-title/config.js +1 -1
  18. package/dist/markdown-format.js +27 -9
  19. package/dist/schemas/pi-tools-suite-schema.d.ts +29 -16
  20. package/dist/schemas/pi-tools-suite-schema.js +46 -31
  21. package/external/pi-tools-suite/README.md +392 -52
  22. package/external/pi-tools-suite/docs/browser-qa-subagent.md +31 -21
  23. package/external/pi-tools-suite/docs/context-gateway-p00-adr.md +216 -0
  24. package/external/pi-tools-suite/docs/context-gateway-p01n-gate-review.md +122 -0
  25. package/external/pi-tools-suite/docs/context-gateway-p01n-measurement.md +133 -0
  26. package/external/pi-tools-suite/docs/context-gateway-p01r-ra-evidence.md +111 -0
  27. package/external/pi-tools-suite/docs/context-gateway-p01r-rb-evidence.md +100 -0
  28. package/external/pi-tools-suite/docs/context-gateway-p01r-rc-evidence.md +69 -0
  29. package/external/pi-tools-suite/docs/context-gateway-p01r-rd-evidence.md +100 -0
  30. package/external/pi-tools-suite/docs/context-gateway-p01r-re-evidence.md +74 -0
  31. package/external/pi-tools-suite/docs/context-gateway-p01r-rf-evidence.md +153 -0
  32. package/external/pi-tools-suite/docs/context-gateway-p01r-rg-evidence.md +235 -0
  33. package/external/pi-tools-suite/docs/evals.md +684 -0
  34. package/external/pi-tools-suite/docs/subagent-model-pools.md +109 -0
  35. package/external/pi-tools-suite/package.json +10 -3
  36. package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/scripts/browser-qa-runner.mjs +82 -1
  37. package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/SKILL.md → agents/browser-qa.md} +261 -12
  38. package/external/pi-tools-suite/src/async-subagents/agents/implement.md +20 -0
  39. package/external/pi-tools-suite/src/async-subagents/agents/oracle.md +16 -0
  40. package/external/pi-tools-suite/src/async-subagents/agents/research.md +18 -0
  41. package/external/pi-tools-suite/src/async-subagents/agents/verify.md +18 -0
  42. package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +27 -243
  43. package/external/pi-tools-suite/src/async-subagents/commands.ts +6 -2
  44. package/external/pi-tools-suite/src/async-subagents/core/agent-catalog.ts +41 -0
  45. package/external/pi-tools-suite/src/async-subagents/core/agent-strategy.ts +13 -93
  46. package/external/pi-tools-suite/src/async-subagents/core/agents-dir.ts +494 -0
  47. package/external/pi-tools-suite/src/async-subagents/core/browser-qa.ts +9 -0
  48. package/external/pi-tools-suite/src/async-subagents/core/config.ts +200 -143
  49. package/external/pi-tools-suite/src/async-subagents/core/model-fallback.ts +1 -1
  50. package/external/pi-tools-suite/src/async-subagents/core/model-selection.ts +54 -0
  51. package/external/pi-tools-suite/src/async-subagents/core/prompt.ts +7 -6
  52. package/external/pi-tools-suite/src/async-subagents/core/routing.ts +52 -45
  53. package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +12 -4
  54. package/external/pi-tools-suite/src/async-subagents/index.ts +11 -1
  55. package/external/pi-tools-suite/src/async-subagents/lib.ts +6 -2
  56. package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +46 -18
  57. package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +3 -2
  58. package/external/pi-tools-suite/src/async-subagents/types.ts +2 -0
  59. package/external/pi-tools-suite/src/coding-discipline/index.ts +41 -142
  60. package/external/pi-tools-suite/src/config.ts +1 -22
  61. package/external/pi-tools-suite/src/context-gateway/accounting.ts +151 -0
  62. package/external/pi-tools-suite/src/context-gateway/config.ts +111 -0
  63. package/external/pi-tools-suite/src/context-gateway/index.ts +160 -0
  64. package/external/pi-tools-suite/src/context-gateway/metadata-normalization.ts +88 -0
  65. package/external/pi-tools-suite/src/context-gateway/storeless-capabilities.ts +89 -0
  66. package/external/pi-tools-suite/src/context-gateway/telemetry.ts +429 -0
  67. package/external/pi-tools-suite/src/context-gateway/test-output-parser.ts +326 -0
  68. package/external/pi-tools-suite/src/context-gateway/types.ts +152 -0
  69. package/external/pi-tools-suite/src/dcp/auto-compress-budget.ts +106 -0
  70. package/external/pi-tools-suite/src/dcp/auto-compress.ts +810 -106
  71. package/external/pi-tools-suite/src/dcp/commands.ts +64 -139
  72. package/external/pi-tools-suite/src/dcp/compress-tool.ts +369 -35
  73. package/external/pi-tools-suite/src/dcp/compression-blocks.ts +510 -64
  74. package/external/pi-tools-suite/src/dcp/compression-preview.ts +113 -0
  75. package/external/pi-tools-suite/src/dcp/compression-progress.ts +70 -0
  76. package/external/pi-tools-suite/src/dcp/config.ts +36 -61
  77. package/external/pi-tools-suite/src/dcp/conversation-index.ts +421 -0
  78. package/external/pi-tools-suite/src/dcp/debug-log.ts +7 -5
  79. package/external/pi-tools-suite/src/dcp/index.ts +617 -203
  80. package/external/pi-tools-suite/src/dcp/journal.ts +566 -0
  81. package/external/pi-tools-suite/src/dcp/progress-controller.ts +244 -0
  82. package/external/pi-tools-suite/src/dcp/prompts.ts +10 -7
  83. package/external/pi-tools-suite/src/dcp/provider-tool-results.ts +189 -0
  84. package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +298 -78
  85. package/external/pi-tools-suite/src/dcp/pruner-compression-blocks.ts +173 -281
  86. package/external/pi-tools-suite/src/dcp/pruner-emergency.ts +2 -4
  87. package/external/pi-tools-suite/src/dcp/pruner-message-ids.ts +17 -5
  88. package/external/pi-tools-suite/src/dcp/pruner-metadata.ts +11 -1
  89. package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +30 -82
  90. package/external/pi-tools-suite/src/dcp/pruner-tools.ts +22 -133
  91. package/external/pi-tools-suite/src/dcp/pruner.ts +18 -33
  92. package/external/pi-tools-suite/src/dcp/recovery.ts +129 -0
  93. package/external/pi-tools-suite/src/dcp/shadow-plan.ts +127 -0
  94. package/external/pi-tools-suite/src/dcp/state-transaction.ts +102 -0
  95. package/external/pi-tools-suite/src/dcp/state.ts +158 -580
  96. package/external/pi-tools-suite/src/dcp/ui.ts +1 -0
  97. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +55 -220
  98. package/external/pi-tools-suite/src/index.ts +9 -0
  99. package/external/pi-tools-suite/src/model-tools/index.ts +76 -42
  100. package/external/pi-tools-suite/src/repo-discovery/index.ts +84 -18
  101. package/external/pi-tools-suite/src/repo-discovery/native-compact.ts +458 -0
  102. package/external/pi-tools-suite/src/session-recovery/index.ts +189 -43
  103. package/external/pi-tools-suite/src/tool-descriptions.ts +43 -38
  104. package/external/pi-tools-suite/src/truncation-metadata-normalizer/index.ts +17 -0
  105. package/package.json +6 -6
  106. package/schemas/pi-tools-suite.json +159 -78
  107. package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/references/auth-scaffold-spec.md +0 -78
  108. package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/references/qa-design.md +0 -223
  109. package/external/pi-tools-suite/src/dcp/state-persistence.ts +0 -195
  110. /package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/references → agents/browser-qa/examples}/qa-auth.example.jsonc +0 -0
  111. /package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/references → agents/browser-qa/examples}/qa-flow.example.jsonc +0 -0
  112. /package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/vendor/fflate.LICENSE +0 -0
  113. /package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/vendor/fflate.mjs +0 -0
package/README.md CHANGED
@@ -140,7 +140,7 @@ Optional:
140
140
  - an audio recorder for dictation: SoX (`rec`/`sox`), `ffmpeg`, or `arecord` on Linux
141
141
  - `tmux` and `rsvg-convert` only if you want to regenerate README screenshots
142
142
  - Ollama with web search enabled for the bundled `web_search` and `web_fetch` tools
143
- - language servers for the LSP module
143
+ - language servers for the LSP module (see [LSP setup](external/pi-tools-suite/README.md#lsp-setup) for install commands and ready-to-paste configs for popular servers, including Svelte, Vue, Go, clangd, Lua, and bash)
144
144
 
145
145
  ### One-shot use
146
146
 
@@ -249,9 +249,9 @@ export function createSlashCommands(actions, host) {
249
249
  },
250
250
  {
251
251
  name: "delete",
252
- description: "Delete the current (or specified) session file plus its sidecar DCP state",
252
+ description: "Delete the current (or specified) session file",
253
253
  kind: "builtin",
254
- keywords: ["remove", "destroy", "purge", "session", "sidecar", "dcp"],
254
+ keywords: ["remove", "destroy", "purge", "session", "dcp"],
255
255
  allowArguments: true,
256
256
  run: (argumentsText) => actions.runDeleteCommand(argumentsText),
257
257
  },
@@ -15,7 +15,6 @@ export declare class SessionCommandActions {
15
15
  runHotkeysCommand(): Promise<void>;
16
16
  runReloadCommand(): Promise<void>;
17
17
  runDeleteCommand(argumentsText: string): Promise<void>;
18
- private removeDcpSidecarState;
19
18
  runNewSessionCommand(): Promise<void>;
20
19
  runCompactCommand(customInstructions?: string): Promise<void>;
21
20
  private piPackageRoot;
@@ -342,7 +342,7 @@ export class SessionCommandActions {
342
342
  {
343
343
  value: true,
344
344
  label: `Yes, delete ${isCurrent ? "the current session" : basename(targetSessionFile)}`,
345
- description: "This permanently removes the session file and any sidecar data. This cannot be undone.",
345
+ description: "This permanently removes the session file. This cannot be undone.",
346
346
  variant: "error",
347
347
  },
348
348
  { value: false, label: "Cancel" },
@@ -356,11 +356,28 @@ export class SessionCommandActions {
356
356
  }
357
357
  this.host.setStatus("deleting session");
358
358
  this.host.render();
359
- const sidecarRemoved = await this.removeDcpSidecarState(dirname(targetSessionFile), targetSessionId).catch(() => false);
360
- await rm(targetSessionFile, { force: true }).catch(() => undefined);
361
359
  const deleteCurrent = isCurrent || targetSessionFile === currentSessionFile;
362
360
  if (deleteCurrent) {
361
+ // Do not race a final extension persistence operation with destructive
362
+ // session cleanup.
363
363
  await this.host.awaitCurrentSessionExtensions(runtime);
364
+ if (!isCommandRuntimeActive(this.host, runtime))
365
+ return;
366
+ }
367
+ try {
368
+ await rm(targetSessionFile, { force: true });
369
+ }
370
+ catch (error) {
371
+ this.host.addEntry({
372
+ id: createId("system"),
373
+ kind: "system",
374
+ text: `Could not delete session file ${targetSessionFile}: ${error instanceof Error ? error.message : String(error)}`,
375
+ });
376
+ this.host.toast.error("Session deletion failed");
377
+ this.host.setSessionStatus(runtime.session);
378
+ return;
379
+ }
380
+ if (deleteCurrent) {
364
381
  const result = await runtime.newSession();
365
382
  if (!isCommandRuntimeActive(this.host, runtime))
366
383
  return;
@@ -374,7 +391,7 @@ export class SessionCommandActions {
374
391
  this.host.addEntry({
375
392
  id: createId("system"),
376
393
  kind: "system",
377
- text: `Deleted session ${targetSessionId}. ${sidecarRemoved ? "Sidecar DCP state removed. " : ""}Started a new session. cwd=${runtime.cwd}`,
394
+ text: `Deleted session ${targetSessionId}. Started a new session. cwd=${runtime.cwd}`,
378
395
  });
379
396
  if (runtime.modelFallbackMessage)
380
397
  this.host.addEntry({ id: createId("system"), kind: "system", text: runtime.modelFallbackMessage });
@@ -386,20 +403,12 @@ export class SessionCommandActions {
386
403
  this.host.addEntry({
387
404
  id: createId("system"),
388
405
  kind: "system",
389
- text: `Deleted session file ${targetSessionFile}${sidecarRemoved ? " and its sidecar DCP state" : ""}.`,
406
+ text: `Deleted session file ${targetSessionFile}.`,
390
407
  });
391
408
  this.host.setSessionStatus(runtime.session);
392
409
  }
393
410
  this.host.toast.success("Session deleted");
394
411
  }
395
- async removeDcpSidecarState(sessionDir, sessionId) {
396
- if (!sessionId)
397
- return false;
398
- const safeName = `${sessionId.replace(/[^a-zA-Z0-9._-]/g, "_")}.json`;
399
- const statePath = join(sessionDir, "dcp-state", safeName);
400
- await rm(statePath, { force: true });
401
- return true;
402
- }
403
412
  async runNewSessionCommand() {
404
413
  const runtime = getIdleRuntime(this.host, "new");
405
414
  if (!runtime)
@@ -33,6 +33,20 @@ declare const NERD_FONT_ICONS: {
33
33
  readonly toolPreviewTruncated: "⊞";
34
34
  readonly up: "↑";
35
35
  readonly down: "↓";
36
+ readonly agent: "󰚩";
37
+ readonly search: "󰍉";
38
+ readonly code: "󰅴";
39
+ readonly flask: "󰂓";
40
+ readonly globe: "󰇧";
41
+ readonly sparkles: "󰫢";
42
+ readonly brain: "󰧑";
43
+ readonly wrench: "󰖷";
44
+ readonly terminal: "󰆍";
45
+ readonly bug: "󰃤";
46
+ readonly book: "󰂺";
47
+ readonly eye: "󰈈";
48
+ readonly zap: "󰉁";
49
+ readonly rocket: "󰑣";
36
50
  };
37
51
  export type AppIconName = keyof typeof NERD_FONT_ICONS;
38
52
  export type AppIconMap = Record<AppIconName, string>;
package/dist/app/icons.js CHANGED
@@ -44,6 +44,22 @@ const NERD_FONT_ICONS = {
44
44
  toolPreviewTruncated: "⊞",
45
45
  up: "↑",
46
46
  down: "↓",
47
+ // Sub-agent icons (agent .md frontmatter `icon:` names). Values are shared
48
+ // with the Pix Desktop lucide mapping in desktop/src/lib/agent-icons.ts.
49
+ agent: "\u{f06a9}", // nf-md-robot
50
+ search: "\u{f0349}", // nf-md-magnify
51
+ code: "\u{f0174}", // nf-md-code_tags
52
+ flask: "\u{f0093}", // nf-md-flask
53
+ globe: "\u{f01e7}", // nf-md-earth
54
+ sparkles: "\u{f0ae2}", // nf-md-star_four_points
55
+ brain: "\u{f09d1}", // nf-md-brain
56
+ wrench: "\u{f05b7}", // nf-md-wrench
57
+ terminal: "\u{f018d}", // nf-md-console
58
+ bug: "\u{f00e4}", // nf-md-bug
59
+ book: "\u{f00ba}", // nf-md-book
60
+ eye: "\u{f0208}", // nf-md-eye
61
+ zap: "\u{f0241}", // nf-md-flash
62
+ rocket: "\u{f0463}", // nf-md-rocket
47
63
  };
48
64
  const FALLBACK_ICONS = {
49
65
  alert: "⚠",
@@ -78,6 +94,23 @@ const FALLBACK_ICONS = {
78
94
  toolPreviewTruncated: "+",
79
95
  up: "↑",
80
96
  down: "↓",
97
+ // Sub-agent icon fallbacks (no icon-font codepoints; readable when the
98
+ // terminal font lacks Nerd Font glyphs). Emoji follow the existing fallback
99
+ // precedent (lightbulb, timerSand).
100
+ agent: "◇",
101
+ search: "⌕",
102
+ code: "{}",
103
+ flask: "∆",
104
+ globe: "⊕",
105
+ sparkles: "✦",
106
+ brain: "∞",
107
+ wrench: "⚙",
108
+ terminal: ">_",
109
+ bug: "🐛",
110
+ book: "▤",
111
+ eye: "◉",
112
+ zap: "↯",
113
+ rocket: "🚀",
81
114
  };
82
115
  export const APP_ICON_THEMES = {
83
116
  nerdFont: NERD_FONT_ICONS,
@@ -4,7 +4,7 @@ import { renderToolDisplay } from "../../tool-renderers/index.js";
4
4
  import { SUBAGENT_STATUSES, THINKING_TOOL_NAME, TODO_TOOL_NAME } from "../constants.js";
5
5
  import { attachImageClickTargets } from "../screen/image-click-targets.js";
6
6
  import { formatStructuredText } from "./message-content.js";
7
- import { formatSubagentTimestamp, isSubagentRunRenderDetails, isSubagentsToolName, subagentRunName, subagentStatusIcon, taskPreviewMap, } from "../subagents/subagents-model.js";
7
+ import { formatSubagentTimestamp, isSubagentRunRenderDetails, isSubagentsToolName, subagentIcon, subagentRunName, taskPreviewMap, } from "../subagents/subagents-model.js";
8
8
  import { formatTodoTaskLine, isTodoDetails, visibleTodoTasks } from "../todo/todo-model.js";
9
9
  import { renderToolBlock } from "./tool-block-renderer.js";
10
10
  export function renderConversationToolEntry(entry, width, options) {
@@ -202,7 +202,7 @@ function subagentsDetailsText(details) {
202
202
  return lines.join("\n");
203
203
  }
204
204
  function formatSubagentToolLine(agent, preview) {
205
- const parts = [subagentStatusIcon(agent.status), agent.status, agent.id];
205
+ const parts = [subagentIcon(preview), agent.status, agent.id];
206
206
  if (preview?.model)
207
207
  parts.push(`model:${preview.model}`);
208
208
  if (preview?.task)
@@ -1,2 +1,7 @@
1
1
  import type { AgentSession } from "@earendil-works/pi-coding-agent";
2
- export declare function formatDcpStatsToast(session: AgentSession): string;
2
+ type FormatDcpStatsOptions = {
3
+ manualModeBaseline?: boolean;
4
+ configPath?: string;
5
+ };
6
+ export declare function formatDcpStatsToast(session: AgentSession, options?: FormatDcpStatsOptions): string;
7
+ export {};
@@ -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, 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,8 +106,8 @@ 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
113
  const suffix = ` ${formatElapsedSince(agent.startedAt, now)}`;
@@ -126,9 +126,12 @@ export function renderSubagentsPanel(state, expanded, width, colors) {
126
126
  lines.push({ text: padOrTrimPlain(`+${hidden} more`, width), variant: "muted", target });
127
127
  return lines;
128
128
  }
129
+ function singleLinePreview(value) {
130
+ return value ? sanitizeText(value).replace(/\s+/gu, " ").trim() : "";
131
+ }
129
132
  function subagentPanelLineSegments(input, colors) {
130
- const iconStart = input.text.indexOf(input.icon);
131
133
  const runLabelStart = input.runLabel ? input.text.indexOf(input.runLabel) : -1;
134
+ const iconStart = input.runLabel.length;
132
135
  const nameStart = input.text.indexOf(input.agentId, iconStart + input.icon.length);
133
136
  const modelStart = input.text.indexOf(input.model, nameStart + input.agentId.length);
134
137
  const taskStart = input.prefix.length;
@@ -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) {
@@ -184,13 +184,10 @@ export declare class AppTabsController {
184
184
  private prewarmTabs;
185
185
  private cleanupOldProjectSessions;
186
186
  /**
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:
187
+ * Unlink a project session file. Everything here is best-effort:
191
188
  * session retention must never interrupt the terminal UI.
192
189
  */
193
- private unlinkSessionAndDcpSidecar;
190
+ private unlinkSessionFile;
194
191
  private preservedSessionPaths;
195
192
  private maxProjectSessions;
196
193
  }