tau-coding-agent 0.1.3 → 0.1.5

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 (60) hide show
  1. package/README.md +1 -1
  2. package/extensions/answer.ts +49 -43
  3. package/extensions/branch-term.ts +11 -16
  4. package/extensions/btw.ts +8 -8
  5. package/extensions/ghostty.ts +65 -12
  6. package/extensions/git-diff-stats.ts +1 -1
  7. package/extensions/git-pr-status.ts +1 -1
  8. package/extensions/insights.ts +6 -5
  9. package/extensions/loop.ts +39 -11
  10. package/extensions/memory.ts +6 -5
  11. package/extensions/notify.ts +25 -6
  12. package/extensions/openai-fast.ts +6 -4
  13. package/extensions/openai-verbosity.ts +6 -4
  14. package/extensions/review/fix.ts +239 -0
  15. package/extensions/review/git.ts +517 -0
  16. package/extensions/review/index.ts +232 -0
  17. package/extensions/review/message-queue.ts +249 -0
  18. package/extensions/review/models.ts +589 -0
  19. package/extensions/review/prompts.ts +337 -0
  20. package/extensions/review/renderers/inline.ts +126 -0
  21. package/extensions/review/request.ts +504 -0
  22. package/extensions/review/review.ts +1305 -0
  23. package/extensions/review/runner.ts +522 -0
  24. package/extensions/review/runtime.ts +275 -0
  25. package/extensions/review/schema.ts +160 -0
  26. package/extensions/review/submit-review-tool.ts +66 -0
  27. package/extensions/review/submit-triage-tool.ts +59 -0
  28. package/extensions/review/triage.ts +746 -0
  29. package/extensions/sandbox/index.ts +428 -103
  30. package/extensions/{interlude.ts → stash.ts} +13 -11
  31. package/extensions/tool-display-mode.ts +173 -17
  32. package/extensions/usage/anthropic.ts +1 -1
  33. package/extensions/usage/github-copilot.ts +1 -1
  34. package/extensions/usage/index.ts +9 -5
  35. package/extensions/usage/minimax.ts +1 -1
  36. package/extensions/usage/openai-codex.ts +1 -1
  37. package/extensions/usage/openrouter.ts +1 -1
  38. package/extensions/usage/providers.ts +0 -2
  39. package/extensions/usage/types.ts +1 -2
  40. package/extensions/usage/zai.ts +1 -1
  41. package/extensions/websearch/README.md +1 -1
  42. package/extensions/websearch/browser/chromium.ts +42 -31
  43. package/extensions/websearch/browser/discovery.ts +11 -9
  44. package/extensions/websearch/browser/firefox.ts +10 -7
  45. package/extensions/websearch/browser/fs.ts +10 -0
  46. package/extensions/websearch/browser/sqlite.ts +22 -15
  47. package/extensions/websearch/config.ts +2 -2
  48. package/extensions/websearch/index.ts +9 -9
  49. package/extensions/websearch/providers/anthropic.pi.ts +1 -1
  50. package/extensions/websearch/providers/gemini.pi.ts +1 -1
  51. package/extensions/websearch/providers/openai-codex.pi.ts +1 -1
  52. package/extensions/websearch/providers/pi-model.shared.ts +2 -2
  53. package/extensions/worktree.ts +36 -36
  54. package/package.json +7 -7
  55. package/skills/git-clean-history/SKILL.md +1 -0
  56. package/skills/git-commit/SKILL.md +2 -2
  57. package/skills/oracle/scripts/oracle +28 -12
  58. package/themes/tau-dark.json +2 -1
  59. package/extensions/review.ts +0 -4347
  60. package/extensions/usage/google-gemini-cli.ts +0 -232
package/README.md CHANGED
@@ -27,7 +27,7 @@ pi install -l npm:tau-coding-agent
27
27
  | `git-diff-stats` | — | Status bar diff stats for local changes in the current repo. |
28
28
  | `git-pr-status` | — | Status bar PR number and link for the current branch. |
29
29
  | `insights` | `/insights` | Analyze Pi sessions and suggest reusable instructions, templates, skills, and extensions. |
30
- | `interlude` | `alt+x` | Stash the current message draft, send one interlude message, then restore the draft. |
30
+ | `stash` | `alt+x` | Stash the current message draft, send one message, then restore it. |
31
31
  | `loop` | `/loop` | Repeat a prompt until the agent signals success. |
32
32
  | `memory` | `/memory` | Opt-in project-local memory for learning and continuity across sessions. |
33
33
  | `notify` | — | Terminal notification when the agent is waiting for input. |
@@ -10,20 +10,21 @@
10
10
  * 4. Submits the compiled answers when done
11
11
  */
12
12
 
13
- import { complete, type Model, type Api, type UserMessage } from "@mariozechner/pi-ai";
14
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
15
- import { BorderedLoader } from "@mariozechner/pi-coding-agent";
13
+ import { complete } from "@earendil-works/pi-ai/compat";
14
+ import type { Model, Api, UserMessage } from "@earendil-works/pi-ai";
15
+ import { BorderedLoader, type ExtensionAPI, type Theme } from "@earendil-works/pi-coding-agent";
16
16
  import {
17
17
  type Component,
18
18
  Editor,
19
19
  type EditorTheme,
20
+ type Focusable,
20
21
  Key,
21
22
  matchesKey,
22
23
  truncateToWidth,
23
24
  type TUI,
24
25
  visibleWidth,
25
26
  wrapTextWithAnsi,
26
- } from "@mariozechner/pi-tui";
27
+ } from "@earendil-works/pi-tui";
27
28
 
28
29
  const SYSTEM_PROMPT = `You extract items that need user input from assistant text:
29
30
 
@@ -178,12 +179,13 @@ function parseExtractionResult(text: string): ExtractionResult | null {
178
179
  /**
179
180
  * Interactive Q&A component for answering extracted questions
180
181
  */
181
- class QnAComponent implements Component {
182
+ class QnAComponent implements Component, Focusable {
182
183
  private questions: ExtractedQuestion[];
183
184
  private answers: string[];
184
185
  private currentIndex: number = 0;
185
186
  private editor: Editor;
186
187
  private tui: TUI;
188
+ private theme: Theme;
187
189
  private onDone: (result: string | null) => void;
188
190
  private showingConfirmation: boolean = false;
189
191
 
@@ -191,30 +193,38 @@ class QnAComponent implements Component {
191
193
  private cachedWidth?: number;
192
194
  private cachedLines?: string[];
193
195
 
194
- // Colors - using proper reset sequences
195
- private dim = (s: string) => `\x1b[2m${s}\x1b[0m`;
196
- private bold = (s: string) => `\x1b[1m${s}\x1b[0m`;
197
- private cyan = (s: string) => `\x1b[36m${s}\x1b[0m`;
198
- private green = (s: string) => `\x1b[32m${s}\x1b[0m`;
199
- private yellow = (s: string) => `\x1b[33m${s}\x1b[0m`;
200
- private gray = (s: string) => `\x1b[90m${s}\x1b[0m`;
196
+ private _focused = false;
201
197
 
202
- constructor(questions: ExtractedQuestion[], tui: TUI, onDone: (result: string | null) => void) {
198
+ get focused(): boolean {
199
+ return this._focused;
200
+ }
201
+
202
+ set focused(value: boolean) {
203
+ this._focused = value;
204
+ this.editor.focused = value;
205
+ }
206
+
207
+ constructor(
208
+ questions: ExtractedQuestion[],
209
+ tui: TUI,
210
+ theme: Theme,
211
+ onDone: (result: string | null) => void,
212
+ ) {
203
213
  this.questions = questions;
204
214
  this.answers = questions.map(() => "");
205
215
  this.tui = tui;
216
+ this.theme = theme;
206
217
  this.onDone = onDone;
207
218
 
208
- // Create a minimal theme for the editor
209
- const selectedLine = (s: string) => `\x1b[44m${s}\x1b[0m`;
219
+ const selectedLine = (s: string) => theme.bg("selectedBg", s);
210
220
  const editorTheme: EditorTheme = {
211
- borderColor: this.dim,
221
+ borderColor: (s) => theme.fg("dim", s),
212
222
  selectList: {
213
223
  selectedPrefix: selectedLine,
214
224
  selectedText: selectedLine,
215
- description: this.gray,
216
- scrollInfo: this.gray,
217
- noMatch: this.gray,
225
+ description: (s) => theme.fg("muted", s),
226
+ scrollInfo: (s) => theme.fg("muted", s),
227
+ noMatch: (s) => theme.fg("warning", s),
218
228
  },
219
229
  };
220
230
 
@@ -228,11 +238,6 @@ class QnAComponent implements Component {
228
238
  };
229
239
  }
230
240
 
231
- private allQuestionsAnswered(): boolean {
232
- this.saveCurrentAnswer();
233
- return this.answers.every((a) => (a?.trim() || "").length > 0);
234
- }
235
-
236
241
  private saveCurrentAnswer(): void {
237
242
  this.answers[this.currentIndex] = this.editor.getText();
238
243
  }
@@ -362,6 +367,7 @@ class QnAComponent implements Component {
362
367
  return this.cachedLines;
363
368
  }
364
369
 
370
+ const theme = this.theme;
365
371
  const lines: string[] = [];
366
372
  const boxWidth = Math.min(width - 4, 120); // Allow wider box
367
373
  const contentWidth = boxWidth - 4; // 2 chars padding on each side
@@ -374,11 +380,11 @@ class QnAComponent implements Component {
374
380
  const paddedContent = " ".repeat(leftPad) + content;
375
381
  const contentLen = visibleWidth(paddedContent);
376
382
  const rightPad = Math.max(0, boxWidth - contentLen - 2);
377
- return this.dim("│") + paddedContent + " ".repeat(rightPad) + this.dim("│");
383
+ return theme.fg("dim", "│") + paddedContent + " ".repeat(rightPad) + theme.fg("dim", "│");
378
384
  };
379
385
 
380
386
  const emptyBoxLine = (): string => {
381
- return this.dim("│") + " ".repeat(boxWidth - 2) + this.dim("│");
387
+ return theme.fg("dim", "│") + " ".repeat(boxWidth - 2) + theme.fg("dim", "│");
382
388
  };
383
389
 
384
390
  const padToWidth = (line: string): string => {
@@ -387,10 +393,10 @@ class QnAComponent implements Component {
387
393
  };
388
394
 
389
395
  // Title
390
- lines.push(padToWidth(this.dim("╭" + horizontalLine(boxWidth - 2) + "╮")));
391
- const title = `${this.bold(this.cyan("Questions"))} ${this.dim(`(${this.currentIndex + 1}/${this.questions.length})`)}`;
396
+ lines.push(padToWidth(theme.fg("dim", "╭" + horizontalLine(boxWidth - 2) + "╮")));
397
+ const title = `${theme.bold(theme.fg("accent", "Questions"))} ${theme.fg("dim", `(${this.currentIndex + 1}/${this.questions.length})`)}`;
392
398
  lines.push(padToWidth(boxLine(title)));
393
- lines.push(padToWidth(this.dim("├" + horizontalLine(boxWidth - 2) + "┤")));
399
+ lines.push(padToWidth(theme.fg("dim", "├" + horizontalLine(boxWidth - 2) + "┤")));
394
400
 
395
401
  // Progress indicator
396
402
  const progressParts: string[] = [];
@@ -398,11 +404,11 @@ class QnAComponent implements Component {
398
404
  const answered = (this.answers[i]?.trim() || "").length > 0;
399
405
  const current = i === this.currentIndex;
400
406
  if (current) {
401
- progressParts.push(this.cyan("●"));
407
+ progressParts.push(theme.fg("accent", "●"));
402
408
  } else if (answered) {
403
- progressParts.push(this.green("●"));
409
+ progressParts.push(theme.fg("success", "●"));
404
410
  } else {
405
- progressParts.push(this.dim("○"));
411
+ progressParts.push(theme.fg("dim", "○"));
406
412
  }
407
413
  }
408
414
  lines.push(padToWidth(boxLine(progressParts.join(" "))));
@@ -419,7 +425,7 @@ class QnAComponent implements Component {
419
425
  // Context if present
420
426
  if (q.context) {
421
427
  lines.push(padToWidth(emptyBoxLine()));
422
- const contextText = this.gray(`${q.context}`);
428
+ const contextText = theme.fg("muted", `${q.context}`);
423
429
  const wrappedContext = wrapTextWithAnsi(contextText, contentWidth - 2);
424
430
  for (const line of wrappedContext) {
425
431
  lines.push(padToWidth(boxLine(line)));
@@ -430,7 +436,7 @@ class QnAComponent implements Component {
430
436
 
431
437
  // Render the editor component (multi-line input) with padding
432
438
  // Skip the first and last lines (editor's own border lines)
433
- const answerPrefix = this.bold("A: ");
439
+ const answerPrefix = theme.bold("A: ");
434
440
  const editorWidth = contentWidth - 4 - 3; // Extra padding + space for "A: "
435
441
  const editorLines = this.editor.render(editorWidth);
436
442
  for (let i = 1; i < editorLines.length - 1; i++) {
@@ -447,15 +453,15 @@ class QnAComponent implements Component {
447
453
 
448
454
  // Confirmation dialog or footer with controls
449
455
  if (this.showingConfirmation) {
450
- lines.push(padToWidth(this.dim("├" + horizontalLine(boxWidth - 2) + "┤")));
451
- const confirmMsg = `${this.yellow("Submit all answers?")} ${this.dim("(Enter/y to confirm, Esc/n to cancel)")}`;
456
+ lines.push(padToWidth(theme.fg("dim", "├" + horizontalLine(boxWidth - 2) + "┤")));
457
+ const confirmMsg = `${theme.fg("warning", "Submit all answers?")} ${theme.fg("dim", "(Enter/y to confirm, Esc/n to cancel)")}`;
452
458
  lines.push(padToWidth(boxLine(truncateToWidth(confirmMsg, contentWidth))));
453
459
  } else {
454
- lines.push(padToWidth(this.dim("├" + horizontalLine(boxWidth - 2) + "┤")));
455
- const controls = `${this.dim("Tab/Enter")} next · ${this.dim("Shift+Tab")} prev · ${this.dim("Shift+Enter")} newline · ${this.dim("Esc")} cancel`;
460
+ lines.push(padToWidth(theme.fg("dim", "├" + horizontalLine(boxWidth - 2) + "┤")));
461
+ const controls = `${theme.fg("dim", "Tab/Enter")} next · ${theme.fg("dim", "Shift+Tab")} prev · ${theme.fg("dim", "Shift+Enter")} newline · ${theme.fg("dim", "Esc")} cancel`;
456
462
  lines.push(padToWidth(boxLine(truncateToWidth(controls, contentWidth))));
457
463
  }
458
- lines.push(padToWidth(this.dim("╰" + horizontalLine(boxWidth - 2) + "╯")));
464
+ lines.push(padToWidth(theme.fg("dim", "╰" + horizontalLine(boxWidth - 2) + "╯")));
459
465
 
460
466
  this.cachedWidth = width;
461
467
  this.cachedLines = lines;
@@ -467,8 +473,8 @@ export default function (pi: ExtensionAPI) {
467
473
  pi.registerCommand("answer", {
468
474
  description: "Extract questions from last assistant message into interactive Q&A",
469
475
  handler: async (_args, ctx) => {
470
- if (!ctx.hasUI) {
471
- ctx.ui.notify("answer requires interactive mode", "error");
476
+ if (ctx.mode !== "tui") {
477
+ ctx.ui.notify("answer requires TUI mode", "error");
472
478
  return;
473
479
  }
474
480
 
@@ -571,8 +577,8 @@ export default function (pi: ExtensionAPI) {
571
577
  }
572
578
 
573
579
  // Show the Q&A component
574
- const answersResult = await ctx.ui.custom<string | null>((tui, _theme, _kb, done) => {
575
- return new QnAComponent(extractionResult.questions, tui, done);
580
+ const answersResult = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
581
+ return new QnAComponent(extractionResult.questions, tui, theme, done);
576
582
  });
577
583
 
578
584
  if (answersResult === null) {
@@ -2,18 +2,18 @@ import { spawn, spawnSync } from "node:child_process";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import * as fs from "node:fs";
4
4
  import * as path from "node:path";
5
- import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
5
+ import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
6
6
  import {
7
7
  CURRENT_SESSION_VERSION,
8
8
  SessionManager,
9
9
  highlightCode,
10
10
  type SessionHeader,
11
- } from "@mariozechner/pi-coding-agent";
12
- import { Box, Text } from "@mariozechner/pi-tui";
11
+ } from "@earendil-works/pi-coding-agent";
12
+ import { Box, Text } from "@earendil-works/pi-tui";
13
13
 
14
14
  const TERMINAL_FLAG = "branch-term";
15
15
  const TMUX_LAYOUT_FLAG = "branch-tmux-layout";
16
- const BRANCH_MESSAGE_TYPE = "branch-term-message";
16
+ const BRANCH_ENTRY_TYPE = "branch-term-message";
17
17
  const MANUAL_RESUME_INTRO =
18
18
  "Branch session ready. Run this command in a separate terminal or tmux pane";
19
19
 
@@ -220,15 +220,10 @@ function showCommandMessage(
220
220
  return;
221
221
  }
222
222
 
223
- pi.sendMessage({
224
- customType: BRANCH_MESSAGE_TYPE,
225
- content: intro,
226
- display: true,
227
- details: {
228
- intro,
229
- command,
230
- copiedToClipboard,
231
- } satisfies CommandMessageDetails,
223
+ pi.appendEntry<CommandMessageDetails>(BRANCH_ENTRY_TYPE, {
224
+ intro,
225
+ command,
226
+ copiedToClipboard,
232
227
  });
233
228
  }
234
229
 
@@ -266,10 +261,10 @@ function createFreshSessionFile(cwd: string, sessionDir: string): string {
266
261
  }
267
262
 
268
263
  export default function (pi: ExtensionAPI) {
269
- pi.registerMessageRenderer(BRANCH_MESSAGE_TYPE, (message, _options, theme) => {
270
- const details = message.details as CommandMessageDetails | undefined;
264
+ pi.registerEntryRenderer<CommandMessageDetails>(BRANCH_ENTRY_TYPE, (entry, _options, theme) => {
265
+ const details = entry.data;
271
266
  if (!details || typeof details.intro !== "string" || typeof details.command !== "string") {
272
- return new Text(typeof message.content === "string" ? message.content : "", 0, 0);
267
+ return undefined;
273
268
  }
274
269
 
275
270
  const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
package/extensions/btw.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type { AgentMessage } from "@mariozechner/pi-agent-core";
2
- import type { AssistantMessage, ThinkingLevel } from "@mariozechner/pi-ai";
1
+ import type { AgentMessage } from "@earendil-works/pi-agent-core";
2
+ import type { AssistantMessage, ThinkingLevel } from "@earendil-works/pi-ai";
3
3
  import {
4
4
  DefaultResourceLoader,
5
5
  SessionManager,
@@ -11,7 +11,7 @@ import {
11
11
  type ExtensionCommandContext,
12
12
  type ExtensionContext,
13
13
  type Theme,
14
- } from "@mariozechner/pi-coding-agent";
14
+ } from "@earendil-works/pi-coding-agent";
15
15
  import {
16
16
  Key,
17
17
  Markdown,
@@ -21,7 +21,7 @@ import {
21
21
  type TUI,
22
22
  visibleWidth,
23
23
  wrapTextWithAnsi,
24
- } from "@mariozechner/pi-tui";
24
+ } from "@earendil-works/pi-tui";
25
25
 
26
26
  const STATUS_KEY = "0-btw";
27
27
  const RESULT_MARKDOWN_THEME = getMarkdownTheme();
@@ -116,8 +116,8 @@ export default function btwExtension(pi: ExtensionAPI): void {
116
116
  return;
117
117
  }
118
118
 
119
- if (!ctx.hasUI) {
120
- ctx.ui.notify("btw requires interactive mode", "error");
119
+ if (ctx.mode !== "tui") {
120
+ ctx.ui.notify("btw requires TUI mode", "error");
121
121
  return;
122
122
  }
123
123
 
@@ -278,8 +278,8 @@ function seedSessionManager(sessionManager: SessionManager, messages: AgentMessa
278
278
  }
279
279
 
280
280
  async function showResultDialog(ctx: ExtensionContext, result: BtwResult): Promise<void> {
281
- await ctx.ui.custom<void>((tui, _theme, _keybindings, done) => {
282
- return new BtwResultComponent(result, tui, ctx.ui.theme, done);
281
+ await ctx.ui.custom<void>((tui, theme, _keybindings, done) => {
282
+ return new BtwResultComponent(result, tui, theme, done);
283
283
  });
284
284
  }
285
285
 
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * - Shows project/session in the terminal title
5
5
  * - Shows a braille spinner in the title while the agent is working
6
+ * - Shows the braille spinner backwards while context is compacting
6
7
  * - Shows a ? marker while an extension prompt is waiting for input
7
8
  * - Updates title with the current tool name during tool execution
8
9
  * - Treats background /review runs as working state via review:start/review:end events
@@ -10,7 +11,7 @@
10
11
  * Pi now emits native OSC 9;4 progress indicators, so this extension only manages the title.
11
12
  */
12
13
 
13
- import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
14
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
14
15
  import path from "node:path";
15
16
 
16
17
  const STATUS_SPINNER_INTERVAL_MS = 80;
@@ -22,6 +23,7 @@ let sessionName: string | undefined;
22
23
  let sessionCwd: string | undefined;
23
24
  let currentTool: string | undefined;
24
25
  let isWorking = false;
26
+ let isCompacting = false;
25
27
  let frameIndex = 0;
26
28
  let spinnerTimer: ReturnType<typeof setInterval> | undefined;
27
29
  let pendingPromptCount = 0;
@@ -47,6 +49,15 @@ function currentFrame(): string {
47
49
  return STATUS_SPINNER_FRAMES[frameIndex % STATUS_SPINNER_FRAMES.length];
48
50
  }
49
51
 
52
+ function resetFrame(): void {
53
+ frameIndex = 0;
54
+ }
55
+
56
+ function advanceFrame(): void {
57
+ const step = isCompacting ? -1 : 1;
58
+ frameIndex = (frameIndex + step + STATUS_SPINNER_FRAMES.length) % STATUS_SPINNER_FRAMES.length;
59
+ }
60
+
50
61
  function hasPendingPrompts(): boolean {
51
62
  return pendingPromptCount > 0;
52
63
  }
@@ -69,11 +80,12 @@ function hasActiveReviewRuns(): boolean {
69
80
  }
70
81
 
71
82
  function isBusy(): boolean {
72
- return isWorking || hasActiveReviewRuns();
83
+ return isWorking || isCompacting || hasActiveReviewRuns();
73
84
  }
74
85
 
75
86
  function getWorkingExtra(): string | undefined {
76
87
  if (currentTool) return currentTool;
88
+ if (isCompacting) return "compacting";
77
89
  if (!isWorking && hasActiveReviewRuns()) return "review";
78
90
  return undefined;
79
91
  }
@@ -105,7 +117,7 @@ function startSpinnerTimer(ctx: ExtensionContext): void {
105
117
  clearSpinnerTimer();
106
118
  spinnerTimer = setInterval(() => {
107
119
  if (!isBusy() || hasPendingPrompts()) return;
108
- frameIndex = (frameIndex + 1) % STATUS_SPINNER_FRAMES.length;
120
+ advanceFrame();
109
121
  renderWorkingTitle(ctx);
110
122
  }, STATUS_SPINNER_INTERVAL_MS);
111
123
  }
@@ -114,7 +126,7 @@ function startSpinner(ctx: ExtensionContext): void {
114
126
  clearSpinnerTimer();
115
127
  isWorking = true;
116
128
  currentTool = undefined;
117
- frameIndex = 0;
129
+ resetFrame();
118
130
  renderActiveTitle(ctx);
119
131
 
120
132
  if (!hasPendingPrompts()) {
@@ -132,7 +144,7 @@ function stopSpinner(ctx: ExtensionContext): void {
132
144
  return;
133
145
  }
134
146
 
135
- if (hasActiveReviewRuns()) {
147
+ if (isBusy()) {
136
148
  renderWorkingTitle(ctx);
137
149
  startSpinnerTimer(ctx);
138
150
  return;
@@ -161,6 +173,38 @@ function handlePromptEnd(ctx: ExtensionContext): void {
161
173
  renderActiveTitle(ctx);
162
174
  }
163
175
 
176
+ function startCompaction(ctx: ExtensionContext, signal: AbortSignal): void {
177
+ isWorking = false;
178
+ isCompacting = true;
179
+ currentTool = undefined;
180
+ resetFrame();
181
+ signal.addEventListener("abort", () => stopCompaction(ctx), { once: true });
182
+
183
+ renderActiveTitle(ctx);
184
+ if (!hasPendingPrompts()) {
185
+ startSpinnerTimer(ctx);
186
+ }
187
+ }
188
+
189
+ function stopCompaction(ctx: ExtensionContext, options?: { render?: boolean }): void {
190
+ if (!isCompacting) return;
191
+
192
+ isCompacting = false;
193
+
194
+ if (options?.render === false) return;
195
+
196
+ if (isBusy()) {
197
+ renderActiveTitle(ctx);
198
+ if (!hasPendingPrompts()) {
199
+ startSpinnerTimer(ctx);
200
+ }
201
+ return;
202
+ }
203
+
204
+ clearSpinnerTimer();
205
+ renderActiveTitle(ctx);
206
+ }
207
+
164
208
  function syncSessionTitle(ctx: ExtensionContext): void {
165
209
  renderActiveTitle(ctx);
166
210
  }
@@ -181,17 +225,12 @@ function handleReviewStart(ctx: ExtensionContext): void {
181
225
  }
182
226
 
183
227
  function handleReviewEnd(ctx: ExtensionContext): void {
184
- if (isWorking) {
185
- renderActiveTitle(ctx);
186
- return;
187
- }
188
-
189
228
  if (hasPendingPrompts()) {
190
229
  renderActiveTitle(ctx);
191
230
  return;
192
231
  }
193
232
 
194
- if (hasActiveReviewRuns()) {
233
+ if (isBusy()) {
195
234
  renderActiveTitle(ctx);
196
235
  startSpinnerTimer(ctx);
197
236
  return;
@@ -218,10 +257,11 @@ export default function (pi: ExtensionAPI) {
218
257
  pi.on("agent_start", async (_event, ctx) => {
219
258
  if (!ctx.hasUI) return;
220
259
  latestCtx = ctx;
260
+ stopCompaction(ctx, { render: false });
221
261
  startSpinner(ctx);
222
262
  });
223
263
 
224
- pi.on("agent_end", async (_event, ctx) => {
264
+ pi.on("agent_settled", async (_event, ctx) => {
225
265
  if (!ctx.hasUI) return;
226
266
  latestCtx = ctx;
227
267
  stopSpinner(ctx);
@@ -243,6 +283,18 @@ export default function (pi: ExtensionAPI) {
243
283
  renderActiveTitle(ctx);
244
284
  });
245
285
 
286
+ pi.on("session_before_compact", async (event, ctx) => {
287
+ if (!ctx.hasUI) return;
288
+ latestCtx = ctx;
289
+ startCompaction(ctx, event.signal);
290
+ });
291
+
292
+ pi.on("session_compact", async (_event, ctx) => {
293
+ if (!ctx.hasUI) return;
294
+ latestCtx = ctx;
295
+ stopCompaction(ctx);
296
+ });
297
+
246
298
  pi.events.on("ui:prompt_start", () => {
247
299
  pendingPromptCount += 1;
248
300
 
@@ -287,6 +339,7 @@ export default function (pi: ExtensionAPI) {
287
339
  pi.on("session_shutdown", async (_event, ctx) => {
288
340
  clearSpinnerTimer();
289
341
  isWorking = false;
342
+ isCompacting = false;
290
343
  currentTool = undefined;
291
344
  pendingPromptCount = 0;
292
345
  const sessionKey = getSessionKey(ctx);
@@ -3,7 +3,7 @@ import {
3
3
  isWriteToolResult,
4
4
  type ExtensionAPI,
5
5
  type ExtensionContext,
6
- } from "@mariozechner/pi-coding-agent";
6
+ } from "@earendil-works/pi-coding-agent";
7
7
  import { spawn } from "node:child_process";
8
8
  import { existsSync } from "node:fs";
9
9
  import { copyFile, mkdtemp, rm } from "node:fs/promises";
@@ -2,7 +2,7 @@ import {
2
2
  isBashToolResult,
3
3
  type ExtensionAPI,
4
4
  type ExtensionContext,
5
- } from "@mariozechner/pi-coding-agent";
5
+ } from "@earendil-works/pi-coding-agent";
6
6
 
7
7
  const STATUS_KEY = "1-git-pr-status";
8
8
 
@@ -1,4 +1,5 @@
1
- import { complete, type Api, type Model, type UserMessage } from "@mariozechner/pi-ai";
1
+ import { complete } from "@earendil-works/pi-ai/compat";
2
+ import type { Api, Model, UserMessage } from "@earendil-works/pi-ai";
2
3
  import {
3
4
  BorderedLoader,
4
5
  SessionManager,
@@ -13,7 +14,7 @@ import {
13
14
  type SessionEntry,
14
15
  type SessionHeader,
15
16
  type Theme,
16
- } from "@mariozechner/pi-coding-agent";
17
+ } from "@earendil-works/pi-coding-agent";
17
18
  import {
18
19
  Key,
19
20
  Markdown,
@@ -23,7 +24,7 @@ import {
23
24
  type TUI,
24
25
  visibleWidth,
25
26
  wrapTextWithAnsi,
26
- } from "@mariozechner/pi-tui";
27
+ } from "@earendil-works/pi-tui";
27
28
  import { createHash, randomBytes } from "node:crypto";
28
29
  import fs from "node:fs/promises";
29
30
  import path from "node:path";
@@ -480,8 +481,8 @@ async function runInsightsCommand(
480
481
  return;
481
482
  }
482
483
 
483
- if (!ctx.hasUI) {
484
- ctx.ui.notify("/insights currently requires interactive mode", "warning");
484
+ if (ctx.mode !== "tui") {
485
+ ctx.ui.notify("/insights currently requires TUI mode", "warning");
485
486
  return;
486
487
  }
487
488