@astralyn/pi 0.82.5 → 0.82.6

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 (94) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/cli/project-trust.d.ts.map +1 -1
  3. package/dist/cli/project-trust.js +4 -1
  4. package/dist/cli/project-trust.js.map +1 -1
  5. package/dist/core/bash-executor.d.ts.map +1 -1
  6. package/dist/core/bash-executor.js +4 -22
  7. package/dist/core/bash-executor.js.map +1 -1
  8. package/dist/core/extensions/index.d.ts +1 -1
  9. package/dist/core/extensions/index.d.ts.map +1 -1
  10. package/dist/core/extensions/index.js.map +1 -1
  11. package/dist/core/extensions/types.d.ts +28 -5
  12. package/dist/core/extensions/types.d.ts.map +1 -1
  13. package/dist/core/extensions/types.js.map +1 -1
  14. package/dist/core/tools/output-accumulator.d.ts +3 -13
  15. package/dist/core/tools/output-accumulator.d.ts.map +1 -1
  16. package/dist/core/tools/output-accumulator.js +6 -36
  17. package/dist/core/tools/output-accumulator.js.map +1 -1
  18. package/dist/extensions/plan/constants.d.ts +11 -2
  19. package/dist/extensions/plan/constants.d.ts.map +1 -1
  20. package/dist/extensions/plan/constants.js +14 -2
  21. package/dist/extensions/plan/constants.js.map +1 -1
  22. package/dist/extensions/plan/index.d.ts +0 -18
  23. package/dist/extensions/plan/index.d.ts.map +1 -1
  24. package/dist/extensions/plan/index.js +35 -3
  25. package/dist/extensions/plan/index.js.map +1 -1
  26. package/dist/extensions/plan/view.d.ts +36 -0
  27. package/dist/extensions/plan/view.d.ts.map +1 -0
  28. package/dist/extensions/plan/view.js +74 -0
  29. package/dist/extensions/plan/view.js.map +1 -0
  30. package/dist/extensions/router/stream.d.ts.map +1 -1
  31. package/dist/extensions/router/stream.js +44 -0
  32. package/dist/extensions/router/stream.js.map +1 -1
  33. package/dist/extensions/subagent/activity.d.ts.map +1 -1
  34. package/dist/extensions/subagent/activity.js +25 -4
  35. package/dist/extensions/subagent/activity.js.map +1 -1
  36. package/dist/extensions/subagent/index.d.ts.map +1 -1
  37. package/dist/extensions/subagent/index.js +14 -0
  38. package/dist/extensions/subagent/index.js.map +1 -1
  39. package/dist/extensions/subagent/render.d.ts.map +1 -1
  40. package/dist/extensions/subagent/render.js +243 -116
  41. package/dist/extensions/subagent/render.js.map +1 -1
  42. package/dist/extensions/subagent/resolve.d.ts.map +1 -1
  43. package/dist/extensions/subagent/resolve.js +13 -9
  44. package/dist/extensions/subagent/resolve.js.map +1 -1
  45. package/dist/extensions/subagent/runner.d.ts.map +1 -1
  46. package/dist/extensions/subagent/runner.js +4 -2
  47. package/dist/extensions/subagent/runner.js.map +1 -1
  48. package/dist/extensions/subagent/schema.d.ts.map +1 -1
  49. package/dist/extensions/subagent/schema.js +2 -2
  50. package/dist/extensions/subagent/schema.js.map +1 -1
  51. package/dist/extensions/todo/constants.d.ts +17 -6
  52. package/dist/extensions/todo/constants.d.ts.map +1 -1
  53. package/dist/extensions/todo/constants.js +22 -9
  54. package/dist/extensions/todo/constants.js.map +1 -1
  55. package/dist/extensions/todo/index.d.ts +0 -14
  56. package/dist/extensions/todo/index.d.ts.map +1 -1
  57. package/dist/extensions/todo/index.js +13 -1
  58. package/dist/extensions/todo/index.js.map +1 -1
  59. package/dist/extensions/todo/schema.d.ts +53 -2
  60. package/dist/extensions/todo/schema.d.ts.map +1 -1
  61. package/dist/extensions/todo/schema.js +57 -8
  62. package/dist/extensions/todo/schema.js.map +1 -1
  63. package/dist/extensions/todo/state.d.ts +16 -5
  64. package/dist/extensions/todo/state.d.ts.map +1 -1
  65. package/dist/extensions/todo/state.js +866 -130
  66. package/dist/extensions/todo/state.js.map +1 -1
  67. package/dist/extensions/todo/view.d.ts +16 -1
  68. package/dist/extensions/todo/view.d.ts.map +1 -1
  69. package/dist/extensions/todo/view.js +283 -1
  70. package/dist/extensions/todo/view.js.map +1 -1
  71. package/dist/modes/interactive/components/extension-selector.d.ts +6 -1
  72. package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
  73. package/dist/modes/interactive/components/extension-selector.js +14 -6
  74. package/dist/modes/interactive/components/extension-selector.js.map +1 -1
  75. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  76. package/dist/modes/interactive/components/tool-execution.js +6 -0
  77. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  78. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  79. package/dist/modes/interactive/interactive-mode.js +7 -1
  80. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  81. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  82. package/dist/modes/rpc/rpc-mode.js +8 -1
  83. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  84. package/docs/bundled/extensions/plan.md +6 -4
  85. package/docs/bundled/extensions/subagent.md +2 -2
  86. package/docs/bundled/extensions/todo.md +8 -6
  87. package/docs/bundled/tool-presentation.md +1 -1
  88. package/docs/extensions.md +14 -0
  89. package/npm-shrinkwrap.json +2 -2
  90. package/package.json +1 -1
  91. package/dist/utils/output-decoder.d.ts +0 -52
  92. package/dist/utils/output-decoder.d.ts.map +0 -1
  93. package/dist/utils/output-decoder.js +0 -134
  94. package/dist/utils/output-decoder.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../src/extensions/plan/constants.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,eAAO,MAAM,mBAAmB,cAAc,CAAC;AAC/C,eAAO,MAAM,oBAAoB,cAAc,CAAC;AAChD,eAAO,MAAM,iBAAiB,SAAS,CAAC;AACxC,eAAO,MAAM,cAAc,SAAS,CAAC;AACrC,eAAO,MAAM,eAAe,cAAc,CAAC;AAC3C,eAAO,MAAM,eAAe,cAAc,CAAC;AAE3C;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,EAAE,WAAW,CAAC,MAAM,CAAkD,CAAC;AAEtG,uFAAuF;AACvF,eAAO,MAAM,oBAAoB,QAAS,CAAC;AAE3C,eAAO,MAAM,YAAY,wCAAwC,CAAC;AAClE,eAAO,MAAM,oBAAoB,wDAAwD,CAAC;AAC1F,eAAO,MAAM,kBAAkB,kBAAkB,CAAC;AAElD,eAAO,MAAM,0BAA0B,+YACsW,CAAC;AAE9Y,eAAO,MAAM,wBAAwB,mEAAmE,CAAC;AAEzG,eAAO,MAAM,2BAA2B,UAGvC,CAAC;AAiBF,4FAA4F;AAC5F,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,MAAM,CAQ/D","sourcesContent":["/**\n * plan/constants.ts — tool identity, command names, and prompt copy.\n *\n * Name/label/command live here so `index.ts` only assembles the extension,\n * matching the todo/question constants.ts pattern. Prompt copy is the\n * model-facing contract; keep it stable.\n */\n\nexport const EXIT_PLAN_TOOL_NAME = \"exit_plan\";\nexport const EXIT_PLAN_TOOL_LABEL = \"Exit Plan\";\nexport const PLAN_COMMAND_NAME = \"plan\";\nexport const PLAN_FLAG_NAME = \"plan\";\nexport const PLAN_ENTRY_TYPE = \"plan-mode\";\nexport const PLAN_STATUS_KEY = \"plan-mode\";\n\n/**\n * Tools removed while planning. edit/write/bash grant write access directly;\n * subagent spawns child sessions that do not inherit the restricted tool set,\n * so leaving it active would let the model write through a child agent.\n * Other extension tools (question, todo, deepwiki, ...) stay available.\n */\nexport const PLAN_BLOCKED_TOOLS: ReadonlySet<string> = new Set([\"edit\", \"write\", \"bash\", \"subagent\"]);\n\n/** Max characters of plan markdown echoed into the post-compaction kickoff message. */\nexport const PLAN_EMBED_MAX_CHARS = 24_000;\n\nexport const MENU_EXECUTE = \"Start executing (keep full context)\";\nexport const MENU_COMPACT_EXECUTE = \"Compact context, then execute (best for long tasks)\";\nexport const MENU_KEEP_PLANNING = \"Keep planning\";\n\nexport const EXIT_PLAN_TOOL_DESCRIPTION =\n\t\"Submit the finished plan and ask the user to approve leaving plan mode. Present the complete plan as normal response text first, then call this tool with a short title and the same plan markdown; on approval the plan is saved to disk. The user chooses between executing with full context, compacting the context before executing, or continuing to plan. Only available while plan mode is active.\";\n\nexport const EXIT_PLAN_PROMPT_SNIPPET = \"Submit the finished plan for user approval and leave plan mode\";\n\nexport const EXIT_PLAN_PROMPT_GUIDELINES = [\n\t\"Call `exit_plan` only after presenting the complete plan as response text; make it the only tool call in that step and stop after it returns.\",\n\t\"`exit_plan` results are authoritative for plan-mode state: follow them even when the plan-mode system prompt block is still present later in the run.\",\n];\n\nconst PLAN_MODE_PROMPT = `[PLAN MODE]\nYou are in plan mode: a read-only phase for exploring the codebase and agreeing on a plan with the user before any changes are made.\n\nRestrictions:\n- edit, write, bash, and subagent are unavailable; do not attempt changes or workarounds. Explore with read, grep, find, and ls instead.\n\nWorking style:\n- Answer questions from the code, not from the user: explore before asking.\n- When product intent, scope, or risk tolerance is unclear, interview the user with the question tool. Focus on one decision per exchange (batch only tightly coupled decisions) and always include your recommendation and the trade-offs.\n- Skip the interview for small, clear tasks. Pick sensible defaults for low-risk details and state them in the plan.\n\nDelivering the plan:\n- When the plan is ready, present it in full as normal response text (goal, steps, verification criteria) so the user can read it, then call exit_plan with a short title and the same markdown as the plan parameter.\n- The exit_plan tool result is authoritative for mode state: this block can outlive plan mode within a run, so if the result says plan mode has ended, it has ended.`;\n\n/** Build the plan-mode system prompt block, listing plans already saved for this branch. */\nexport function buildPlanModePrompt(planFiles: string[]): string {\n\tif (!planFiles.length) return PLAN_MODE_PROMPT;\n\tconst listing = planFiles.map((path) => `- ${path}`).join(\"\\n\");\n\treturn `${PLAN_MODE_PROMPT}\n\nPlans already saved for this session (read them with the read tool):\n${listing}\nWhen revising a saved plan, read the latest one first, account for work already completed, and produce a revision that states what changed and why; set the revises parameter to the file name of the plan being revised.`;\n}\n"]}
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../src/extensions/plan/constants.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAEnE,eAAO,MAAM,mBAAmB,cAAc,CAAC;AAC/C,eAAO,MAAM,oBAAoB,cAAc,CAAC;AAChD,eAAO,MAAM,iBAAiB,SAAS,CAAC;AACxC,eAAO,MAAM,cAAc,SAAS,CAAC;AACrC,eAAO,MAAM,eAAe,cAAc,CAAC;AAC3C,eAAO,MAAM,eAAe,cAAc,CAAC;AAE3C;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,EAAE,WAAW,CAAC,MAAM,CAAkD,CAAC;AAEtG,uFAAuF;AACvF,eAAO,MAAM,oBAAoB,QAAS,CAAC;AAE3C,eAAO,MAAM,YAAY,oBAAoB,CAAC;AAC9C,eAAO,MAAM,oBAAoB,kCAAkC,CAAC;AACpE,eAAO,MAAM,kBAAkB,kBAAkB,CAAC;AAElD;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,EAAE,SAAS,YAAY,EAIzD,CAAC;AAEF,mFAAmF;AACnF,eAAO,MAAM,qBAAqB,kBAAkB,CAAC;AAErD,eAAO,MAAM,0BAA0B,+YACsW,CAAC;AAE9Y,eAAO,MAAM,wBAAwB,mEAAmE,CAAC;AAEzG,eAAO,MAAM,2BAA2B,UAGvC,CAAC;AAiBF,4FAA4F;AAC5F,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,MAAM,CAQ/D","sourcesContent":["/**\n * plan/constants.ts — tool identity, command names, and prompt copy.\n *\n * Name/label/command live here so `index.ts` only assembles the extension,\n * matching the todo/question constants.ts pattern. Prompt copy is the\n * model-facing contract; keep it stable.\n */\n\nimport type { SelectOption } from \"../../core/extensions/types.ts\";\n\nexport const EXIT_PLAN_TOOL_NAME = \"exit_plan\";\nexport const EXIT_PLAN_TOOL_LABEL = \"Exit Plan\";\nexport const PLAN_COMMAND_NAME = \"plan\";\nexport const PLAN_FLAG_NAME = \"plan\";\nexport const PLAN_ENTRY_TYPE = \"plan-mode\";\nexport const PLAN_STATUS_KEY = \"plan-mode\";\n\n/**\n * Tools removed while planning. edit/write/bash grant write access directly;\n * subagent spawns child sessions that do not inherit the restricted tool set,\n * so leaving it active would let the model write through a child agent.\n * Other extension tools (question, todo, deepwiki, ...) stay available.\n */\nexport const PLAN_BLOCKED_TOOLS: ReadonlySet<string> = new Set([\"edit\", \"write\", \"bash\", \"subagent\"]);\n\n/** Max characters of plan markdown echoed into the post-compaction kickoff message. */\nexport const PLAN_EMBED_MAX_CHARS = 24_000;\n\nexport const MENU_EXECUTE = \"Start executing\";\nexport const MENU_COMPACT_EXECUTE = \"Compact context, then execute\";\nexport const MENU_KEEP_PLANNING = \"Keep planning\";\n\n/**\n * Approval choices. The label is the decision, the description is the\n * trade-off, so the three options can be told apart without reading to the\n * end of a sentence. select() resolves to the label.\n */\nexport const EXIT_PLAN_MENU_OPTIONS: readonly SelectOption[] = [\n\t{ label: MENU_EXECUTE, description: \"keep full context\" },\n\t{ label: MENU_COMPACT_EXECUTE, description: \"best for long tasks\" },\n\t{ label: MENU_KEEP_PLANNING, description: \"refine before running\" },\n];\n\n/** Esc maps to \"keep planning\", so the dismiss hint must not read as \"discard\". */\nexport const EXIT_PLAN_CANCEL_HINT = \"keep planning\";\n\nexport const EXIT_PLAN_TOOL_DESCRIPTION =\n\t\"Submit the finished plan and ask the user to approve leaving plan mode. Present the complete plan as normal response text first, then call this tool with a short title and the same plan markdown; on approval the plan is saved to disk. The user chooses between executing with full context, compacting the context before executing, or continuing to plan. Only available while plan mode is active.\";\n\nexport const EXIT_PLAN_PROMPT_SNIPPET = \"Submit the finished plan for user approval and leave plan mode\";\n\nexport const EXIT_PLAN_PROMPT_GUIDELINES = [\n\t\"Call `exit_plan` only after presenting the complete plan as response text; make it the only tool call in that step and stop after it returns.\",\n\t\"`exit_plan` results are authoritative for plan-mode state: follow them even when the plan-mode system prompt block is still present later in the run.\",\n];\n\nconst PLAN_MODE_PROMPT = `[PLAN MODE]\nYou are in plan mode: a read-only phase for exploring the codebase and agreeing on a plan with the user before any changes are made.\n\nRestrictions:\n- edit, write, bash, and subagent are unavailable; do not attempt changes or workarounds. Explore with read, grep, find, and ls instead.\n\nWorking style:\n- Answer questions from the code, not from the user: explore before asking.\n- When product intent, scope, or risk tolerance is unclear, interview the user with the question tool. Focus on one decision per exchange (batch only tightly coupled decisions) and always include your recommendation and the trade-offs.\n- Skip the interview for small, clear tasks. Pick sensible defaults for low-risk details and state them in the plan.\n\nDelivering the plan:\n- When the plan is ready, present it in full as normal response text (goal, steps, verification criteria) so the user can read it, then call exit_plan with a short title and the same markdown as the plan parameter.\n- The exit_plan tool result is authoritative for mode state: this block can outlive plan mode within a run, so if the result says plan mode has ended, it has ended.`;\n\n/** Build the plan-mode system prompt block, listing plans already saved for this branch. */\nexport function buildPlanModePrompt(planFiles: string[]): string {\n\tif (!planFiles.length) return PLAN_MODE_PROMPT;\n\tconst listing = planFiles.map((path) => `- ${path}`).join(\"\\n\");\n\treturn `${PLAN_MODE_PROMPT}\n\nPlans already saved for this session (read them with the read tool):\n${listing}\nWhen revising a saved plan, read the latest one first, account for work already completed, and produce a revision that states what changed and why; set the revises parameter to the file name of the plan being revised.`;\n}\n"]}
@@ -20,9 +20,21 @@ export const PLAN_STATUS_KEY = "plan-mode";
20
20
  export const PLAN_BLOCKED_TOOLS = new Set(["edit", "write", "bash", "subagent"]);
21
21
  /** Max characters of plan markdown echoed into the post-compaction kickoff message. */
22
22
  export const PLAN_EMBED_MAX_CHARS = 24_000;
23
- export const MENU_EXECUTE = "Start executing (keep full context)";
24
- export const MENU_COMPACT_EXECUTE = "Compact context, then execute (best for long tasks)";
23
+ export const MENU_EXECUTE = "Start executing";
24
+ export const MENU_COMPACT_EXECUTE = "Compact context, then execute";
25
25
  export const MENU_KEEP_PLANNING = "Keep planning";
26
+ /**
27
+ * Approval choices. The label is the decision, the description is the
28
+ * trade-off, so the three options can be told apart without reading to the
29
+ * end of a sentence. select() resolves to the label.
30
+ */
31
+ export const EXIT_PLAN_MENU_OPTIONS = [
32
+ { label: MENU_EXECUTE, description: "keep full context" },
33
+ { label: MENU_COMPACT_EXECUTE, description: "best for long tasks" },
34
+ { label: MENU_KEEP_PLANNING, description: "refine before running" },
35
+ ];
36
+ /** Esc maps to "keep planning", so the dismiss hint must not read as "discard". */
37
+ export const EXIT_PLAN_CANCEL_HINT = "keep planning";
26
38
  export const EXIT_PLAN_TOOL_DESCRIPTION = "Submit the finished plan and ask the user to approve leaving plan mode. Present the complete plan as normal response text first, then call this tool with a short title and the same plan markdown; on approval the plan is saved to disk. The user chooses between executing with full context, compacting the context before executing, or continuing to plan. Only available while plan mode is active.";
27
39
  export const EXIT_PLAN_PROMPT_SNIPPET = "Submit the finished plan for user approval and leave plan mode";
28
40
  export const EXIT_PLAN_PROMPT_GUIDELINES = [
@@ -1 +1 @@
1
- {"version":3,"file":"constants.js","sourceRoot":"","sources":["../../../src/extensions/plan/constants.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,CAAC,MAAM,mBAAmB,GAAG,WAAW,CAAC;AAC/C,MAAM,CAAC,MAAM,oBAAoB,GAAG,WAAW,CAAC;AAChD,MAAM,CAAC,MAAM,iBAAiB,GAAG,MAAM,CAAC;AACxC,MAAM,CAAC,MAAM,cAAc,GAAG,MAAM,CAAC;AACrC,MAAM,CAAC,MAAM,eAAe,GAAG,WAAW,CAAC;AAC3C,MAAM,CAAC,MAAM,eAAe,GAAG,WAAW,CAAC;AAE3C;;;;;GAKG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAwB,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC;AAEtG,uFAAuF;AACvF,MAAM,CAAC,MAAM,oBAAoB,GAAG,MAAM,CAAC;AAE3C,MAAM,CAAC,MAAM,YAAY,GAAG,qCAAqC,CAAC;AAClE,MAAM,CAAC,MAAM,oBAAoB,GAAG,qDAAqD,CAAC;AAC1F,MAAM,CAAC,MAAM,kBAAkB,GAAG,eAAe,CAAC;AAElD,MAAM,CAAC,MAAM,0BAA0B,GACtC,4YAA4Y,CAAC;AAE9Y,MAAM,CAAC,MAAM,wBAAwB,GAAG,gEAAgE,CAAC;AAEzG,MAAM,CAAC,MAAM,2BAA2B,GAAG;IAC1C,+IAA+I;IAC/I,uJAAuJ;CACvJ,CAAC;AAEF,MAAM,gBAAgB,GAAG;;;;;;;;;;;;;qKAa4I,CAAC;AAEtK,4FAA4F;AAC5F,MAAM,UAAU,mBAAmB,CAAC,SAAmB,EAAU;IAChE,IAAI,CAAC,SAAS,CAAC,MAAM;QAAE,OAAO,gBAAgB,CAAC;IAC/C,MAAM,OAAO,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAChE,OAAO,GAAG,gBAAgB;;;EAGzB,OAAO;0NACiN,CAAC;AAAA,CAC1N","sourcesContent":["/**\n * plan/constants.ts — tool identity, command names, and prompt copy.\n *\n * Name/label/command live here so `index.ts` only assembles the extension,\n * matching the todo/question constants.ts pattern. Prompt copy is the\n * model-facing contract; keep it stable.\n */\n\nexport const EXIT_PLAN_TOOL_NAME = \"exit_plan\";\nexport const EXIT_PLAN_TOOL_LABEL = \"Exit Plan\";\nexport const PLAN_COMMAND_NAME = \"plan\";\nexport const PLAN_FLAG_NAME = \"plan\";\nexport const PLAN_ENTRY_TYPE = \"plan-mode\";\nexport const PLAN_STATUS_KEY = \"plan-mode\";\n\n/**\n * Tools removed while planning. edit/write/bash grant write access directly;\n * subagent spawns child sessions that do not inherit the restricted tool set,\n * so leaving it active would let the model write through a child agent.\n * Other extension tools (question, todo, deepwiki, ...) stay available.\n */\nexport const PLAN_BLOCKED_TOOLS: ReadonlySet<string> = new Set([\"edit\", \"write\", \"bash\", \"subagent\"]);\n\n/** Max characters of plan markdown echoed into the post-compaction kickoff message. */\nexport const PLAN_EMBED_MAX_CHARS = 24_000;\n\nexport const MENU_EXECUTE = \"Start executing (keep full context)\";\nexport const MENU_COMPACT_EXECUTE = \"Compact context, then execute (best for long tasks)\";\nexport const MENU_KEEP_PLANNING = \"Keep planning\";\n\nexport const EXIT_PLAN_TOOL_DESCRIPTION =\n\t\"Submit the finished plan and ask the user to approve leaving plan mode. Present the complete plan as normal response text first, then call this tool with a short title and the same plan markdown; on approval the plan is saved to disk. The user chooses between executing with full context, compacting the context before executing, or continuing to plan. Only available while plan mode is active.\";\n\nexport const EXIT_PLAN_PROMPT_SNIPPET = \"Submit the finished plan for user approval and leave plan mode\";\n\nexport const EXIT_PLAN_PROMPT_GUIDELINES = [\n\t\"Call `exit_plan` only after presenting the complete plan as response text; make it the only tool call in that step and stop after it returns.\",\n\t\"`exit_plan` results are authoritative for plan-mode state: follow them even when the plan-mode system prompt block is still present later in the run.\",\n];\n\nconst PLAN_MODE_PROMPT = `[PLAN MODE]\nYou are in plan mode: a read-only phase for exploring the codebase and agreeing on a plan with the user before any changes are made.\n\nRestrictions:\n- edit, write, bash, and subagent are unavailable; do not attempt changes or workarounds. Explore with read, grep, find, and ls instead.\n\nWorking style:\n- Answer questions from the code, not from the user: explore before asking.\n- When product intent, scope, or risk tolerance is unclear, interview the user with the question tool. Focus on one decision per exchange (batch only tightly coupled decisions) and always include your recommendation and the trade-offs.\n- Skip the interview for small, clear tasks. Pick sensible defaults for low-risk details and state them in the plan.\n\nDelivering the plan:\n- When the plan is ready, present it in full as normal response text (goal, steps, verification criteria) so the user can read it, then call exit_plan with a short title and the same markdown as the plan parameter.\n- The exit_plan tool result is authoritative for mode state: this block can outlive plan mode within a run, so if the result says plan mode has ended, it has ended.`;\n\n/** Build the plan-mode system prompt block, listing plans already saved for this branch. */\nexport function buildPlanModePrompt(planFiles: string[]): string {\n\tif (!planFiles.length) return PLAN_MODE_PROMPT;\n\tconst listing = planFiles.map((path) => `- ${path}`).join(\"\\n\");\n\treturn `${PLAN_MODE_PROMPT}\n\nPlans already saved for this session (read them with the read tool):\n${listing}\nWhen revising a saved plan, read the latest one first, account for work already completed, and produce a revision that states what changed and why; set the revises parameter to the file name of the plan being revised.`;\n}\n"]}
1
+ {"version":3,"file":"constants.js","sourceRoot":"","sources":["../../../src/extensions/plan/constants.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,MAAM,CAAC,MAAM,mBAAmB,GAAG,WAAW,CAAC;AAC/C,MAAM,CAAC,MAAM,oBAAoB,GAAG,WAAW,CAAC;AAChD,MAAM,CAAC,MAAM,iBAAiB,GAAG,MAAM,CAAC;AACxC,MAAM,CAAC,MAAM,cAAc,GAAG,MAAM,CAAC;AACrC,MAAM,CAAC,MAAM,eAAe,GAAG,WAAW,CAAC;AAC3C,MAAM,CAAC,MAAM,eAAe,GAAG,WAAW,CAAC;AAE3C;;;;;GAKG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAwB,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC;AAEtG,uFAAuF;AACvF,MAAM,CAAC,MAAM,oBAAoB,GAAG,MAAM,CAAC;AAE3C,MAAM,CAAC,MAAM,YAAY,GAAG,iBAAiB,CAAC;AAC9C,MAAM,CAAC,MAAM,oBAAoB,GAAG,+BAA+B,CAAC;AACpE,MAAM,CAAC,MAAM,kBAAkB,GAAG,eAAe,CAAC;AAElD;;;;GAIG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAA4B;IAC9D,EAAE,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,mBAAmB,EAAE;IACzD,EAAE,KAAK,EAAE,oBAAoB,EAAE,WAAW,EAAE,qBAAqB,EAAE;IACnE,EAAE,KAAK,EAAE,kBAAkB,EAAE,WAAW,EAAE,uBAAuB,EAAE;CACnE,CAAC;AAEF,mFAAmF;AACnF,MAAM,CAAC,MAAM,qBAAqB,GAAG,eAAe,CAAC;AAErD,MAAM,CAAC,MAAM,0BAA0B,GACtC,4YAA4Y,CAAC;AAE9Y,MAAM,CAAC,MAAM,wBAAwB,GAAG,gEAAgE,CAAC;AAEzG,MAAM,CAAC,MAAM,2BAA2B,GAAG;IAC1C,+IAA+I;IAC/I,uJAAuJ;CACvJ,CAAC;AAEF,MAAM,gBAAgB,GAAG;;;;;;;;;;;;;qKAa4I,CAAC;AAEtK,4FAA4F;AAC5F,MAAM,UAAU,mBAAmB,CAAC,SAAmB,EAAU;IAChE,IAAI,CAAC,SAAS,CAAC,MAAM;QAAE,OAAO,gBAAgB,CAAC;IAC/C,MAAM,OAAO,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAChE,OAAO,GAAG,gBAAgB;;;EAGzB,OAAO;0NACiN,CAAC;AAAA,CAC1N","sourcesContent":["/**\n * plan/constants.ts — tool identity, command names, and prompt copy.\n *\n * Name/label/command live here so `index.ts` only assembles the extension,\n * matching the todo/question constants.ts pattern. Prompt copy is the\n * model-facing contract; keep it stable.\n */\n\nimport type { SelectOption } from \"../../core/extensions/types.ts\";\n\nexport const EXIT_PLAN_TOOL_NAME = \"exit_plan\";\nexport const EXIT_PLAN_TOOL_LABEL = \"Exit Plan\";\nexport const PLAN_COMMAND_NAME = \"plan\";\nexport const PLAN_FLAG_NAME = \"plan\";\nexport const PLAN_ENTRY_TYPE = \"plan-mode\";\nexport const PLAN_STATUS_KEY = \"plan-mode\";\n\n/**\n * Tools removed while planning. edit/write/bash grant write access directly;\n * subagent spawns child sessions that do not inherit the restricted tool set,\n * so leaving it active would let the model write through a child agent.\n * Other extension tools (question, todo, deepwiki, ...) stay available.\n */\nexport const PLAN_BLOCKED_TOOLS: ReadonlySet<string> = new Set([\"edit\", \"write\", \"bash\", \"subagent\"]);\n\n/** Max characters of plan markdown echoed into the post-compaction kickoff message. */\nexport const PLAN_EMBED_MAX_CHARS = 24_000;\n\nexport const MENU_EXECUTE = \"Start executing\";\nexport const MENU_COMPACT_EXECUTE = \"Compact context, then execute\";\nexport const MENU_KEEP_PLANNING = \"Keep planning\";\n\n/**\n * Approval choices. The label is the decision, the description is the\n * trade-off, so the three options can be told apart without reading to the\n * end of a sentence. select() resolves to the label.\n */\nexport const EXIT_PLAN_MENU_OPTIONS: readonly SelectOption[] = [\n\t{ label: MENU_EXECUTE, description: \"keep full context\" },\n\t{ label: MENU_COMPACT_EXECUTE, description: \"best for long tasks\" },\n\t{ label: MENU_KEEP_PLANNING, description: \"refine before running\" },\n];\n\n/** Esc maps to \"keep planning\", so the dismiss hint must not read as \"discard\". */\nexport const EXIT_PLAN_CANCEL_HINT = \"keep planning\";\n\nexport const EXIT_PLAN_TOOL_DESCRIPTION =\n\t\"Submit the finished plan and ask the user to approve leaving plan mode. Present the complete plan as normal response text first, then call this tool with a short title and the same plan markdown; on approval the plan is saved to disk. The user chooses between executing with full context, compacting the context before executing, or continuing to plan. Only available while plan mode is active.\";\n\nexport const EXIT_PLAN_PROMPT_SNIPPET = \"Submit the finished plan for user approval and leave plan mode\";\n\nexport const EXIT_PLAN_PROMPT_GUIDELINES = [\n\t\"Call `exit_plan` only after presenting the complete plan as response text; make it the only tool call in that step and stop after it returns.\",\n\t\"`exit_plan` results are authoritative for plan-mode state: follow them even when the plan-mode system prompt block is still present later in the run.\",\n];\n\nconst PLAN_MODE_PROMPT = `[PLAN MODE]\nYou are in plan mode: a read-only phase for exploring the codebase and agreeing on a plan with the user before any changes are made.\n\nRestrictions:\n- edit, write, bash, and subagent are unavailable; do not attempt changes or workarounds. Explore with read, grep, find, and ls instead.\n\nWorking style:\n- Answer questions from the code, not from the user: explore before asking.\n- When product intent, scope, or risk tolerance is unclear, interview the user with the question tool. Focus on one decision per exchange (batch only tightly coupled decisions) and always include your recommendation and the trade-offs.\n- Skip the interview for small, clear tasks. Pick sensible defaults for low-risk details and state them in the plan.\n\nDelivering the plan:\n- When the plan is ready, present it in full as normal response text (goal, steps, verification criteria) so the user can read it, then call exit_plan with a short title and the same markdown as the plan parameter.\n- The exit_plan tool result is authoritative for mode state: this block can outlive plan mode within a run, so if the result says plan mode has ended, it has ended.`;\n\n/** Build the plan-mode system prompt block, listing plans already saved for this branch. */\nexport function buildPlanModePrompt(planFiles: string[]): string {\n\tif (!planFiles.length) return PLAN_MODE_PROMPT;\n\tconst listing = planFiles.map((path) => `- ${path}`).join(\"\\n\");\n\treturn `${PLAN_MODE_PROMPT}\n\nPlans already saved for this session (read them with the read tool):\n${listing}\nWhen revising a saved plan, read the latest one first, account for work already completed, and produce a revision that states what changed and why; set the revises parameter to the file name of the plan being revised.`;\n}\n"]}
@@ -1,21 +1,3 @@
1
- /**
2
- * plan — Claude-Code-style plan mode: read-only exploration and interview,
3
- * then a user-approved exit that saves the plan and optionally compacts.
4
- *
5
- * Shape:
6
- * - Entry (/plan or --plan) removes write-capable tools (edit/write/bash/
7
- * subagent) and activates exit_plan; a tool_call guard covers requests
8
- * already in flight before the tool-set change lands.
9
- * - The plan text travels as an exit_plan parameter, so the model needs no
10
- * write access to produce the plan file.
11
- * - "Compact, then execute" keeps plan mode active until compaction completes
12
- * (tools are restored in onComplete) and terminates the run via the tool
13
- * result, so there is no window where the model holds full context plus
14
- * write tools. The kickoff message embeds the plan itself — continuation
15
- * never depends on the compaction summary preserving a path.
16
- * - State is conversation-backed via custom entries and branch replay (same
17
- * pattern as todo): /reload, /tree, resume, and fork all restore.
18
- */
19
1
  import type { ExtensionAPI } from "../../core/extensions/types.ts";
20
2
  export default function plan(pi: ExtensionAPI): void;
21
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/extensions/plan/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,KAAK,EAEX,YAAY,EAGZ,MAAM,gCAAgC,CAAC;AAgDxC,MAAM,CAAC,OAAO,UAAU,IAAI,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAySnD","sourcesContent":["/**\n * plan — Claude-Code-style plan mode: read-only exploration and interview,\n * then a user-approved exit that saves the plan and optionally compacts.\n *\n * Shape:\n * - Entry (/plan or --plan) removes write-capable tools (edit/write/bash/\n * subagent) and activates exit_plan; a tool_call guard covers requests\n * already in flight before the tool-set change lands.\n * - The plan text travels as an exit_plan parameter, so the model needs no\n * write access to produce the plan file.\n * - \"Compact, then execute\" keeps plan mode active until compaction completes\n * (tools are restored in onComplete) and terminates the run via the tool\n * result, so there is no window where the model holds full context plus\n * write tools. The kickoff message embeds the plan itself — continuation\n * never depends on the compaction summary preserving a path.\n * - State is conversation-backed via custom entries and branch replay (same\n * pattern as todo): /reload, /tree, resume, and fork all restore.\n */\nimport type {\n\tAgentToolResult,\n\tExtensionAPI,\n\tExtensionContext,\n\tToolExecutionMode,\n} from \"../../core/extensions/types.ts\";\nimport {\n\tbuildPlanModePrompt,\n\tEXIT_PLAN_PROMPT_GUIDELINES,\n\tEXIT_PLAN_PROMPT_SNIPPET,\n\tEXIT_PLAN_TOOL_DESCRIPTION,\n\tEXIT_PLAN_TOOL_LABEL,\n\tEXIT_PLAN_TOOL_NAME,\n\tMENU_COMPACT_EXECUTE,\n\tMENU_EXECUTE,\n\tMENU_KEEP_PLANNING,\n\tPLAN_BLOCKED_TOOLS,\n\tPLAN_COMMAND_NAME,\n\tPLAN_EMBED_MAX_CHARS,\n\tPLAN_ENTRY_TYPE,\n\tPLAN_FLAG_NAME,\n\tPLAN_STATUS_KEY,\n} from \"./constants.ts\";\nimport { type ExitPlanDetails, type ExitPlanParams, ExitPlanParamsSchema } from \"./schema.ts\";\nimport {\n\tclonePlanState,\n\tdisposePlanSession,\n\tEMPTY_PLAN_STATE,\n\tgetPlanState,\n\treplacePlanState,\n\treplayPlanFromBranch,\n\tsetActivePlanSession,\n} from \"./state.ts\";\nimport { savePlanFile } from \"./storage.ts\";\n\ninterface PendingCompact {\n\ttitle: string;\n\tplan: string;\n\tplanPath: string;\n}\n\nfunction buildKickoffMessage(pending: PendingCompact): string {\n\tconst truncated = pending.plan.length > PLAN_EMBED_MAX_CHARS;\n\tconst body = truncated\n\t\t? `${pending.plan.slice(0, PLAN_EMBED_MAX_CHARS)}\\n\\n[Plan truncated — read the full plan at ${pending.planPath}]`\n\t\t: pending.plan;\n\treturn `Execute the approved plan \"${pending.title}\" (saved at ${pending.planPath}).\n\n${body}\n\nStart now. Track multi-step work with the todo tool, and report back instead of improvising if the plan turns out to need revision.`;\n}\n\nexport default function plan(pi: ExtensionAPI): void {\n\tlet menuOpen = false;\n\tlet pendingCompact: PendingCompact | undefined;\n\n\tpi.registerFlag(PLAN_FLAG_NAME, {\n\t\tdescription: \"Start in plan mode (read-only planning)\",\n\t\ttype: \"boolean\",\n\t\tdefault: false,\n\t});\n\n\t/** Remove blocked tools, activate exit_plan, and return what was actually removed. */\n\tfunction applyPlanTools(): string[] {\n\t\tconst active = pi.getActiveTools();\n\t\tconst removed = active.filter((name) => PLAN_BLOCKED_TOOLS.has(name));\n\t\tpi.setActiveTools([...new Set([...active.filter((name) => !PLAN_BLOCKED_TOOLS.has(name)), EXIT_PLAN_TOOL_NAME])]);\n\t\treturn removed;\n\t}\n\n\t/** Inverse of applyPlanTools: drop exit_plan, re-add exactly what was removed. */\n\tfunction restoreTools(removedTools: string[]): void {\n\t\tconst active = pi.getActiveTools().filter((name) => name !== EXIT_PLAN_TOOL_NAME);\n\t\tpi.setActiveTools([...new Set([...active, ...removedTools])]);\n\t}\n\n\tfunction persist(): void {\n\t\tpi.appendEntry(PLAN_ENTRY_TYPE, getPlanState());\n\t}\n\n\tfunction updateStatus(ctx: ExtensionContext): void {\n\t\tconst state = getPlanState();\n\t\tctx.ui.setStatus(PLAN_STATUS_KEY, state.planning ? ctx.ui.theme.fg(\"warning\", \"plan\") : undefined);\n\t}\n\n\tfunction enterPlanMode(ctx: ExtensionContext): void {\n\t\tconst state = getPlanState();\n\t\tif (state.planning) return;\n\t\tconst removedTools = applyPlanTools();\n\t\treplacePlanState({ planning: true, removedTools, awaitingCompact: false, planFiles: state.planFiles });\n\t\tpersist();\n\t\tupdateStatus(ctx);\n\t\tctx.ui.notify(\"Plan mode enabled: edit, write, bash, and subagent are disabled.\", \"info\");\n\t}\n\n\tfunction exitPlanMode(ctx: ExtensionContext, notifyText?: string): void {\n\t\tconst state = getPlanState();\n\t\trestoreTools(state.removedTools);\n\t\treplacePlanState({ ...state, planning: false, awaitingCompact: false, removedTools: [] });\n\t\tpersist();\n\t\tupdateStatus(ctx);\n\t\tif (notifyText) ctx.ui.notify(notifyText, \"info\");\n\t}\n\n\t/** Save the approved plan, index it in state, and name the session if unnamed. */\n\tfunction saveApprovedPlan(ctx: ExtensionContext, params: ExitPlanParams, title: string): string {\n\t\tconst planPath = savePlanFile({\n\t\t\tsessionId: ctx.sessionManager.getSessionId(),\n\t\t\ttitle,\n\t\t\tplan: params.plan,\n\t\t\tcwd: ctx.cwd,\n\t\t\trevises: params.revises,\n\t\t});\n\t\tconst state = getPlanState();\n\t\treplacePlanState({ ...state, planFiles: [...state.planFiles, planPath] });\n\t\tif (!pi.getSessionName()) pi.setSessionName(title);\n\t\treturn planPath;\n\t}\n\n\t/**\n\t * Rebuild state from the current branch and reconcile the live tool set.\n\t * An interrupted compact-and-execute approval (restart, resume, branch\n\t * switch) degrades to the keep-context exit so the approval is honored.\n\t */\n\tfunction syncFromBranch(ctx: ExtensionContext, options?: { applyFlag?: boolean }): void {\n\t\tsetActivePlanSession(ctx.sessionManager.getSessionId());\n\t\tconst before = getPlanState();\n\t\tconst replayed = replayPlanFromBranch(ctx);\n\t\tconst next = replayed ?? clonePlanState(EMPTY_PLAN_STATE);\n\t\tif (!replayed && options?.applyFlag && pi.getFlag(PLAN_FLAG_NAME) === true) {\n\t\t\tnext.planning = true;\n\t\t}\n\n\t\tlet interruptedCompact = false;\n\t\tif (next.awaitingCompact) {\n\t\t\tnext.planning = false;\n\t\t\tnext.awaitingCompact = false;\n\t\t\tinterruptedCompact = true;\n\t\t\tpendingCompact = undefined;\n\t\t}\n\n\t\tif (next.planning) {\n\t\t\t// The fresh removal set is authoritative for the eventual restore:\n\t\t\t// the live tool set may differ from what the entry recorded.\n\t\t\tnext.removedTools = applyPlanTools();\n\t\t} else {\n\t\t\tif (pi.getActiveTools().includes(EXIT_PLAN_TOOL_NAME)) {\n\t\t\t\trestoreTools(before.planning ? before.removedTools : next.removedTools);\n\t\t\t}\n\t\t\tnext.removedTools = [];\n\t\t}\n\n\t\treplacePlanState(next);\n\t\tif (interruptedCompact) {\n\t\t\tpersist();\n\t\t\tctx.ui.notify(\"Pending compaction was interrupted; plan mode exited with context kept.\", \"warning\");\n\t\t}\n\t\tupdateStatus(ctx);\n\t}\n\n\tpi.registerTool({\n\t\tname: EXIT_PLAN_TOOL_NAME,\n\t\tlabel: EXIT_PLAN_TOOL_LABEL,\n\t\tdescription: EXIT_PLAN_TOOL_DESCRIPTION,\n\t\tpromptSnippet: EXIT_PLAN_PROMPT_SNIPPET,\n\t\tpromptGuidelines: EXIT_PLAN_PROMPT_GUIDELINES,\n\t\tparameters: ExitPlanParamsSchema,\n\t\texecutionMode: \"sequential\" as ToolExecutionMode,\n\n\t\tasync execute(_toolCallId, params, signal, _onUpdate, ctx): Promise<AgentToolResult<ExitPlanDetails>> {\n\t\t\tsetActivePlanSession(ctx.sessionManager.getSessionId());\n\t\t\tconst state = getPlanState();\n\t\t\tconst title = params.title.trim() || \"plan\";\n\n\t\t\tif (!state.planning || state.awaitingCompact) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{ type: \"text\", text: \"Plan mode is not awaiting an exit; continue with your current instructions.\" },\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { decision: \"cancelled\", title },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tif (!ctx.hasUI) {\n\t\t\t\t// json/print: no way to ask, so honor the least surprising default —\n\t\t\t\t// exit with context kept. Documented as a headless-mode semantic.\n\t\t\t\tconst planPath = saveApprovedPlan(ctx, params, title);\n\t\t\t\texitPlanMode(ctx);\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: `Plan saved to ${planPath}. No interactive UI, so plan mode exited with context kept (authoritative: write tools are restored). Begin executing the plan now.`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { decision: \"execute\", title, planPath },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tlet choice: string | undefined;\n\t\t\tmenuOpen = true;\n\t\t\ttry {\n\t\t\t\tchoice = await ctx.ui.select(\n\t\t\t\t\t`Approve plan \"${title}\"?`,\n\t\t\t\t\t[MENU_EXECUTE, MENU_COMPACT_EXECUTE, MENU_KEEP_PLANNING],\n\t\t\t\t\t{\n\t\t\t\t\t\tsignal,\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t} finally {\n\t\t\t\tmenuOpen = false;\n\t\t\t}\n\n\t\t\tif (signal?.aborted) {\n\t\t\t\t// Throwing lets Pi mark the result as an error, matching the todo convention.\n\t\t\t\tthrow new Error(\"Plan approval was interrupted; plan mode is still active.\");\n\t\t\t}\n\n\t\t\tif (choice === MENU_EXECUTE) {\n\t\t\t\tconst planPath = saveApprovedPlan(ctx, params, title);\n\t\t\t\texitPlanMode(ctx, \"Plan approved: executing with full context.\");\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: `Plan approved and saved to ${planPath}. Plan mode has ended and write tools are restored (authoritative — overrides the plan-mode system prompt block). Begin executing the plan now; track multi-step work with the todo tool.`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { decision: \"execute\", title, planPath },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tif (choice === MENU_COMPACT_EXECUTE) {\n\t\t\t\tconst planPath = saveApprovedPlan(ctx, params, title);\n\t\t\t\t// Stay in plan mode (tools restricted) until compaction completes;\n\t\t\t\t// terminate ends the run so agent_settled can start the compaction.\n\t\t\t\treplacePlanState({ ...getPlanState(), awaitingCompact: true });\n\t\t\t\tpersist();\n\t\t\t\tpendingCompact = { title, plan: params.plan, planPath };\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: `Plan approved and saved to ${planPath}. The context will now be compacted; stop here — execution restarts automatically after compaction.`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { decision: \"compactAndExecute\", title, planPath },\n\t\t\t\t\tterminate: true,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Explicit \"Keep planning\", Esc, or timeout: the only side-effect-free\n\t\t\t// choice, so any dismissal maps here.\n\t\t\tlet feedback: string | undefined;\n\t\t\tif (choice === MENU_KEEP_PLANNING) {\n\t\t\t\tfeedback =\n\t\t\t\t\tctx.mode === \"tui\"\n\t\t\t\t\t\t? await ctx.ui.editor(\"Feedback for the plan (optional):\", \"\")\n\t\t\t\t\t\t: await ctx.ui.input(\"Feedback for the plan (optional):\");\n\t\t\t}\n\t\t\tconst trimmedFeedback = feedback?.trim();\n\t\t\treturn {\n\t\t\t\tcontent: [\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\ttext: trimmedFeedback\n\t\t\t\t\t\t\t? `The user wants to keep planning. Feedback:\\n${trimmedFeedback}`\n\t\t\t\t\t\t\t: \"The user wants to keep planning. Ask what should change, or keep refining the open points.\",\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t\tdetails: { decision: \"keepPlanning\", title },\n\t\t\t};\n\t\t},\n\t});\n\n\tpi.registerCommand(PLAN_COMMAND_NAME, {\n\t\tdescription: \"Toggle plan mode (read-only planning with user-approved exit)\",\n\t\thandler: async (_args, ctx) => {\n\t\t\tsetActivePlanSession(ctx.sessionManager.getSessionId());\n\t\t\tconst state = getPlanState();\n\t\t\tif (menuOpen) {\n\t\t\t\tctx.ui.notify(\"The exit_plan approval menu is open — answer it first.\", \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (state.awaitingCompact) {\n\t\t\t\tctx.ui.notify(\"Waiting for compaction to finish before leaving plan mode.\", \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (state.planning) {\n\t\t\t\texitPlanMode(ctx, \"Plan mode disabled: full tool access restored.\");\n\t\t\t} else {\n\t\t\t\tenterPlanMode(ctx);\n\t\t\t}\n\t\t},\n\t});\n\n\t// Defense in depth: the active-tools change lands on the next LLM request,\n\t// so a request already in flight when plan mode toggled can still carry\n\t// blocked tool calls.\n\tpi.on(\"tool_call\", async (event, ctx) => {\n\t\tsetActivePlanSession(ctx.sessionManager.getSessionId());\n\t\tconst state = getPlanState();\n\t\tif (!state.planning || !PLAN_BLOCKED_TOOLS.has(event.toolName)) return;\n\t\treturn {\n\t\t\tblock: true,\n\t\t\treason: `Plan mode: ${event.toolName} is disabled. Keep exploring with read-only tools, or call ${EXIT_PLAN_TOOL_NAME} to submit the plan for approval.`,\n\t\t};\n\t});\n\n\tpi.on(\"before_agent_start\", async (event, ctx) => {\n\t\tsetActivePlanSession(ctx.sessionManager.getSessionId());\n\t\tconst state = getPlanState();\n\t\tif (!state.planning) return;\n\t\treturn { systemPrompt: `${event.systemPrompt}\\n\\n${buildPlanModePrompt(state.planFiles)}` };\n\t});\n\n\t// agent_settled fires once per fully settled run (agent_end can repeat on\n\t// retries and queued continuations), so the approved compaction starts here.\n\tpi.on(\"agent_settled\", async (_event, ctx) => {\n\t\tsetActivePlanSession(ctx.sessionManager.getSessionId());\n\t\tif (!getPlanState().awaitingCompact || !pendingCompact) return;\n\t\tconst pending = pendingCompact;\n\t\tpendingCompact = undefined;\n\t\tctx.compact({\n\t\t\tcustomInstructions: `The user approved an implementation plan saved at ${pending.planPath}. Preserve decisions, constraints, and rejected alternatives from the planning discussion that are not already written in the plan file.`,\n\t\t\tonComplete: () => {\n\t\t\t\texitPlanMode(ctx, \"Context compacted; executing the plan.\");\n\t\t\t\tpi.sendUserMessage(buildKickoffMessage(pending));\n\t\t\t},\n\t\t\tonError: (error) => {\n\t\t\t\t// Never leave the user stuck read-only: degrade to keep-context execution.\n\t\t\t\texitPlanMode(ctx, `Compaction failed (${error.message}); executing with full context instead.`);\n\t\t\t\tpi.sendUserMessage(buildKickoffMessage(pending));\n\t\t\t},\n\t\t});\n\t});\n\n\tpi.on(\"session_start\", async (_event, ctx) => {\n\t\tsyncFromBranch(ctx, { applyFlag: true });\n\t});\n\n\tpi.on(\"session_tree\", async (_event, ctx) => {\n\t\tsyncFromBranch(ctx);\n\t});\n\n\tpi.on(\"session_shutdown\", async (_event, ctx) => {\n\t\tpendingCompact = undefined;\n\t\tdisposePlanSession(ctx.sessionManager.getSessionId());\n\t});\n}\n"]}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/extensions/plan/index.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAEX,YAAY,EAGZ,MAAM,gCAAgC,CAAC;AAmDxC,MAAM,CAAC,OAAO,UAAU,IAAI,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAmTnD","sourcesContent":["/**\n * plan — Claude-Code-style plan mode: read-only exploration and interview,\n * then a user-approved exit that saves the plan and optionally compacts.\n *\n * Shape:\n * - Entry (/plan or --plan) removes write-capable tools (edit/write/bash/\n * subagent) and activates exit_plan; a tool_call guard covers requests\n * already in flight before the tool-set change lands.\n * - The plan text travels as an exit_plan parameter, so the model needs no\n * write access to produce the plan file.\n * - \"Compact, then execute\" keeps plan mode active until compaction completes\n * (tools are restored in onComplete) and terminates the run via the tool\n * result, so there is no window where the model holds full context plus\n * write tools. The kickoff message embeds the plan itself — continuation\n * never depends on the compaction summary preserving a path.\n * - State is conversation-backed via custom entries and branch replay (same\n * pattern as todo): /reload, /tree, resume, and fork all restore.\n */\nimport { Text } from \"@earendil-works/pi-tui\";\nimport type {\n\tAgentToolResult,\n\tExtensionAPI,\n\tExtensionContext,\n\tToolExecutionMode,\n} from \"../../core/extensions/types.ts\";\nimport {\n\tbuildPlanModePrompt,\n\tEXIT_PLAN_CANCEL_HINT,\n\tEXIT_PLAN_MENU_OPTIONS,\n\tEXIT_PLAN_PROMPT_GUIDELINES,\n\tEXIT_PLAN_PROMPT_SNIPPET,\n\tEXIT_PLAN_TOOL_DESCRIPTION,\n\tEXIT_PLAN_TOOL_LABEL,\n\tEXIT_PLAN_TOOL_NAME,\n\tMENU_COMPACT_EXECUTE,\n\tMENU_EXECUTE,\n\tMENU_KEEP_PLANNING,\n\tPLAN_BLOCKED_TOOLS,\n\tPLAN_COMMAND_NAME,\n\tPLAN_EMBED_MAX_CHARS,\n\tPLAN_ENTRY_TYPE,\n\tPLAN_FLAG_NAME,\n\tPLAN_STATUS_KEY,\n} from \"./constants.ts\";\nimport { type ExitPlanDetails, type ExitPlanParams, ExitPlanParamsSchema } from \"./schema.ts\";\nimport {\n\tclonePlanState,\n\tdisposePlanSession,\n\tEMPTY_PLAN_STATE,\n\tgetPlanState,\n\treplacePlanState,\n\treplayPlanFromBranch,\n\tsetActivePlanSession,\n} from \"./state.ts\";\nimport { savePlanFile } from \"./storage.ts\";\nimport { formatApprovalSubtitle, formatExitPlanCall, formatExitPlanResult } from \"./view.ts\";\n\ninterface PendingCompact {\n\ttitle: string;\n\tplan: string;\n\tplanPath: string;\n}\n\nfunction buildKickoffMessage(pending: PendingCompact): string {\n\tconst truncated = pending.plan.length > PLAN_EMBED_MAX_CHARS;\n\tconst body = truncated\n\t\t? `${pending.plan.slice(0, PLAN_EMBED_MAX_CHARS)}\\n\\n[Plan truncated — read the full plan at ${pending.planPath}]`\n\t\t: pending.plan;\n\treturn `Execute the approved plan \"${pending.title}\" (saved at ${pending.planPath}).\n\n${body}\n\nStart now. Track multi-step work with the todo tool, and report back instead of improvising if the plan turns out to need revision.`;\n}\n\nexport default function plan(pi: ExtensionAPI): void {\n\tlet menuOpen = false;\n\tlet pendingCompact: PendingCompact | undefined;\n\n\tpi.registerFlag(PLAN_FLAG_NAME, {\n\t\tdescription: \"Start in plan mode (read-only planning)\",\n\t\ttype: \"boolean\",\n\t\tdefault: false,\n\t});\n\n\t/** Remove blocked tools, activate exit_plan, and return what was actually removed. */\n\tfunction applyPlanTools(): string[] {\n\t\tconst active = pi.getActiveTools();\n\t\tconst removed = active.filter((name) => PLAN_BLOCKED_TOOLS.has(name));\n\t\tpi.setActiveTools([...new Set([...active.filter((name) => !PLAN_BLOCKED_TOOLS.has(name)), EXIT_PLAN_TOOL_NAME])]);\n\t\treturn removed;\n\t}\n\n\t/** Inverse of applyPlanTools: drop exit_plan, re-add exactly what was removed. */\n\tfunction restoreTools(removedTools: string[]): void {\n\t\tconst active = pi.getActiveTools().filter((name) => name !== EXIT_PLAN_TOOL_NAME);\n\t\tpi.setActiveTools([...new Set([...active, ...removedTools])]);\n\t}\n\n\tfunction persist(): void {\n\t\tpi.appendEntry(PLAN_ENTRY_TYPE, getPlanState());\n\t}\n\n\tfunction updateStatus(ctx: ExtensionContext): void {\n\t\tconst state = getPlanState();\n\t\tctx.ui.setStatus(PLAN_STATUS_KEY, state.planning ? ctx.ui.theme.fg(\"accent\", \"Plan Mode\") : undefined);\n\t}\n\n\tfunction enterPlanMode(ctx: ExtensionContext): void {\n\t\tconst state = getPlanState();\n\t\tif (state.planning) return;\n\t\tconst removedTools = applyPlanTools();\n\t\treplacePlanState({ planning: true, removedTools, awaitingCompact: false, planFiles: state.planFiles });\n\t\tpersist();\n\t\tupdateStatus(ctx);\n\t\tctx.ui.notify(\"Plan mode enabled: edit, write, bash, and subagent are disabled.\", \"info\");\n\t}\n\n\tfunction exitPlanMode(ctx: ExtensionContext, notifyText?: string): void {\n\t\tconst state = getPlanState();\n\t\trestoreTools(state.removedTools);\n\t\treplacePlanState({ ...state, planning: false, awaitingCompact: false, removedTools: [] });\n\t\tpersist();\n\t\tupdateStatus(ctx);\n\t\tif (notifyText) ctx.ui.notify(notifyText, \"info\");\n\t}\n\n\t/** Save the approved plan, index it in state, and name the session if unnamed. */\n\tfunction saveApprovedPlan(ctx: ExtensionContext, params: ExitPlanParams, title: string): string {\n\t\tconst planPath = savePlanFile({\n\t\t\tsessionId: ctx.sessionManager.getSessionId(),\n\t\t\ttitle,\n\t\t\tplan: params.plan,\n\t\t\tcwd: ctx.cwd,\n\t\t\trevises: params.revises,\n\t\t});\n\t\tconst state = getPlanState();\n\t\treplacePlanState({ ...state, planFiles: [...state.planFiles, planPath] });\n\t\tif (!pi.getSessionName()) pi.setSessionName(title);\n\t\treturn planPath;\n\t}\n\n\t/**\n\t * Rebuild state from the current branch and reconcile the live tool set.\n\t * An interrupted compact-and-execute approval (restart, resume, branch\n\t * switch) degrades to the keep-context exit so the approval is honored.\n\t */\n\tfunction syncFromBranch(ctx: ExtensionContext, options?: { applyFlag?: boolean }): void {\n\t\tsetActivePlanSession(ctx.sessionManager.getSessionId());\n\t\tconst before = getPlanState();\n\t\tconst replayed = replayPlanFromBranch(ctx);\n\t\tconst next = replayed ?? clonePlanState(EMPTY_PLAN_STATE);\n\t\tif (!replayed && options?.applyFlag && pi.getFlag(PLAN_FLAG_NAME) === true) {\n\t\t\tnext.planning = true;\n\t\t}\n\n\t\tlet interruptedCompact = false;\n\t\tif (next.awaitingCompact) {\n\t\t\tnext.planning = false;\n\t\t\tnext.awaitingCompact = false;\n\t\t\tinterruptedCompact = true;\n\t\t\tpendingCompact = undefined;\n\t\t}\n\n\t\tif (next.planning) {\n\t\t\t// The fresh removal set is authoritative for the eventual restore:\n\t\t\t// the live tool set may differ from what the entry recorded.\n\t\t\tnext.removedTools = applyPlanTools();\n\t\t} else {\n\t\t\tif (pi.getActiveTools().includes(EXIT_PLAN_TOOL_NAME)) {\n\t\t\t\trestoreTools(before.planning ? before.removedTools : next.removedTools);\n\t\t\t}\n\t\t\tnext.removedTools = [];\n\t\t}\n\n\t\treplacePlanState(next);\n\t\tif (interruptedCompact) {\n\t\t\tpersist();\n\t\t\tctx.ui.notify(\"Pending compaction was interrupted; plan mode exited with context kept.\", \"warning\");\n\t\t}\n\t\tupdateStatus(ctx);\n\t}\n\n\tpi.registerTool({\n\t\tname: EXIT_PLAN_TOOL_NAME,\n\t\tlabel: EXIT_PLAN_TOOL_LABEL,\n\t\tdescription: EXIT_PLAN_TOOL_DESCRIPTION,\n\t\tpromptSnippet: EXIT_PLAN_PROMPT_SNIPPET,\n\t\tpromptGuidelines: EXIT_PLAN_PROMPT_GUIDELINES,\n\t\tparameters: ExitPlanParamsSchema,\n\t\texecutionMode: \"sequential\" as ToolExecutionMode,\n\n\t\tasync execute(_toolCallId, params, signal, _onUpdate, ctx): Promise<AgentToolResult<ExitPlanDetails>> {\n\t\t\tsetActivePlanSession(ctx.sessionManager.getSessionId());\n\t\t\tconst state = getPlanState();\n\t\t\tconst title = params.title.trim() || \"plan\";\n\n\t\t\tif (!state.planning || state.awaitingCompact) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{ type: \"text\", text: \"Plan mode is not awaiting an exit; continue with your current instructions.\" },\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { decision: \"cancelled\", title },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tif (!ctx.hasUI) {\n\t\t\t\t// json/print: no way to ask, so honor the least surprising default —\n\t\t\t\t// exit with context kept. Documented as a headless-mode semantic.\n\t\t\t\tconst planPath = saveApprovedPlan(ctx, params, title);\n\t\t\t\texitPlanMode(ctx);\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: `Plan saved to ${planPath}. No interactive UI, so plan mode exited with context kept (authoritative: write tools are restored). Begin executing the plan now.`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { decision: \"execute\", title, planPath },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tlet choice: string | undefined;\n\t\t\tmenuOpen = true;\n\t\t\ttry {\n\t\t\t\tchoice = await ctx.ui.select(`Approve plan \"${title}\"?`, EXIT_PLAN_MENU_OPTIONS, {\n\t\t\t\t\tsignal,\n\t\t\t\t\tsubtitle: formatApprovalSubtitle(ctx),\n\t\t\t\t\tcancelHint: EXIT_PLAN_CANCEL_HINT,\n\t\t\t\t});\n\t\t\t} finally {\n\t\t\t\tmenuOpen = false;\n\t\t\t}\n\n\t\t\tif (signal?.aborted) {\n\t\t\t\t// Throwing lets Pi mark the result as an error, matching the todo convention.\n\t\t\t\tthrow new Error(\"Plan approval was interrupted; plan mode is still active.\");\n\t\t\t}\n\n\t\t\tif (choice === MENU_EXECUTE) {\n\t\t\t\tconst planPath = saveApprovedPlan(ctx, params, title);\n\t\t\t\texitPlanMode(ctx, \"Plan approved: executing with full context.\");\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: `Plan approved and saved to ${planPath}. Plan mode has ended and write tools are restored (authoritative — overrides the plan-mode system prompt block). Begin executing the plan now; track multi-step work with the todo tool.`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { decision: \"execute\", title, planPath },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tif (choice === MENU_COMPACT_EXECUTE) {\n\t\t\t\tconst planPath = saveApprovedPlan(ctx, params, title);\n\t\t\t\t// Stay in plan mode (tools restricted) until compaction completes;\n\t\t\t\t// terminate ends the run so agent_settled can start the compaction.\n\t\t\t\treplacePlanState({ ...getPlanState(), awaitingCompact: true });\n\t\t\t\tpersist();\n\t\t\t\tpendingCompact = { title, plan: params.plan, planPath };\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: `Plan approved and saved to ${planPath}. The context will now be compacted; stop here — execution restarts automatically after compaction.`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { decision: \"compactAndExecute\", title, planPath },\n\t\t\t\t\tterminate: true,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Explicit \"Keep planning\", Esc, or timeout: the only side-effect-free\n\t\t\t// choice, so any dismissal maps here.\n\t\t\tlet feedback: string | undefined;\n\t\t\tif (choice === MENU_KEEP_PLANNING) {\n\t\t\t\tfeedback =\n\t\t\t\t\tctx.mode === \"tui\"\n\t\t\t\t\t\t? await ctx.ui.editor(\"Feedback for the plan (optional):\", \"\")\n\t\t\t\t\t\t: await ctx.ui.input(\"Feedback for the plan (optional):\");\n\t\t\t}\n\t\t\tconst trimmedFeedback = feedback?.trim();\n\t\t\treturn {\n\t\t\t\tcontent: [\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\ttext: trimmedFeedback\n\t\t\t\t\t\t\t? `The user wants to keep planning. Feedback:\\n${trimmedFeedback}`\n\t\t\t\t\t\t\t: \"The user wants to keep planning. Ask what should change, or keep refining the open points.\",\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t\tdetails: { decision: \"keepPlanning\", title },\n\t\t\t};\n\t\t},\n\n\t\trenderCall(args, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatExitPlanCall(args, theme, context.expanded));\n\t\t\treturn text;\n\t\t},\n\n\t\trenderResult(result, _options, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatExitPlanResult(result, theme));\n\t\t\treturn text;\n\t\t},\n\t});\n\n\tpi.registerCommand(PLAN_COMMAND_NAME, {\n\t\tdescription: \"Toggle plan mode (read-only planning with user-approved exit)\",\n\t\thandler: async (_args, ctx) => {\n\t\t\tsetActivePlanSession(ctx.sessionManager.getSessionId());\n\t\t\tconst state = getPlanState();\n\t\t\tif (menuOpen) {\n\t\t\t\tctx.ui.notify(\"The exit_plan approval menu is open — answer it first.\", \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (state.awaitingCompact) {\n\t\t\t\tctx.ui.notify(\"Waiting for compaction to finish before leaving plan mode.\", \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (state.planning) {\n\t\t\t\texitPlanMode(ctx, \"Plan mode disabled: full tool access restored.\");\n\t\t\t} else {\n\t\t\t\tenterPlanMode(ctx);\n\t\t\t}\n\t\t},\n\t});\n\n\t// Defense in depth: the active-tools change lands on the next LLM request,\n\t// so a request already in flight when plan mode toggled can still carry\n\t// blocked tool calls.\n\tpi.on(\"tool_call\", async (event, ctx) => {\n\t\tsetActivePlanSession(ctx.sessionManager.getSessionId());\n\t\tconst state = getPlanState();\n\t\tif (!state.planning || !PLAN_BLOCKED_TOOLS.has(event.toolName)) return;\n\t\treturn {\n\t\t\tblock: true,\n\t\t\treason: `Plan mode: ${event.toolName} is disabled. Keep exploring with read-only tools, or call ${EXIT_PLAN_TOOL_NAME} to submit the plan for approval.`,\n\t\t};\n\t});\n\n\tpi.on(\"before_agent_start\", async (event, ctx) => {\n\t\tsetActivePlanSession(ctx.sessionManager.getSessionId());\n\t\tconst state = getPlanState();\n\t\tif (!state.planning) return;\n\t\treturn { systemPrompt: `${event.systemPrompt}\\n\\n${buildPlanModePrompt(state.planFiles)}` };\n\t});\n\n\t// agent_settled fires once per fully settled run (agent_end can repeat on\n\t// retries and queued continuations), so the approved compaction starts here.\n\tpi.on(\"agent_settled\", async (_event, ctx) => {\n\t\tsetActivePlanSession(ctx.sessionManager.getSessionId());\n\t\tif (!getPlanState().awaitingCompact || !pendingCompact) return;\n\t\tconst pending = pendingCompact;\n\t\tpendingCompact = undefined;\n\t\tctx.compact({\n\t\t\tcustomInstructions: `The user approved an implementation plan saved at ${pending.planPath}. Preserve decisions, constraints, and rejected alternatives from the planning discussion that are not already written in the plan file.`,\n\t\t\tonComplete: () => {\n\t\t\t\texitPlanMode(ctx, \"Context compacted; executing the plan.\");\n\t\t\t\tpi.sendUserMessage(buildKickoffMessage(pending));\n\t\t\t},\n\t\t\tonError: (error) => {\n\t\t\t\t// Never leave the user stuck read-only: degrade to keep-context execution.\n\t\t\t\texitPlanMode(ctx, `Compaction failed (${error.message}); executing with full context instead.`);\n\t\t\t\tpi.sendUserMessage(buildKickoffMessage(pending));\n\t\t\t},\n\t\t});\n\t});\n\n\tpi.on(\"session_start\", async (_event, ctx) => {\n\t\tsyncFromBranch(ctx, { applyFlag: true });\n\t});\n\n\tpi.on(\"session_tree\", async (_event, ctx) => {\n\t\tsyncFromBranch(ctx);\n\t});\n\n\tpi.on(\"session_shutdown\", async (_event, ctx) => {\n\t\tpendingCompact = undefined;\n\t\tdisposePlanSession(ctx.sessionManager.getSessionId());\n\t});\n}\n"]}
@@ -1,7 +1,27 @@
1
- import { buildPlanModePrompt, EXIT_PLAN_PROMPT_GUIDELINES, EXIT_PLAN_PROMPT_SNIPPET, EXIT_PLAN_TOOL_DESCRIPTION, EXIT_PLAN_TOOL_LABEL, EXIT_PLAN_TOOL_NAME, MENU_COMPACT_EXECUTE, MENU_EXECUTE, MENU_KEEP_PLANNING, PLAN_BLOCKED_TOOLS, PLAN_COMMAND_NAME, PLAN_EMBED_MAX_CHARS, PLAN_ENTRY_TYPE, PLAN_FLAG_NAME, PLAN_STATUS_KEY, } from "./constants.js";
1
+ /**
2
+ * plan — Claude-Code-style plan mode: read-only exploration and interview,
3
+ * then a user-approved exit that saves the plan and optionally compacts.
4
+ *
5
+ * Shape:
6
+ * - Entry (/plan or --plan) removes write-capable tools (edit/write/bash/
7
+ * subagent) and activates exit_plan; a tool_call guard covers requests
8
+ * already in flight before the tool-set change lands.
9
+ * - The plan text travels as an exit_plan parameter, so the model needs no
10
+ * write access to produce the plan file.
11
+ * - "Compact, then execute" keeps plan mode active until compaction completes
12
+ * (tools are restored in onComplete) and terminates the run via the tool
13
+ * result, so there is no window where the model holds full context plus
14
+ * write tools. The kickoff message embeds the plan itself — continuation
15
+ * never depends on the compaction summary preserving a path.
16
+ * - State is conversation-backed via custom entries and branch replay (same
17
+ * pattern as todo): /reload, /tree, resume, and fork all restore.
18
+ */
19
+ import { Text } from "@earendil-works/pi-tui";
20
+ import { buildPlanModePrompt, EXIT_PLAN_CANCEL_HINT, EXIT_PLAN_MENU_OPTIONS, EXIT_PLAN_PROMPT_GUIDELINES, EXIT_PLAN_PROMPT_SNIPPET, EXIT_PLAN_TOOL_DESCRIPTION, EXIT_PLAN_TOOL_LABEL, EXIT_PLAN_TOOL_NAME, MENU_COMPACT_EXECUTE, MENU_EXECUTE, MENU_KEEP_PLANNING, PLAN_BLOCKED_TOOLS, PLAN_COMMAND_NAME, PLAN_EMBED_MAX_CHARS, PLAN_ENTRY_TYPE, PLAN_FLAG_NAME, PLAN_STATUS_KEY, } from "./constants.js";
2
21
  import { ExitPlanParamsSchema } from "./schema.js";
3
22
  import { clonePlanState, disposePlanSession, EMPTY_PLAN_STATE, getPlanState, replacePlanState, replayPlanFromBranch, setActivePlanSession, } from "./state.js";
4
23
  import { savePlanFile } from "./storage.js";
24
+ import { formatApprovalSubtitle, formatExitPlanCall, formatExitPlanResult } from "./view.js";
5
25
  function buildKickoffMessage(pending) {
6
26
  const truncated = pending.plan.length > PLAN_EMBED_MAX_CHARS;
7
27
  const body = truncated
@@ -38,7 +58,7 @@ export default function plan(pi) {
38
58
  }
39
59
  function updateStatus(ctx) {
40
60
  const state = getPlanState();
41
- ctx.ui.setStatus(PLAN_STATUS_KEY, state.planning ? ctx.ui.theme.fg("warning", "plan") : undefined);
61
+ ctx.ui.setStatus(PLAN_STATUS_KEY, state.planning ? ctx.ui.theme.fg("accent", "Plan Mode") : undefined);
42
62
  }
43
63
  function enterPlanMode(ctx) {
44
64
  const state = getPlanState();
@@ -150,8 +170,10 @@ export default function plan(pi) {
150
170
  let choice;
151
171
  menuOpen = true;
152
172
  try {
153
- choice = await ctx.ui.select(`Approve plan "${title}"?`, [MENU_EXECUTE, MENU_COMPACT_EXECUTE, MENU_KEEP_PLANNING], {
173
+ choice = await ctx.ui.select(`Approve plan "${title}"?`, EXIT_PLAN_MENU_OPTIONS, {
154
174
  signal,
175
+ subtitle: formatApprovalSubtitle(ctx),
176
+ cancelHint: EXIT_PLAN_CANCEL_HINT,
155
177
  });
156
178
  }
157
179
  finally {
@@ -214,6 +236,16 @@ export default function plan(pi) {
214
236
  details: { decision: "keepPlanning", title },
215
237
  };
216
238
  },
239
+ renderCall(args, theme, context) {
240
+ const text = context.lastComponent ?? new Text("", 0, 0);
241
+ text.setText(formatExitPlanCall(args, theme, context.expanded));
242
+ return text;
243
+ },
244
+ renderResult(result, _options, theme, context) {
245
+ const text = context.lastComponent ?? new Text("", 0, 0);
246
+ text.setText(formatExitPlanResult(result, theme));
247
+ return text;
248
+ },
217
249
  });
218
250
  pi.registerCommand(PLAN_COMMAND_NAME, {
219
251
  description: "Toggle plan mode (read-only planning with user-approved exit)",
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/extensions/plan/index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * plan — Claude-Code-style plan mode: read-only exploration and interview,\n * then a user-approved exit that saves the plan and optionally compacts.\n *\n * Shape:\n * - Entry (/plan or --plan) removes write-capable tools (edit/write/bash/\n * subagent) and activates exit_plan; a tool_call guard covers requests\n * already in flight before the tool-set change lands.\n * - The plan text travels as an exit_plan parameter, so the model needs no\n * write access to produce the plan file.\n * - \"Compact, then execute\" keeps plan mode active until compaction completes\n * (tools are restored in onComplete) and terminates the run via the tool\n * result, so there is no window where the model holds full context plus\n * write tools. The kickoff message embeds the plan itself — continuation\n * never depends on the compaction summary preserving a path.\n * - State is conversation-backed via custom entries and branch replay (same\n * pattern as todo): /reload, /tree, resume, and fork all restore.\n */\nimport type {\n\tAgentToolResult,\n\tExtensionAPI,\n\tExtensionContext,\n\tToolExecutionMode,\n} from \"../../core/extensions/types.ts\";\nimport {\n\tbuildPlanModePrompt,\n\tEXIT_PLAN_PROMPT_GUIDELINES,\n\tEXIT_PLAN_PROMPT_SNIPPET,\n\tEXIT_PLAN_TOOL_DESCRIPTION,\n\tEXIT_PLAN_TOOL_LABEL,\n\tEXIT_PLAN_TOOL_NAME,\n\tMENU_COMPACT_EXECUTE,\n\tMENU_EXECUTE,\n\tMENU_KEEP_PLANNING,\n\tPLAN_BLOCKED_TOOLS,\n\tPLAN_COMMAND_NAME,\n\tPLAN_EMBED_MAX_CHARS,\n\tPLAN_ENTRY_TYPE,\n\tPLAN_FLAG_NAME,\n\tPLAN_STATUS_KEY,\n} from \"./constants.ts\";\nimport { type ExitPlanDetails, type ExitPlanParams, ExitPlanParamsSchema } from \"./schema.ts\";\nimport {\n\tclonePlanState,\n\tdisposePlanSession,\n\tEMPTY_PLAN_STATE,\n\tgetPlanState,\n\treplacePlanState,\n\treplayPlanFromBranch,\n\tsetActivePlanSession,\n} from \"./state.ts\";\nimport { savePlanFile } from \"./storage.ts\";\n\ninterface PendingCompact {\n\ttitle: string;\n\tplan: string;\n\tplanPath: string;\n}\n\nfunction buildKickoffMessage(pending: PendingCompact): string {\n\tconst truncated = pending.plan.length > PLAN_EMBED_MAX_CHARS;\n\tconst body = truncated\n\t\t? `${pending.plan.slice(0, PLAN_EMBED_MAX_CHARS)}\\n\\n[Plan truncated — read the full plan at ${pending.planPath}]`\n\t\t: pending.plan;\n\treturn `Execute the approved plan \"${pending.title}\" (saved at ${pending.planPath}).\n\n${body}\n\nStart now. Track multi-step work with the todo tool, and report back instead of improvising if the plan turns out to need revision.`;\n}\n\nexport default function plan(pi: ExtensionAPI): void {\n\tlet menuOpen = false;\n\tlet pendingCompact: PendingCompact | undefined;\n\n\tpi.registerFlag(PLAN_FLAG_NAME, {\n\t\tdescription: \"Start in plan mode (read-only planning)\",\n\t\ttype: \"boolean\",\n\t\tdefault: false,\n\t});\n\n\t/** Remove blocked tools, activate exit_plan, and return what was actually removed. */\n\tfunction applyPlanTools(): string[] {\n\t\tconst active = pi.getActiveTools();\n\t\tconst removed = active.filter((name) => PLAN_BLOCKED_TOOLS.has(name));\n\t\tpi.setActiveTools([...new Set([...active.filter((name) => !PLAN_BLOCKED_TOOLS.has(name)), EXIT_PLAN_TOOL_NAME])]);\n\t\treturn removed;\n\t}\n\n\t/** Inverse of applyPlanTools: drop exit_plan, re-add exactly what was removed. */\n\tfunction restoreTools(removedTools: string[]): void {\n\t\tconst active = pi.getActiveTools().filter((name) => name !== EXIT_PLAN_TOOL_NAME);\n\t\tpi.setActiveTools([...new Set([...active, ...removedTools])]);\n\t}\n\n\tfunction persist(): void {\n\t\tpi.appendEntry(PLAN_ENTRY_TYPE, getPlanState());\n\t}\n\n\tfunction updateStatus(ctx: ExtensionContext): void {\n\t\tconst state = getPlanState();\n\t\tctx.ui.setStatus(PLAN_STATUS_KEY, state.planning ? ctx.ui.theme.fg(\"warning\", \"plan\") : undefined);\n\t}\n\n\tfunction enterPlanMode(ctx: ExtensionContext): void {\n\t\tconst state = getPlanState();\n\t\tif (state.planning) return;\n\t\tconst removedTools = applyPlanTools();\n\t\treplacePlanState({ planning: true, removedTools, awaitingCompact: false, planFiles: state.planFiles });\n\t\tpersist();\n\t\tupdateStatus(ctx);\n\t\tctx.ui.notify(\"Plan mode enabled: edit, write, bash, and subagent are disabled.\", \"info\");\n\t}\n\n\tfunction exitPlanMode(ctx: ExtensionContext, notifyText?: string): void {\n\t\tconst state = getPlanState();\n\t\trestoreTools(state.removedTools);\n\t\treplacePlanState({ ...state, planning: false, awaitingCompact: false, removedTools: [] });\n\t\tpersist();\n\t\tupdateStatus(ctx);\n\t\tif (notifyText) ctx.ui.notify(notifyText, \"info\");\n\t}\n\n\t/** Save the approved plan, index it in state, and name the session if unnamed. */\n\tfunction saveApprovedPlan(ctx: ExtensionContext, params: ExitPlanParams, title: string): string {\n\t\tconst planPath = savePlanFile({\n\t\t\tsessionId: ctx.sessionManager.getSessionId(),\n\t\t\ttitle,\n\t\t\tplan: params.plan,\n\t\t\tcwd: ctx.cwd,\n\t\t\trevises: params.revises,\n\t\t});\n\t\tconst state = getPlanState();\n\t\treplacePlanState({ ...state, planFiles: [...state.planFiles, planPath] });\n\t\tif (!pi.getSessionName()) pi.setSessionName(title);\n\t\treturn planPath;\n\t}\n\n\t/**\n\t * Rebuild state from the current branch and reconcile the live tool set.\n\t * An interrupted compact-and-execute approval (restart, resume, branch\n\t * switch) degrades to the keep-context exit so the approval is honored.\n\t */\n\tfunction syncFromBranch(ctx: ExtensionContext, options?: { applyFlag?: boolean }): void {\n\t\tsetActivePlanSession(ctx.sessionManager.getSessionId());\n\t\tconst before = getPlanState();\n\t\tconst replayed = replayPlanFromBranch(ctx);\n\t\tconst next = replayed ?? clonePlanState(EMPTY_PLAN_STATE);\n\t\tif (!replayed && options?.applyFlag && pi.getFlag(PLAN_FLAG_NAME) === true) {\n\t\t\tnext.planning = true;\n\t\t}\n\n\t\tlet interruptedCompact = false;\n\t\tif (next.awaitingCompact) {\n\t\t\tnext.planning = false;\n\t\t\tnext.awaitingCompact = false;\n\t\t\tinterruptedCompact = true;\n\t\t\tpendingCompact = undefined;\n\t\t}\n\n\t\tif (next.planning) {\n\t\t\t// The fresh removal set is authoritative for the eventual restore:\n\t\t\t// the live tool set may differ from what the entry recorded.\n\t\t\tnext.removedTools = applyPlanTools();\n\t\t} else {\n\t\t\tif (pi.getActiveTools().includes(EXIT_PLAN_TOOL_NAME)) {\n\t\t\t\trestoreTools(before.planning ? before.removedTools : next.removedTools);\n\t\t\t}\n\t\t\tnext.removedTools = [];\n\t\t}\n\n\t\treplacePlanState(next);\n\t\tif (interruptedCompact) {\n\t\t\tpersist();\n\t\t\tctx.ui.notify(\"Pending compaction was interrupted; plan mode exited with context kept.\", \"warning\");\n\t\t}\n\t\tupdateStatus(ctx);\n\t}\n\n\tpi.registerTool({\n\t\tname: EXIT_PLAN_TOOL_NAME,\n\t\tlabel: EXIT_PLAN_TOOL_LABEL,\n\t\tdescription: EXIT_PLAN_TOOL_DESCRIPTION,\n\t\tpromptSnippet: EXIT_PLAN_PROMPT_SNIPPET,\n\t\tpromptGuidelines: EXIT_PLAN_PROMPT_GUIDELINES,\n\t\tparameters: ExitPlanParamsSchema,\n\t\texecutionMode: \"sequential\" as ToolExecutionMode,\n\n\t\tasync execute(_toolCallId, params, signal, _onUpdate, ctx): Promise<AgentToolResult<ExitPlanDetails>> {\n\t\t\tsetActivePlanSession(ctx.sessionManager.getSessionId());\n\t\t\tconst state = getPlanState();\n\t\t\tconst title = params.title.trim() || \"plan\";\n\n\t\t\tif (!state.planning || state.awaitingCompact) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{ type: \"text\", text: \"Plan mode is not awaiting an exit; continue with your current instructions.\" },\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { decision: \"cancelled\", title },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tif (!ctx.hasUI) {\n\t\t\t\t// json/print: no way to ask, so honor the least surprising default —\n\t\t\t\t// exit with context kept. Documented as a headless-mode semantic.\n\t\t\t\tconst planPath = saveApprovedPlan(ctx, params, title);\n\t\t\t\texitPlanMode(ctx);\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: `Plan saved to ${planPath}. No interactive UI, so plan mode exited with context kept (authoritative: write tools are restored). Begin executing the plan now.`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { decision: \"execute\", title, planPath },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tlet choice: string | undefined;\n\t\t\tmenuOpen = true;\n\t\t\ttry {\n\t\t\t\tchoice = await ctx.ui.select(\n\t\t\t\t\t`Approve plan \"${title}\"?`,\n\t\t\t\t\t[MENU_EXECUTE, MENU_COMPACT_EXECUTE, MENU_KEEP_PLANNING],\n\t\t\t\t\t{\n\t\t\t\t\t\tsignal,\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t} finally {\n\t\t\t\tmenuOpen = false;\n\t\t\t}\n\n\t\t\tif (signal?.aborted) {\n\t\t\t\t// Throwing lets Pi mark the result as an error, matching the todo convention.\n\t\t\t\tthrow new Error(\"Plan approval was interrupted; plan mode is still active.\");\n\t\t\t}\n\n\t\t\tif (choice === MENU_EXECUTE) {\n\t\t\t\tconst planPath = saveApprovedPlan(ctx, params, title);\n\t\t\t\texitPlanMode(ctx, \"Plan approved: executing with full context.\");\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: `Plan approved and saved to ${planPath}. Plan mode has ended and write tools are restored (authoritative — overrides the plan-mode system prompt block). Begin executing the plan now; track multi-step work with the todo tool.`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { decision: \"execute\", title, planPath },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tif (choice === MENU_COMPACT_EXECUTE) {\n\t\t\t\tconst planPath = saveApprovedPlan(ctx, params, title);\n\t\t\t\t// Stay in plan mode (tools restricted) until compaction completes;\n\t\t\t\t// terminate ends the run so agent_settled can start the compaction.\n\t\t\t\treplacePlanState({ ...getPlanState(), awaitingCompact: true });\n\t\t\t\tpersist();\n\t\t\t\tpendingCompact = { title, plan: params.plan, planPath };\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: `Plan approved and saved to ${planPath}. The context will now be compacted; stop here — execution restarts automatically after compaction.`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { decision: \"compactAndExecute\", title, planPath },\n\t\t\t\t\tterminate: true,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Explicit \"Keep planning\", Esc, or timeout: the only side-effect-free\n\t\t\t// choice, so any dismissal maps here.\n\t\t\tlet feedback: string | undefined;\n\t\t\tif (choice === MENU_KEEP_PLANNING) {\n\t\t\t\tfeedback =\n\t\t\t\t\tctx.mode === \"tui\"\n\t\t\t\t\t\t? await ctx.ui.editor(\"Feedback for the plan (optional):\", \"\")\n\t\t\t\t\t\t: await ctx.ui.input(\"Feedback for the plan (optional):\");\n\t\t\t}\n\t\t\tconst trimmedFeedback = feedback?.trim();\n\t\t\treturn {\n\t\t\t\tcontent: [\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\ttext: trimmedFeedback\n\t\t\t\t\t\t\t? `The user wants to keep planning. Feedback:\\n${trimmedFeedback}`\n\t\t\t\t\t\t\t: \"The user wants to keep planning. Ask what should change, or keep refining the open points.\",\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t\tdetails: { decision: \"keepPlanning\", title },\n\t\t\t};\n\t\t},\n\t});\n\n\tpi.registerCommand(PLAN_COMMAND_NAME, {\n\t\tdescription: \"Toggle plan mode (read-only planning with user-approved exit)\",\n\t\thandler: async (_args, ctx) => {\n\t\t\tsetActivePlanSession(ctx.sessionManager.getSessionId());\n\t\t\tconst state = getPlanState();\n\t\t\tif (menuOpen) {\n\t\t\t\tctx.ui.notify(\"The exit_plan approval menu is open — answer it first.\", \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (state.awaitingCompact) {\n\t\t\t\tctx.ui.notify(\"Waiting for compaction to finish before leaving plan mode.\", \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (state.planning) {\n\t\t\t\texitPlanMode(ctx, \"Plan mode disabled: full tool access restored.\");\n\t\t\t} else {\n\t\t\t\tenterPlanMode(ctx);\n\t\t\t}\n\t\t},\n\t});\n\n\t// Defense in depth: the active-tools change lands on the next LLM request,\n\t// so a request already in flight when plan mode toggled can still carry\n\t// blocked tool calls.\n\tpi.on(\"tool_call\", async (event, ctx) => {\n\t\tsetActivePlanSession(ctx.sessionManager.getSessionId());\n\t\tconst state = getPlanState();\n\t\tif (!state.planning || !PLAN_BLOCKED_TOOLS.has(event.toolName)) return;\n\t\treturn {\n\t\t\tblock: true,\n\t\t\treason: `Plan mode: ${event.toolName} is disabled. Keep exploring with read-only tools, or call ${EXIT_PLAN_TOOL_NAME} to submit the plan for approval.`,\n\t\t};\n\t});\n\n\tpi.on(\"before_agent_start\", async (event, ctx) => {\n\t\tsetActivePlanSession(ctx.sessionManager.getSessionId());\n\t\tconst state = getPlanState();\n\t\tif (!state.planning) return;\n\t\treturn { systemPrompt: `${event.systemPrompt}\\n\\n${buildPlanModePrompt(state.planFiles)}` };\n\t});\n\n\t// agent_settled fires once per fully settled run (agent_end can repeat on\n\t// retries and queued continuations), so the approved compaction starts here.\n\tpi.on(\"agent_settled\", async (_event, ctx) => {\n\t\tsetActivePlanSession(ctx.sessionManager.getSessionId());\n\t\tif (!getPlanState().awaitingCompact || !pendingCompact) return;\n\t\tconst pending = pendingCompact;\n\t\tpendingCompact = undefined;\n\t\tctx.compact({\n\t\t\tcustomInstructions: `The user approved an implementation plan saved at ${pending.planPath}. Preserve decisions, constraints, and rejected alternatives from the planning discussion that are not already written in the plan file.`,\n\t\t\tonComplete: () => {\n\t\t\t\texitPlanMode(ctx, \"Context compacted; executing the plan.\");\n\t\t\t\tpi.sendUserMessage(buildKickoffMessage(pending));\n\t\t\t},\n\t\t\tonError: (error) => {\n\t\t\t\t// Never leave the user stuck read-only: degrade to keep-context execution.\n\t\t\t\texitPlanMode(ctx, `Compaction failed (${error.message}); executing with full context instead.`);\n\t\t\t\tpi.sendUserMessage(buildKickoffMessage(pending));\n\t\t\t},\n\t\t});\n\t});\n\n\tpi.on(\"session_start\", async (_event, ctx) => {\n\t\tsyncFromBranch(ctx, { applyFlag: true });\n\t});\n\n\tpi.on(\"session_tree\", async (_event, ctx) => {\n\t\tsyncFromBranch(ctx);\n\t});\n\n\tpi.on(\"session_shutdown\", async (_event, ctx) => {\n\t\tpendingCompact = undefined;\n\t\tdisposePlanSession(ctx.sessionManager.getSessionId());\n\t});\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/extensions/plan/index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * plan — Claude-Code-style plan mode: read-only exploration and interview,\n * then a user-approved exit that saves the plan and optionally compacts.\n *\n * Shape:\n * - Entry (/plan or --plan) removes write-capable tools (edit/write/bash/\n * subagent) and activates exit_plan; a tool_call guard covers requests\n * already in flight before the tool-set change lands.\n * - The plan text travels as an exit_plan parameter, so the model needs no\n * write access to produce the plan file.\n * - \"Compact, then execute\" keeps plan mode active until compaction completes\n * (tools are restored in onComplete) and terminates the run via the tool\n * result, so there is no window where the model holds full context plus\n * write tools. The kickoff message embeds the plan itself — continuation\n * never depends on the compaction summary preserving a path.\n * - State is conversation-backed via custom entries and branch replay (same\n * pattern as todo): /reload, /tree, resume, and fork all restore.\n */\nimport { Text } from \"@earendil-works/pi-tui\";\nimport type {\n\tAgentToolResult,\n\tExtensionAPI,\n\tExtensionContext,\n\tToolExecutionMode,\n} from \"../../core/extensions/types.ts\";\nimport {\n\tbuildPlanModePrompt,\n\tEXIT_PLAN_CANCEL_HINT,\n\tEXIT_PLAN_MENU_OPTIONS,\n\tEXIT_PLAN_PROMPT_GUIDELINES,\n\tEXIT_PLAN_PROMPT_SNIPPET,\n\tEXIT_PLAN_TOOL_DESCRIPTION,\n\tEXIT_PLAN_TOOL_LABEL,\n\tEXIT_PLAN_TOOL_NAME,\n\tMENU_COMPACT_EXECUTE,\n\tMENU_EXECUTE,\n\tMENU_KEEP_PLANNING,\n\tPLAN_BLOCKED_TOOLS,\n\tPLAN_COMMAND_NAME,\n\tPLAN_EMBED_MAX_CHARS,\n\tPLAN_ENTRY_TYPE,\n\tPLAN_FLAG_NAME,\n\tPLAN_STATUS_KEY,\n} from \"./constants.ts\";\nimport { type ExitPlanDetails, type ExitPlanParams, ExitPlanParamsSchema } from \"./schema.ts\";\nimport {\n\tclonePlanState,\n\tdisposePlanSession,\n\tEMPTY_PLAN_STATE,\n\tgetPlanState,\n\treplacePlanState,\n\treplayPlanFromBranch,\n\tsetActivePlanSession,\n} from \"./state.ts\";\nimport { savePlanFile } from \"./storage.ts\";\nimport { formatApprovalSubtitle, formatExitPlanCall, formatExitPlanResult } from \"./view.ts\";\n\ninterface PendingCompact {\n\ttitle: string;\n\tplan: string;\n\tplanPath: string;\n}\n\nfunction buildKickoffMessage(pending: PendingCompact): string {\n\tconst truncated = pending.plan.length > PLAN_EMBED_MAX_CHARS;\n\tconst body = truncated\n\t\t? `${pending.plan.slice(0, PLAN_EMBED_MAX_CHARS)}\\n\\n[Plan truncated — read the full plan at ${pending.planPath}]`\n\t\t: pending.plan;\n\treturn `Execute the approved plan \"${pending.title}\" (saved at ${pending.planPath}).\n\n${body}\n\nStart now. Track multi-step work with the todo tool, and report back instead of improvising if the plan turns out to need revision.`;\n}\n\nexport default function plan(pi: ExtensionAPI): void {\n\tlet menuOpen = false;\n\tlet pendingCompact: PendingCompact | undefined;\n\n\tpi.registerFlag(PLAN_FLAG_NAME, {\n\t\tdescription: \"Start in plan mode (read-only planning)\",\n\t\ttype: \"boolean\",\n\t\tdefault: false,\n\t});\n\n\t/** Remove blocked tools, activate exit_plan, and return what was actually removed. */\n\tfunction applyPlanTools(): string[] {\n\t\tconst active = pi.getActiveTools();\n\t\tconst removed = active.filter((name) => PLAN_BLOCKED_TOOLS.has(name));\n\t\tpi.setActiveTools([...new Set([...active.filter((name) => !PLAN_BLOCKED_TOOLS.has(name)), EXIT_PLAN_TOOL_NAME])]);\n\t\treturn removed;\n\t}\n\n\t/** Inverse of applyPlanTools: drop exit_plan, re-add exactly what was removed. */\n\tfunction restoreTools(removedTools: string[]): void {\n\t\tconst active = pi.getActiveTools().filter((name) => name !== EXIT_PLAN_TOOL_NAME);\n\t\tpi.setActiveTools([...new Set([...active, ...removedTools])]);\n\t}\n\n\tfunction persist(): void {\n\t\tpi.appendEntry(PLAN_ENTRY_TYPE, getPlanState());\n\t}\n\n\tfunction updateStatus(ctx: ExtensionContext): void {\n\t\tconst state = getPlanState();\n\t\tctx.ui.setStatus(PLAN_STATUS_KEY, state.planning ? ctx.ui.theme.fg(\"accent\", \"Plan Mode\") : undefined);\n\t}\n\n\tfunction enterPlanMode(ctx: ExtensionContext): void {\n\t\tconst state = getPlanState();\n\t\tif (state.planning) return;\n\t\tconst removedTools = applyPlanTools();\n\t\treplacePlanState({ planning: true, removedTools, awaitingCompact: false, planFiles: state.planFiles });\n\t\tpersist();\n\t\tupdateStatus(ctx);\n\t\tctx.ui.notify(\"Plan mode enabled: edit, write, bash, and subagent are disabled.\", \"info\");\n\t}\n\n\tfunction exitPlanMode(ctx: ExtensionContext, notifyText?: string): void {\n\t\tconst state = getPlanState();\n\t\trestoreTools(state.removedTools);\n\t\treplacePlanState({ ...state, planning: false, awaitingCompact: false, removedTools: [] });\n\t\tpersist();\n\t\tupdateStatus(ctx);\n\t\tif (notifyText) ctx.ui.notify(notifyText, \"info\");\n\t}\n\n\t/** Save the approved plan, index it in state, and name the session if unnamed. */\n\tfunction saveApprovedPlan(ctx: ExtensionContext, params: ExitPlanParams, title: string): string {\n\t\tconst planPath = savePlanFile({\n\t\t\tsessionId: ctx.sessionManager.getSessionId(),\n\t\t\ttitle,\n\t\t\tplan: params.plan,\n\t\t\tcwd: ctx.cwd,\n\t\t\trevises: params.revises,\n\t\t});\n\t\tconst state = getPlanState();\n\t\treplacePlanState({ ...state, planFiles: [...state.planFiles, planPath] });\n\t\tif (!pi.getSessionName()) pi.setSessionName(title);\n\t\treturn planPath;\n\t}\n\n\t/**\n\t * Rebuild state from the current branch and reconcile the live tool set.\n\t * An interrupted compact-and-execute approval (restart, resume, branch\n\t * switch) degrades to the keep-context exit so the approval is honored.\n\t */\n\tfunction syncFromBranch(ctx: ExtensionContext, options?: { applyFlag?: boolean }): void {\n\t\tsetActivePlanSession(ctx.sessionManager.getSessionId());\n\t\tconst before = getPlanState();\n\t\tconst replayed = replayPlanFromBranch(ctx);\n\t\tconst next = replayed ?? clonePlanState(EMPTY_PLAN_STATE);\n\t\tif (!replayed && options?.applyFlag && pi.getFlag(PLAN_FLAG_NAME) === true) {\n\t\t\tnext.planning = true;\n\t\t}\n\n\t\tlet interruptedCompact = false;\n\t\tif (next.awaitingCompact) {\n\t\t\tnext.planning = false;\n\t\t\tnext.awaitingCompact = false;\n\t\t\tinterruptedCompact = true;\n\t\t\tpendingCompact = undefined;\n\t\t}\n\n\t\tif (next.planning) {\n\t\t\t// The fresh removal set is authoritative for the eventual restore:\n\t\t\t// the live tool set may differ from what the entry recorded.\n\t\t\tnext.removedTools = applyPlanTools();\n\t\t} else {\n\t\t\tif (pi.getActiveTools().includes(EXIT_PLAN_TOOL_NAME)) {\n\t\t\t\trestoreTools(before.planning ? before.removedTools : next.removedTools);\n\t\t\t}\n\t\t\tnext.removedTools = [];\n\t\t}\n\n\t\treplacePlanState(next);\n\t\tif (interruptedCompact) {\n\t\t\tpersist();\n\t\t\tctx.ui.notify(\"Pending compaction was interrupted; plan mode exited with context kept.\", \"warning\");\n\t\t}\n\t\tupdateStatus(ctx);\n\t}\n\n\tpi.registerTool({\n\t\tname: EXIT_PLAN_TOOL_NAME,\n\t\tlabel: EXIT_PLAN_TOOL_LABEL,\n\t\tdescription: EXIT_PLAN_TOOL_DESCRIPTION,\n\t\tpromptSnippet: EXIT_PLAN_PROMPT_SNIPPET,\n\t\tpromptGuidelines: EXIT_PLAN_PROMPT_GUIDELINES,\n\t\tparameters: ExitPlanParamsSchema,\n\t\texecutionMode: \"sequential\" as ToolExecutionMode,\n\n\t\tasync execute(_toolCallId, params, signal, _onUpdate, ctx): Promise<AgentToolResult<ExitPlanDetails>> {\n\t\t\tsetActivePlanSession(ctx.sessionManager.getSessionId());\n\t\t\tconst state = getPlanState();\n\t\t\tconst title = params.title.trim() || \"plan\";\n\n\t\t\tif (!state.planning || state.awaitingCompact) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{ type: \"text\", text: \"Plan mode is not awaiting an exit; continue with your current instructions.\" },\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { decision: \"cancelled\", title },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tif (!ctx.hasUI) {\n\t\t\t\t// json/print: no way to ask, so honor the least surprising default —\n\t\t\t\t// exit with context kept. Documented as a headless-mode semantic.\n\t\t\t\tconst planPath = saveApprovedPlan(ctx, params, title);\n\t\t\t\texitPlanMode(ctx);\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: `Plan saved to ${planPath}. No interactive UI, so plan mode exited with context kept (authoritative: write tools are restored). Begin executing the plan now.`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { decision: \"execute\", title, planPath },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tlet choice: string | undefined;\n\t\t\tmenuOpen = true;\n\t\t\ttry {\n\t\t\t\tchoice = await ctx.ui.select(`Approve plan \"${title}\"?`, EXIT_PLAN_MENU_OPTIONS, {\n\t\t\t\t\tsignal,\n\t\t\t\t\tsubtitle: formatApprovalSubtitle(ctx),\n\t\t\t\t\tcancelHint: EXIT_PLAN_CANCEL_HINT,\n\t\t\t\t});\n\t\t\t} finally {\n\t\t\t\tmenuOpen = false;\n\t\t\t}\n\n\t\t\tif (signal?.aborted) {\n\t\t\t\t// Throwing lets Pi mark the result as an error, matching the todo convention.\n\t\t\t\tthrow new Error(\"Plan approval was interrupted; plan mode is still active.\");\n\t\t\t}\n\n\t\t\tif (choice === MENU_EXECUTE) {\n\t\t\t\tconst planPath = saveApprovedPlan(ctx, params, title);\n\t\t\t\texitPlanMode(ctx, \"Plan approved: executing with full context.\");\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: `Plan approved and saved to ${planPath}. Plan mode has ended and write tools are restored (authoritative — overrides the plan-mode system prompt block). Begin executing the plan now; track multi-step work with the todo tool.`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { decision: \"execute\", title, planPath },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tif (choice === MENU_COMPACT_EXECUTE) {\n\t\t\t\tconst planPath = saveApprovedPlan(ctx, params, title);\n\t\t\t\t// Stay in plan mode (tools restricted) until compaction completes;\n\t\t\t\t// terminate ends the run so agent_settled can start the compaction.\n\t\t\t\treplacePlanState({ ...getPlanState(), awaitingCompact: true });\n\t\t\t\tpersist();\n\t\t\t\tpendingCompact = { title, plan: params.plan, planPath };\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: `Plan approved and saved to ${planPath}. The context will now be compacted; stop here — execution restarts automatically after compaction.`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { decision: \"compactAndExecute\", title, planPath },\n\t\t\t\t\tterminate: true,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Explicit \"Keep planning\", Esc, or timeout: the only side-effect-free\n\t\t\t// choice, so any dismissal maps here.\n\t\t\tlet feedback: string | undefined;\n\t\t\tif (choice === MENU_KEEP_PLANNING) {\n\t\t\t\tfeedback =\n\t\t\t\t\tctx.mode === \"tui\"\n\t\t\t\t\t\t? await ctx.ui.editor(\"Feedback for the plan (optional):\", \"\")\n\t\t\t\t\t\t: await ctx.ui.input(\"Feedback for the plan (optional):\");\n\t\t\t}\n\t\t\tconst trimmedFeedback = feedback?.trim();\n\t\t\treturn {\n\t\t\t\tcontent: [\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\ttext: trimmedFeedback\n\t\t\t\t\t\t\t? `The user wants to keep planning. Feedback:\\n${trimmedFeedback}`\n\t\t\t\t\t\t\t: \"The user wants to keep planning. Ask what should change, or keep refining the open points.\",\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t\tdetails: { decision: \"keepPlanning\", title },\n\t\t\t};\n\t\t},\n\n\t\trenderCall(args, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatExitPlanCall(args, theme, context.expanded));\n\t\t\treturn text;\n\t\t},\n\n\t\trenderResult(result, _options, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatExitPlanResult(result, theme));\n\t\t\treturn text;\n\t\t},\n\t});\n\n\tpi.registerCommand(PLAN_COMMAND_NAME, {\n\t\tdescription: \"Toggle plan mode (read-only planning with user-approved exit)\",\n\t\thandler: async (_args, ctx) => {\n\t\t\tsetActivePlanSession(ctx.sessionManager.getSessionId());\n\t\t\tconst state = getPlanState();\n\t\t\tif (menuOpen) {\n\t\t\t\tctx.ui.notify(\"The exit_plan approval menu is open — answer it first.\", \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (state.awaitingCompact) {\n\t\t\t\tctx.ui.notify(\"Waiting for compaction to finish before leaving plan mode.\", \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (state.planning) {\n\t\t\t\texitPlanMode(ctx, \"Plan mode disabled: full tool access restored.\");\n\t\t\t} else {\n\t\t\t\tenterPlanMode(ctx);\n\t\t\t}\n\t\t},\n\t});\n\n\t// Defense in depth: the active-tools change lands on the next LLM request,\n\t// so a request already in flight when plan mode toggled can still carry\n\t// blocked tool calls.\n\tpi.on(\"tool_call\", async (event, ctx) => {\n\t\tsetActivePlanSession(ctx.sessionManager.getSessionId());\n\t\tconst state = getPlanState();\n\t\tif (!state.planning || !PLAN_BLOCKED_TOOLS.has(event.toolName)) return;\n\t\treturn {\n\t\t\tblock: true,\n\t\t\treason: `Plan mode: ${event.toolName} is disabled. Keep exploring with read-only tools, or call ${EXIT_PLAN_TOOL_NAME} to submit the plan for approval.`,\n\t\t};\n\t});\n\n\tpi.on(\"before_agent_start\", async (event, ctx) => {\n\t\tsetActivePlanSession(ctx.sessionManager.getSessionId());\n\t\tconst state = getPlanState();\n\t\tif (!state.planning) return;\n\t\treturn { systemPrompt: `${event.systemPrompt}\\n\\n${buildPlanModePrompt(state.planFiles)}` };\n\t});\n\n\t// agent_settled fires once per fully settled run (agent_end can repeat on\n\t// retries and queued continuations), so the approved compaction starts here.\n\tpi.on(\"agent_settled\", async (_event, ctx) => {\n\t\tsetActivePlanSession(ctx.sessionManager.getSessionId());\n\t\tif (!getPlanState().awaitingCompact || !pendingCompact) return;\n\t\tconst pending = pendingCompact;\n\t\tpendingCompact = undefined;\n\t\tctx.compact({\n\t\t\tcustomInstructions: `The user approved an implementation plan saved at ${pending.planPath}. Preserve decisions, constraints, and rejected alternatives from the planning discussion that are not already written in the plan file.`,\n\t\t\tonComplete: () => {\n\t\t\t\texitPlanMode(ctx, \"Context compacted; executing the plan.\");\n\t\t\t\tpi.sendUserMessage(buildKickoffMessage(pending));\n\t\t\t},\n\t\t\tonError: (error) => {\n\t\t\t\t// Never leave the user stuck read-only: degrade to keep-context execution.\n\t\t\t\texitPlanMode(ctx, `Compaction failed (${error.message}); executing with full context instead.`);\n\t\t\t\tpi.sendUserMessage(buildKickoffMessage(pending));\n\t\t\t},\n\t\t});\n\t});\n\n\tpi.on(\"session_start\", async (_event, ctx) => {\n\t\tsyncFromBranch(ctx, { applyFlag: true });\n\t});\n\n\tpi.on(\"session_tree\", async (_event, ctx) => {\n\t\tsyncFromBranch(ctx);\n\t});\n\n\tpi.on(\"session_shutdown\", async (_event, ctx) => {\n\t\tpendingCompact = undefined;\n\t\tdisposePlanSession(ctx.sessionManager.getSessionId());\n\t});\n}\n"]}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * plan/view.ts — approval-dialog context and exit_plan tool rendering.
3
+ *
4
+ * `exit_plan` carries the whole plan as a parameter, and the default renderer
5
+ * echoes parameters verbatim: the row would repeat, escaped onto one line, the
6
+ * markdown the model just printed as response text. Collapsed rows therefore
7
+ * carry the title only, and the plan body is available on expand.
8
+ *
9
+ * The result text is written for the model (it restates tool-state precedence
10
+ * rules), so the result row renders from `details` instead.
11
+ */
12
+ import type { AgentToolResult, ExtensionContext } from "../../core/extensions/types.ts";
13
+ import type { Theme } from "../../modes/interactive/theme/theme.ts";
14
+ import type { ExitPlanDetails } from "./schema.ts";
15
+ /** Collapse the home prefix so a plan path stays on one line. */
16
+ export declare function shortenPlanPath(path: string): string;
17
+ /**
18
+ * Context pressure is the fact the compact-or-not decision turns on, so the
19
+ * dialog states it instead of making the user leave and check the footer.
20
+ */
21
+ export declare function formatApprovalSubtitle(ctx: ExtensionContext): string | undefined;
22
+ type ExitPlanCallArgs = {
23
+ title?: unknown;
24
+ plan?: unknown;
25
+ revises?: unknown;
26
+ };
27
+ /** Title-only headline when collapsed; plan body and revision target on expand. */
28
+ export declare function formatExitPlanCall(args: ExitPlanCallArgs | undefined, theme: Theme, expanded: boolean): string;
29
+ /**
30
+ * What the user needs to know: where the plan landed and what happens next.
31
+ * Falls back to the model-facing text when details are missing — a thrown
32
+ * approval (interrupt) has no details to render from.
33
+ */
34
+ export declare function formatExitPlanResult(result: AgentToolResult<ExitPlanDetails>, theme: Theme): string;
35
+ export {};
36
+ //# sourceMappingURL=view.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"view.d.ts","sourceRoot":"","sources":["../../../src/extensions/plan/view.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AACxF,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,wCAAwC,CAAC;AACpE,OAAO,KAAK,EAAoB,eAAe,EAAE,MAAM,aAAa,CAAC;AAErE,iEAAiE;AACjE,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAOpD;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,gBAAgB,GAAG,MAAM,GAAG,SAAS,CAIhF;AAKD,KAAK,gBAAgB,GAAG;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAM/E,mFAAmF;AACnF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,gBAAgB,GAAG,SAAS,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,GAAG,MAAM,CAiB9G;AAkBD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,eAAe,CAAC,eAAe,CAAC,EAAE,KAAK,EAAE,KAAK,GAAG,MAAM,CAQnG","sourcesContent":["/**\n * plan/view.ts — approval-dialog context and exit_plan tool rendering.\n *\n * `exit_plan` carries the whole plan as a parameter, and the default renderer\n * echoes parameters verbatim: the row would repeat, escaped onto one line, the\n * markdown the model just printed as response text. Collapsed rows therefore\n * carry the title only, and the plan body is available on expand.\n *\n * The result text is written for the model (it restates tool-state precedence\n * rules), so the result row renders from `details` instead.\n */\nimport type { AgentToolResult, ExtensionContext } from \"../../core/extensions/types.ts\";\nimport type { Theme } from \"../../modes/interactive/theme/theme.ts\";\nimport type { ExitPlanDecision, ExitPlanDetails } from \"./schema.ts\";\n\n/** Collapse the home prefix so a plan path stays on one line. */\nexport function shortenPlanPath(path: string): string {\n\tconst rawHome = process.env.USERPROFILE || process.env.HOME || \"\";\n\tconst home = rawHome.replace(/[\\\\/]+$/, \"\");\n\tif (!home) return path;\n\tconst normalize = (value: string) => value.replace(/\\\\/g, \"/\").toLowerCase();\n\tif (!normalize(path).startsWith(`${normalize(home)}/`)) return path;\n\treturn `~${path.slice(home.length).replace(/\\\\/g, \"/\")}`;\n}\n\n/**\n * Context pressure is the fact the compact-or-not decision turns on, so the\n * dialog states it instead of making the user leave and check the footer.\n */\nexport function formatApprovalSubtitle(ctx: ExtensionContext): string | undefined {\n\tconst percent = ctx.getContextUsage()?.percent;\n\tif (percent == null) return undefined;\n\treturn `Context now ${percent.toFixed(0)}% full`;\n}\n\n// ---- tool call rendering ----------------------------------------------------\n// Args arrive off the wire and stream in, so every field is read defensively.\n\ntype ExitPlanCallArgs = { title?: unknown; plan?: unknown; revises?: unknown };\n\nfunction callText(value: unknown): string {\n\treturn typeof value === \"string\" ? value.trim() : \"\";\n}\n\n/** Title-only headline when collapsed; plan body and revision target on expand. */\nexport function formatExitPlanCall(args: ExitPlanCallArgs | undefined, theme: Theme, expanded: boolean): string {\n\tconst title = callText(args?.title);\n\tconst headline = [\n\t\ttheme.fg(\"toolTitle\", theme.bold(\"exit_plan\")),\n\t\ttitle ? theme.fg(\"text\", title) : theme.fg(\"dim\", \"…\"),\n\t].join(\" \");\n\tif (!expanded) return headline;\n\n\tconst lines = [headline];\n\tconst revises = callText(args?.revises);\n\tif (revises) lines.push(theme.fg(\"dim\", `revises ${revises}`));\n\tconst plan = callText(args?.plan);\n\tif (plan) {\n\t\tlines.push(\"\");\n\t\tfor (const line of plan.split(\"\\n\")) lines.push(theme.fg(\"dim\", line));\n\t}\n\treturn lines.join(\"\\n\");\n}\n\n// ---- tool result rendering --------------------------------------------------\n\nconst DECISION_SUMMARY: Record<ExitPlanDecision, string> = {\n\texecute: \"Executing with full context\",\n\tcompactAndExecute: \"Compacting context, then executing\",\n\tkeepPlanning: \"Still planning\",\n\tcancelled: \"No approval was pending\",\n};\n\nfunction resultText(result: AgentToolResult<ExitPlanDetails>): string {\n\treturn result.content\n\t\t.filter((block): block is { type: \"text\"; text: string } => block.type === \"text\")\n\t\t.map((block) => block.text)\n\t\t.join(\"\\n\");\n}\n\n/**\n * What the user needs to know: where the plan landed and what happens next.\n * Falls back to the model-facing text when details are missing — a thrown\n * approval (interrupt) has no details to render from.\n */\nexport function formatExitPlanResult(result: AgentToolResult<ExitPlanDetails>, theme: Theme): string {\n\tconst details = result.details;\n\tif (!details) return theme.fg(\"dim\", resultText(result));\n\n\tconst lines: string[] = [];\n\tif (details.planPath) lines.push(theme.fg(\"success\", `Saved to ${shortenPlanPath(details.planPath)}`));\n\tlines.push(theme.fg(\"dim\", DECISION_SUMMARY[details.decision]));\n\treturn lines.join(\"\\n\");\n}\n"]}
@@ -0,0 +1,74 @@
1
+ /** Collapse the home prefix so a plan path stays on one line. */
2
+ export function shortenPlanPath(path) {
3
+ const rawHome = process.env.USERPROFILE || process.env.HOME || "";
4
+ const home = rawHome.replace(/[\\/]+$/, "");
5
+ if (!home)
6
+ return path;
7
+ const normalize = (value) => value.replace(/\\/g, "/").toLowerCase();
8
+ if (!normalize(path).startsWith(`${normalize(home)}/`))
9
+ return path;
10
+ return `~${path.slice(home.length).replace(/\\/g, "/")}`;
11
+ }
12
+ /**
13
+ * Context pressure is the fact the compact-or-not decision turns on, so the
14
+ * dialog states it instead of making the user leave and check the footer.
15
+ */
16
+ export function formatApprovalSubtitle(ctx) {
17
+ const percent = ctx.getContextUsage()?.percent;
18
+ if (percent == null)
19
+ return undefined;
20
+ return `Context now ${percent.toFixed(0)}% full`;
21
+ }
22
+ function callText(value) {
23
+ return typeof value === "string" ? value.trim() : "";
24
+ }
25
+ /** Title-only headline when collapsed; plan body and revision target on expand. */
26
+ export function formatExitPlanCall(args, theme, expanded) {
27
+ const title = callText(args?.title);
28
+ const headline = [
29
+ theme.fg("toolTitle", theme.bold("exit_plan")),
30
+ title ? theme.fg("text", title) : theme.fg("dim", "…"),
31
+ ].join(" ");
32
+ if (!expanded)
33
+ return headline;
34
+ const lines = [headline];
35
+ const revises = callText(args?.revises);
36
+ if (revises)
37
+ lines.push(theme.fg("dim", `revises ${revises}`));
38
+ const plan = callText(args?.plan);
39
+ if (plan) {
40
+ lines.push("");
41
+ for (const line of plan.split("\n"))
42
+ lines.push(theme.fg("dim", line));
43
+ }
44
+ return lines.join("\n");
45
+ }
46
+ // ---- tool result rendering --------------------------------------------------
47
+ const DECISION_SUMMARY = {
48
+ execute: "Executing with full context",
49
+ compactAndExecute: "Compacting context, then executing",
50
+ keepPlanning: "Still planning",
51
+ cancelled: "No approval was pending",
52
+ };
53
+ function resultText(result) {
54
+ return result.content
55
+ .filter((block) => block.type === "text")
56
+ .map((block) => block.text)
57
+ .join("\n");
58
+ }
59
+ /**
60
+ * What the user needs to know: where the plan landed and what happens next.
61
+ * Falls back to the model-facing text when details are missing — a thrown
62
+ * approval (interrupt) has no details to render from.
63
+ */
64
+ export function formatExitPlanResult(result, theme) {
65
+ const details = result.details;
66
+ if (!details)
67
+ return theme.fg("dim", resultText(result));
68
+ const lines = [];
69
+ if (details.planPath)
70
+ lines.push(theme.fg("success", `Saved to ${shortenPlanPath(details.planPath)}`));
71
+ lines.push(theme.fg("dim", DECISION_SUMMARY[details.decision]));
72
+ return lines.join("\n");
73
+ }
74
+ //# sourceMappingURL=view.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"view.js","sourceRoot":"","sources":["../../../src/extensions/plan/view.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * plan/view.ts — approval-dialog context and exit_plan tool rendering.\n *\n * `exit_plan` carries the whole plan as a parameter, and the default renderer\n * echoes parameters verbatim: the row would repeat, escaped onto one line, the\n * markdown the model just printed as response text. Collapsed rows therefore\n * carry the title only, and the plan body is available on expand.\n *\n * The result text is written for the model (it restates tool-state precedence\n * rules), so the result row renders from `details` instead.\n */\nimport type { AgentToolResult, ExtensionContext } from \"../../core/extensions/types.ts\";\nimport type { Theme } from \"../../modes/interactive/theme/theme.ts\";\nimport type { ExitPlanDecision, ExitPlanDetails } from \"./schema.ts\";\n\n/** Collapse the home prefix so a plan path stays on one line. */\nexport function shortenPlanPath(path: string): string {\n\tconst rawHome = process.env.USERPROFILE || process.env.HOME || \"\";\n\tconst home = rawHome.replace(/[\\\\/]+$/, \"\");\n\tif (!home) return path;\n\tconst normalize = (value: string) => value.replace(/\\\\/g, \"/\").toLowerCase();\n\tif (!normalize(path).startsWith(`${normalize(home)}/`)) return path;\n\treturn `~${path.slice(home.length).replace(/\\\\/g, \"/\")}`;\n}\n\n/**\n * Context pressure is the fact the compact-or-not decision turns on, so the\n * dialog states it instead of making the user leave and check the footer.\n */\nexport function formatApprovalSubtitle(ctx: ExtensionContext): string | undefined {\n\tconst percent = ctx.getContextUsage()?.percent;\n\tif (percent == null) return undefined;\n\treturn `Context now ${percent.toFixed(0)}% full`;\n}\n\n// ---- tool call rendering ----------------------------------------------------\n// Args arrive off the wire and stream in, so every field is read defensively.\n\ntype ExitPlanCallArgs = { title?: unknown; plan?: unknown; revises?: unknown };\n\nfunction callText(value: unknown): string {\n\treturn typeof value === \"string\" ? value.trim() : \"\";\n}\n\n/** Title-only headline when collapsed; plan body and revision target on expand. */\nexport function formatExitPlanCall(args: ExitPlanCallArgs | undefined, theme: Theme, expanded: boolean): string {\n\tconst title = callText(args?.title);\n\tconst headline = [\n\t\ttheme.fg(\"toolTitle\", theme.bold(\"exit_plan\")),\n\t\ttitle ? theme.fg(\"text\", title) : theme.fg(\"dim\", \"…\"),\n\t].join(\" \");\n\tif (!expanded) return headline;\n\n\tconst lines = [headline];\n\tconst revises = callText(args?.revises);\n\tif (revises) lines.push(theme.fg(\"dim\", `revises ${revises}`));\n\tconst plan = callText(args?.plan);\n\tif (plan) {\n\t\tlines.push(\"\");\n\t\tfor (const line of plan.split(\"\\n\")) lines.push(theme.fg(\"dim\", line));\n\t}\n\treturn lines.join(\"\\n\");\n}\n\n// ---- tool result rendering --------------------------------------------------\n\nconst DECISION_SUMMARY: Record<ExitPlanDecision, string> = {\n\texecute: \"Executing with full context\",\n\tcompactAndExecute: \"Compacting context, then executing\",\n\tkeepPlanning: \"Still planning\",\n\tcancelled: \"No approval was pending\",\n};\n\nfunction resultText(result: AgentToolResult<ExitPlanDetails>): string {\n\treturn result.content\n\t\t.filter((block): block is { type: \"text\"; text: string } => block.type === \"text\")\n\t\t.map((block) => block.text)\n\t\t.join(\"\\n\");\n}\n\n/**\n * What the user needs to know: where the plan landed and what happens next.\n * Falls back to the model-facing text when details are missing — a thrown\n * approval (interrupt) has no details to render from.\n */\nexport function formatExitPlanResult(result: AgentToolResult<ExitPlanDetails>, theme: Theme): string {\n\tconst details = result.details;\n\tif (!details) return theme.fg(\"dim\", resultText(result));\n\n\tconst lines: string[] = [];\n\tif (details.planPath) lines.push(theme.fg(\"success\", `Saved to ${shortenPlanPath(details.planPath)}`));\n\tlines.push(theme.fg(\"dim\", DECISION_SUMMARY[details.decision]));\n\treturn lines.join(\"\\n\");\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"stream.d.ts","sourceRoot":"","sources":["../../../src/extensions/router/stream.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EACN,KAAK,GAAG,EACR,KAAK,OAAO,EACZ,iCAAiC,EACjC,KAAK,KAAK,EAEV,KAAK,mBAAmB,EACxB,MAAM,8BAA8B,CAAC;AAMtC,wBAAgB,iBAAiB,CAChC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,EACjB,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE,mBAAmB,GAC3B,UAAU,CAAC,OAAO,iCAAiC,CAAC,CA0EtD;AA4HD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAK3D;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAEhD","sourcesContent":["/**\n * Codex-oriented Responses client for API relays.\n *\n * Follows Pi's documented custom-provider pattern (see coding-agent docs\n * providers.md / custom-provider.md and examples/custom-provider-gitlab-duo):\n * wrap a built-in pi-ai stream API from `@earendil-works/pi-ai/compat` instead of\n * reimplementing SSE or deep-importing internal modules.\n *\n * We use openAIResponsesApi (works with relay sk- keys) and reshape the request\n * payload toward Codex CLI style so transparent gateways receive a friendlier body.\n */\n\nimport {\n\ttype Api,\n\ttype Context,\n\tcreateAssistantMessageEventStream,\n\ttype Model,\n\topenAIResponsesApi,\n\ttype SimpleStreamOptions,\n} from \"@earendil-works/pi-ai/compat\";\n\nimport { DEFAULTS, formatError, ROUTER_API } from \"./constants.ts\";\n\nconst responsesApi = openAIResponsesApi();\n\nexport function streamRouterCodex(\n\tmodel: Model<Api>,\n\tcontext: Context,\n\toptions?: SimpleStreamOptions,\n): ReturnType<typeof createAssistantMessageEventStream> {\n\tconst stream = createAssistantMessageEventStream();\n\n\tvoid (async () => {\n\t\ttry {\n\t\t\tconst apiKey = options?.apiKey;\n\t\t\tif (!apiKey) throw new Error(`No API key for provider: ${model.provider}`);\n\n\t\t\t// Built-in Responses stream expects api \"openai-responses\" on the model object.\n\t\t\tconst requestModel = {\n\t\t\t\t...model,\n\t\t\t\tapi: \"openai-responses\" as const,\n\t\t\t\tcompat: {\n\t\t\t\t\tsupportsDeveloperRole: true,\n\t\t\t\t\t// Avoid underscore session_id header that strict proxies reject.\n\t\t\t\t\tsessionAffinityFormat: \"openai-nosession\" as const,\n\t\t\t\t\t// Codex-style upstreams reject prompt_cache_retention: \"24h\".\n\t\t\t\t\tsupportsLongCacheRetention: false,\n\t\t\t\t\t...(model.compat ?? {}),\n\t\t\t\t},\n\t\t\t} as Model<\"openai-responses\">;\n\n\t\t\tconst headers: Record<string, string> = {\n\t\t\t\toriginator: DEFAULTS.originator,\n\t\t\t\t...(options?.headers as Record<string, string> | undefined),\n\t\t\t};\n\n\t\t\t// Prefer hyphenated Codex-style session affinity when we have a session id.\n\t\t\tconst sessionId = clampCacheKey(options?.sessionId);\n\t\t\tif (sessionId) {\n\t\t\t\tif (!headers[\"session-id\"]) headers[\"session-id\"] = sessionId;\n\t\t\t\tif (!headers[\"x-client-request-id\"]) headers[\"x-client-request-id\"] = sessionId;\n\t\t\t}\n\n\t\t\tconst inner = responsesApi.streamSimple(requestModel, context, {\n\t\t\t\t...options,\n\t\t\t\tapiKey,\n\t\t\t\theaders,\n\t\t\t\tonPayload: (payload) => reshapePayloadForRelay(payload, context, options?.onPayload, requestModel),\n\t\t\t});\n\n\t\t\tfor await (const event of inner) {\n\t\t\t\tstream.push(event);\n\t\t\t}\n\t\t\tstream.end();\n\t\t} catch (error) {\n\t\t\t// Match examples/custom-provider-gitlab-duo error event shape.\n\t\t\tstream.push({\n\t\t\t\ttype: \"error\",\n\t\t\t\treason: options?.signal?.aborted ? \"aborted\" : \"error\",\n\t\t\t\terror: {\n\t\t\t\t\trole: \"assistant\",\n\t\t\t\t\tcontent: [],\n\t\t\t\t\tapi: model.api,\n\t\t\t\t\tprovider: model.provider,\n\t\t\t\t\tmodel: model.id,\n\t\t\t\t\tusage: {\n\t\t\t\t\t\tinput: 0,\n\t\t\t\t\t\toutput: 0,\n\t\t\t\t\t\tcacheRead: 0,\n\t\t\t\t\t\tcacheWrite: 0,\n\t\t\t\t\t\ttotalTokens: 0,\n\t\t\t\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n\t\t\t\t\t},\n\t\t\t\t\tstopReason: options?.signal?.aborted ? \"aborted\" : \"error\",\n\t\t\t\t\terrorMessage: formatError(error),\n\t\t\t\t\ttimestamp: Date.now(),\n\t\t\t\t},\n\t\t\t});\n\t\t\tstream.end();\n\t\t}\n\t})();\n\n\treturn stream;\n}\n\n/**\n * Nudge the OpenAI Responses payload toward Codex CLI request shape for\n * transparent relays: instructions + input, store:false, no max_output_tokens /\n * prompt_cache_retention, verbosity + parallel_tool_calls.\n */\nasync function reshapePayloadForRelay(\n\tpayload: unknown,\n\tcontext: Context,\n\tuserOnPayload: SimpleStreamOptions[\"onPayload\"],\n\tmodel: Model<\"openai-responses\">,\n): Promise<unknown> {\n\tconst base =\n\t\tpayload && typeof payload === \"object\" && !Array.isArray(payload)\n\t\t\t? { ...(payload as Record<string, unknown>) }\n\t\t\t: ({} as Record<string, unknown>);\n\n\t// Always safe for ChatGPT/Codex-style backends.\n\tbase.store = false;\n\tbase.stream = true;\n\n\t// Codex CLI sends system prompt as `instructions`, not as a role message in input.\n\tconst { instructions, input } = extractInstructions(base.input, context.systemPrompt);\n\tif (instructions) base.instructions = instructions;\n\tif (input !== undefined) base.input = input;\n\n\t// Fields common on Codex CLI / rejected by many transparent Codex upstreams.\n\tif (!base.text || typeof base.text !== \"object\") {\n\t\tbase.text = { verbosity: \"low\" };\n\t}\n\tif (base.tool_choice === undefined) base.tool_choice = \"auto\";\n\tif (base.parallel_tool_calls === undefined) base.parallel_tool_calls = true;\n\n\t// Prefer encrypted reasoning content for multi-turn store:false sessions.\n\tconst include = Array.isArray(base.include) ? [...(base.include as unknown[])] : [];\n\tif (!include.includes(\"reasoning.encrypted_content\")) {\n\t\tinclude.push(\"reasoning.encrypted_content\");\n\t}\n\tbase.include = include;\n\n\t// Drop Platform-only fields that Codex OAuth endpoints often 400 on.\n\tdelete base.prompt_cache_retention;\n\tdelete base.max_output_tokens;\n\tdelete base.temperature;\n\tdelete base.top_p;\n\tdelete base.user;\n\tdelete base.metadata;\n\tdelete base.service_tier;\n\tdelete base.truncation;\n\tdelete base.context_management;\n\tdelete base.safety_identifier;\n\tdelete base.stream_options;\n\n\tif (userOnPayload) {\n\t\tconst next = await userOnPayload(base, model);\n\t\tif (next !== undefined) return next;\n\t}\n\treturn base;\n}\n\nfunction extractInstructions(\n\tinput: unknown,\n\tsystemPrompt: string | undefined,\n): { instructions?: string; input: unknown } {\n\t// Prefer context.systemPrompt (matches Codex stream path).\n\tif (systemPrompt && systemPrompt.length > 0) {\n\t\tconst stripped = stripLeadingSystemRoles(input);\n\t\treturn { instructions: systemPrompt, input: stripped ?? input };\n\t}\n\n\tif (!Array.isArray(input) || input.length === 0) {\n\t\treturn { input };\n\t}\n\n\tconst first = input[0];\n\tif (\n\t\tfirst &&\n\t\ttypeof first === \"object\" &&\n\t\t!Array.isArray(first) &&\n\t\t\"role\" in first &&\n\t\t((first as { role?: string }).role === \"system\" || (first as { role?: string }).role === \"developer\")\n\t) {\n\t\tconst content = (first as { content?: unknown }).content;\n\t\tconst text =\n\t\t\ttypeof content === \"string\"\n\t\t\t\t? content\n\t\t\t\t: Array.isArray(content)\n\t\t\t\t\t? content\n\t\t\t\t\t\t\t.map((part) =>\n\t\t\t\t\t\t\t\tpart && typeof part === \"object\" && \"text\" in part\n\t\t\t\t\t\t\t\t\t? String((part as { text: unknown }).text)\n\t\t\t\t\t\t\t\t\t: \"\",\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t.join(\"\")\n\t\t\t\t\t: \"\";\n\t\treturn {\n\t\t\tinstructions: text || undefined,\n\t\t\tinput: input.slice(1),\n\t\t};\n\t}\n\n\treturn { input };\n}\n\nfunction stripLeadingSystemRoles(input: unknown): unknown {\n\tif (!Array.isArray(input) || input.length === 0) return input;\n\tconst first = input[0];\n\tif (\n\t\tfirst &&\n\t\ttypeof first === \"object\" &&\n\t\t!Array.isArray(first) &&\n\t\t((first as { role?: string }).role === \"system\" || (first as { role?: string }).role === \"developer\")\n\t) {\n\t\treturn input.slice(1);\n\t}\n\treturn input;\n}\n\nfunction clampCacheKey(sessionId: string | undefined): string | undefined {\n\tif (!sessionId) return undefined;\n\treturn sessionId.length <= 64 ? sessionId : sessionId.slice(0, 64);\n}\n\nexport function resolveResponsesUrl(baseUrl: string): string {\n\tconst normalized = baseUrl.trim().replace(/\\/+$/, \"\");\n\tif (!normalized) throw new Error(\"Model baseUrl is empty.\");\n\tif (normalized.endsWith(\"/responses\")) return normalized;\n\treturn `${normalized}/responses`;\n}\n\nexport function isRouterApi(api: string): boolean {\n\treturn api === ROUTER_API;\n}\n"]}
1
+ {"version":3,"file":"stream.d.ts","sourceRoot":"","sources":["../../../src/extensions/router/stream.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EACN,KAAK,GAAG,EACR,KAAK,OAAO,EACZ,iCAAiC,EACjC,KAAK,KAAK,EAEV,KAAK,mBAAmB,EACxB,MAAM,8BAA8B,CAAC;AAMtC,wBAAgB,iBAAiB,CAChC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,EACjB,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE,mBAAmB,GAC3B,UAAU,CAAC,OAAO,iCAAiC,CAAC,CA0EtD;AA2KD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAK3D;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAEhD","sourcesContent":["/**\n * Codex-oriented Responses client for API relays.\n *\n * Follows Pi's documented custom-provider pattern (see coding-agent docs\n * providers.md / custom-provider.md and examples/custom-provider-gitlab-duo):\n * wrap a built-in pi-ai stream API from `@earendil-works/pi-ai/compat` instead of\n * reimplementing SSE or deep-importing internal modules.\n *\n * We use openAIResponsesApi (works with relay sk- keys) and reshape the request\n * payload toward Codex CLI style so transparent gateways receive a friendlier body.\n */\n\nimport {\n\ttype Api,\n\ttype Context,\n\tcreateAssistantMessageEventStream,\n\ttype Model,\n\topenAIResponsesApi,\n\ttype SimpleStreamOptions,\n} from \"@earendil-works/pi-ai/compat\";\n\nimport { DEFAULTS, formatError, ROUTER_API } from \"./constants.ts\";\n\nconst responsesApi = openAIResponsesApi();\n\nexport function streamRouterCodex(\n\tmodel: Model<Api>,\n\tcontext: Context,\n\toptions?: SimpleStreamOptions,\n): ReturnType<typeof createAssistantMessageEventStream> {\n\tconst stream = createAssistantMessageEventStream();\n\n\tvoid (async () => {\n\t\ttry {\n\t\t\tconst apiKey = options?.apiKey;\n\t\t\tif (!apiKey) throw new Error(`No API key for provider: ${model.provider}`);\n\n\t\t\t// Built-in Responses stream expects api \"openai-responses\" on the model object.\n\t\t\tconst requestModel = {\n\t\t\t\t...model,\n\t\t\t\tapi: \"openai-responses\" as const,\n\t\t\t\tcompat: {\n\t\t\t\t\tsupportsDeveloperRole: true,\n\t\t\t\t\t// Avoid underscore session_id header that strict proxies reject.\n\t\t\t\t\tsessionAffinityFormat: \"openai-nosession\" as const,\n\t\t\t\t\t// Codex-style upstreams reject prompt_cache_retention: \"24h\".\n\t\t\t\t\tsupportsLongCacheRetention: false,\n\t\t\t\t\t...(model.compat ?? {}),\n\t\t\t\t},\n\t\t\t} as Model<\"openai-responses\">;\n\n\t\t\tconst headers: Record<string, string> = {\n\t\t\t\toriginator: DEFAULTS.originator,\n\t\t\t\t...(options?.headers as Record<string, string> | undefined),\n\t\t\t};\n\n\t\t\t// Prefer hyphenated Codex-style session affinity when we have a session id.\n\t\t\tconst sessionId = clampCacheKey(options?.sessionId);\n\t\t\tif (sessionId) {\n\t\t\t\tif (!headers[\"session-id\"]) headers[\"session-id\"] = sessionId;\n\t\t\t\tif (!headers[\"x-client-request-id\"]) headers[\"x-client-request-id\"] = sessionId;\n\t\t\t}\n\n\t\t\tconst inner = responsesApi.streamSimple(requestModel, context, {\n\t\t\t\t...options,\n\t\t\t\tapiKey,\n\t\t\t\theaders,\n\t\t\t\tonPayload: (payload) => reshapePayloadForRelay(payload, context, options?.onPayload, requestModel),\n\t\t\t});\n\n\t\t\tfor await (const event of inner) {\n\t\t\t\tstream.push(event);\n\t\t\t}\n\t\t\tstream.end();\n\t\t} catch (error) {\n\t\t\t// Match examples/custom-provider-gitlab-duo error event shape.\n\t\t\tstream.push({\n\t\t\t\ttype: \"error\",\n\t\t\t\treason: options?.signal?.aborted ? \"aborted\" : \"error\",\n\t\t\t\terror: {\n\t\t\t\t\trole: \"assistant\",\n\t\t\t\t\tcontent: [],\n\t\t\t\t\tapi: model.api,\n\t\t\t\t\tprovider: model.provider,\n\t\t\t\t\tmodel: model.id,\n\t\t\t\t\tusage: {\n\t\t\t\t\t\tinput: 0,\n\t\t\t\t\t\toutput: 0,\n\t\t\t\t\t\tcacheRead: 0,\n\t\t\t\t\t\tcacheWrite: 0,\n\t\t\t\t\t\ttotalTokens: 0,\n\t\t\t\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n\t\t\t\t\t},\n\t\t\t\t\tstopReason: options?.signal?.aborted ? \"aborted\" : \"error\",\n\t\t\t\t\terrorMessage: formatError(error),\n\t\t\t\t\ttimestamp: Date.now(),\n\t\t\t\t},\n\t\t\t});\n\t\t\tstream.end();\n\t\t}\n\t})();\n\n\treturn stream;\n}\n\n/**\n * Nudge the OpenAI Responses payload toward Codex CLI request shape for\n * transparent relays: instructions + input, store:false, no max_output_tokens /\n * prompt_cache_retention, verbosity + parallel_tool_calls.\n */\nasync function reshapePayloadForRelay(\n\tpayload: unknown,\n\tcontext: Context,\n\tuserOnPayload: SimpleStreamOptions[\"onPayload\"],\n\tmodel: Model<\"openai-responses\">,\n): Promise<unknown> {\n\tconst base =\n\t\tpayload && typeof payload === \"object\" && !Array.isArray(payload)\n\t\t\t? { ...(payload as Record<string, unknown>) }\n\t\t\t: ({} as Record<string, unknown>);\n\n\t// Always safe for ChatGPT/Codex-style backends.\n\tbase.store = false;\n\tbase.stream = true;\n\n\t// Codex CLI sends system prompt as `instructions`, not as a role message in input.\n\tconst { instructions, input } = extractInstructions(base.input, context.systemPrompt);\n\tif (instructions) base.instructions = instructions;\n\tif (input !== undefined) base.input = input;\n\n\t// Codex CLI's ResponseItem serialization never emits `status` on replayed\n\t// message/function_call/function_call_output/reasoning items (only\n\t// local_shell_call has it), and output_text content has no `annotations`.\n\t// Strict Codex-schema gateways 400 on both (e.g. \"input[i].status:\n\t// unknown_parameter\"), while pi-ai emits them per the Platform schema.\n\tsanitizeInputItemsForCodex(base.input);\n\n\t// Fields common on Codex CLI / rejected by many transparent Codex upstreams.\n\tif (!base.text || typeof base.text !== \"object\") {\n\t\tbase.text = { verbosity: \"low\" };\n\t}\n\tif (base.tool_choice === undefined) base.tool_choice = \"auto\";\n\tif (base.parallel_tool_calls === undefined) base.parallel_tool_calls = true;\n\n\t// Prefer encrypted reasoning content for multi-turn store:false sessions.\n\tconst include = Array.isArray(base.include) ? [...(base.include as unknown[])] : [];\n\tif (!include.includes(\"reasoning.encrypted_content\")) {\n\t\tinclude.push(\"reasoning.encrypted_content\");\n\t}\n\tbase.include = include;\n\n\t// Drop Platform-only fields that Codex OAuth endpoints often 400 on.\n\tdelete base.prompt_cache_retention;\n\tdelete base.prompt_cache_options;\n\tdelete base.max_output_tokens;\n\tdelete base.temperature;\n\tdelete base.top_p;\n\tdelete base.user;\n\tdelete base.metadata;\n\tdelete base.service_tier;\n\tdelete base.truncation;\n\tdelete base.context_management;\n\tdelete base.safety_identifier;\n\tdelete base.stream_options;\n\n\tif (userOnPayload) {\n\t\tconst next = await userOnPayload(base, model);\n\t\tif (next !== undefined) return next;\n\t}\n\treturn base;\n}\n\n/**\n * Item types where Codex CLI's ResponseItem serialization has NO `status`\n * field (codex-rs/protocol/src/models.rs): Message, Reasoning, FunctionCall,\n * FunctionCallOutput, CustomToolCallOutput. Other item types either require\n * `status` (local_shell_call, tool_search_output, image_generation_call) or\n * accept it optionally (tool_search_call, custom_tool_call, web_search_call),\n * so we must not strip it there.\n */\nconst STATUS_LESS_ITEM_TYPES = new Set([\n\t\"message\",\n\t\"reasoning\",\n\t\"function_call\",\n\t\"function_call_output\",\n\t\"custom_tool_call_output\",\n]);\n\nfunction sanitizeInputItemsForCodex(input: unknown): void {\n\tif (!Array.isArray(input)) return;\n\tfor (const item of input) {\n\t\tif (!item || typeof item !== \"object\" || Array.isArray(item)) continue;\n\t\tconst record = item as Record<string, unknown>;\n\t\tif (typeof record.type === \"string\" && STATUS_LESS_ITEM_TYPES.has(record.type) && \"status\" in record) {\n\t\t\tdelete record.status;\n\t\t}\n\t\tif (Array.isArray(record.content)) {\n\t\t\tfor (const part of record.content) {\n\t\t\t\tif (\n\t\t\t\t\tpart &&\n\t\t\t\t\ttypeof part === \"object\" &&\n\t\t\t\t\t(part as { type?: string }).type === \"output_text\" &&\n\t\t\t\t\t\"annotations\" in part\n\t\t\t\t) {\n\t\t\t\t\tdelete (part as Record<string, unknown>).annotations;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunction extractInstructions(\n\tinput: unknown,\n\tsystemPrompt: string | undefined,\n): { instructions?: string; input: unknown } {\n\t// Prefer context.systemPrompt (matches Codex stream path).\n\tif (systemPrompt && systemPrompt.length > 0) {\n\t\tconst stripped = stripLeadingSystemRoles(input);\n\t\treturn { instructions: systemPrompt, input: stripped ?? input };\n\t}\n\n\tif (!Array.isArray(input) || input.length === 0) {\n\t\treturn { input };\n\t}\n\n\tconst first = input[0];\n\tif (\n\t\tfirst &&\n\t\ttypeof first === \"object\" &&\n\t\t!Array.isArray(first) &&\n\t\t\"role\" in first &&\n\t\t((first as { role?: string }).role === \"system\" || (first as { role?: string }).role === \"developer\")\n\t) {\n\t\tconst content = (first as { content?: unknown }).content;\n\t\tconst text =\n\t\t\ttypeof content === \"string\"\n\t\t\t\t? content\n\t\t\t\t: Array.isArray(content)\n\t\t\t\t\t? content\n\t\t\t\t\t\t\t.map((part) =>\n\t\t\t\t\t\t\t\tpart && typeof part === \"object\" && \"text\" in part\n\t\t\t\t\t\t\t\t\t? String((part as { text: unknown }).text)\n\t\t\t\t\t\t\t\t\t: \"\",\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t.join(\"\")\n\t\t\t\t\t: \"\";\n\t\treturn {\n\t\t\tinstructions: text || undefined,\n\t\t\tinput: input.slice(1),\n\t\t};\n\t}\n\n\treturn { input };\n}\n\nfunction stripLeadingSystemRoles(input: unknown): unknown {\n\tif (!Array.isArray(input) || input.length === 0) return input;\n\tconst first = input[0];\n\tif (\n\t\tfirst &&\n\t\ttypeof first === \"object\" &&\n\t\t!Array.isArray(first) &&\n\t\t((first as { role?: string }).role === \"system\" || (first as { role?: string }).role === \"developer\")\n\t) {\n\t\treturn input.slice(1);\n\t}\n\treturn input;\n}\n\nfunction clampCacheKey(sessionId: string | undefined): string | undefined {\n\tif (!sessionId) return undefined;\n\treturn sessionId.length <= 64 ? sessionId : sessionId.slice(0, 64);\n}\n\nexport function resolveResponsesUrl(baseUrl: string): string {\n\tconst normalized = baseUrl.trim().replace(/\\/+$/, \"\");\n\tif (!normalized) throw new Error(\"Model baseUrl is empty.\");\n\tif (normalized.endsWith(\"/responses\")) return normalized;\n\treturn `${normalized}/responses`;\n}\n\nexport function isRouterApi(api: string): boolean {\n\treturn api === ROUTER_API;\n}\n"]}