@tt-a1i/openpi 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/README.md +184 -59
  2. package/SETUP.md +23 -7
  3. package/assets/openpi-launch-card-v1.webp +0 -0
  4. package/bin/openpi.js +145 -0
  5. package/extensions/ask-user/index.ts +30 -14
  6. package/extensions/background-terminals/index.ts +30 -2
  7. package/extensions/background-terminals/src/domain.ts +2 -0
  8. package/extensions/background-terminals/src/manager.ts +486 -106
  9. package/extensions/background-terminals/src/output.ts +33 -0
  10. package/extensions/background-terminals/src/prompt.ts +14 -6
  11. package/extensions/background-terminals/src/result-delivery.ts +4 -1
  12. package/extensions/background-terminals/src/ui/ps.ts +132 -129
  13. package/extensions/capabilities/index.ts +30 -42
  14. package/extensions/capabilities/src/ui.ts +93 -0
  15. package/extensions/clear-context/index.ts +83 -0
  16. package/extensions/context-pivot/index.ts +16 -6
  17. package/extensions/cron/schedule.ts +7 -1
  18. package/extensions/file-mutation-display/index.ts +34 -76
  19. package/extensions/file-mutation-display/render.ts +146 -87
  20. package/extensions/file-search/index.ts +8 -7
  21. package/extensions/file-search/src/binaries.ts +75 -59
  22. package/extensions/git-info/src/changed-files-view.ts +47 -14
  23. package/extensions/git-read/index.ts +328 -0
  24. package/extensions/git-read/src/args.ts +171 -0
  25. package/extensions/git-read/src/process.ts +81 -0
  26. package/extensions/git-read/src/prompt.ts +56 -0
  27. package/extensions/model-info/index.ts +21 -33
  28. package/extensions/model-info/session-metrics.ts +96 -0
  29. package/extensions/plan-mode/bash-policy.ts +54 -9
  30. package/extensions/plan-mode/index.ts +7 -2
  31. package/extensions/post-edit/index.ts +16 -6
  32. package/extensions/sessions/git-stats.ts +258 -72
  33. package/extensions/sessions/index.ts +222 -140
  34. package/extensions/sessions/preview-cache.ts +104 -0
  35. package/extensions/sessions/preview-loader.ts +856 -0
  36. package/extensions/sessions/sessions.ts +43 -4
  37. package/extensions/setup/index.ts +127 -131
  38. package/extensions/shared/activity-status.ts +36 -5
  39. package/extensions/shared/agent-session-page.ts +319 -0
  40. package/extensions/shared/agent-tool-renderer.ts +218 -0
  41. package/extensions/shared/agent-transcript.ts +524 -0
  42. package/extensions/shared/below-editor-navigation.ts +26 -0
  43. package/extensions/shared/capability-intent.ts +53 -0
  44. package/extensions/shared/child-session.ts +444 -22
  45. package/extensions/shared/result-budget.ts +134 -0
  46. package/extensions/shared/result-delivery.ts +34 -0
  47. package/extensions/shared/screen-chrome.ts +133 -0
  48. package/extensions/shared/setup-config.ts +97 -38
  49. package/extensions/shared/setup-episode-state.ts +1 -1
  50. package/extensions/shared/spinner.ts +28 -0
  51. package/extensions/shared/terminal-text.ts +110 -23
  52. package/extensions/shared/text-projection.ts +113 -0
  53. package/extensions/shared/tool-activity.ts +382 -0
  54. package/extensions/shared/tool-surface.ts +42 -8
  55. package/extensions/shared/transcript-viewport.ts +46 -0
  56. package/extensions/shared/web-observer-registry.ts +390 -0
  57. package/extensions/shared/worktree.ts +11 -0
  58. package/extensions/subagents/index.ts +461 -186
  59. package/extensions/subagents/navigation.ts +86 -28
  60. package/extensions/subagents/src/agent-types.ts +37 -15
  61. package/extensions/subagents/src/backend.ts +12 -1
  62. package/extensions/subagents/src/backends/pi.ts +375 -66
  63. package/extensions/subagents/src/domain.ts +5 -0
  64. package/extensions/subagents/src/id-sequence.ts +84 -0
  65. package/extensions/subagents/src/manager.ts +651 -536
  66. package/extensions/subagents/src/prompt.ts +185 -42
  67. package/extensions/subagents/src/result-artifact.ts +146 -0
  68. package/extensions/subagents/src/result-delivery.ts +7 -1
  69. package/extensions/subagents/src/runtime.ts +23 -6
  70. package/extensions/subagents/src/ui/takeover.ts +128 -337
  71. package/extensions/subagents/src/ui/transcript.ts +38 -501
  72. package/extensions/subagents/src/ui/wait-result.ts +103 -15
  73. package/extensions/suggestions/src/ui.ts +10 -4
  74. package/extensions/tasks/index.ts +0 -3
  75. package/extensions/tasks/ui.ts +79 -62
  76. package/extensions/ui-customization/footer.ts +7 -44
  77. package/extensions/ui-customization/index.ts +0 -4
  78. package/extensions/user-input-fold/index.ts +185 -0
  79. package/extensions/web/index.ts +234 -0
  80. package/extensions/workflows/artifacts.ts +147 -22
  81. package/extensions/workflows/completion-projection.ts +457 -0
  82. package/extensions/workflows/controller.ts +14 -2
  83. package/extensions/workflows/coordinator.ts +62 -0
  84. package/extensions/workflows/dashboard.ts +458 -339
  85. package/extensions/workflows/handoff.ts +121 -25
  86. package/extensions/workflows/index.ts +1042 -492
  87. package/extensions/workflows/journal.ts +148 -13
  88. package/extensions/workflows/model.ts +131 -19
  89. package/extensions/workflows/navigation.ts +61 -18
  90. package/extensions/workflows/progress-projection.ts +306 -0
  91. package/extensions/workflows/prompt.ts +166 -10
  92. package/extensions/workflows/replay-safety.ts +58 -27
  93. package/extensions/workflows/result-delivery.ts +253 -0
  94. package/extensions/workflows/retention.ts +593 -0
  95. package/extensions/workflows/runner.ts +388 -279
  96. package/extensions/workflows/sandbox-child.cjs +36 -3
  97. package/extensions/workflows/sandbox.ts +62 -8
  98. package/extensions/workflows/serialization.ts +325 -17
  99. package/extensions/workflows/tool-renderer.ts +22 -0
  100. package/extensions/workflows/transcript.ts +149 -0
  101. package/extensions/workspace-cleanup-guard/index.ts +54 -0
  102. package/extensions/workspace-cleanup-guard/workspace-provenance.ts +563 -0
  103. package/package.json +28 -8
  104. package/skills/subagents/REFERENCE.md +189 -0
  105. package/skills/subagents/SKILL.md +2 -2
  106. package/skills/workflows/REFERENCE.md +10 -5
  107. package/skills/workflows/SKILL.md +53 -10
  108. package/web/adapter/pi-adapter.ts +661 -0
  109. package/web/host/browser-launcher.ts +20 -0
  110. package/web/host/static-assets.ts +4 -0
  111. package/web/host/terminal-status.ts +38 -0
  112. package/web/host/web-host.ts +789 -0
  113. package/web/http-dispatcher.ts +125 -0
  114. package/web/protocol/types.ts +462 -0
  115. package/web/runtime/pi-runtime.ts +991 -0
  116. package/web/runtime/types.ts +71 -0
  117. package/web/runtime/web-host-lease.ts +497 -0
  118. package/web/trace.ts +18 -0
  119. package/web/ui/app.js +1398 -0
  120. package/web/ui/index.html +139 -0
  121. package/web/ui/styles.css +598 -0
  122. package/web/vite.config.mjs +34 -0
  123. package/extensions/execution-convergence/active-evidence.ts +0 -129
  124. package/extensions/execution-convergence/index.ts +0 -442
  125. package/extensions/execution-convergence/workspace-provenance.ts +0 -338
  126. package/extensions/setup/intercom-fs-helper.cjs +0 -130
  127. package/extensions/setup/intercom.ts +0 -603
  128. package/extensions/subagents/src/backends/stub.ts +0 -296
  129. package/extensions/subagents/src/format.ts +0 -48
@@ -7,37 +7,11 @@ import {
7
7
  MODEL_INFO_CHANNEL,
8
8
  REFRESH_CHANNEL,
9
9
  } from "../shared/dashboard-state.ts";
10
+ import { createSessionMetricsTracker } from "./session-metrics.ts";
10
11
 
11
12
  const CHARS_PER_ESTIMATED_TOKEN = 4;
12
13
  const LIVE_UPDATE_INTERVAL_MS = 200;
13
14
 
14
- function getSessionMetrics(ctx: ExtensionContext) {
15
- let cost = 0;
16
- let cacheRead = 0;
17
- let promptTokens = 0;
18
-
19
- for (const entry of ctx.sessionManager.getBranch()) {
20
- let usage;
21
- if (entry.type === "message") {
22
- const message = entry.message;
23
- if (message.role === "assistant" || message.role === "toolResult") {
24
- usage = message.usage;
25
- }
26
- } else if (entry.type === "compaction" || entry.type === "branch_summary") {
27
- usage = entry.usage;
28
- }
29
- if (!usage) continue;
30
- cost += usage.cost.total;
31
- cacheRead += usage.cacheRead;
32
- promptTokens += usage.input + usage.cacheRead + usage.cacheWrite;
33
- }
34
-
35
- return {
36
- cost,
37
- cachePercent: promptTokens > 0 ? (cacheRead / promptTokens) * 100 : null,
38
- };
39
- }
40
-
41
15
  function estimateContentTokens(characters: number) {
42
16
  return Math.ceil(characters / CHARS_PER_ESTIMATED_TOKEN);
43
17
  }
@@ -54,6 +28,7 @@ export default function modelInfo(pi: ExtensionAPI) {
54
28
  let runContentStreamMs = 0;
55
29
  let lastLiveUpdate = 0;
56
30
  let currentContext: ExtensionContext | undefined;
31
+ const sessionMetrics = createSessionMetricsTracker();
57
32
 
58
33
  const publish = () => pi.events.emit(MODEL_INFO_CHANNEL, { ...state });
59
34
 
@@ -61,7 +36,6 @@ export default function modelInfo(pi: ExtensionAPI) {
61
36
  currentContext = ctx;
62
37
  const model = ctx.model;
63
38
  const usage = ctx.getContextUsage();
64
- const metrics = getSessionMetrics(ctx);
65
39
 
66
40
  state = {
67
41
  ...state,
@@ -72,12 +46,14 @@ export default function modelInfo(pi: ExtensionAPI) {
72
46
  contextTokens: usage?.tokens ?? null,
73
47
  contextWindow: usage?.contextWindow ?? model?.contextWindow ?? 0,
74
48
  contextPercent: usage?.percent ?? null,
75
- cachePercent: metrics.cachePercent,
76
- cost: metrics.cost,
77
49
  };
78
50
  publish();
79
51
  }
80
52
 
53
+ function syncSessionMetrics(ctx: ExtensionContext) {
54
+ state = { ...state, ...sessionMetrics.sync(ctx.sessionManager) };
55
+ }
56
+
81
57
  function resetMessageTracking() {
82
58
  contentStreamStart = null;
83
59
  lastContentDeltaAt = null;
@@ -97,6 +73,8 @@ export default function modelInfo(pi: ExtensionAPI) {
97
73
  runContentTokens = 0;
98
74
  runContentStreamMs = 0;
99
75
  state = { ...state, tokensPerSecond: null, generating: false };
76
+ sessionMetrics.reset();
77
+ syncSessionMetrics(ctx);
100
78
  refresh(ctx);
101
79
  });
102
80
 
@@ -213,14 +191,23 @@ export default function modelInfo(pi: ExtensionAPI) {
213
191
  refresh(ctx);
214
192
  });
215
193
 
216
- pi.on("turn_end", (_event, ctx) => refresh(ctx));
194
+ pi.on("turn_end", (_event, ctx) => {
195
+ syncSessionMetrics(ctx);
196
+ refresh(ctx);
197
+ });
217
198
 
218
199
  // Compaction and branch moves rewrite history, so the cached percentage is
219
200
  // stale the moment they land. Pi reports unknown occupancy until the next
220
201
  // assistant reply, which is the honest state to show.
221
- pi.on("session_compact", (_event, ctx) => refresh(ctx));
202
+ pi.on("session_compact", (_event, ctx) => {
203
+ syncSessionMetrics(ctx);
204
+ refresh(ctx);
205
+ });
222
206
 
223
- pi.on("session_tree", (_event, ctx) => refresh(ctx));
207
+ pi.on("session_tree", (_event, ctx) => {
208
+ syncSessionMetrics(ctx);
209
+ refresh(ctx);
210
+ });
224
211
 
225
212
  pi.on("agent_settled", (_event, ctx) => {
226
213
  state = { ...state, generating: false };
@@ -230,5 +217,6 @@ export default function modelInfo(pi: ExtensionAPI) {
230
217
  pi.on("session_shutdown", () => {
231
218
  stopRefreshListener();
232
219
  currentContext = undefined;
220
+ sessionMetrics.reset();
233
221
  });
234
222
  }
@@ -0,0 +1,96 @@
1
+ import type { SessionEntry } from "@earendil-works/pi-coding-agent";
2
+ import type { Usage } from "@earendil-works/pi-ai";
3
+
4
+ type SessionMetricsManager = {
5
+ getSessionId(): string;
6
+ getLeafId(): string | null;
7
+ getEntry(id: string): SessionEntry | undefined;
8
+ };
9
+
10
+ type PrefixMetrics = Readonly<{
11
+ cost: number;
12
+ cacheRead: number;
13
+ promptTokens: number;
14
+ }>;
15
+
16
+ const EMPTY_PREFIX: PrefixMetrics = Object.freeze({
17
+ cost: 0,
18
+ cacheRead: 0,
19
+ promptTokens: 0,
20
+ });
21
+
22
+ function getUsage(entry: SessionEntry): Usage | undefined {
23
+ if (entry.type === "message") {
24
+ return entry.message.role === "assistant" ||
25
+ entry.message.role === "toolResult"
26
+ ? entry.message.usage
27
+ : undefined;
28
+ }
29
+ return entry.type === "compaction" || entry.type === "branch_summary"
30
+ ? entry.usage
31
+ : undefined;
32
+ }
33
+
34
+ function addUsage(prefix: PrefixMetrics, entry: SessionEntry): PrefixMetrics {
35
+ const usage = getUsage(entry);
36
+ if (!usage) return prefix;
37
+ return Object.freeze({
38
+ cost: prefix.cost + usage.cost.total,
39
+ cacheRead: prefix.cacheRead + usage.cacheRead,
40
+ promptTokens:
41
+ prefix.promptTokens + usage.input + usage.cacheRead + usage.cacheWrite,
42
+ });
43
+ }
44
+
45
+ function toMetrics(prefix: PrefixMetrics) {
46
+ return {
47
+ cost: prefix.cost,
48
+ cachePercent:
49
+ prefix.promptTokens > 0
50
+ ? (prefix.cacheRead / prefix.promptTokens) * 100
51
+ : null,
52
+ };
53
+ }
54
+
55
+ export function createSessionMetricsTracker() {
56
+ let sessionId: string | undefined;
57
+ let prefixes = new Map<string, PrefixMetrics>();
58
+
59
+ const reset = () => {
60
+ sessionId = undefined;
61
+ prefixes = new Map();
62
+ };
63
+
64
+ const sync = (manager: SessionMetricsManager) => {
65
+ const nextSessionId = manager.getSessionId();
66
+ if (sessionId !== nextSessionId) reset();
67
+ sessionId = nextSessionId;
68
+
69
+ const leafId = manager.getLeafId();
70
+ if (!leafId) return toMetrics(EMPTY_PREFIX);
71
+
72
+ const cachedLeaf = prefixes.get(leafId);
73
+ if (cachedLeaf) return toMetrics(cachedLeaf);
74
+
75
+ const suffix: SessionEntry[] = [];
76
+ let currentId: string | null = leafId;
77
+ while (currentId && !prefixes.has(currentId)) {
78
+ const entry = manager.getEntry(currentId);
79
+ if (!entry) break;
80
+ suffix.push(entry);
81
+ currentId = entry.parentId;
82
+ }
83
+
84
+ let prefix = currentId
85
+ ? (prefixes.get(currentId) ?? EMPTY_PREFIX)
86
+ : EMPTY_PREFIX;
87
+ for (const entry of suffix.reverse()) {
88
+ prefix = addUsage(prefix, entry);
89
+ prefixes.set(entry.id, prefix);
90
+ }
91
+
92
+ return toMetrics(prefixes.get(leafId) ?? EMPTY_PREFIX);
93
+ };
94
+
95
+ return { sync, reset };
96
+ }
@@ -11,11 +11,13 @@
11
11
  *
12
12
  * THREAT MODEL. The adversary is a model that would start changing things
13
13
  * before the user approved a plan — including one steered by injected text it
14
- * read while investigating. It is NOT a hostile repository: `git diff` honors
15
- * `diff.external` from the repo's own config, so a checkout whose `.git/config`
16
- * is attacker-controlled can run a program through a command this module
17
- * allows. Nothing here can prevent that, and nothing above it tries to: pi
18
- * already runs the project's own tooling under the user's trust decision.
14
+ * read while investigating. Repository-configured diff drivers are also in
15
+ * scope for commands that render patches: raw `git diff`, `git show`,
16
+ * `git whatchanged`, and diff-generating `git log` forms are refused here. Plan
17
+ * Mode exposes the structured git-read tools instead; they force
18
+ * `--no-ext-diff --no-textconv --no-color` in argv. This is not a claim that
19
+ * every hostile Git configuration is inert: the remaining Git investigation
20
+ * commands still run under Pi's existing project trust boundary.
19
21
  *
20
22
  * EVERY LIST BELOW IS AN ALLOWLIST, deliberately. An earlier version scanned
21
23
  * for known-dangerous flags instead, and review found four separate escapes in
@@ -50,9 +52,7 @@ const UNQUOTED_GLOB_CHARACTERS = new Set(["*", "?", "[", "]"]);
50
52
  */
51
53
  const GIT_SUBCOMMANDS = new Set([
52
54
  "log",
53
- "diff",
54
55
  "status",
55
- "show",
56
56
  "blame",
57
57
  "shortlog",
58
58
  "describe",
@@ -63,10 +63,11 @@ const GIT_SUBCOMMANDS = new Set([
63
63
  "cat-file",
64
64
  "merge-base",
65
65
  "name-rev",
66
- "whatchanged",
67
66
  "grep",
68
67
  ]);
69
68
 
69
+ const GIT_DIFF_SUBCOMMANDS = new Set(["diff", "show", "whatchanged"]);
70
+
70
71
  /**
71
72
  * Flags accepted after a git subcommand. The admission rule for this list is
72
73
  * narrow: a flag qualifies only if it shapes OUTPUT or SELECTS commits, and
@@ -159,6 +160,27 @@ const GIT_FLAGS = new Set([
159
160
  "--count",
160
161
  ]);
161
162
 
163
+ /**
164
+ * Options that make `git log` enter the diff machinery. Git enables textconv
165
+ * by default for porcelain log/diff output, so these must use the structured
166
+ * git tools even though plain history browsing remains safe and useful.
167
+ */
168
+ const GIT_LOG_DIFF_FLAGS = new Set([
169
+ "-p",
170
+ "--patch",
171
+ "--stat",
172
+ "--shortstat",
173
+ "--numstat",
174
+ "--summary",
175
+ "--raw",
176
+ "--name-only",
177
+ "--name-status",
178
+ "--word-diff",
179
+ "-U",
180
+ "--unified",
181
+ "-L",
182
+ ]);
183
+
162
184
  /** gh subcommands paired with the verbs under each that only read. */
163
185
  const GH_SUBCOMMANDS = new Map<string, ReadonlySet<string>>([
164
186
  ["pr", new Set(["view", "list", "diff", "checks", "status"])],
@@ -377,6 +399,16 @@ function flagName(word: string) {
377
399
  return eq === -1 ? word : word.slice(0, eq);
378
400
  }
379
401
 
402
+ function firstMatchedFlag(
403
+ words: readonly string[],
404
+ flags: ReadonlySet<string>,
405
+ ) {
406
+ for (const word of words) {
407
+ if (word === "--") break;
408
+ if (flags.has(flagName(word))) return word;
409
+ }
410
+ }
411
+
380
412
  /**
381
413
  * Check the argument tail. Words after `--` are pathspecs by definition and
382
414
  * need no check; before it, a word either is an allowlisted flag or is not a
@@ -439,11 +471,24 @@ export function planBashDecision(command: unknown): BashPlanDecision {
439
471
  */
440
472
  if (program === "git") {
441
473
  const [subcommand, ...args] = rest;
474
+ if (subcommand && GIT_DIFF_SUBCOMMANDS.has(subcommand)) {
475
+ return refuse(
476
+ `plan mode does not run raw "git ${subcommand}" because repository diff drivers may execute external programs — use git_diff or git_show instead (and git_log to find commits)`,
477
+ );
478
+ }
442
479
  if (!subcommand || !GIT_SUBCOMMANDS.has(subcommand)) {
443
480
  return refuse(
444
- `plan mode allows only a read-only git subcommand immediately after "git" (log, diff, status, show, blame, …), not "${subcommand ?? "(none)"}"`,
481
+ `plan mode allows only a read-only git subcommand immediately after "git" (log, status, blame, …), not "${subcommand ?? "(none)"}"`,
445
482
  );
446
483
  }
484
+ if (subcommand === "log") {
485
+ const diffFlag = firstMatchedFlag(args, GIT_LOG_DIFF_FLAGS);
486
+ if (diffFlag) {
487
+ return refuse(
488
+ `plan mode does not run raw "git log ${diffFlag}" because diff-generating log options may execute repository diff drivers — use git_log to find commits, then git_show or git_diff to inspect changes`,
489
+ );
490
+ }
491
+ }
447
492
  return scanArguments(args, GIT_FLAGS, "git");
448
493
  }
449
494
 
@@ -37,6 +37,7 @@ import {
37
37
  type PlanModeState,
38
38
  } from "../shared/plan-mode-state.ts";
39
39
  import {
40
+ loadOpenPiCapabilities,
40
41
  OPENPI_TOOL_SURFACE,
41
42
  patchOwnedTools,
42
43
  } from "../shared/tool-surface.ts";
@@ -173,6 +174,9 @@ export const PLAN_SAFE_TOOLS = new Set([
173
174
  "ls",
174
175
  "fd",
175
176
  "rg",
177
+ "git_show",
178
+ "git_diff",
179
+ "git_log",
176
180
  // Web research without write actions.
177
181
  "web_search",
178
182
  "source_check",
@@ -208,7 +212,7 @@ export const PLAN_SAFE_TOOLS = new Set([
208
212
  ]);
209
213
 
210
214
  export const BLOCK_REASON =
211
- "Plan mode is active: no changes yet. Keep investigating with read-only tools (read, fd, rg, web search, read-only bash like git log/diff/status, and subagent_spawn for parallel exploration — planning children get read-only tools). When the plan is decision-complete, call plan_ready alone with the complete Markdown plan; the user then chooses the next action with `/plan`, or cancels with `/plan off`.";
215
+ "Plan mode is active: no changes yet. Keep investigating with read-only tools (read, fd, rg, git_log/git_diff/git_show, web search, read-only bash like git log/status, and subagent_spawn for parallel exploration — planning children get read-only tools). Raw git diff/show and diff-generating git log forms are blocked; use the structured Git tools instead. When the plan is decision-complete, call plan_ready alone with the complete Markdown plan; the user then chooses the next action with `/plan`, or cancels with `/plan off`.";
212
216
 
213
217
  export function planToolCallDecision(
214
218
  toolName: string,
@@ -256,6 +260,7 @@ export default function planMode(pi: ExtensionAPI) {
256
260
  hasUI: boolean;
257
261
  ui: { setStatus: (key: string, value?: string) => void };
258
262
  }) => {
263
+ if (planning) loadOpenPiCapabilities(pi, ["search"]);
259
264
  syncPlanTool();
260
265
  pi.events.emit(PLAN_MODE_CHANNEL, { planning } satisfies PlanModeState);
261
266
  if (!ctx.hasUI) return;
@@ -519,7 +524,7 @@ export default function planMode(pi: ExtensionAPI) {
519
524
  {
520
525
  customType: "plan-mode-armed",
521
526
  content:
522
- `Plan mode is on: investigate and propose a plan, but do not change anything yet. Edits and writes are blocked until the user explicitly chooses an implementation action. For files use the read, ls, grep and fd tools; bash is limited to read-only git and gh history commands such as \`git log\`, \`git diff\`, \`git status\`, \`git show\`, \`git blame\` and \`gh pr view\` — one plain command, no pipes or redirects. You can still delegate with \`subagent_spawn\` to explore several parts of the codebase at once: children spawned while planning receive read-only tools, so use them for investigation rather than for work to be done later.${objective ? `\n\nPlan for: ${objective}` : ""}` +
527
+ `Plan mode is on: investigate and propose a plan, but do not change anything yet. Edits and writes are blocked until the user explicitly chooses an implementation action. For files use the read, ls, grep, fd and rg tools. Use \`git_log\`, \`git_diff\` and \`git_show\` for history and changes; they disable repository external diff and textconv programs. Bash is limited to read-only git and gh commands such as \`git log\`, \`git status\`, \`git blame\` and \`gh pr view\` — raw Git commands that render diffs are blocked, and every Bash call must be one plain command with no pipes or redirects. You can still delegate with \`subagent_spawn\` to explore several parts of the codebase at once: children spawned while planning receive read-only tools, so use them for investigation rather than for work to be done later.${objective ? `\n\nPlan for: ${objective}` : ""}` +
523
528
  "\n\nUse read-only tools to ground the plan. When it is decision-complete, call `plan_ready` alone with the complete Markdown plan. That records the plan but does not start implementation; the user chooses the next action with `/plan`.",
524
529
  display: true,
525
530
  details: {},
@@ -21,6 +21,14 @@ import { sanitizeTerminalText } from "../shared/terminal-text.ts";
21
21
 
22
22
  /** Tools whose success means a file on disk changed. */
23
23
  const MUTATING_TOOLS = new Set(["write", "edit"]);
24
+ const NOTICE_COMMAND_MAX_CHARS = 160;
25
+ const NOTICE_DETAIL_MAX_CHARS = 320;
26
+
27
+ function boundedNoticeText(value: string, maxChars: number) {
28
+ const chars = [...sanitizeTerminalText(value).trim()];
29
+ if (chars.length <= maxChars) return chars.join("");
30
+ return `${chars.slice(0, maxChars - 1).join("")}…`;
31
+ }
24
32
 
25
33
  export default function postEdit(
26
34
  pi: ExtensionAPI,
@@ -75,18 +83,20 @@ export default function postEdit(
75
83
  })
76
84
  .then((result) => {
77
85
  if (result.code === 0) return;
78
- const detail = sanitizeTerminalText(
86
+ const commandPreview = boundedNoticeText(ran, NOTICE_COMMAND_MAX_CHARS);
87
+ const detail = boundedNoticeText(
79
88
  result.stderr || result.stdout || "",
80
- )
81
- .trim()
82
- .slice(0, 500);
89
+ NOTICE_DETAIL_MAX_CHARS,
90
+ );
83
91
  warn(
84
- `post-edit command failed (exit ${result.code}): ${sanitizeTerminalText(ran)}${detail ? `\n${detail}` : ""}`,
92
+ `post-edit command failed (exit ${result.code}): ${commandPreview}${detail ? `\n${detail}` : ""}`,
85
93
  );
86
94
  })
87
95
  .catch((error: unknown) => {
88
96
  const detail = error instanceof Error ? error.message : String(error);
89
- warn(`post-edit command could not run: ${detail}`);
97
+ warn(
98
+ `post-edit command could not run: ${boundedNoticeText(detail, NOTICE_DETAIL_MAX_CHARS)}`,
99
+ );
90
100
  })
91
101
  .finally(() => {
92
102
  if (active?.controller === controller) active = undefined;