@oh-my-pi/pi-coding-agent 17.1.7 → 17.2.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 (246) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/dist/{CHANGELOG-5k4dq4g6.md → CHANGELOG-be1f2t8h.md} +127 -0
  3. package/dist/cli.js +9554 -12379
  4. package/dist/template-8vdv6xb4.js +1671 -0
  5. package/dist/template-f8wx9vfn.css +1355 -0
  6. package/dist/template-qat058wr.html +55 -0
  7. package/dist/tool-views.generated-jdfmzwmn.js +35 -0
  8. package/dist/types/advisor/advise-tool.d.ts +0 -7
  9. package/dist/types/advisor/runtime.d.ts +8 -11
  10. package/dist/types/advisor/transcript-recorder.d.ts +15 -0
  11. package/dist/types/async/job-manager.d.ts +11 -0
  12. package/dist/types/auto-thinking/classifier.d.ts +9 -2
  13. package/dist/types/cleanse/agent.d.ts +19 -0
  14. package/dist/types/cleanse/balance.d.ts +7 -0
  15. package/dist/types/cleanse/checkers.d.ts +13 -0
  16. package/dist/types/cleanse/index.d.ts +16 -0
  17. package/dist/types/cleanse/loop.d.ts +16 -0
  18. package/dist/types/cleanse/parsers.d.ts +13 -0
  19. package/dist/types/cleanse/progress.d.ts +14 -0
  20. package/dist/types/cleanse/types.d.ts +64 -0
  21. package/dist/types/cli/args.d.ts +2 -0
  22. package/dist/types/cli/session-picker.d.ts +14 -7
  23. package/dist/types/collab/guest.d.ts +21 -2
  24. package/dist/types/commands/cleanse.d.ts +23 -0
  25. package/dist/types/commands/launch.d.ts +6 -0
  26. package/dist/types/config/settings-schema.d.ts +63 -8
  27. package/dist/types/cursor-bridge-tools.d.ts +54 -0
  28. package/dist/types/cursor.d.ts +193 -8
  29. package/dist/types/edit/edit-clipboard.d.ts +19 -0
  30. package/dist/types/edit/hashline/diff.d.ts +9 -1
  31. package/dist/types/edit/index.d.ts +8 -1
  32. package/dist/types/edit/streaming.d.ts +8 -1
  33. package/dist/types/export/html/index.d.ts +2 -0
  34. package/dist/types/extensibility/extensions/runner.d.ts +19 -2
  35. package/dist/types/extensibility/extensions/types.d.ts +25 -1
  36. package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +27 -1
  37. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +2 -0
  38. package/dist/types/internal-urls/local-protocol.d.ts +3 -2
  39. package/dist/types/launch/terminal-output-worker-client.d.ts +5 -0
  40. package/dist/types/launch/terminal-output-worker-protocol.d.ts +14 -0
  41. package/dist/types/launch/terminal-output-worker.d.ts +1 -0
  42. package/dist/types/lsp/client.d.ts +2 -0
  43. package/dist/types/main.d.ts +2 -0
  44. package/dist/types/mcp/manager.d.ts +33 -4
  45. package/dist/types/modes/components/codex-reset-fireworks.d.ts +51 -0
  46. package/dist/types/modes/components/read-tool-group.d.ts +12 -0
  47. package/dist/types/modes/components/session-selector.d.ts +6 -0
  48. package/dist/types/modes/components/status-line/component.d.ts +23 -0
  49. package/dist/types/modes/components/tool-execution.d.ts +16 -7
  50. package/dist/types/modes/components/usage-row.d.ts +2 -0
  51. package/dist/types/modes/controllers/mcp-command-controller.d.ts +26 -1
  52. package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
  53. package/dist/types/modes/interactive-mode.d.ts +8 -5
  54. package/dist/types/modes/rpc/rpc-client.d.ts +7 -0
  55. package/dist/types/modes/rpc/rpc-types.d.ts +16 -0
  56. package/dist/types/modes/theme/theme.d.ts +12 -1
  57. package/dist/types/modes/types.d.ts +2 -1
  58. package/dist/types/sdk.d.ts +1 -0
  59. package/dist/types/session/agent-session-events.d.ts +2 -0
  60. package/dist/types/session/agent-session-types.d.ts +36 -1
  61. package/dist/types/session/agent-session.d.ts +4 -2
  62. package/dist/types/session/async-job-delivery.d.ts +8 -0
  63. package/dist/types/session/claude-session-store.d.ts +13 -0
  64. package/dist/types/session/codex-session-store.d.ts +14 -0
  65. package/dist/types/session/credential-pin.d.ts +58 -0
  66. package/dist/types/session/foreign-session-import.d.ts +15 -0
  67. package/dist/types/session/foreign-session-jsonl.d.ts +9 -0
  68. package/dist/types/session/foreign-session-store.d.ts +23 -0
  69. package/dist/types/session/model-controls.d.ts +3 -3
  70. package/dist/types/session/session-advisors.d.ts +47 -2
  71. package/dist/types/session/session-entries.d.ts +19 -1
  72. package/dist/types/session/session-handoff.d.ts +4 -1
  73. package/dist/types/session/session-loader.d.ts +2 -0
  74. package/dist/types/session/session-maintenance.d.ts +1 -1
  75. package/dist/types/session/session-manager.d.ts +21 -0
  76. package/dist/types/session/session-tools.d.ts +25 -1
  77. package/dist/types/session/turn-recovery.d.ts +10 -4
  78. package/dist/types/system-prompt.d.ts +2 -1
  79. package/dist/types/task/executor.d.ts +11 -2
  80. package/dist/types/task/isolation-ownership.d.ts +34 -0
  81. package/dist/types/task/render.d.ts +7 -0
  82. package/dist/types/thinking.d.ts +18 -6
  83. package/dist/types/tools/acp-bridge.d.ts +28 -5
  84. package/dist/types/tools/browser/cmux/cmux-tab.d.ts +1 -2
  85. package/dist/types/tools/browser/tab-worker.d.ts +1 -4
  86. package/dist/types/tools/grep.d.ts +17 -1
  87. package/dist/types/tools/hub/launch.d.ts +5 -1
  88. package/dist/types/tools/index.d.ts +4 -1
  89. package/dist/types/tools/path-utils.d.ts +19 -0
  90. package/dist/types/tools/read.d.ts +2 -0
  91. package/dist/types/tools/xdev.d.ts +2 -1
  92. package/dist/types/tui/output-block.d.ts +5 -5
  93. package/dist/types/utils/changelog.d.ts +19 -0
  94. package/dist/types/utils/git.d.ts +10 -0
  95. package/dist/types/utils/jj.d.ts +11 -4
  96. package/dist/types/web/search/types.d.ts +2 -2
  97. package/package.json +16 -12
  98. package/scripts/bundle-dist.ts +3 -1
  99. package/src/advisor/advise-tool.ts +0 -11
  100. package/src/advisor/runtime.ts +138 -24
  101. package/src/advisor/transcript-recorder.ts +56 -0
  102. package/src/async/job-manager.ts +30 -7
  103. package/src/auto-thinking/classifier.ts +48 -10
  104. package/src/cleanse/agent.ts +226 -0
  105. package/src/cleanse/balance.ts +79 -0
  106. package/src/cleanse/checkers.ts +996 -0
  107. package/src/cleanse/index.ts +190 -0
  108. package/src/cleanse/loop.ts +51 -0
  109. package/src/cleanse/parsers.ts +726 -0
  110. package/src/cleanse/progress.ts +50 -0
  111. package/src/cleanse/prompts/assignment.md +47 -0
  112. package/src/cleanse/types.ts +72 -0
  113. package/src/cli/args.ts +6 -0
  114. package/src/cli/flag-tables.ts +2 -0
  115. package/src/cli/gallery-fixtures/fs.ts +25 -0
  116. package/src/cli/session-picker.ts +34 -17
  117. package/src/cli/ttsr-cli.ts +19 -1
  118. package/src/cli/update-cli.ts +3 -0
  119. package/src/cli/usage-cli.ts +29 -4
  120. package/src/cli/worktree-cli.ts +28 -11
  121. package/src/cli-commands.ts +1 -0
  122. package/src/cli.ts +11 -1
  123. package/src/collab/guest.ts +37 -4
  124. package/src/collab/host.ts +1 -0
  125. package/src/commands/cleanse.ts +45 -0
  126. package/src/commands/launch.ts +6 -0
  127. package/src/config/model-registry.ts +103 -25
  128. package/src/config/settings-schema.ts +65 -6
  129. package/src/config/settings.ts +25 -0
  130. package/src/cursor-bridge-tools.ts +81 -0
  131. package/src/cursor.ts +463 -12
  132. package/src/discovery/claude-plugins.ts +144 -34
  133. package/src/edit/edit-clipboard.ts +23 -0
  134. package/src/edit/hashline/diff.ts +49 -10
  135. package/src/edit/hashline/execute.ts +38 -6
  136. package/src/edit/hashline/filesystem.ts +27 -3
  137. package/src/edit/index.ts +12 -2
  138. package/src/edit/renderer.ts +4 -4
  139. package/src/edit/streaming.ts +17 -2
  140. package/src/export/html/index.ts +17 -10
  141. package/src/export/html/template.js +1 -1
  142. package/src/extensibility/extensions/runner.ts +70 -0
  143. package/src/extensibility/extensions/types.ts +30 -0
  144. package/src/extensibility/legacy-pi-ai-shim.ts +46 -1
  145. package/src/extensibility/legacy-pi-coding-agent-shim.ts +12 -13
  146. package/src/internal-urls/local-protocol.ts +3 -2
  147. package/src/internal-urls/mcp-protocol.ts +4 -1
  148. package/src/launch/broker.ts +14 -4
  149. package/src/launch/terminal-output-worker-client.ts +53 -0
  150. package/src/launch/terminal-output-worker-protocol.ts +11 -0
  151. package/src/launch/terminal-output-worker.ts +23 -0
  152. package/src/lsp/client.ts +18 -3
  153. package/src/lsp/index.ts +4 -0
  154. package/src/lsp/render.ts +1 -1
  155. package/src/main.ts +127 -56
  156. package/src/mcp/manager.ts +120 -18
  157. package/src/modes/acp/acp-agent.ts +27 -18
  158. package/src/modes/acp/acp-event-mapper.ts +38 -4
  159. package/src/modes/components/chat-transcript-builder.ts +30 -11
  160. package/src/modes/components/codex-reset-fireworks.ts +369 -0
  161. package/src/modes/components/read-tool-group.ts +159 -17
  162. package/src/modes/components/session-selector.ts +19 -3
  163. package/src/modes/components/settings-selector.ts +9 -1
  164. package/src/modes/components/status-line/component.ts +381 -54
  165. package/src/modes/components/tool-execution.ts +78 -31
  166. package/src/modes/components/usage-row.ts +10 -5
  167. package/src/modes/controllers/event-controller.ts +26 -4
  168. package/src/modes/controllers/mcp-command-controller.ts +68 -3
  169. package/src/modes/controllers/selector-controller.ts +94 -36
  170. package/src/modes/controllers/tan-command-controller.ts +14 -0
  171. package/src/modes/interactive-mode.ts +67 -64
  172. package/src/modes/prompt-action-autocomplete.ts +5 -3
  173. package/src/modes/rpc/rpc-client.ts +18 -0
  174. package/src/modes/rpc/rpc-mode.ts +16 -1
  175. package/src/modes/rpc/rpc-types.ts +11 -0
  176. package/src/modes/theme/theme.ts +151 -6
  177. package/src/modes/types.ts +2 -1
  178. package/src/modes/utils/ui-helpers.ts +38 -21
  179. package/src/prompts/goals/guided-goal-interview.md +41 -6
  180. package/src/prompts/system/auto-thinking-difficulty.md +4 -2
  181. package/src/prompts/system/plan-mode-active.md +2 -2
  182. package/src/prompts/system/system-prompt.md +3 -0
  183. package/src/prompts/system/vibe-mode-active.md +4 -1
  184. package/src/prompts/system/xdev-mount-notice.md +1 -1
  185. package/src/prompts/tools/bash.md +15 -17
  186. package/src/prompts/tools/browser.md +1 -0
  187. package/src/prompts/tools/checkpoint.md +1 -1
  188. package/src/prompts/tools/glob.md +7 -6
  189. package/src/prompts/tools/goal.md +1 -1
  190. package/src/prompts/tools/grep.md +7 -6
  191. package/src/sdk.ts +178 -15
  192. package/src/session/agent-session-events.ts +1 -0
  193. package/src/session/agent-session-types.ts +40 -1
  194. package/src/session/agent-session.ts +330 -179
  195. package/src/session/async-job-delivery.ts +8 -0
  196. package/src/session/claude-session-store.ts +426 -0
  197. package/src/session/codex-session-store.ts +673 -0
  198. package/src/session/credential-pin.ts +93 -0
  199. package/src/session/foreign-session-import.ts +52 -0
  200. package/src/session/foreign-session-jsonl.ts +29 -0
  201. package/src/session/foreign-session-store.ts +26 -0
  202. package/src/session/model-controls.ts +15 -7
  203. package/src/session/session-advisors.ts +142 -23
  204. package/src/session/session-entries.ts +21 -1
  205. package/src/session/session-handoff.ts +20 -3
  206. package/src/session/session-history-format.ts +31 -6
  207. package/src/session/session-loader.ts +25 -9
  208. package/src/session/session-maintenance.ts +59 -11
  209. package/src/session/session-manager.ts +61 -0
  210. package/src/session/session-tools.ts +107 -5
  211. package/src/session/settings-stream-fn.ts +1 -0
  212. package/src/session/turn-recovery.ts +97 -51
  213. package/src/slash-commands/builtin-registry.ts +136 -5
  214. package/src/system-prompt.ts +3 -2
  215. package/src/task/executor.ts +17 -4
  216. package/src/task/isolation-ownership.ts +106 -0
  217. package/src/task/render.ts +20 -4
  218. package/src/task/worktree.ts +8 -0
  219. package/src/thinking.ts +29 -10
  220. package/src/tools/acp-bridge.ts +52 -8
  221. package/src/tools/ask.ts +3 -3
  222. package/src/tools/ast-edit.ts +9 -2
  223. package/src/tools/browser/cmux/cmux-tab.ts +9 -14
  224. package/src/tools/browser/tab-worker.ts +12 -35
  225. package/src/tools/browser.ts +7 -2
  226. package/src/tools/checkpoint.ts +0 -13
  227. package/src/tools/gh-renderer.ts +3 -3
  228. package/src/tools/grep.ts +64 -8
  229. package/src/tools/hub/launch.ts +10 -15
  230. package/src/tools/index.ts +38 -5
  231. package/src/tools/path-utils.ts +88 -0
  232. package/src/tools/read.ts +3 -0
  233. package/src/tools/write.ts +44 -3
  234. package/src/tools/xdev.ts +39 -14
  235. package/src/tui/code-cell.ts +4 -4
  236. package/src/tui/output-block.ts +25 -8
  237. package/src/utils/changelog.ts +144 -4
  238. package/src/utils/git.ts +61 -14
  239. package/src/utils/jj.ts +19 -13
  240. package/src/utils/shell-snapshot-fn-env.sh +5 -2
  241. package/src/web/search/providers/codex.ts +34 -0
  242. package/src/web/search/render.ts +2 -2
  243. package/src/web/search/types.ts +1 -1
  244. package/dist/types/goals/guided-setup.d.ts +0 -30
  245. package/src/goals/guided-setup.ts +0 -171
  246. package/src/prompts/goals/guided-goal-system.md +0 -33
@@ -0,0 +1,50 @@
1
+ const BAR_WIDTH = 16;
2
+
3
+ /** Minimal output contract used by the interactive cleanse progress reporter. */
4
+ export interface CleanseProgressOutput {
5
+ isTTY?: boolean;
6
+ write(text: string): boolean;
7
+ }
8
+
9
+ /** Renders completed repair workers on one transient terminal line. */
10
+ export interface CleanseProgressReporter {
11
+ readonly interactive: boolean;
12
+ start(total: number): void;
13
+ complete(): void;
14
+ finish(): void;
15
+ }
16
+
17
+ /** Create the TTY-only worker completion reporter used by `omp cleanse`. */
18
+ export function createCleanseProgressReporter(output: CleanseProgressOutput = process.stdout): CleanseProgressReporter {
19
+ const interactive = output.isTTY === true;
20
+ let total = 0;
21
+ let completed = 0;
22
+ let rendered = false;
23
+
24
+ const render = (): void => {
25
+ if (!interactive || total === 0) return;
26
+ const ratio = Math.min(completed / total, 1);
27
+ const filled = Math.round(ratio * BAR_WIDTH);
28
+ const bar = `${"█".repeat(filled)}${"░".repeat(BAR_WIDTH - filled)}`;
29
+ output.write(`\rRepairing [${bar}] ${completed}/${total}\x1b[K`);
30
+ rendered = true;
31
+ };
32
+
33
+ return {
34
+ interactive,
35
+ start(nextTotal) {
36
+ total = Math.max(nextTotal, 0);
37
+ completed = 0;
38
+ render();
39
+ },
40
+ complete() {
41
+ completed = Math.min(completed + 1, total);
42
+ render();
43
+ },
44
+ finish() {
45
+ if (!rendered) return;
46
+ output.write("\n");
47
+ rendered = false;
48
+ },
49
+ };
50
+ }
@@ -0,0 +1,47 @@
1
+ <critical>
2
+ - You MUST fix every assigned diagnostic at its root cause.
3
+ - You MUST stay inside the write scope below.
4
+ - You NEVER suppress valid diagnostics to make checks pass.
5
+ - You NEVER delegate or spawn another agent.
6
+ - You NEVER run project-wide checks or formatters; the orchestrator reruns them.
7
+ </critical>
8
+
9
+ # Assignment
10
+
11
+ Repair wave {{wave}}, worker {{worker}}.
12
+
13
+ ## Write scope
14
+
15
+ {{write_scope}}
16
+
17
+ Read related code freely. Project-level diagnostics MAY require the smallest necessary edit outside named files; use `hub` before touching a peer-owned file.
18
+
19
+ ## Diagnostics
20
+
21
+ {{diagnostics}}
22
+
23
+ ## Checker commands
24
+
25
+ {{checker_commands}}
26
+
27
+ ## Concurrent peer ownership
28
+
29
+ {{peer_assignments}}
30
+
31
+ <workflow>
32
+ 1. Inspect every assigned location and its local context.
33
+ 2. Fix causes, not emitted text or checker configuration.
34
+ 3. Preserve behavior unless a diagnostic proves it wrong.
35
+ 4. Run only targeted checks for assigned files when available.
36
+ 5. Re-read changed sections and resolve every assigned item.
37
+ </workflow>
38
+
39
+ <completeness>
40
+ - Done means every assigned diagnostic has a concrete fix.
41
+ - Blocked items MUST name the missing prerequisite in the final result.
42
+ - Final output MUST list changed files and unresolved items.
43
+ </completeness>
44
+
45
+ <critical>
46
+ You MUST leave peer-owned files untouched and complete the full assignment.
47
+ </critical>
@@ -0,0 +1,72 @@
1
+ /** Severity normalized across checker-specific output formats. */
2
+ export type CleanseSeverity = "error" | "warning" | "info";
3
+
4
+ /** One actionable problem reported by a project checker. */
5
+ export interface CleanseDiagnostic {
6
+ checker: string;
7
+ file?: string;
8
+ line?: number;
9
+ column?: number;
10
+ endLine?: number;
11
+ endColumn?: number;
12
+ code?: string;
13
+ severity: CleanseSeverity;
14
+ message: string;
15
+ suggestion?: string;
16
+ }
17
+
18
+ /** Result metadata for one checker invocation. */
19
+ export interface CleanseCheckResult {
20
+ id: string;
21
+ label: string;
22
+ language: string;
23
+ cwd: string;
24
+ command: string;
25
+ exitCode: number | null;
26
+ diagnostics: CleanseDiagnostic[];
27
+ }
28
+
29
+ /** Checker discovery result omitted because its required executable was unavailable. */
30
+ export interface SkippedCleanseCheck {
31
+ label: string;
32
+ language: string;
33
+ reason: string;
34
+ }
35
+
36
+ /** Aggregate diagnostics from every checker selected for a project. */
37
+ export interface CleanseDiagnosticReport {
38
+ checks: CleanseCheckResult[];
39
+ diagnostics: CleanseDiagnostic[];
40
+ skipped: SkippedCleanseCheck[];
41
+ }
42
+
43
+ /** All diagnostics attached to one file, which scheduling never splits across agents. */
44
+ export interface CleanseFileIssues {
45
+ file?: string;
46
+ diagnostics: CleanseDiagnostic[];
47
+ weight: number;
48
+ }
49
+
50
+ /** One weighted, file-disjoint workload handed to a subagent. */
51
+ export interface CleanseAssignment {
52
+ index: number;
53
+ groups: CleanseFileIssues[];
54
+ weight: number;
55
+ }
56
+
57
+ /** Settled result from one cleanse subagent. */
58
+ export interface CleanseAgentOutcome {
59
+ name: string;
60
+ success: boolean;
61
+ output: string;
62
+ error?: string;
63
+ resolvedModel?: string;
64
+ }
65
+
66
+ /** Final state after one bounded repair wave and verification pass. */
67
+ export interface CleanseLoopResult {
68
+ status: "clean" | "stalled" | "cancelled";
69
+ waves: number;
70
+ report: CleanseDiagnosticReport;
71
+ outcomes: CleanseAgentOutcome[];
72
+ }
package/src/cli/args.ts CHANGED
@@ -44,6 +44,8 @@ export interface Args {
44
44
  advisor?: boolean;
45
45
  continue?: boolean;
46
46
  resume?: string | true;
47
+ fromClaude?: boolean;
48
+ fromCodex?: boolean;
47
49
  help?: boolean;
48
50
  version?: boolean;
49
51
  mode?: Mode;
@@ -227,6 +229,10 @@ export function parseArgs(inputArgs: string[], extensionFlags?: Map<string, { ty
227
229
  result.alias = arg.slice("--alias=".length);
228
230
  } else if (arg === "--continue" || arg === "-c") {
229
231
  result.continue = true;
232
+ } else if (arg === "--from-claude") {
233
+ result.fromClaude = true;
234
+ } else if (arg === "--from-codex") {
235
+ result.fromCodex = true;
230
236
  } else if (arg === "--no-session") {
231
237
  result.noSession = true;
232
238
  } else if (arg === "--no-tools") {
@@ -290,6 +290,8 @@ export const VALUELESS_FLAGS: ReadonlySet<string> = new Set([
290
290
  "--version",
291
291
  "--allow-home",
292
292
  "--continue",
293
+ "--from-claude",
294
+ "--from-codex",
293
295
  "--no-session",
294
296
  "--no-tools",
295
297
  "--no-lsp",
@@ -1,5 +1,6 @@
1
1
  // biome-ignore-all lint/suspicious/noTemplateCurlyInString: sample source-code strings (read fixtures) intentionally contain literal ${...}.
2
2
  // Gallery fixtures for the filesystem tools (read, write, glob).
3
+ import type { Usage } from "@oh-my-pi/pi-ai";
3
4
  import { ReadToolGroupComponent } from "../../modes/components/read-tool-group";
4
5
  import type { GalleryFixture, GalleryFixtureState, GalleryResult } from "./types";
5
6
 
@@ -47,6 +48,15 @@ const groupedReadDelimitedPath = groupedReadTargets.join(",");
47
48
  const groupedReadRepeatedFile = "packages/coding-agent/src/task/render.ts";
48
49
  const groupedReadRepeatedRanges = `${groupedReadRepeatedFile}:507-605,1070-1194,1210-1240,1270-1274`;
49
50
 
51
+ const GROUPED_READ_USAGE: Usage = {
52
+ input: 2400,
53
+ output: 113,
54
+ cacheRead: 103_000,
55
+ cacheWrite: 0,
56
+ totalTokens: 105_513,
57
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
58
+ };
59
+
50
60
  function textResult(text: string, details?: unknown, isError?: boolean): GalleryResult {
51
61
  return { content: [{ type: "text", text }], details, isError };
52
62
  }
@@ -81,6 +91,13 @@ function renderReadGroupFixtureState(state: GalleryFixtureState, width: number,
81
91
  false,
82
92
  "read-delimited",
83
93
  );
94
+ component.attachUsage(
95
+ ["read-delimited"],
96
+ GROUPED_READ_USAGE,
97
+ 5300,
98
+ 2200,
99
+ new Date(2026, 6, 28, 21, 5, 47).getTime(),
100
+ );
84
101
 
85
102
  if (state === "error") {
86
103
  component.updateResult(
@@ -88,10 +105,18 @@ function renderReadGroupFixtureState(state: GalleryFixtureState, width: number,
88
105
  false,
89
106
  "read-ranges",
90
107
  );
108
+ component.attachUsage(
109
+ ["read-ranges"],
110
+ GROUPED_READ_USAGE,
111
+ 4700,
112
+ 1900,
113
+ new Date(2026, 6, 28, 21, 5, 52).getTime(),
114
+ );
91
115
  return component.render(width);
92
116
  }
93
117
 
94
118
  component.updateResult(textResult("Read four render.ts ranges."), false, "read-ranges");
119
+ component.attachUsage(["read-ranges"], GROUPED_READ_USAGE, 4700, 1900, new Date(2026, 6, 28, 21, 5, 52).getTime());
95
120
  return component.render(width);
96
121
  }
97
122
 
@@ -6,16 +6,26 @@ import type { SessionInfo } from "../session/session-listing";
6
6
  import { SessionManager } from "../session/session-manager";
7
7
  import { FileSessionStorage } from "../session/session-storage";
8
8
 
9
+ /** Presentation and capability controls for the standalone session picker. */
10
+ export interface SessionPickerOptions {
11
+ allSessions?: SessionInfo[];
12
+ title?: string;
13
+ scopeLabel?: string | false;
14
+ showCwd?: boolean;
15
+ allowDelete?: boolean;
16
+ allowGlobalScope?: boolean;
17
+ historySearch?: boolean;
18
+ }
19
+
9
20
  /**
10
21
  * Show the TUI session selector and return the selected session, or null if
11
- * cancelled. Rendered as a fullscreen overlay on the terminal's alternate
12
- * screen, so the list scrolls and rows are clickable with the mouse. Tab
13
- * toggles between current-folder and all-projects scope; the all-projects list
14
- * is loaded lazily via `SessionManager.listAll`.
22
+ * cancelled. The default OMP picker supports deletion, transcript-history
23
+ * search, and an all-projects scope; foreign import pickers disable those
24
+ * source-owned capabilities.
15
25
  */
16
26
  export async function selectSession(
17
27
  sessions: SessionInfo[],
18
- options?: { allSessions?: SessionInfo[] },
28
+ options: SessionPickerOptions = {},
19
29
  ): Promise<SessionInfo | null> {
20
30
  const { promise, resolve } = Promise.withResolvers<SessionInfo | null>();
21
31
  const ui = new TUI(new ProcessTerminal());
@@ -26,11 +36,13 @@ export async function selectSession(
26
36
  // session-list prefix never sees. Best-effort: a missing/locked history.db
27
37
  // must not break the picker.
28
38
  let historyMatcher: ((query: string) => string[]) | undefined;
29
- try {
30
- const history = HistoryStorage.open();
31
- historyMatcher = (query: string) => history.matchingSessionIds(query);
32
- } catch (error) {
33
- logger.warn("History storage unavailable for session ranking", { error: String(error) });
39
+ if (options.historySearch !== false) {
40
+ try {
41
+ const history = HistoryStorage.open();
42
+ historyMatcher = (query: string) => history.matchingSessionIds(query);
43
+ } catch (error) {
44
+ logger.warn("History storage unavailable for session ranking", { error: String(error) });
45
+ }
34
46
  }
35
47
 
36
48
  const showSelector = () => {
@@ -58,16 +70,21 @@ export async function selectSession(
58
70
  }
59
71
  },
60
72
  {
61
- onDelete: async (session: SessionInfo) => {
62
- // Delete handler - SessionList will show confirmation internally
63
- await storage.deleteSessionWithArtifacts(session.path);
64
- return true;
65
- },
73
+ onDelete:
74
+ options.allowDelete === false
75
+ ? undefined
76
+ : async (session: SessionInfo) => {
77
+ await storage.deleteSessionWithArtifacts(session.path);
78
+ return true;
79
+ },
66
80
  historyMatcher,
67
- loadAllSessions: () => SessionManager.listAll(storage),
68
- allSessions: options?.allSessions,
81
+ loadAllSessions: options.allowGlobalScope === false ? undefined : () => SessionManager.listAll(storage),
82
+ allSessions: options.allSessions,
69
83
  getTerminalRows: () => ui.terminal.rows,
70
84
  fillHeight: true,
85
+ title: options.title,
86
+ scopeLabel: options.scopeLabel,
87
+ showCwd: options.showCwd,
71
88
  },
72
89
  );
73
90
  return selector;
@@ -94,12 +94,18 @@ interface TestReport {
94
94
  evaluated: number;
95
95
  triggered: RuleMatchDetail[];
96
96
  notTriggered: RuleMatchDetail[];
97
+ /**
98
+ * Set when a file path was supplied but the match source was inferred as
99
+ * `text` (extension absent from {@link SOURCE_FILE_EXT}), so callers can
100
+ * surface why a source file was evaluated against a prose context.
101
+ */
102
+ inferenceNote?: string;
97
103
  }
98
104
 
99
105
  const STDIN_MARKER = "-";
100
106
  /** Extensions treated as source files for default tool-context inference. */
101
107
  const SOURCE_FILE_EXT =
102
- /^\.(ts|tsx|js|jsx|mjs|cjs|rs|py|go|java|kt|swift|c|cc|cpp|h|hpp|rb|php|lua|css|scss|html|json|ya?ml|toml|md|mdc)$/i;
108
+ /^\.(ts|tsx|js|jsx|mjs|cjs|rs|py|go|java|kt|swift|c|cc|cpp|h|hpp|rb|php|lua|css|scss|html|json|ya?ml|toml|md|mdc|cs|razor|cshtml|fs|fsx|vb|sh|bash|sql|zig|dart|scala|ex|exs|proto|tf)$/i;
103
109
 
104
110
  const BINARY_PROBE_BYTES = 8192;
105
111
  const DEFAULT_MAX_SCAN_BYTES = 5 * 1024 * 1024;
@@ -342,6 +348,14 @@ async function runTest(args: TtsrTestArgs, json: boolean, cwd: string): Promise<
342
348
  args.source ?? (filePath && SOURCE_FILE_EXT.test(path.extname(filePath)) ? "tool" : "text");
343
349
  const tool = args.tool ?? (source === "tool" ? "edit" : undefined);
344
350
 
351
+ // A supplied source file whose extension is unknown falls through to the
352
+ // text (prose) context, where tool-scoped rules can never match. Surface
353
+ // that so a false negative reads as a context mismatch, not a bad regex.
354
+ const inferenceNote =
355
+ !args.source && filePath && source === "text"
356
+ ? `inferred --source text from '${path.extname(filePath) || filePath}' (not in the source-file extension set); pass --source tool --tool edit to evaluate tool-scoped rules`
357
+ : undefined;
358
+
345
359
  const context: TtsrMatchContext = {
346
360
  source,
347
361
  toolName: tool,
@@ -373,6 +387,7 @@ async function runTest(args: TtsrTestArgs, json: boolean, cwd: string): Promise<
373
387
  evaluated: rules.length,
374
388
  triggered,
375
389
  notTriggered,
390
+ inferenceNote,
376
391
  };
377
392
 
378
393
  if (json) {
@@ -390,6 +405,9 @@ function renderTestReport(report: TestReport, verbose: boolean, isolated: boolea
390
405
  `${chalk.bold("TTSR test")} — source=${chalk.cyan(ctxLabel)}${pathLabel} snippet=${chalk.dim(`${report.snippetBytes}b`)}\n`,
391
406
  );
392
407
  process.stdout.write(`${chalk.dim(` "${report.snippetPreview}"`)}\n\n`);
408
+ if (report.inferenceNote) {
409
+ process.stdout.write(`${chalk.yellow(`note: ${report.inferenceNote}`)}\n\n`);
410
+ }
393
411
 
394
412
  if (report.triggered.length === 0) {
395
413
  process.stdout.write(`${chalk.red("No rules triggered.")} (evaluated ${report.evaluated})\n`);
@@ -231,6 +231,9 @@ export async function downloadVerifiedBinary(options: VerifiedBinaryDownloadOpti
231
231
  await fs.promises.chmod(options.targetPath, 0o755);
232
232
  } catch (err) {
233
233
  await unlinkIfExists(options.targetPath);
234
+ if (isTimeoutError(err)) {
235
+ throw new Error("Timed out downloading release binary after 15 minutes", { cause: err });
236
+ }
234
237
  throw err;
235
238
  }
236
239
  }
@@ -560,9 +560,34 @@ function formatReloginDeadline(
560
560
  * automatically (refresh failure, upstream invalidation). Rows the user
561
561
  * replaced or deleted deliberately are lifecycle noise, not lost capacity.
562
562
  */
563
- function isActionableDisable(summary: DisabledCredentialSummary): boolean {
563
+ function isActionableDisable(summary: DisabledCredentialSummary, activeAccounts: UsageAccountIdentity[] = []): boolean {
564
564
  if (summary.type !== "oauth") return false;
565
- return !/^(replaced by|deleted by user)/i.test(summary.cause);
565
+ if (/^(replaced by|deleted by user)/i.test(summary.cause)) return false;
566
+
567
+ // Do not display tombstone if there is an active account for the same provider
568
+ // matching the same identity (email, accountId, or org).
569
+ const summaryEmail = summary.email?.toLowerCase();
570
+ const summaryAccountId = summary.accountId?.toLowerCase();
571
+ const summaryOrgId = summary.orgId?.toLowerCase();
572
+
573
+ const matchesActive = activeAccounts.some(account => {
574
+ if (account.provider !== summary.provider) return false;
575
+
576
+ const accountEmail = account.email?.toLowerCase();
577
+ const accountAccountId = account.accountId?.toLowerCase();
578
+ const accountOrgId = account.orgId?.toLowerCase();
579
+
580
+ // If email or accountId match, it's the same identity
581
+ if (summaryEmail && accountEmail && summaryEmail === accountEmail) return true;
582
+ if (summaryAccountId && accountAccountId && summaryAccountId === accountAccountId) return true;
583
+
584
+ // Fallback: if orgId matches and neither email nor accountId contradicts
585
+ if (summaryOrgId && accountOrgId && summaryOrgId === accountOrgId) return true;
586
+
587
+ return false;
588
+ });
589
+
590
+ return !matchesActive;
566
591
  }
567
592
 
568
593
  /** Human-sized disable cause: the upstream `error_description` when embedded, else the first clause. */
@@ -610,7 +635,7 @@ export function formatUsageBreakdown(
610
635
  }
611
636
  const disabledByProvider = new Map<string, DisabledCredentialSummary[]>();
612
637
  for (const summary of disabled) {
613
- if (!isActionableDisable(summary)) continue;
638
+ if (!isActionableDisable(summary, accounts)) continue;
614
639
  const list = disabledByProvider.get(summary.provider) ?? [];
615
640
  list.push(summary);
616
641
  disabledByProvider.set(summary.provider, list);
@@ -1018,7 +1043,7 @@ export async function runUsageCommand(cmd: UsageCommandArgs): Promise<void> {
1018
1043
  const stats = computeProviderWindowStats(filteredReports.filter(peer => peer.provider === report.provider));
1019
1044
  if (stats.length > 0) capacity[report.provider] = stats;
1020
1045
  }
1021
- let disabledForJson = disabled.filter(isActionableDisable);
1046
+ let disabledForJson = disabled.filter(summary => isActionableDisable(summary, accounts));
1022
1047
  if (redaction) {
1023
1048
  disabledForJson = disabledForJson.map(summary => ({
1024
1049
  ...summary,
@@ -8,8 +8,10 @@
8
8
  * `<parent-repo>/.git/worktrees/<name>/`.
9
9
  * - **Task-isolation dirs** (`task/worktree.ts`): a wrapper dir with a
10
10
  * compact `m` subdir mounted/cloned by `natives.isoStart`. Legacy `merged`
11
- * subdirs are still recognized. These are ephemeral; `ensureIsolation`
12
- * removes the base before re-creating it, so leftovers are crashed runs.
11
+ * subdirs are still recognized. `ensureIsolation` writes an ownership
12
+ * marker naming the live omp process; a
13
+ * sandbox whose owner is still running is reported `live` and never
14
+ * removed without `--all`, so `clear` reclaims only crashed leftovers.
13
15
  *
14
16
  * Legacy entries from before the encoding change keep working because git still
15
17
  * tracks them by branch name. This command exists to GC them on demand.
@@ -18,6 +20,7 @@ import * as fs from "node:fs/promises";
18
20
  import * as path from "node:path";
19
21
  import { getWorktreesDir, isEnoent } from "@oh-my-pi/pi-utils";
20
22
  import chalk from "chalk";
23
+ import { hasLiveIsolationOwner, ISOLATION_OWNER_FILE } from "../task/isolation-ownership";
21
24
  import * as git from "../utils/git";
22
25
 
23
26
  type WorktreeKind = "pr-checkout" | "task-isolation" | "empty" | "stray";
@@ -211,16 +214,30 @@ async function classifyDir(dir: string): Promise<WorktreeEntry | null> {
211
214
  if (gitStat?.isFile()) {
212
215
  return classifyPrCheckout(dir, gitEntry);
213
216
  }
214
- for (const mountDir of TASK_ISOLATION_MOUNT_DIRS) {
215
- const mountStat = await fs.stat(path.join(dir, mountDir)).catch(() => null);
216
- if (!mountStat?.isDirectory()) continue;
217
- return {
218
- path: dir,
219
- kind: "task-isolation",
220
- orphanReason: "task-isolation leftover (no live task owns it)",
221
- };
217
+ // A task-isolation sandbox is identified by its ownership marker — written
218
+ // before the backend materialises the mount — or by the `m`/`merged` mount
219
+ // dir itself (legacy dirs and crashed pre-marker runs). Recognizing the
220
+ // marker alone keeps an in-progress sandbox from being mistaken for a stray
221
+ // during the window between marker creation and mount materialisation.
222
+ let isIsolation = await Bun.file(path.join(dir, ISOLATION_OWNER_FILE)).exists();
223
+ if (!isIsolation) {
224
+ for (const mountDir of TASK_ISOLATION_MOUNT_DIRS) {
225
+ const mountStat = await fs.stat(path.join(dir, mountDir)).catch(() => null);
226
+ if (mountStat?.isDirectory()) {
227
+ isIsolation = true;
228
+ break;
229
+ }
230
+ }
222
231
  }
223
- return null;
232
+ if (!isIsolation) return null;
233
+ const live = await hasLiveIsolationOwner(dir);
234
+ return {
235
+ path: dir,
236
+ kind: "task-isolation",
237
+ // Only after confirming no live owner is the "no live task" claim true.
238
+ // A running subagent's sandbox stays live so `clear` won't delete it.
239
+ orphanReason: live ? undefined : "task-isolation leftover (no live task owns it)",
240
+ };
224
241
  }
225
242
 
226
243
  async function classifyPrCheckout(dir: string, gitEntry: string): Promise<WorktreeEntry> {
@@ -18,6 +18,7 @@ export const commands: CommandEntry[] = [
18
18
  { name: "auth-gateway", load: () => import("./commands/auth-gateway").then(m => m.default) },
19
19
  { name: "agents", load: () => import("./commands/agents").then(m => m.default) },
20
20
  { name: "bench", load: () => import("./commands/bench").then(m => m.default) },
21
+ { name: "cleanse", load: () => import("./commands/cleanse").then(m => m.default) },
21
22
  { name: "commit", load: () => import("./commands/commit").then(m => m.default) },
22
23
  { name: "completions", load: () => import("./commands/completions").then(m => m.default) },
23
24
  { name: "__complete", load: () => import("./commands/complete").then(m => m.default) },
package/src/cli.ts CHANGED
@@ -25,12 +25,14 @@ import {
25
25
  VERSION,
26
26
  } from "@oh-my-pi/pi-utils/dirs";
27
27
  import { interceptUnhandledRejections } from "@oh-my-pi/pi-utils/postmortem";
28
+ import { setProcessName } from "@oh-my-pi/pi-utils/process-name";
28
29
  import { declareWorkerHostEntry, installWorkerInbox, isWorkerHostSelector } from "@oh-my-pi/pi-utils/worker-host";
29
30
  import { installProfileAlias, resolveProfileAliasCommandFromProcess } from "./cli/profile-alias";
30
31
  import { extractProfileFlags } from "./cli/profile-bootstrap";
31
32
  import { startJsEvalProcess } from "./eval/js/process-entry";
32
33
  import type { WorkerInbound as JsWorkerInbound, WorkerOutbound as JsWorkerOutbound } from "./eval/js/worker-protocol";
33
34
  import { DAEMON_BROKER_WORKER_ARG } from "./launch/protocol";
35
+ import { TERMINAL_OUTPUT_WORKER_ARG } from "./launch/terminal-output-worker-protocol";
34
36
  import { COMPUTER_WORKER_ARG } from "./tools/computer/protocol";
35
37
  import { smokeTestComputerWorker } from "./tools/computer/supervisor";
36
38
  import { startComputerWorker } from "./tools/computer/worker-entry";
@@ -42,7 +44,7 @@ if (Bun.semver.order(Bun.version, MIN_BUN_VERSION) < 0) {
42
44
  process.exit(1);
43
45
  }
44
46
 
45
- process.title = APP_NAME;
47
+ setProcessName(APP_NAME);
46
48
 
47
49
  // `Bun.build`-API compiled Windows executables report `import.meta.main ===
48
50
  // false`: the standalone loader keys the entry module with native backslashes
@@ -87,6 +89,7 @@ async function runSmokeTest(): Promise<void> {
87
89
  const { smokeTestJsEvalWorker } = await import("./eval/js/context-manager");
88
90
  // Other smoke dependencies stay lazy so normal CLI startup does not load their worker clients.
89
91
  const { smokeTestDaemonBroker } = await import("./launch/client");
92
+ const { smokeTestTerminalOutputWorker } = await import("./launch/terminal-output-worker-client");
90
93
  await smokeTestSyncWorker();
91
94
 
92
95
  const statsServer = await startServer(0);
@@ -108,6 +111,7 @@ async function runSmokeTest(): Promise<void> {
108
111
  await smokeTestTtsWorker();
109
112
  await smokeTestMnemopiEmbedWorker();
110
113
  await smokeTestDaemonBroker();
114
+ await smokeTestTerminalOutputWorker();
111
115
  process.stdout.write("smoke-test: ok\n");
112
116
  }
113
117
 
@@ -194,6 +198,12 @@ async function runWorkerEntrypoint(arg: string | undefined): Promise<boolean> {
194
198
  await runIpcSubprocessWorker(startMnemopiEmbedWorker);
195
199
  return true;
196
200
  }
201
+ if (arg === TERMINAL_OUTPUT_WORKER_ARG) {
202
+ if (parentPort) installWorkerInbox(parentPort);
203
+ // This selector is the isolation boundary; a static import would evaluate xterm in normal CLI startup.
204
+ await import("./launch/terminal-output-worker");
205
+ return true;
206
+ }
197
207
  if (arg === DAEMON_BROKER_WORKER_ARG) {
198
208
  // Worker selectors must dispatch before the normal command graph loads.
199
209
  const { startDaemonBrokerFromEnvironment } = await import("./launch/broker");
@@ -85,14 +85,15 @@ interface PendingSnapshot {
85
85
  /** Minimal context surface the idle-state reconciler mutates. */
86
86
  export interface GuestIdleReconcilerCtx {
87
87
  statusLine: { markActivityEnd: () => void };
88
+ statusContainer: Pick<InteractiveModeContext["statusContainer"], "disposeChildren">;
88
89
  loadingAnimation: { stop: () => void } | undefined;
89
90
  }
90
91
 
91
92
  /**
92
93
  * Close the guest UI state held open by an earlier `agent_start` whose
93
94
  * matching `agent_end` never reached us — most often because a reconnect
94
- * dropped the event mid-stream. Triggered from {@link CollabGuestLink}'s
95
- * `state` reconciler when the host reports `isStreaming === false`:
95
+ * dropped the event mid-stream. Reached via {@link reconcileGuestSnapshotHostState}
96
+ * (the live `state`-frame and welcome/resync reconciler) when the host reports `isStreaming === false`:
96
97
  * folds the in-flight active-time window into the per-session meter (so
97
98
  * `time_spent` stops ticking) and stops the `Working…` loader if one is
98
99
  * still animating. No-op when the host is still streaming.
@@ -106,17 +107,49 @@ export function reconcileGuestIdleHostState(ctx: GuestIdleReconcilerCtx, isStrea
106
107
  if (ctx.loadingAnimation) {
107
108
  ctx.loadingAnimation.stop();
108
109
  ctx.loadingAnimation = undefined;
110
+ ctx.statusContainer.disposeChildren();
109
111
  }
110
112
  }
111
113
 
112
114
  /** Reconcile a welcome/resync snapshot's host activity state into the guest meter. */
113
115
  export interface GuestSnapshotActivityReconcilerCtx extends GuestIdleReconcilerCtx {
114
116
  statusLine: GuestIdleReconcilerCtx["statusLine"] & { markActivityStart: () => void };
117
+ /**
118
+ * Start (or re-attach) the live "Working…" loader. Mirrors
119
+ * `InteractiveModeContext.ensureLoadingAnimation`, which is what
120
+ * `EventController` calls on `agent_start`. Required so a guest that
121
+ * missed an earlier `agent_start` (a reconnect dropped it mid-stream)
122
+ * starts its spinner when the host later reports it is streaming.
123
+ */
124
+ ensureLoadingAnimation: InteractiveModeContext["ensureLoadingAnimation"];
125
+ autoCompactionLoader: InteractiveModeContext["autoCompactionLoader"];
126
+ retryLoader: InteractiveModeContext["retryLoader"];
127
+ }
128
+
129
+ /** Status-area state which cannot outlive removal of its child components. */
130
+ export interface GuestTransientStatusCtx {
131
+ statusContainer: Pick<InteractiveModeContext["statusContainer"], "clear">;
132
+ autoCompactionLoader: InteractiveModeContext["autoCompactionLoader"];
133
+ retryLoader: InteractiveModeContext["retryLoader"];
134
+ }
135
+
136
+ /** Stop and forget status-area loaders before detaching their components. */
137
+ export function clearGuestTransientStatus(ctx: GuestTransientStatusCtx): void {
138
+ if (ctx.autoCompactionLoader) {
139
+ ctx.autoCompactionLoader.stop();
140
+ ctx.autoCompactionLoader = undefined;
141
+ }
142
+ if (ctx.retryLoader) {
143
+ ctx.retryLoader.stop();
144
+ ctx.retryLoader = undefined;
145
+ }
146
+ ctx.statusContainer.clear();
115
147
  }
116
148
 
117
149
  export function reconcileGuestSnapshotHostState(ctx: GuestSnapshotActivityReconcilerCtx, isStreaming: boolean): void {
118
150
  if (isStreaming) {
119
151
  ctx.statusLine.markActivityStart();
152
+ if (!ctx.autoCompactionLoader && !ctx.retryLoader) ctx.ensureLoadingAnimation();
120
153
  return;
121
154
  }
122
155
  reconcileGuestIdleHostState(ctx, false);
@@ -482,7 +515,7 @@ export class CollabGuestLink {
482
515
  this.#applyHostState(frame.state);
483
516
  setSessionTerminalTitle(frame.state.sessionName, frame.state.cwd);
484
517
  this.#updateStatusSegment();
485
- reconcileGuestIdleHostState(this.#ctx, frame.state.isStreaming);
518
+ reconcileGuestSnapshotHostState(this.#ctx, frame.state.isStreaming);
486
519
  this.#ctx.statusLine.invalidate();
487
520
  this.#ctx.ui.requestRender();
488
521
  break;
@@ -681,7 +714,7 @@ export class CollabGuestLink {
681
714
 
682
715
  #clearTransientUi(): void {
683
716
  this.#clearUiRequests();
684
- this.#ctx.statusContainer.clear();
717
+ clearGuestTransientStatus(this.#ctx);
685
718
  this.#ctx.pendingMessagesContainer.clear();
686
719
  this.#ctx.compactionQueuedMessages = [];
687
720
  this.#ctx.streamingComponent = undefined;