@oh-my-pi/pi-coding-agent 12.19.2 → 13.0.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 (106) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/package.json +7 -7
  3. package/src/commit/prompts/analysis-system.md +3 -3
  4. package/src/commit/prompts/analysis-user.md +14 -14
  5. package/src/commit/prompts/changelog-system.md +4 -4
  6. package/src/commit/prompts/changelog-user.md +4 -4
  7. package/src/commit/prompts/file-observer-system.md +2 -2
  8. package/src/commit/prompts/file-observer-user.md +2 -2
  9. package/src/commit/prompts/reduce-system.md +4 -4
  10. package/src/commit/prompts/reduce-user.md +6 -6
  11. package/src/commit/prompts/summary-system.md +4 -4
  12. package/src/commit/prompts/summary-user.md +6 -6
  13. package/src/config/settings-schema.ts +0 -11
  14. package/src/discovery/helpers.ts +13 -1
  15. package/src/internal-urls/docs-index.generated.ts +2 -2
  16. package/src/internal-urls/index.ts +8 -3
  17. package/src/internal-urls/local-protocol.ts +223 -0
  18. package/src/internal-urls/{docs-protocol.ts → pi-protocol.ts} +12 -12
  19. package/src/internal-urls/router.ts +1 -1
  20. package/src/internal-urls/types.ts +1 -1
  21. package/src/ipy/executor.ts +4 -32
  22. package/src/main.ts +0 -1
  23. package/src/memories/index.ts +1 -1
  24. package/src/modes/components/settings-defs.ts +0 -5
  25. package/src/modes/controllers/event-controller.ts +4 -4
  26. package/src/modes/interactive-mode.ts +84 -64
  27. package/src/modes/types.ts +11 -3
  28. package/src/modes/utils/ui-helpers.ts +5 -3
  29. package/src/patch/hashline.ts +42 -42
  30. package/src/patch/index.ts +24 -21
  31. package/src/patch/shared.ts +21 -43
  32. package/src/plan-mode/approved-plan.ts +55 -0
  33. package/src/prompts/agents/designer.md +6 -6
  34. package/src/prompts/agents/explore.md +4 -4
  35. package/src/prompts/agents/frontmatter.md +1 -0
  36. package/src/prompts/agents/init.md +10 -10
  37. package/src/prompts/agents/plan.md +6 -6
  38. package/src/prompts/agents/reviewer.md +4 -3
  39. package/src/prompts/agents/task.md +10 -10
  40. package/src/prompts/compaction/branch-summary.md +3 -3
  41. package/src/prompts/compaction/compaction-short-summary.md +7 -7
  42. package/src/prompts/compaction/compaction-summary-context.md +1 -1
  43. package/src/prompts/compaction/compaction-summary.md +5 -5
  44. package/src/prompts/compaction/compaction-turn-prefix.md +3 -3
  45. package/src/prompts/compaction/compaction-update-summary.md +11 -11
  46. package/src/prompts/memories/consolidation.md +5 -5
  47. package/src/prompts/memories/read-path.md +11 -0
  48. package/src/prompts/memories/stage_one_input.md +1 -1
  49. package/src/prompts/memories/stage_one_system.md +5 -5
  50. package/src/prompts/review-request.md +4 -4
  51. package/src/prompts/system/agent-creation-architect.md +17 -17
  52. package/src/prompts/system/agent-creation-user.md +2 -2
  53. package/src/prompts/system/custom-system-prompt.md +6 -6
  54. package/src/prompts/system/plan-mode-active.md +20 -20
  55. package/src/prompts/system/plan-mode-approved.md +9 -7
  56. package/src/prompts/system/plan-mode-reference.md +2 -2
  57. package/src/prompts/system/plan-mode-subagent.md +8 -8
  58. package/src/prompts/system/subagent-submit-reminder.md +5 -5
  59. package/src/prompts/system/subagent-system-prompt.md +9 -9
  60. package/src/prompts/system/subagent-user-prompt.md +3 -5
  61. package/src/prompts/system/summarization-system.md +1 -1
  62. package/src/prompts/system/system-prompt.md +109 -84
  63. package/src/prompts/system/title-system.md +2 -2
  64. package/src/prompts/system/ttsr-interrupt.md +2 -2
  65. package/src/prompts/system/web-search.md +16 -16
  66. package/src/prompts/tools/ask.md +6 -6
  67. package/src/prompts/tools/bash.md +9 -9
  68. package/src/prompts/tools/browser.md +5 -5
  69. package/src/prompts/tools/cancel-job.md +2 -2
  70. package/src/prompts/tools/exit-plan-mode.md +13 -10
  71. package/src/prompts/tools/find.md +2 -2
  72. package/src/prompts/tools/gemini-image.md +7 -7
  73. package/src/prompts/tools/grep.md +4 -3
  74. package/src/prompts/tools/hashline.md +37 -39
  75. package/src/prompts/tools/patch.md +5 -5
  76. package/src/prompts/tools/poll-jobs.md +1 -1
  77. package/src/prompts/tools/python.md +8 -10
  78. package/src/prompts/tools/read.md +2 -12
  79. package/src/prompts/tools/replace.md +6 -6
  80. package/src/prompts/tools/ssh.md +2 -7
  81. package/src/prompts/tools/task.md +34 -23
  82. package/src/prompts/tools/todo-write.md +65 -49
  83. package/src/prompts/tools/web-search.md +2 -2
  84. package/src/prompts/tools/write.md +4 -3
  85. package/src/sdk.ts +11 -9
  86. package/src/session/agent-session.ts +92 -51
  87. package/src/session/artifacts.ts +1 -1
  88. package/src/session/messages.ts +1 -0
  89. package/src/task/agents.ts +1 -0
  90. package/src/task/index.ts +2 -1
  91. package/src/task/render.ts +2 -2
  92. package/src/task/types.ts +1 -0
  93. package/src/tools/bash-interactive.ts +1 -1
  94. package/src/tools/bash-skill-urls.ts +3 -2
  95. package/src/tools/bash.ts +38 -19
  96. package/src/tools/exit-plan-mode.ts +30 -2
  97. package/src/tools/grep.ts +131 -75
  98. package/src/tools/index.ts +13 -3
  99. package/src/tools/path-utils.ts +2 -1
  100. package/src/tools/plan-mode-guard.ts +8 -8
  101. package/src/tools/python.ts +0 -2
  102. package/src/tools/read.ts +2 -2
  103. package/src/tools/todo-write.ts +276 -146
  104. package/src/internal-urls/plan-protocol.ts +0 -95
  105. package/src/modes/components/todo-display.ts +0 -114
  106. package/src/prompts/memories/read_path.md +0 -11
@@ -1,23 +1,23 @@
1
1
  /**
2
- * Protocol handler for docs:// URLs.
2
+ * Protocol handler for pi:// URLs.
3
3
  *
4
4
  * Serves statically embedded documentation files bundled at build time.
5
5
  *
6
6
  * URL forms:
7
- * - docs:// - Lists all available documentation files
8
- * - docs://<file>.md - Reads a specific documentation file
7
+ * - pi:// - Lists all available documentation files
8
+ * - pi://<file>.md - Reads a specific documentation file
9
9
  */
10
10
  import * as path from "node:path";
11
11
  import { EMBEDDED_DOC_FILENAMES, EMBEDDED_DOCS } from "./docs-index.generated";
12
12
  import type { InternalResource, InternalUrl, ProtocolHandler } from "./types";
13
13
 
14
14
  /**
15
- * Handler for docs:// URLs.
15
+ * Handler for pi:// URLs.
16
16
  *
17
17
  * Resolves documentation file names to their content, or lists available docs.
18
18
  */
19
- export class DocsProtocolHandler implements ProtocolHandler {
20
- readonly scheme = "docs";
19
+ export class PiProtocolHandler implements ProtocolHandler {
20
+ readonly scheme = "pi";
21
21
 
22
22
  async resolve(url: InternalUrl): Promise<InternalResource> {
23
23
  // Extract filename from host + path
@@ -37,7 +37,7 @@ export class DocsProtocolHandler implements ProtocolHandler {
37
37
  throw new Error("No documentation files found");
38
38
  }
39
39
 
40
- const listing = EMBEDDED_DOC_FILENAMES.map(f => `- [${f}](docs://${f})`).join("\n");
40
+ const listing = EMBEDDED_DOC_FILENAMES.map(f => `- [${f}](pi://${f})`).join("\n");
41
41
  const content = `# Documentation\n\n${EMBEDDED_DOC_FILENAMES.length} files available:\n\n${listing}\n`;
42
42
 
43
43
  return {
@@ -45,19 +45,19 @@ export class DocsProtocolHandler implements ProtocolHandler {
45
45
  content,
46
46
  contentType: "text/markdown",
47
47
  size: Buffer.byteLength(content, "utf-8"),
48
- sourcePath: "docs://",
48
+ sourcePath: "pi://",
49
49
  };
50
50
  }
51
51
 
52
52
  async #readDoc(filename: string, url: InternalUrl): Promise<InternalResource> {
53
53
  // Validate: no traversal, no absolute paths
54
54
  if (path.isAbsolute(filename)) {
55
- throw new Error("Absolute paths are not allowed in docs:// URLs");
55
+ throw new Error("Absolute paths are not allowed in pi:// URLs");
56
56
  }
57
57
 
58
58
  const normalized = path.posix.normalize(filename.replaceAll("\\", "/"));
59
59
  if (normalized === ".." || normalized.startsWith("../") || normalized.includes("/../")) {
60
- throw new Error("Path traversal (..) is not allowed in docs:// URLs");
60
+ throw new Error("Path traversal (..) is not allowed in pi:// URLs");
61
61
  }
62
62
 
63
63
  const content = EMBEDDED_DOCS[normalized];
@@ -69,7 +69,7 @@ export class DocsProtocolHandler implements ProtocolHandler {
69
69
  const suffix =
70
70
  suggestions.length > 0
71
71
  ? `\nDid you mean: ${suggestions.join(", ")}`
72
- : "\nUse docs:// to list available files.";
72
+ : "\nUse pi:// to list available files.";
73
73
  throw new Error(`Documentation file not found: ${filename}${suffix}`);
74
74
  }
75
75
 
@@ -78,7 +78,7 @@ export class DocsProtocolHandler implements ProtocolHandler {
78
78
  content,
79
79
  contentType: "text/markdown",
80
80
  size: Buffer.byteLength(content, "utf-8"),
81
- sourcePath: `docs://${normalized}`,
81
+ sourcePath: `pi://${normalized}`,
82
82
  };
83
83
  }
84
84
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Internal URL router for internal protocols (agent://, artifact://, plan://, memory://, skill://, rule://, docs://).
2
+ * Internal URL router for internal protocols (agent://, artifact://, memory://, skill://, rule://, pi://, local://).
3
3
  */
4
4
  import type { InternalResource, InternalUrl, ProtocolHandler } from "./types";
5
5
 
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Types for the internal URL routing system.
3
3
  *
4
- * Internal URLs (agent://, artifact://, plan://, memory://, skill://, rule://) are resolved by tools like fetch and read,
4
+ * Internal URLs (agent://, artifact://, memory://, skill://, rule://, pi://, local://) are resolved by tools like fetch and read,
5
5
  * providing access to agent outputs and skill files without exposing filesystem paths.
6
6
  */
7
7
 
@@ -39,8 +39,6 @@ export interface PythonExecutorOptions {
39
39
  useSharedGateway?: boolean;
40
40
  /** Session file path for accessing task outputs */
41
41
  sessionFile?: string;
42
- /** Artifacts directory for $ARTIFACTS env var and artifact storage */
43
- artifactsDir?: string;
44
42
  /** Artifact path/id for full output storage */
45
43
  artifactPath?: string;
46
44
  artifactId?: string;
@@ -311,16 +309,9 @@ async function createKernelSession(
311
309
  cwd: string,
312
310
  useSharedGateway?: boolean,
313
311
  sessionFile?: string,
314
- artifactsDir?: string,
315
312
  isRetry?: boolean,
316
313
  ): Promise<KernelSession> {
317
- const env: Record<string, string> | undefined =
318
- sessionFile || artifactsDir
319
- ? {
320
- ...(sessionFile ? { PI_SESSION_FILE: sessionFile } : {}),
321
- ...(artifactsDir ? { ARTIFACTS: artifactsDir } : {}),
322
- }
323
- : undefined;
314
+ const env: Record<string, string> | undefined = sessionFile ? { PI_SESSION_FILE: sessionFile } : undefined;
324
315
 
325
316
  let kernel: PythonKernel;
326
317
  try {
@@ -330,7 +321,7 @@ async function createKernelSession(
330
321
  } catch (err) {
331
322
  if (!isRetry && isResourceExhaustionError(err)) {
332
323
  await recoverFromResourceExhaustion();
333
- return createKernelSession(sessionId, cwd, useSharedGateway, sessionFile, artifactsDir, true);
324
+ return createKernelSession(sessionId, cwd, useSharedGateway, sessionFile, true);
334
325
  }
335
326
  throw err;
336
327
  }
@@ -359,7 +350,6 @@ async function restartKernelSession(
359
350
  cwd: string,
360
351
  useSharedGateway?: boolean,
361
352
  sessionFile?: string,
362
- artifactsDir?: string,
363
353
  ): Promise<void> {
364
354
  session.restartCount += 1;
365
355
  if (session.restartCount > 1) {
@@ -370,13 +360,7 @@ async function restartKernelSession(
370
360
  } catch (err) {
371
361
  logger.warn("Failed to shutdown crashed kernel", { error: err instanceof Error ? err.message : String(err) });
372
362
  }
373
- const env: Record<string, string> | undefined =
374
- sessionFile || artifactsDir
375
- ? {
376
- ...(sessionFile ? { PI_SESSION_FILE: sessionFile } : {}),
377
- ...(artifactsDir ? { ARTIFACTS: artifactsDir } : {}),
378
- }
379
- : undefined;
363
+ const env: Record<string, string> | undefined = sessionFile ? { PI_SESSION_FILE: sessionFile } : undefined;
380
364
  const kernel = await PythonKernel.start({ cwd, useSharedGateway, env });
381
365
  session.kernel = kernel;
382
366
  session.dead = false;
@@ -401,7 +385,6 @@ async function withKernelSession<T>(
401
385
  handler: (kernel: PythonKernel) => Promise<T>,
402
386
  useSharedGateway?: boolean,
403
387
  sessionFile?: string,
404
- artifactsDir?: string,
405
388
  ): Promise<T> {
406
389
  let session = kernelSessions.get(sessionId);
407
390
  if (!session) {
@@ -416,7 +399,6 @@ async function withKernelSession<T>(
416
399
  cwd,
417
400
  useSharedGateway,
418
401
  sessionFile,
419
- artifactsDir,
420
402
  );
421
403
  kernelSessions.set(sessionId, session);
422
404
  startCleanupTimer();
@@ -432,7 +414,6 @@ async function withKernelSession<T>(
432
414
  cwd,
433
415
  useSharedGateway,
434
416
  sessionFile,
435
- artifactsDir,
436
417
  );
437
418
  }
438
419
  try {
@@ -450,7 +431,6 @@ async function withKernelSession<T>(
450
431
  cwd,
451
432
  useSharedGateway,
452
433
  sessionFile,
453
- artifactsDir,
454
434
  );
455
435
  const result = await logger.timeAsync("kernel:postRestart:handler", handler, session!.kernel);
456
436
  session!.restartCount = 0;
@@ -539,16 +519,9 @@ export async function executePython(code: string, options?: PythonExecutorOption
539
519
  const kernelMode = options?.kernelMode ?? "session";
540
520
  const useSharedGateway = options?.useSharedGateway;
541
521
  const sessionFile = options?.sessionFile;
542
- const artifactsDir = options?.artifactsDir;
543
522
 
544
523
  if (kernelMode === "per-call") {
545
- const env: Record<string, string> | undefined =
546
- sessionFile || artifactsDir
547
- ? {
548
- ...(sessionFile ? { PI_SESSION_FILE: sessionFile } : {}),
549
- ...(artifactsDir ? { ARTIFACTS: artifactsDir } : {}),
550
- }
551
- : undefined;
524
+ const env: Record<string, string> | undefined = sessionFile ? { PI_SESSION_FILE: sessionFile } : undefined;
552
525
  const kernel = await PythonKernel.start({ cwd, useSharedGateway, env });
553
526
  try {
554
527
  return await executeWithKernel(kernel, code, options);
@@ -570,6 +543,5 @@ export async function executePython(code: string, options?: PythonExecutorOption
570
543
  async kernel => executeWithKernel(kernel, code, options),
571
544
  useSharedGateway,
572
545
  sessionFile,
573
- artifactsDir,
574
546
  );
575
547
  }
package/src/main.ts CHANGED
@@ -538,7 +538,6 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
538
538
  const cwd = getProjectDir();
539
539
  await logger.timeAsync("settings:init", () => Settings.init({ cwd }));
540
540
  if (parsedArgs.noPty) {
541
- settings.override("bash.virtualTerminal", "off");
542
541
  Bun.env.PI_NO_PTY = "1";
543
542
  }
544
543
  const {
@@ -11,7 +11,7 @@ import { parseModelString } from "../config/model-resolver";
11
11
  import { renderPromptTemplate } from "../config/prompt-templates";
12
12
  import type { Settings } from "../config/settings";
13
13
  import consolidationTemplate from "../prompts/memories/consolidation.md" with { type: "text" };
14
- import readPathTemplate from "../prompts/memories/read_path.md" with { type: "text" };
14
+ import readPathTemplate from "../prompts/memories/read-path.md" with { type: "text" };
15
15
  import stageOneInputTemplate from "../prompts/memories/stage_one_input.md" with { type: "text" };
16
16
  import stageOneSystemTemplate from "../prompts/memories/stage_one_system.md" with { type: "text" };
17
17
  import type { AgentSession } from "../session/agent-session";
@@ -154,11 +154,6 @@ const OPTION_PROVIDERS: Partial<Record<SettingPath, OptionProvider>> = {
154
154
  { value: "tool-only", label: "tool-only", description: "Interrupt only on tool-call argument matches" },
155
155
  { value: "never", label: "never", description: "Never interrupt; inject warning after completion" },
156
156
  ],
157
- // Virtual terminal
158
- "bash.virtualTerminal": [
159
- { value: "on", label: "On", description: "PTY-backed interactive execution" },
160
- { value: "off", label: "Off", description: "Standard non-interactive execution" },
161
- ],
162
157
  // Provider options
163
158
  "providers.webSearch": [
164
159
  {
@@ -7,7 +7,7 @@ import { TodoReminderComponent } from "../../modes/components/todo-reminder";
7
7
  import { ToolExecutionComponent } from "../../modes/components/tool-execution";
8
8
  import { TtsrNotificationComponent } from "../../modes/components/ttsr-notification";
9
9
  import { getSymbolTheme, theme } from "../../modes/theme/theme";
10
- import type { InteractiveModeContext, TodoItem } from "../../modes/types";
10
+ import type { InteractiveModeContext, TodoPhase } from "../../modes/types";
11
11
  import type { AgentSessionEvent } from "../../session/agent-session";
12
12
  import type { ExitPlanModeDetails } from "../../tools";
13
13
 
@@ -288,9 +288,9 @@ export class EventController {
288
288
  }
289
289
  // Update todo display when todo_write tool completes
290
290
  if (event.toolName === "todo_write" && !event.isError) {
291
- const details = event.result.details as { todos?: TodoItem[] } | undefined;
292
- if (details?.todos) {
293
- this.ctx.setTodos(details.todos);
291
+ const details = event.result.details as { phases?: TodoPhase[] } | undefined;
292
+ if (details?.phases) {
293
+ this.ctx.setTodos(details.phases);
294
294
  }
295
295
  } else if (event.toolName === "todo_write" && event.isError) {
296
296
  const textContent = event.result.content.find(
@@ -24,7 +24,8 @@ import { type Settings, settings } from "../config/settings";
24
24
  import type { ExtensionUIContext, ExtensionUIDialogOptions } from "../extensibility/extensions";
25
25
  import type { CompactOptions } from "../extensibility/extensions/types";
26
26
  import { BUILTIN_SLASH_COMMANDS, loadSlashCommands } from "../extensibility/slash-commands";
27
- import { resolvePlanUrlToPath } from "../internal-urls";
27
+ import { resolveLocalUrlToPath } from "../internal-urls";
28
+ import { renameApprovedPlanFile } from "../plan-mode/approved-plan";
28
29
  import planModeApprovedPrompt from "../prompts/system/plan-mode-approved.md" with { type: "text" };
29
30
  import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
30
31
  import { HistoryStorage } from "../session/history-storage";
@@ -54,10 +55,9 @@ import { SSHCommandController } from "./controllers/ssh-command-controller";
54
55
  import { setMermaidRenderCallback } from "./theme/mermaid-cache";
55
56
  import type { Theme } from "./theme/theme";
56
57
  import { getEditorTheme, getMarkdownTheme, onThemeChange, theme } from "./theme/theme";
57
- import type { CompactionQueuedMessage, InteractiveModeContext, TodoItem } from "./types";
58
+ import type { CompactionQueuedMessage, InteractiveModeContext, TodoItem, TodoPhase } from "./types";
58
59
  import { UiHelpers } from "./utils/ui-helpers";
59
60
 
60
- const TODO_FILE_NAME = "todos.json";
61
61
  const EDITOR_MAX_HEIGHT_MIN = 6;
62
62
  const EDITOR_MAX_HEIGHT_MAX = 18;
63
63
  const EDITOR_RESERVED_ROWS = 12;
@@ -102,7 +102,7 @@ export class InteractiveMode implements InteractiveModeContext {
102
102
  planModeEnabled = false;
103
103
  planModePaused = false;
104
104
  planModePlanFilePath: string | undefined = undefined;
105
- todoItems: TodoItem[] = [];
105
+ todoPhases: TodoPhase[] = [];
106
106
  hideThinkingBlock = false;
107
107
  pendingImages: ImageContent[] = [];
108
108
  compactionQueuedMessages: CompactionQueuedMessage[] = [];
@@ -436,92 +436,82 @@ export class InteractiveMode implements InteractiveModeContext {
436
436
 
437
437
  #formatTodoLine(todo: TodoItem, prefix: string): string {
438
438
  const checkbox = theme.checkbox;
439
- const label = todo.content;
440
439
  switch (todo.status) {
441
440
  case "completed":
442
441
  return theme.fg("success", `${prefix}${checkbox.checked} ${chalk.strikethrough(todo.content)}`);
443
442
  case "in_progress":
444
- return theme.fg("accent", `${prefix}${checkbox.unchecked} ${label}`);
443
+ return theme.fg("accent", `${prefix}${checkbox.unchecked} ${todo.content}`);
444
+ case "abandoned":
445
+ return theme.fg("error", `${prefix}${checkbox.unchecked} ${chalk.strikethrough(todo.content)}`);
445
446
  default:
446
- return theme.fg("dim", `${prefix}${checkbox.unchecked} ${label}`);
447
+ return theme.fg("dim", `${prefix}${checkbox.unchecked} ${todo.content}`);
447
448
  }
448
449
  }
449
450
 
450
- #getCollapsedTodos(todos: TodoItem[]): TodoItem[] {
451
- let startIndex = 0;
452
- for (let i = todos.length - 1; i >= 0; i -= 1) {
453
- if (todos[i].status === "completed") {
454
- startIndex = i;
455
- break;
456
- }
457
- }
458
- return todos.slice(startIndex, startIndex + 5);
451
+ #getActivePhase(phases: TodoPhase[]): TodoPhase | undefined {
452
+ const nonEmpty = phases.filter(phase => phase.tasks.length > 0);
453
+ const active = nonEmpty.find(phase =>
454
+ phase.tasks.some(task => task.status === "pending" || task.status === "in_progress"),
455
+ );
456
+ return active ?? nonEmpty[nonEmpty.length - 1];
459
457
  }
460
458
 
461
459
  #renderTodoList(): void {
462
460
  this.todoContainer.clear();
463
- if (this.todoItems.length === 0) {
461
+ const phases = this.todoPhases.filter(phase => phase.tasks.length > 0);
462
+ if (phases.length === 0) {
464
463
  return;
465
464
  }
466
465
 
467
- const visibleTodos = this.todoExpanded ? this.todoItems : this.#getCollapsedTodos(this.todoItems);
468
466
  const indent = " ";
469
467
  const hook = theme.tree.hook;
470
468
  const lines = [indent + theme.bold(theme.fg("accent", "Todos"))];
471
469
 
472
- visibleTodos.forEach((todo, index) => {
473
- const prefix = `${indent}${index === 0 ? hook : " "} `;
474
- lines.push(this.#formatTodoLine(todo, prefix));
475
- });
470
+ if (!this.todoExpanded) {
471
+ const activePhase = this.#getActivePhase(phases);
472
+ if (!activePhase) return;
473
+ lines.push(`${indent}${theme.fg("accent", `${hook} ${activePhase.name}`)}`);
474
+ const visibleTasks = activePhase.tasks.slice(0, 5);
475
+ visibleTasks.forEach((todo, index) => {
476
+ const prefix = `${indent}${index === 0 ? hook : " "} `;
477
+ lines.push(this.#formatTodoLine(todo, prefix));
478
+ });
479
+ if (visibleTasks.length < activePhase.tasks.length) {
480
+ const remaining = activePhase.tasks.length - visibleTasks.length;
481
+ lines.push(theme.fg("muted", `${indent} ${hook} +${remaining} more (Ctrl+T to expand)`));
482
+ }
483
+ this.todoContainer.addChild(new Text(lines.join("\n"), 1, 0));
484
+ return;
485
+ }
476
486
 
477
- if (!this.todoExpanded && visibleTodos.length < this.todoItems.length) {
478
- const remaining = this.todoItems.length - visibleTodos.length;
479
- lines.push(theme.fg("muted", `${indent} ${hook} +${remaining} more (Ctrl+T to expand)`));
487
+ for (const phase of phases) {
488
+ lines.push(`${indent}${theme.fg("accent", `${hook} ${phase.name}`)}`);
489
+ phase.tasks.forEach((todo, index) => {
490
+ const prefix = `${indent}${index === 0 ? hook : " "} `;
491
+ lines.push(this.#formatTodoLine(todo, prefix));
492
+ });
480
493
  }
481
494
 
482
495
  this.todoContainer.addChild(new Text(lines.join("\n"), 1, 0));
483
496
  }
484
497
 
485
498
  async #loadTodoList(): Promise<void> {
486
- const sessionFile = this.sessionManager.getSessionFile() ?? null;
487
- if (!sessionFile) {
488
- this.todoItems = [];
489
- this.#renderTodoList();
490
- return;
491
- }
492
- const artifactsDir = sessionFile.slice(0, -6);
493
- const todoPath = path.join(artifactsDir, TODO_FILE_NAME);
494
- try {
495
- const data = (await Bun.file(todoPath).json()) as { todos?: TodoItem[] };
496
- if (data?.todos && Array.isArray(data.todos)) {
497
- this.todoItems = data.todos;
498
- } else {
499
- this.todoItems = [];
500
- }
501
- } catch (error) {
502
- if (isEnoent(error)) {
503
- this.todoItems = [];
504
- this.#renderTodoList();
505
- return;
506
- }
507
- logger.warn("Failed to load todos", { path: todoPath, error: String(error) });
508
- }
499
+ this.todoPhases = this.session.getTodoPhases();
509
500
  this.#renderTodoList();
510
501
  }
511
502
 
512
- #getPlanFilePath(): string {
513
- const sessionId = this.sessionManager.getSessionId();
514
- return `plan://${sessionId}/plan.md`;
503
+ async #getPlanFilePath(): Promise<string> {
504
+ return "local://PLAN.md";
515
505
  }
516
506
 
517
507
  #resolvePlanFilePath(planFilePath: string): string {
518
- if (planFilePath.startsWith("plan://")) {
519
- return resolvePlanUrlToPath(planFilePath, {
520
- getPlansDirectory: () => this.settings.getPlansDirectory(),
521
- cwd: this.sessionManager.getCwd(),
508
+ if (planFilePath.startsWith("local://")) {
509
+ return resolveLocalUrlToPath(planFilePath, {
510
+ getArtifactsDir: () => this.sessionManager.getArtifactsDir(),
511
+ getSessionId: () => this.sessionManager.getSessionId(),
522
512
  });
523
513
  }
524
- return planFilePath;
514
+ return path.resolve(this.sessionManager.getCwd(), planFilePath);
525
515
  }
526
516
 
527
517
  #updatePlanModeStatus(): void {
@@ -592,7 +582,7 @@ export class InteractiveMode implements InteractiveModeContext {
592
582
 
593
583
  this.planModePaused = false;
594
584
 
595
- const planFilePath = options?.planFilePath ?? this.#getPlanFilePath();
585
+ const planFilePath = options?.planFilePath ?? (await this.#getPlanFilePath());
596
586
  const previousTools = this.session.getActiveToolNames();
597
587
  const hasExitTool = this.session.getToolByName("exit_plan_mode") !== undefined;
598
588
  const planTools = hasExitTool ? [...previousTools, "exit_plan_mode"] : previousTools;
@@ -672,16 +662,29 @@ export class InteractiveMode implements InteractiveModeContext {
672
662
  this.ui.requestRender();
673
663
  }
674
664
 
675
- async #approvePlan(planContent: string): Promise<void> {
665
+ async #approvePlan(
666
+ planContent: string,
667
+ options: { planFilePath: string; finalPlanFilePath: string },
668
+ ): Promise<void> {
669
+ await renameApprovedPlanFile({
670
+ planFilePath: options.planFilePath,
671
+ finalPlanFilePath: options.finalPlanFilePath,
672
+ getArtifactsDir: () => this.sessionManager.getArtifactsDir(),
673
+ getSessionId: () => this.sessionManager.getSessionId(),
674
+ });
676
675
  const previousTools = this.#planModePreviousTools ?? this.session.getActiveToolNames();
677
676
  await this.#exitPlanMode({ silent: true, paused: false });
678
677
  await this.handleClearCommand();
679
678
  if (previousTools.length > 0) {
680
679
  await this.session.setActiveToolsByName(previousTools);
681
680
  }
681
+ this.session.setPlanReferencePath(options.finalPlanFilePath);
682
682
  this.session.markPlanReferenceSent();
683
- const prompt = renderPromptTemplate(planModeApprovedPrompt, { planContent });
684
- await this.session.prompt(prompt);
683
+ const prompt = renderPromptTemplate(planModeApprovedPrompt, {
684
+ planContent,
685
+ finalPlanFilePath: options.finalPlanFilePath,
686
+ });
687
+ await this.session.prompt(prompt, { synthetic: true });
685
688
  }
686
689
 
687
690
  async handlePlanModeCommand(): Promise<void> {
@@ -703,7 +706,7 @@ export class InteractiveMode implements InteractiveModeContext {
703
706
  return;
704
707
  }
705
708
 
706
- const planFilePath = details.planFilePath || this.planModePlanFilePath || this.#getPlanFilePath();
709
+ const planFilePath = details.planFilePath || this.planModePlanFilePath || (await this.#getPlanFilePath());
707
710
  this.planModePlanFilePath = planFilePath;
708
711
  const planContent = await this.#readPlanFile(planFilePath);
709
712
  if (!planContent) {
@@ -719,7 +722,14 @@ export class InteractiveMode implements InteractiveModeContext {
719
722
  ]);
720
723
 
721
724
  if (choice === "Approve and execute") {
722
- await this.#approvePlan(planContent);
725
+ const finalPlanFilePath = details.finalPlanFilePath || planFilePath;
726
+ try {
727
+ await this.#approvePlan(planContent, { planFilePath, finalPlanFilePath });
728
+ } catch (error) {
729
+ this.showError(
730
+ `Failed to finalize approved plan: ${error instanceof Error ? error.message : String(error)}`,
731
+ );
732
+ }
723
733
  return;
724
734
  }
725
735
  if (choice === "Refine plan") {
@@ -1170,8 +1180,18 @@ export class InteractiveMode implements InteractiveModeContext {
1170
1180
  this.ui.requestRender();
1171
1181
  }
1172
1182
 
1173
- setTodos(todos: TodoItem[]): void {
1174
- this.todoItems = todos;
1183
+ setTodos(todos: TodoItem[] | TodoPhase[]): void {
1184
+ if (todos.length > 0 && "tasks" in todos[0]) {
1185
+ this.todoPhases = todos as TodoPhase[];
1186
+ } else {
1187
+ this.todoPhases = [
1188
+ {
1189
+ id: "default",
1190
+ name: "Todos",
1191
+ tasks: todos as TodoItem[],
1192
+ },
1193
+ ];
1194
+ }
1175
1195
  this.#renderTodoList();
1176
1196
  this.ui.requestRender();
1177
1197
  }
@@ -26,10 +26,18 @@ export type CompactionQueuedMessage = {
26
26
  mode: "steer" | "followUp";
27
27
  };
28
28
 
29
+ export type TodoStatus = "pending" | "in_progress" | "completed" | "abandoned";
30
+
29
31
  export type TodoItem = {
30
32
  id: string;
31
33
  content: string;
32
- status: "pending" | "in_progress" | "completed";
34
+ status: TodoStatus;
35
+ };
36
+
37
+ export type TodoPhase = {
38
+ id: string;
39
+ name: string;
40
+ tasks: TodoItem[];
33
41
  };
34
42
 
35
43
  export interface InteractiveModeContext {
@@ -89,7 +97,7 @@ export interface InteractiveModeContext {
89
97
  lastStatusText: Text | undefined;
90
98
  fileSlashCommands: Set<string>;
91
99
  skillCommands: Map<string, string>;
92
- todoItems: TodoItem[];
100
+ todoPhases: TodoPhase[];
93
101
 
94
102
  // Lifecycle
95
103
  init(): Promise<void>;
@@ -130,7 +138,7 @@ export interface InteractiveModeContext {
130
138
  updateEditorTopBorder(): void;
131
139
  updateEditorBorderColor(): void;
132
140
  rebuildChatFromMessages(): void;
133
- setTodos(todos: TodoItem[]): void;
141
+ setTodos(todos: TodoItem[] | TodoPhase[]): void;
134
142
  reloadTodos(): Promise<void>;
135
143
  toggleTodoExpansion(): void;
136
144
 
@@ -171,12 +171,14 @@ export class UiHelpers {
171
171
  }
172
172
  break;
173
173
  }
174
- case "user": {
174
+ case "user":
175
+ case "developer": {
175
176
  const textContent = this.ctx.getUserMessageText(message);
176
177
  if (textContent) {
177
- const userComponent = new UserMessageComponent(textContent, message.synthetic ?? false);
178
+ const isSynthetic = message.role === "developer" ? true : (message.synthetic ?? false);
179
+ const userComponent = new UserMessageComponent(textContent, isSynthetic);
178
180
  this.ctx.chatContainer.addChild(userComponent);
179
- if (options?.populateHistory && !message.synthetic) {
181
+ if (options?.populateHistory && message.role === "user" && !isSynthetic) {
180
182
  this.ctx.editor.addToHistory(textContent);
181
183
  }
182
184
  }