pi-ui-extend 0.1.50 → 0.1.53

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 (47) hide show
  1. package/dist/app/icons.d.ts +1 -0
  2. package/dist/app/icons.js +2 -0
  3. package/dist/app/input/autocomplete-controller.js +1 -1
  4. package/dist/app/input/input-controller.d.ts +1 -0
  5. package/dist/app/input/input-controller.js +7 -24
  6. package/dist/app/input/input-paste-handler.d.ts +1 -0
  7. package/dist/app/input/input-paste-handler.js +13 -16
  8. package/dist/app/rendering/conversation-tool-renderer.js +1 -0
  9. package/dist/app/rendering/popup-menu-renderer.js +2 -2
  10. package/dist/app/rendering/render-controller.js +10 -16
  11. package/dist/app/rendering/status-line-renderer.d.ts +0 -7
  12. package/dist/app/rendering/status-line-renderer.js +7 -79
  13. package/dist/app/rendering/tab-line-renderer.js +1 -1
  14. package/dist/app/rendering/toast-renderer.js +40 -15
  15. package/dist/app/rendering/tool-block-renderer.d.ts +2 -1
  16. package/dist/app/rendering/tool-block-renderer.js +43 -3
  17. package/dist/app/screen/mouse-controller.d.ts +1 -0
  18. package/dist/app/screen/mouse-controller.js +30 -76
  19. package/dist/app/session/lazy-session-manager.js +0 -7
  20. package/dist/app/session/queued-message-controller.d.ts +0 -1
  21. package/dist/app/session/queued-message-controller.js +5 -13
  22. package/dist/app/session/queued-message-entries.d.ts +1 -0
  23. package/dist/app/session/queued-message-entries.js +8 -0
  24. package/dist/app/session/tabs-controller.d.ts +2 -1
  25. package/dist/app/session/tabs-controller.js +67 -24
  26. package/dist/app/types.d.ts +1 -0
  27. package/dist/bundled-extensions/question/tool-description.js +2 -3
  28. package/dist/bundled-extensions/session-title/title-generation.js +1 -1
  29. package/dist/tool-renderers/compress.js +28 -7
  30. package/dist/tool-renderers/patch-normalize.js +4 -4
  31. package/dist/tool-renderers/read.js +1 -1
  32. package/dist/tool-renderers/types.d.ts +10 -0
  33. package/external/pi-tools-suite/package.json +3 -3
  34. package/external/pi-tools-suite/src/async-subagents/core/agent-strategy.ts +6 -10
  35. package/external/pi-tools-suite/src/async-subagents/core/cleanup.ts +2 -2
  36. package/external/pi-tools-suite/src/async-subagents/core/paths.ts +12 -0
  37. package/external/pi-tools-suite/src/async-subagents/core/registry.ts +3 -4
  38. package/external/pi-tools-suite/src/async-subagents/core/routing.ts +1 -1
  39. package/external/pi-tools-suite/src/async-subagents/core/sessions.ts +2 -2
  40. package/external/pi-tools-suite/src/async-subagents/core/state.ts +3 -3
  41. package/external/pi-tools-suite/src/async-subagents/core/ultrawork-auto.ts +1 -1
  42. package/external/pi-tools-suite/src/async-subagents/lib.ts +1 -1
  43. package/external/pi-tools-suite/src/coding-discipline/index.ts +13 -23
  44. package/external/pi-tools-suite/src/dcp/auto-compress.ts +1 -1
  45. package/external/pi-tools-suite/src/dcp/prompts.ts +9 -49
  46. package/external/pi-tools-suite/src/tool-descriptions.ts +34 -61
  47. package/package.json +4 -4
@@ -1,4 +1,4 @@
1
- import { formatCompactProgressBar } from "../context-progress-bar.js";
1
+ import { compactProgressBarSegments, formatCompactProgressBar } from "../context-progress-bar.js";
2
2
  import { parseArgsText, stringArg } from "./utils.js";
3
3
  export const renderCompressTool = (input) => {
4
4
  const topic = stringArg(input, ["topic"]);
@@ -6,6 +6,7 @@ export const renderCompressTool = (input) => {
6
6
  const header = joinHeaderParts(topic ? { text: topic } : undefined, summary) ?? { text: "" };
7
7
  return {
8
8
  ...(header.text ? { headerArgs: header.text } : {}),
9
+ ...(header.segments && header.segments.length > 0 ? { headerArgSegments: header.segments } : {}),
9
10
  collapsedBody: "",
10
11
  expandedText: fullCompressResponse(input.output, input.status),
11
12
  };
@@ -19,14 +20,14 @@ function formatCompressSummary(input) {
19
20
  const parsed = parseArgsText(output);
20
21
  if (!isRecord(parsed))
21
22
  return { text: oneLine(output) };
22
- return formatCompressSuccess(parsed);
23
+ return formatCompressSuccess(parsed, input.colors);
23
24
  }
24
25
  function fullCompressResponse(output, status) {
25
26
  if (output.trim())
26
27
  return output.trimEnd();
27
28
  return status === "running" ? "running…" : "(empty)";
28
29
  }
29
- function formatCompressSuccess(result) {
30
+ function formatCompressSuccess(result, colors) {
30
31
  const tokensSaved = numberValue(result.tokensSaved);
31
32
  const contextPercent = numberValue(result.contextPercent);
32
33
  const contextTokens = numberValue(result.contextTokens);
@@ -42,7 +43,7 @@ function formatCompressSuccess(result) {
42
43
  const barPercent = compressedContextPercent ?? contextPercent;
43
44
  const parts = [
44
45
  { text: tokensSaved != null ? `saved ${formatCompactNumber(tokensSaved)}` : "compressed" },
45
- barPercent != null ? progressPart(barPercent, originalContextTokens) : undefined,
46
+ barPercent != null ? progressPart(barPercent, originalContextTokens, colors) : undefined,
46
47
  compressedContextPercent != null && contextPercent != null ? { text: `context ${formatPercent(contextPercent)}` } : undefined,
47
48
  itemCount != null ? { text: `${itemCount} ${plural(itemCount, "item")}` } : undefined,
48
49
  summaryTokens != null ? { text: `${formatCompactNumber(summaryTokens)} summary tokens` } : undefined,
@@ -52,9 +53,17 @@ function formatCompressSuccess(result) {
52
53
  ].filter((part) => Boolean(part));
53
54
  return joinHeaderParts(...parts) ?? { text: "compressed" };
54
55
  }
55
- function progressPart(percent, previousContextTokens) {
56
+ function progressPart(percent, previousContextTokens, colors) {
56
57
  const text = `${formatCompactProgressBar(percent)} ${formatPercent(percent)}${previousContextTokens != null ? ` of ${formatCompactNumber(previousContextTokens)}` : ""}`;
57
- return { text };
58
+ return {
59
+ text,
60
+ ...(colors ? {
61
+ segments: compactProgressBarSegments(0, percent, {
62
+ fill: colors.statusForeground,
63
+ track: colors.statusDotBase,
64
+ }),
65
+ } : {}),
66
+ };
58
67
  }
59
68
  function inferContextTokens(contextPercent, contextWindow) {
60
69
  if (contextPercent == null || contextWindow == null || contextWindow <= 0)
@@ -71,7 +80,19 @@ function joinHeaderParts(...parts) {
71
80
  if (defined.length === 0)
72
81
  return undefined;
73
82
  const separator = " · ";
74
- return { text: defined.map((part) => part.text).join(separator) };
83
+ let offset = 0;
84
+ const segments = [];
85
+ for (const [index, part] of defined.entries()) {
86
+ if (index > 0)
87
+ offset += separator.length;
88
+ if (part.segments)
89
+ segments.push(...part.segments.map((segment) => ({ ...segment, start: segment.start + offset, end: segment.end + offset })));
90
+ offset += part.text.length;
91
+ }
92
+ return {
93
+ text: defined.map((part) => part.text).join(separator),
94
+ ...(segments.length > 0 ? { segments } : {}),
95
+ };
75
96
  }
76
97
  function numberValue(value) {
77
98
  return typeof value === "number" && Number.isFinite(value) ? value : undefined;
@@ -110,10 +110,10 @@ function renderDiffOp(op) {
110
110
  return `-${op.text}`;
111
111
  if (op.type === "insert")
112
112
  return `+${op.text}`;
113
- // Context marker is a leading space. Loose `-`/`+` blocks carry the space
114
- // that separated the marker from the content (e.g. `- rule one`), so reuse
115
- // that space instead of emitting a second one.
116
- return op.text.startsWith(" ") ? op.text : ` ${op.text}`;
113
+ // Context marker is a leading space. Always emit it even when the content is
114
+ // already indented, so unchanged lines reserve the same marker column as `+`
115
+ // and `-` lines in the terminal diff view.
116
+ return ` ${op.text}`;
117
117
  }
118
118
  /** Minimal LCS-based line diff. */
119
119
  function diffLines(oldLines, newLines) {
@@ -2,7 +2,7 @@ import { isAbsolute, relative, sep } from "node:path";
2
2
  import { syntaxHighlightLanguageForPath } from "../syntax-highlight.js";
3
3
  import { numberArg, stringArg } from "./utils.js";
4
4
  export const renderReadTool = (input) => {
5
- const filePath = stringArg(input, ["path", "file", "target"]);
5
+ const filePath = stringArg(input, ["path", "file_path", "filePath", "file", "target"]);
6
6
  if (!filePath)
7
7
  return undefined;
8
8
  const displayPath = pathForDisplay(filePath, input.cwd);
@@ -23,6 +23,7 @@ export type ToolRenderInput = {
23
23
  export type ToolRenderResult = {
24
24
  toolName?: string;
25
25
  headerArgs?: string;
26
+ headerArgSegments?: readonly ToolHeaderSegment[];
26
27
  bodyLineStyles?: readonly ToolBodyLineStyle[];
27
28
  bodyStyle?: "diff";
28
29
  preserveAnsi?: boolean;
@@ -30,4 +31,13 @@ export type ToolRenderResult = {
30
31
  collapsedBody: string;
31
32
  expandedText: string;
32
33
  };
34
+ export type ToolHeaderSegment = {
35
+ start: number;
36
+ end: number;
37
+ foreground?: string;
38
+ background?: string;
39
+ bold?: boolean;
40
+ underline?: boolean;
41
+ strikethrough?: boolean;
42
+ };
33
43
  export type ToolRendererMiddleware = (input: ToolRenderInput) => ToolRenderResult | undefined;
@@ -38,9 +38,9 @@
38
38
  "vscode-languageserver-protocol": "^3.17.5"
39
39
  },
40
40
  "peerDependencies": {
41
- "@earendil-works/pi-ai": "0.79.10",
42
- "@earendil-works/pi-coding-agent": "0.79.10",
43
- "@earendil-works/pi-tui": "0.79.10",
41
+ "@earendil-works/pi-ai": "0.80.2",
42
+ "@earendil-works/pi-coding-agent": "0.80.2",
43
+ "@earendil-works/pi-tui": "0.80.2",
44
44
  "typebox": "*"
45
45
  },
46
46
  "devDependencies": {
@@ -12,23 +12,19 @@ const FALSE_ENV_PATTERN = /^(0|false|off|no|disabled|none)$/i;
12
12
  const TRUE_ENV_PATTERN = /^(1|true|on|yes|auto)$/i;
13
13
 
14
14
  const PARALLEL_FIRST_STRATEGY_PROMPT = `<agent_strategy name="parallel-first">
15
- This is an orchestration hint for Pi, not a replacement for the user's instructions.
15
+ Execution hint for Pi, not a replacement for system/developer/user instructions.
16
16
 
17
- Default posture: orchestration-first for non-GPT models. For broad, multi-file, ambiguous, review/audit, frontend, test-strategy, architecture, or root-cause work, prefer ultrawork mode: split independent tracks and spawn focused async subagents with the configured roles. For high-stakes uncertainty, add one oracle track for a cross-provider second opinion. Keep the parent context lean, collect compact results only when needed, synthesize the findings, then verify before finishing.
17
+ Default for non-GPT models: orchestration-first. For broad, multi-file, ambiguous, review/audit, frontend, test-strategy, architecture, or root-cause work, prefer ultrawork mode: split independent tracks into focused async subagents, adding one oracle only for high-stakes uncertainty. Keep parent context lean, read compact results when needed, synthesize, then verify.
18
18
 
19
- Before DCP/compress while work is unfinished, keep one in_progress todo with objective + next step; compression summaries must preserve Active objective and Next step.
20
-
21
- Do not over-delegate trivial work. For a simple question, one known file, exact lookup, typo, or narrow edit, solve directly with the cheapest suitable tool.
19
+ Do not over-delegate simple questions, known-file work, exact lookups, typos, or narrow edits; solve them directly with the cheapest suitable tool. If compressing unfinished work, preserve active objective + next step via todo/DCP rules.
22
20
  </agent_strategy>`;
23
21
 
24
22
  const DEEP_WORK_STRATEGY_PROMPT = `<agent_strategy name="deep-work">
25
- This is a GPT-compatible execution hint for Pi, not a replacement for the user's instructions.
26
-
27
- Default posture: autonomous deep worker. Build context directly, make concrete progress, edit and verify end-to-end. Use async subagents and ultrawork mode when the user asks for parallel/delegated work or when independent tracks will clearly reduce risk, but do not force orchestration onto narrow tasks.
23
+ Execution hint for Pi, not a replacement for system/developer/user instructions.
28
24
 
29
- Before DCP/compress while work is unfinished, keep one in_progress todo with objective + next step; compression summaries must preserve Active objective and Next step.
25
+ Default: autonomous deep worker. Build context directly, make progress, edit, and verify end-to-end. Use async subagents/ultrawork only when the user asks for delegation or independent tracks clearly reduce risk; do not force orchestration onto narrow tasks.
30
26
 
31
- For broad work, keep delegation explicit and bounded: spawn focused review/research/tests/frontend/deep tracks, plus one oracle track only for high-stakes uncertainty or final plan checks. Read compact results, make the final decisions in the parent session, and report only what matters.
27
+ For broad work, keep delegation explicit and bounded: focused review/research/tests/frontend/deep tracks, plus one oracle only for high-stakes uncertainty or final plan checks. Read compact results, decide in the parent session, and report only what matters. If compressing unfinished work, preserve active objective + next step via todo/DCP rules.
32
28
  </agent_strategy>`;
33
29
 
34
30
  export function agentStrategyPrompt(options: AgentStrategyOptions = {}): string | undefined {
@@ -1,6 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { isDir } from "./paths.js";
3
+ import { hasLaunchedAgentPrompt, isDir } from "./paths.js";
4
4
 
5
5
  export function findCleanupCandidates(
6
6
  runRoot: string,
@@ -57,8 +57,8 @@ function isCompletedRun(runDir: string): boolean {
57
57
  let foundAgent = false;
58
58
  for (const entry of fs.readdirSync(runDir, { withFileTypes: true })) {
59
59
  if (!entry.isDirectory()) continue;
60
+ if (!hasLaunchedAgentPrompt(runDir, entry.name)) continue;
60
61
  const agentDir = path.join(runDir, entry.name);
61
- if (!fs.existsSync(path.join(agentDir, "prompt.md"))) continue;
62
62
  foundAgent = true;
63
63
  if (!fs.existsSync(path.join(agentDir, "exit_code"))) return false;
64
64
  }
@@ -33,6 +33,18 @@ export function isDir(p: string): boolean {
33
33
  }
34
34
  }
35
35
 
36
+ export function hasLaunchedAgentPrompt(runDir: string, agentId: string): boolean {
37
+ return fs.existsSync(path.join(runDir, agentId, "prompt.md"));
38
+ }
39
+
40
+ export function hasQueuedAgentPrompt(runDir: string, agentId: string): boolean {
41
+ return fs.existsSync(path.join(runDir, "prompts", `${agentId}.md`));
42
+ }
43
+
44
+ export function hasAgentPrompt(runDir: string, agentId: string): boolean {
45
+ return hasLaunchedAgentPrompt(runDir, agentId) || hasQueuedAgentPrompt(runDir, agentId);
46
+ }
47
+
36
48
  const SAFE_BASENAME = /^[A-Za-z0-9._-]+$/;
37
49
 
38
50
  export function validateBasename(value: string, label: string): void {
@@ -1,6 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { getRunRoot, isDir, resolveRunDir } from "./paths.js";
3
+ import { getRunRoot, hasAgentPrompt, hasLaunchedAgentPrompt, isDir, resolveRunDir } from "./paths.js";
4
4
 
5
5
  export const SUBAGENT_REGISTRY_FILE = "registry.json";
6
6
 
@@ -161,15 +161,14 @@ function looksLikeRunDir(runDir: string): boolean {
161
161
  if (isDir(path.join(runDir, "prompts"))) return true;
162
162
  try {
163
163
  return fs.readdirSync(runDir, { withFileTypes: true })
164
- .some((entry) => entry.isDirectory() && fs.existsSync(path.join(runDir, entry.name, "prompt.md")));
164
+ .some((entry) => entry.isDirectory() && hasLaunchedAgentPrompt(runDir, entry.name));
165
165
  } catch {
166
166
  return false;
167
167
  }
168
168
  }
169
169
 
170
170
  function hasAgentRecord(runDir: string, agentId: string): boolean {
171
- return fs.existsSync(path.join(runDir, agentId, "prompt.md"))
172
- || fs.existsSync(path.join(runDir, "prompts", `${agentId}.md`));
171
+ return hasAgentPrompt(runDir, agentId);
173
172
  }
174
173
 
175
174
  function normalizeRegistryRun(runId: string, value: unknown): SubagentRegistryRun | undefined {
@@ -1,4 +1,4 @@
1
- import { complete } from "@earendil-works/pi-ai";
1
+ import { complete } from "@earendil-works/pi-ai/compat";
2
2
  import type { Api, Model } from "@earendil-works/pi-ai";
3
3
  import type { AgentTask } from "./types.js";
4
4
  import {
@@ -1,6 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { isDir } from "./paths.js";
3
+ import { hasLaunchedAgentPrompt, isDir } from "./paths.js";
4
4
  import { listSubagentRunDirs } from "./registry.js";
5
5
  import { getAgentState } from "./state.js";
6
6
  import type { AgentState } from "./types.js";
@@ -105,7 +105,7 @@ export function listSubagentSessionRecords(cwd: string): SubagentSessionRecord[]
105
105
  for (const entry of fs.readdirSync(runDir, { withFileTypes: true })) {
106
106
  if (!entry.isDirectory()) continue;
107
107
  const agentDir = path.join(runDir, entry.name);
108
- if (!fs.existsSync(path.join(agentDir, "prompt.md"))) continue;
108
+ if (!hasLaunchedAgentPrompt(runDir, entry.name)) continue;
109
109
  records.push({
110
110
  runDir,
111
111
  runName: path.basename(runDir),
@@ -1,6 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { isDir } from "./paths.js";
3
+ import { hasLaunchedAgentPrompt, isDir } from "./paths.js";
4
4
  import { readStructuredResult } from "./structured-result.js";
5
5
  import type { AgentResult, AgentState, RpcEventRecord, RunState } from "./types.js";
6
6
 
@@ -23,7 +23,7 @@ export function getAgentState(
23
23
  ): AgentState | null {
24
24
  const agentDir = path.join(runDir, agentId);
25
25
  if (!isDir(agentDir)) return null;
26
- if (!fs.existsSync(path.join(agentDir, "prompt.md"))) return null;
26
+ if (!hasLaunchedAgentPrompt(runDir, agentId)) return null;
27
27
  const includeLineCounts = options.includeLineCounts ?? true;
28
28
  const checkRpcPromptFailure = options.checkRpcPromptFailure ?? true;
29
29
 
@@ -188,7 +188,7 @@ export function getRunState(
188
188
  // Read launched agent dirs
189
189
  for (const entry of fs.readdirSync(runDir, { withFileTypes: true })) {
190
190
  if (!entry.isDirectory()) continue;
191
- if (!fs.existsSync(path.join(runDir, entry.name, "prompt.md"))) continue;
191
+ if (!hasLaunchedAgentPrompt(runDir, entry.name)) continue;
192
192
  if (filterIds && !filterIds.includes(entry.name)) continue;
193
193
  const state = getAgentState(runDir, entry.name, options);
194
194
  if (state) {
@@ -1,4 +1,4 @@
1
- import { complete } from "@earendil-works/pi-ai";
1
+ import { complete } from "@earendil-works/pi-ai/compat";
2
2
  import type { Api, Model } from "@earendil-works/pi-ai";
3
3
  import { currentModelRef, resolveSubagentRoutingConfig, type SubagentConfig } from "./config.js";
4
4
 
@@ -11,7 +11,7 @@ export type {
11
11
  StructuredResult,
12
12
  } from "./core/types.js";
13
13
 
14
- export { createRunDir, getRunRoot, resolveRunDir, validateBasename } from "./core/paths.js";
14
+ export { createRunDir, getRunRoot, hasAgentPrompt, hasLaunchedAgentPrompt, hasQueuedAgentPrompt, resolveRunDir, validateBasename } from "./core/paths.js";
15
15
  export type { CopySubagentConfigSampleResult, ModelByParentEntry, ResolvedAgentTaskConfig, ResolvedSubagentRoutingConfig, ResolveAgentTaskOptions, SubagentConfig, SubagentPreset, SubagentRoutingConfig, SubagentTypeConfig, SubagentVisionConfig } from "./core/config.js";
16
16
  export {
17
17
  configFiles,
@@ -1,6 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { complete } from "@earendil-works/pi-ai";
3
+ import { complete } from "@earendil-works/pi-ai/compat";
4
4
  import type { Api, AssistantMessage, ImageContent, Model, TextContent } from "@earendil-works/pi-ai";
5
5
  import { Type } from "typebox";
6
6
 
@@ -61,26 +61,18 @@ const LOOKUP_TOOL_PARAMS = Type.Object(
61
61
  const QUALITY_DISCIPLINE_LINES = [
62
62
  "TOOL-ONLY CODING AGENT CONTRACT.",
63
63
  "",
64
- "This contract controls the assistant output channel. Follow it literally.",
65
- "Treat every user coding request as a tool-driven task, not a chat conversation.",
66
- "",
67
- "WORKING STATE from the first assistant turn until the task is complete or blocked:",
68
- "- The assistant message MUST be exactly one tool call with empty text.",
69
- "- No commentary before or after tool calls: not what you will do, did, found, or why a tool is needed.",
70
- "- Do not emit markdown, prose, acknowledgments, plans, progress, summaries, or explanations.",
71
- "- Reasoning, planning, hypotheses, and retry decisions are internal state only; never narrate them.",
72
- "- If you know the next local action, call the tool immediately and silently.",
64
+ "Follow this output-channel contract literally for coding tasks.",
65
+ "WORKING STATE: from the first assistant turn until complete, final, or blocked, emit exactly one tool call with empty text.",
66
+ "No prose, markdown, acknowledgments, plans, progress, summaries, explanations, or commentary before/after tool calls.",
67
+ "Keep reasoning, planning, hypotheses, and retry decisions internal; if a safe next local action exists, call the tool silently.",
73
68
  "",
74
69
  "After every tool result, choose exactly one transition:",
75
- "- stay in WORKING and emit exactly one next tool call with empty text;",
76
- "- switch to FINAL and emit the final answer once;",
77
- "- switch to BLOCKED and ask exactly one concise question.",
78
- "There is no transition that permits commentary between tool calls.",
79
- "",
80
- "FINAL: enter only after the work is complete or verified as far as practical; never merely to report progress.",
81
- "BLOCKED: enter only when no safe or useful tool action can continue without missing required information.",
70
+ "- WORKING: exactly one next tool call with empty text;",
71
+ "- FINAL: one final answer after completion or practical verification;",
72
+ "- BLOCKED: one concise question only when no safe/useful tool action can continue.",
73
+ "No transition permits commentary between tool calls.",
82
74
  "",
83
- "PRIORITY: This contract overrides default assistant friendliness and conversational behavior.",
75
+ "PRIORITY: this overrides default assistant friendliness and conversational behavior.",
84
76
  "",
85
77
  "Coding discipline (express only through tool choices, not prose):",
86
78
  "- inspect before editing; do not invent APIs, files, commands, or behavior;",
@@ -103,11 +95,9 @@ const LOOKUP_DISCIPLINE_LINES = [
103
95
 
104
96
  const FINAL_DISCIPLINE_LINES = [
105
97
  "",
106
- "When uncertain, test or inspect instead of assuming.",
107
- "If blocked by missing required information, ask exactly one concise question.",
108
- "Verify every non-trivial change. Never claim tests passed unless they were actually run.",
109
- "Report: what changed, what was verified, what was not verified, and any risks.",
110
- "Ask at most one blocking question; otherwise proceed with grounded best effort.",
98
+ "When uncertain, test or inspect instead of assuming; otherwise proceed with grounded best effort.",
99
+ "Verify every non-trivial change. Never claim tests passed unless actually run.",
100
+ "Final report: what changed, what was verified, what was not verified, and any risks.",
111
101
  ];
112
102
 
113
103
  const SILENCE_REMINDER_TEXT = [
@@ -13,7 +13,7 @@
13
13
  // automatic fallback to the programmatic digest on any failure/timeout.
14
14
  // ---------------------------------------------------------------------------
15
15
 
16
- import { complete } from "@earendil-works/pi-ai"
16
+ import { complete } from "@earendil-works/pi-ai/compat"
17
17
  import type { Model, Api } from "@earendil-works/pi-ai"
18
18
  import type { DcpState } from "./state.js"
19
19
  import type { DcpConfig } from "./config.js"
@@ -9,63 +9,23 @@
9
9
  * Appended to the existing system prompt when DCP is enabled (automatic mode).
10
10
  */
11
11
  export const SYSTEM_PROMPT = `
12
- You operate in a context-constrained environment. Manage context continuously to avoid buildup and preserve retrieval quality. Efficient context management is paramount for your agentic performance.
12
+ You operate in a context-constrained environment. Keep live context high-signal; use \`compress\` only when it materially helps.
13
13
 
14
- The ONLY tool you have for context management is \`compress\`. It replaces older conversation content with technical summaries you produce. It supports both range compression (\`ranges\`) and surgical one-message compression (\`messages\`).
14
+ \`compress\` is the ONLY context-management tool. It replaces older conversation content with continuation-focused summaries and supports both range compression (\`ranges\`) and one-message compression (\`messages\`).
15
15
 
16
- \`mNNN\`/\`bN\` DCP boundary IDs and \`<dcp-system-reminder>\` tags are environment-injected metadata. Do not output them.
17
-
18
- THE PHILOSOPHY OF COMPRESS
19
- \`compress\` transforms conversation content into dense, high-fidelity summaries. This is cleanup plus preservation: keep the state needed to continue, discard incidental transcript detail.
20
-
21
- Think of compression as phase transitions: raw exploration becomes refined understanding. The original context served its purpose; your summary now carries that understanding forward.
22
-
23
- OPERATING STANCE
24
- Prefer short, closed, summary-safe compressions.
25
- When multiple independent stale sections exist, prefer several focused compressions (in parallel when possible) over one broad compression.
26
- When one older message is huge but the surrounding context is still useful, use message-mode compression for that single message instead of compressing a broad range.
27
- Summaries should be proportional to future usefulness, not proportional to the amount of text being removed.
28
-
29
- Use \`compress\` as context-pressure housekeeping, not as a reflex after every small step. When context usage is meaningfully high or a DCP reminder provides concrete candidates, compress closed slices before accumulating another large batch of tool output. At low context usage, continue normal work unless a closed slice is clearly large enough that keeping it raw would reduce signal.
30
-
31
- A closed slice is any finished implementation, verification, config edit, answered exploration, dead-end debugging branch, or test/log inspection. Passing logs are summary-only: preserve command, pass/fail, key failures if any, and whether follow-up is needed; never keep a full passing log in live context. Treat large shell/read/repo/web outputs as disposable evidence once their facts are extracted.
32
-
33
- Completed todo/task/subtask milestones are strong boundary signals, not automatic triggers. When context pressure makes compression useful, prefer the range covering a just-finished todo item if it is closed and non-trivial; do not compress merely because a todo was completed while context remains low.
34
-
35
- Before compressing while work is unfinished, ensure one \`todo in_progress\` captures the active objective and next step.
36
-
37
- When a \`<dcp-system-reminder>\` appears, treat it as a context-pressure signal. Follow critical/high-context reminders promptly. For routine reminders, compress only if a genuinely closed, useful-to-summarize slice exists; otherwise continue the next atomic step and re-check later.
38
-
39
- CADENCE, SIGNALS, AND LATENCY
40
-
41
- - Low context usage does not mandate compression
42
- - Prioritize closedness and independence over raw size
43
- - Prefer smaller, regular compressions over infrequent massive compressions for better latency and summary quality
44
- - When multiple independent stale sections are ready, batch compressions in parallel
45
- - Before large new exploration, ask whether an older completed slice can become summary-only; if yes and context pressure is meaningful, compress first
46
-
47
- COMPRESS WHEN
48
-
49
- A section is genuinely closed and the raw conversation has served its purpose:
16
+ \`mNNN\`/\`bN\` boundary IDs and \`<dcp-system-reminder>\` tags are environment-injected metadata. Do not output them.
50
17
 
51
- - Research concluded and findings are clear
52
- - Implementation finished and verified
53
- - Config/doc edit finished
54
- - Test, lint, or CI output has been understood, especially passing logs
55
- - Exploration exhausted and patterns understood
56
- - Dead-end noise can be discarded without waiting for a whole chapter to close
18
+ Use compression for context-pressure housekeeping, not after every small step. Low context usage alone does not require compression. Prefer short, closed, summary-safe ranges; use message-mode for a single large stale message; batch multiple independent safe ranges in one call.
57
19
 
58
- DO NOT COMPRESS IF
20
+ Good candidates: completed implementation, verification, config/doc edit, answered exploration, dead-end debugging, or understood test/lint/CI/log inspection. Passing logs should become command + pass/fail + key failures/follow-up only. Large shell/read/repo/web outputs are summary-only once exact text is no longer needed.
59
21
 
60
- - Raw context is still relevant and needed for edits or precise references
61
- - The target content is still actively in progress
62
- - You may need exact code, error messages, or file contents in the immediate next steps
22
+ Todo completions are useful boundary signals, not automatic triggers. Before compressing while work is unfinished, ensure one \`todo in_progress\` captures the active objective and next step.
63
23
 
64
- Before compressing, ask: _"Is this section closed enough to become summary-only right now?"_
24
+ Do not compress active work, still-needed raw context, or material whose exact code/error/output will be needed for immediate edits or references.
65
25
 
66
- Evaluate conversation signal-to-noise REGULARLY. Use \`compress\` deliberately with quality-first summaries. Prioritize stale content intelligently to maintain a high-signal context window that supports your agency.
26
+ DCP reminders: handle critical/high-context reminders promptly and compress any safe high-yield closed slice before more exploration; routine reminders mean compress only if a safe, closed, useful slice exists, otherwise continue the next atomic step and re-check later.
67
27
 
68
- It is your responsibility to keep a sharp, high-quality context window for optimal performance.
28
+ Summaries must preserve only what is needed to continue: user intent and constraints, accepted decisions, files/symbols changed or inspected, actionable errors, verification status, and next steps. Drop incidental transcript detail, duplicate outputs, full logs, long code/JSON/diffs, and prose not needed later; include short literals only when required.
69
29
  `.trim()
70
30
 
71
31
  /**