@astralyn/pi 0.82.7 → 0.83.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/CHANGELOG.md +34 -5099
  2. package/README.md +12 -111
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +9 -2
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/cli/credential-print.d.ts +23 -0
  7. package/dist/cli/credential-print.d.ts.map +1 -0
  8. package/dist/cli/credential-print.js +116 -0
  9. package/dist/cli/credential-print.js.map +1 -0
  10. package/dist/core/agent-session-runtime.d.ts.map +1 -1
  11. package/dist/core/agent-session-runtime.js +3 -0
  12. package/dist/core/agent-session-runtime.js.map +1 -1
  13. package/dist/core/agent-session.d.ts +1 -1
  14. package/dist/core/agent-session.d.ts.map +1 -1
  15. package/dist/core/agent-session.js +13 -6
  16. package/dist/core/agent-session.js.map +1 -1
  17. package/dist/core/extensions/runner.d.ts +1 -0
  18. package/dist/core/extensions/runner.d.ts.map +1 -1
  19. package/dist/core/extensions/runner.js +7 -0
  20. package/dist/core/extensions/runner.js.map +1 -1
  21. package/dist/core/extensions/types.d.ts +7 -0
  22. package/dist/core/extensions/types.d.ts.map +1 -1
  23. package/dist/core/extensions/types.js.map +1 -1
  24. package/dist/core/footer-data-provider.d.ts +10 -0
  25. package/dist/core/footer-data-provider.d.ts.map +1 -1
  26. package/dist/core/footer-data-provider.js +1 -1
  27. package/dist/core/footer-data-provider.js.map +1 -1
  28. package/dist/core/model-runtime.d.ts +2 -0
  29. package/dist/core/model-runtime.d.ts.map +1 -1
  30. package/dist/core/model-runtime.js.map +1 -1
  31. package/dist/core/package-manager.d.ts.map +1 -1
  32. package/dist/core/package-manager.js +13 -6
  33. package/dist/core/package-manager.js.map +1 -1
  34. package/dist/core/resource-loader.d.ts +15 -0
  35. package/dist/core/resource-loader.d.ts.map +1 -1
  36. package/dist/core/resource-loader.js +70 -9
  37. package/dist/core/resource-loader.js.map +1 -1
  38. package/dist/core/tools/find.d.ts.map +1 -1
  39. package/dist/core/tools/find.js +34 -4
  40. package/dist/core/tools/find.js.map +1 -1
  41. package/dist/core/trust-manager.d.ts.map +1 -1
  42. package/dist/core/trust-manager.js +48 -9
  43. package/dist/core/trust-manager.js.map +1 -1
  44. package/dist/extensions/index.d.ts.map +1 -1
  45. package/dist/extensions/index.js +0 -2
  46. package/dist/extensions/index.js.map +1 -1
  47. package/dist/extensions/llama/provider.d.ts.map +1 -1
  48. package/dist/extensions/llama/provider.js +1 -1
  49. package/dist/extensions/llama/provider.js.map +1 -1
  50. package/dist/main.d.ts.map +1 -1
  51. package/dist/main.js +43 -0
  52. package/dist/main.js.map +1 -1
  53. package/dist/modes/interactive/components/extension-selector.d.ts +1 -1
  54. package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
  55. package/dist/modes/interactive/components/extension-selector.js.map +1 -1
  56. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  57. package/dist/modes/interactive/components/model-selector.js +4 -1
  58. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  59. package/dist/modes/interactive/external-editor.d.ts.map +1 -1
  60. package/dist/modes/interactive/external-editor.js +40 -3
  61. package/dist/modes/interactive/external-editor.js.map +1 -1
  62. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  63. package/dist/modes/interactive/interactive-mode.js +20 -6
  64. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  65. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  66. package/dist/modes/rpc/rpc-mode.js +13 -0
  67. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  68. package/dist/utils/paths.d.ts +2 -3
  69. package/dist/utils/paths.d.ts.map +1 -1
  70. package/dist/utils/paths.js +2 -3
  71. package/dist/utils/paths.js.map +1 -1
  72. package/docs/bundled/README.md +13 -16
  73. package/docs/bundled/themes.md +1 -1
  74. package/docs/compaction.md +25 -30
  75. package/docs/custom-provider.md +298 -583
  76. package/docs/docs.json +0 -8
  77. package/docs/extensions.md +112 -116
  78. package/docs/index.md +18 -74
  79. package/docs/json.md +20 -14
  80. package/docs/models.md +193 -151
  81. package/docs/providers.md +2 -1
  82. package/docs/quickstart.md +3 -3
  83. package/docs/rpc.md +165 -123
  84. package/docs/sdk.md +119 -178
  85. package/docs/security.md +1 -1
  86. package/docs/session-format.md +139 -119
  87. package/docs/skills.md +0 -1
  88. package/docs/themes.md +2 -2
  89. package/docs/tui.md +411 -549
  90. package/docs/usage.md +3 -11
  91. package/examples/README.md +6 -1
  92. package/examples/extensions/README.md +13 -33
  93. package/examples/extensions/custom-provider-anthropic/index.ts +9 -3
  94. package/examples/extensions/dynamic-resources/dynamic.json +1 -1
  95. package/examples/extensions/truncated-tool.ts +2 -2
  96. package/examples/sdk/12-full-control.ts +2 -0
  97. package/npm-shrinkwrap.json +21 -21
  98. package/package.json +5 -5
  99. package/dist/extensions/plan/constants.d.ts +0 -68
  100. package/dist/extensions/plan/constants.d.ts.map +0 -1
  101. package/dist/extensions/plan/constants.js +0 -100
  102. package/dist/extensions/plan/constants.js.map +0 -1
  103. package/dist/extensions/plan/index.d.ts +0 -30
  104. package/dist/extensions/plan/index.d.ts.map +0 -1
  105. package/dist/extensions/plan/index.js +0 -429
  106. package/dist/extensions/plan/index.js.map +0 -1
  107. package/dist/extensions/plan/schema.d.ts +0 -20
  108. package/dist/extensions/plan/schema.d.ts.map +0 -1
  109. package/dist/extensions/plan/schema.js +0 -13
  110. package/dist/extensions/plan/schema.js.map +0 -1
  111. package/dist/extensions/plan/state.d.ts +0 -38
  112. package/dist/extensions/plan/state.d.ts.map +0 -1
  113. package/dist/extensions/plan/state.js +0 -96
  114. package/dist/extensions/plan/state.js.map +0 -1
  115. package/dist/extensions/plan/storage.d.ts +0 -26
  116. package/dist/extensions/plan/storage.d.ts.map +0 -1
  117. package/dist/extensions/plan/storage.js +0 -83
  118. package/dist/extensions/plan/storage.js.map +0 -1
  119. package/dist/extensions/plan/view.d.ts +0 -44
  120. package/dist/extensions/plan/view.d.ts.map +0 -1
  121. package/dist/extensions/plan/view.js +0 -149
  122. package/dist/extensions/plan/view.js.map +0 -1
  123. package/docs/bundled/extensions/README.md +0 -19
  124. package/docs/bundled/extensions/plan.md +0 -34
  125. package/examples/extensions/plan-mode/README.md +0 -66
  126. package/examples/extensions/plan-mode/index.ts +0 -390
  127. package/examples/extensions/plan-mode/utils.ts +0 -168
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/extensions/plan/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAIH,OAAO,KAAK,EAEX,YAAY,EAGZ,MAAM,gCAAgC,CAAC;AAkExC;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAYhF;AAED,MAAM,CAAC,OAAO,UAAU,IAAI,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CA+YnD","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) snapshots the active tool set and swaps to the\n * exploration set (read/grep/find/ls/bash + exit_plan; edit/write removed,\n * bash prompt-constrained to read-only, subagent guarded to the explorer\n * profile); a tool_call guard covers requests already in flight before the\n * 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 */\n\nimport { basename } from \"node:path\";\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_ALLOWED_SUBAGENT,\n\tPLAN_BLOCKED_TOOLS,\n\tPLAN_COMMAND_NAME,\n\tPLAN_EMBED_MAX_CHARS,\n\tPLAN_ENTRY_TYPE,\n\tPLAN_EXPLORE_TOOLS,\n\tPLAN_FLAG_NAME,\n\tPLAN_KICKOFF_MESSAGE_TYPE,\n\tPLAN_PANEL_CANCEL_HINT,\n\tPLAN_PANEL_EXIT,\n\tPLAN_PANEL_EXIT_DESCRIPTION,\n\tPLAN_PANEL_MAX_FILES,\n\tPLAN_PANEL_TITLE,\n\tPLAN_STATUS_KEY,\n\tPLAN_SUBAGENT_TOOL_NAME,\n} from \"./constants.ts\";\nimport { type ExitPlanDetails, type ExitPlanParams, ExitPlanParamsSchema, type PlanKickoffDetails } 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 { listProjectPlanFiles, savePlanFile } from \"./storage.ts\";\nimport {\n\tformatApprovalSubtitle,\n\tformatExitPlanResult,\n\trenderExitPlanCall,\n\trenderPlanKickoffMessage,\n\tshortenPlanPath,\n} 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\n/**\n * First subagent profile in the call that is not allowed in plan mode, or\n * undefined when every profile is the read-only explorer. An omitted or null\n * agent resolves to the default \"general\" profile, so it is disallowed too.\n * Args arrive off the wire, so every field is read defensively.\n */\nexport function findDisallowedSubagentProfile(input: unknown): string | undefined {\n\tconst record = input && typeof input === \"object\" ? (input as Record<string, unknown>) : {};\n\tconst requested: unknown[] = Array.isArray(record.tasks)\n\t\t? record.tasks.map((task) =>\n\t\t\t\ttask && typeof task === \"object\" ? (task as Record<string, unknown>).agent : undefined,\n\t\t\t)\n\t\t: [record.agent];\n\tfor (const profile of requested) {\n\t\tconst name = typeof profile === \"string\" && profile.trim() ? profile.trim() : \"general\";\n\t\tif (name !== PLAN_ALLOWED_SUBAGENT) return name;\n\t}\n\treturn undefined;\n}\n\nexport default function plan(pi: ExtensionAPI): void {\n\tlet menuOpen = false;\n\tlet pendingCompact: PendingCompact | undefined;\n\n\tpi.registerMessageRenderer<PlanKickoffDetails>(PLAN_KICKOFF_MESSAGE_TYPE, renderPlanKickoffMessage);\n\n\t/**\n\t * Kickoff as a custom message, not a user message: the LLM still receives\n\t * the full kickoff text (custom messages convert to user-role context),\n\t * while the TUI renders the registered collapsed card instead of echoing\n\t * the whole plan again.\n\t */\n\tfunction sendKickoff(pending: PendingCompact): void {\n\t\tpi.sendMessage(\n\t\t\t{\n\t\t\t\tcustomType: PLAN_KICKOFF_MESSAGE_TYPE,\n\t\t\t\tcontent: buildKickoffMessage(pending),\n\t\t\t\tdisplay: true,\n\t\t\t\tdetails: { title: pending.title, planPath: pending.planPath },\n\t\t\t},\n\t\t\t{ triggerTurn: true },\n\t\t);\n\t}\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/**\n\t * Swap to the exploration tool set and return the pre-entry snapshot.\n\t * Snapshot + delta rather than a fixed allowlist: edit/write are dropped,\n\t * the explore tools (grep/find/ls are registered but not active by\n\t * default) and exit_plan are added, and every other active tool — question,\n\t * todo, deepwiki, subagent, ... — stays active.\n\t */\n\tfunction applyPlanTools(): string[] {\n\t\tconst snapshot = pi.getActiveTools();\n\t\tpi.setActiveTools([\n\t\t\t...new Set([\n\t\t\t\t...snapshot.filter((name) => !PLAN_BLOCKED_TOOLS.has(name)),\n\t\t\t\t...PLAN_EXPLORE_TOOLS,\n\t\t\t\tEXIT_PLAN_TOOL_NAME,\n\t\t\t]),\n\t\t]);\n\t\treturn snapshot;\n\t}\n\n\t/**\n\t * Inverse of applyPlanTools: restore the snapshot verbatim. An empty\n\t * snapshot (legacy entries) only drops exit_plan — there is nothing\n\t * recorded to restore to.\n\t */\n\tfunction restoreTools(toolSnapshot: string[]): void {\n\t\tif (toolSnapshot.length > 0) {\n\t\t\tpi.setActiveTools([...new Set(toolSnapshot)].filter((name) => name !== EXIT_PLAN_TOOL_NAME));\n\t\t\treturn;\n\t\t}\n\t\tpi.setActiveTools(pi.getActiveTools().filter((name) => name !== EXIT_PLAN_TOOL_NAME));\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 toolSnapshot = applyPlanTools();\n\t\treplacePlanState({ planning: true, toolSnapshot, awaitingCompact: false, planFiles: state.planFiles });\n\t\tpersist();\n\t\tupdateStatus(ctx);\n\t\tctx.ui.notify(\n\t\t\t\"Plan mode: read-only exploration — edit/write disabled, bash constrained to read-only, subagent limited to explorer.\",\n\t\t\t\"info\",\n\t\t);\n\t}\n\n\tfunction exitPlanMode(ctx: ExtensionContext, notifyText?: string): void {\n\t\tconst state = getPlanState();\n\t\trestoreTools(state.toolSnapshot);\n\t\treplacePlanState({ ...state, planning: false, awaitingCompact: false, toolSnapshot: [] });\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// Re-applying is idempotent, but the returned live snapshot is only\n\t\t\t// authoritative when the live set was NOT already restricted: on a\n\t\t\t// branch switch while planning, re-snapshotting would capture the\n\t\t\t// plan-mode set, so the in-memory snapshot wins there.\n\t\t\tconst liveSnapshot = applyPlanTools();\n\t\t\tnext.toolSnapshot =\n\t\t\t\tbefore.planning && before.toolSnapshot.length > 0 ? [...before.toolSnapshot] : liveSnapshot;\n\t\t} else {\n\t\t\tif (pi.getActiveTools().includes(EXIT_PLAN_TOOL_NAME)) {\n\t\t\t\trestoreTools(before.planning ? before.toolSnapshot : next.toolSnapshot);\n\t\t\t}\n\t\t\tnext.toolSnapshot = [];\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<typeof ExitPlanParamsSchema, ExitPlanDetails>({\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\t\trendersOwnProgress: true,\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\treturn renderExitPlanCall(args, theme, context.expanded);\n\t\t},\n\n\t\trenderResult(result, _options, theme) {\n\t\t\treturn new Text(formatExitPlanResult(result, theme), 0, 0);\n\t\t},\n\t});\n\n\t/**\n\t * Plans offered in the /plan panel: this branch's plans first (newest\n\t * first), then the rest of the project's, deduped, bounded. Legacy plans\n\t * from other sessions' directories only appear via planFiles entries.\n\t */\n\tfunction collectPanelPlanFiles(ctx: ExtensionContext): string[] {\n\t\tconst state = getPlanState();\n\t\tconst seen = new Set<string>();\n\t\tconst files: string[] = [];\n\t\tfor (const path of [...[...state.planFiles].reverse(), ...listProjectPlanFiles(ctx.cwd)]) {\n\t\t\tconst key = path.replace(/\\\\/g, \"/\").toLowerCase();\n\t\t\tif (seen.has(key)) continue;\n\t\t\tseen.add(key);\n\t\t\tfiles.push(path);\n\t\t\tif (files.length >= PLAN_PANEL_MAX_FILES) break;\n\t\t}\n\t\treturn files;\n\t}\n\n\t/** Panel behind /plan while planning: pick a plan file or exit the mode. */\n\tasync function openPlanPanel(ctx: ExtensionContext): Promise<void> {\n\t\tconst files = collectPanelPlanFiles(ctx);\n\t\tconst fileOptions = files.map((path) => ({ label: basename(path), description: shortenPlanPath(path) }));\n\t\tconst options = [{ label: PLAN_PANEL_EXIT, description: PLAN_PANEL_EXIT_DESCRIPTION }, ...fileOptions];\n\t\tconst choice = await ctx.ui.select(PLAN_PANEL_TITLE, options, { cancelHint: PLAN_PANEL_CANCEL_HINT });\n\t\tif (choice === PLAN_PANEL_EXIT) {\n\t\t\texitPlanMode(ctx, \"Plan mode disabled: full tool access restored.\");\n\t\t\treturn;\n\t\t}\n\t\tif (!choice) return; // Esc: keep planning, no side effects.\n\t\tconst selected = files.find((path) => basename(path) === choice);\n\t\tif (selected) ctx.ui.pasteToEditor(selected);\n\t}\n\n\tpi.registerCommand(PLAN_COMMAND_NAME, {\n\t\tdescription: \"Enter plan mode; while planning, open the plan panel (pick a plan file or 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\tenterPlanMode(ctx);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (ctx.mode !== \"tui\") {\n\t\t\t\t// No panel affordances outside the TUI: keep the toggle semantic.\n\t\t\t\texitPlanMode(ctx, \"Plan mode disabled: full tool access restored.\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait openPlanPanel(ctx);\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) return;\n\t\tif (PLAN_BLOCKED_TOOLS.has(event.toolName)) {\n\t\t\treturn {\n\t\t\t\tblock: true,\n\t\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\t};\n\t\t}\n\t\tif (event.toolName === PLAN_SUBAGENT_TOOL_NAME) {\n\t\t\tconst disallowed = findDisallowedSubagentProfile(event.input);\n\t\t\tif (disallowed !== undefined) {\n\t\t\t\treturn {\n\t\t\t\t\tblock: true,\n\t\t\t\t\treason: `Plan mode: subagent profile \"${disallowed}\" is blocked while planning. Delegate read-only exploration with agent: \"${PLAN_ALLOWED_SUBAGENT}\" (set it on every task in tasks mode).`,\n\t\t\t\t};\n\t\t\t}\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\tsendKickoff(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\tsendKickoff(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,429 +0,0 @@
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) snapshots the active tool set and swaps to the
7
- * exploration set (read/grep/find/ls/bash + exit_plan; edit/write removed,
8
- * bash prompt-constrained to read-only, subagent guarded to the explorer
9
- * profile); a tool_call guard covers requests already in flight before the
10
- * tool-set change lands.
11
- * - The plan text travels as an exit_plan parameter, so the model needs no
12
- * write access to produce the plan file.
13
- * - "Compact, then execute" keeps plan mode active until compaction completes
14
- * (tools are restored in onComplete) and terminates the run via the tool
15
- * result, so there is no window where the model holds full context plus
16
- * write tools. The kickoff message embeds the plan itself — continuation
17
- * never depends on the compaction summary preserving a path.
18
- * - State is conversation-backed via custom entries and branch replay (same
19
- * pattern as todo): /reload, /tree, resume, and fork all restore.
20
- */
21
- import { basename } from "node:path";
22
- import { Text } from "@earendil-works/pi-tui";
23
- 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_ALLOWED_SUBAGENT, PLAN_BLOCKED_TOOLS, PLAN_COMMAND_NAME, PLAN_EMBED_MAX_CHARS, PLAN_ENTRY_TYPE, PLAN_EXPLORE_TOOLS, PLAN_FLAG_NAME, PLAN_KICKOFF_MESSAGE_TYPE, PLAN_PANEL_CANCEL_HINT, PLAN_PANEL_EXIT, PLAN_PANEL_EXIT_DESCRIPTION, PLAN_PANEL_MAX_FILES, PLAN_PANEL_TITLE, PLAN_STATUS_KEY, PLAN_SUBAGENT_TOOL_NAME, } from "./constants.js";
24
- import { ExitPlanParamsSchema } from "./schema.js";
25
- import { clonePlanState, disposePlanSession, EMPTY_PLAN_STATE, getPlanState, replacePlanState, replayPlanFromBranch, setActivePlanSession, } from "./state.js";
26
- import { listProjectPlanFiles, savePlanFile } from "./storage.js";
27
- import { formatApprovalSubtitle, formatExitPlanResult, renderExitPlanCall, renderPlanKickoffMessage, shortenPlanPath, } from "./view.js";
28
- function buildKickoffMessage(pending) {
29
- const truncated = pending.plan.length > PLAN_EMBED_MAX_CHARS;
30
- const body = truncated
31
- ? `${pending.plan.slice(0, PLAN_EMBED_MAX_CHARS)}\n\n[Plan truncated — read the full plan at ${pending.planPath}]`
32
- : pending.plan;
33
- return `Execute the approved plan "${pending.title}" (saved at ${pending.planPath}).
34
-
35
- ${body}
36
-
37
- Start now. Track multi-step work with the todo tool, and report back instead of improvising if the plan turns out to need revision.`;
38
- }
39
- /**
40
- * First subagent profile in the call that is not allowed in plan mode, or
41
- * undefined when every profile is the read-only explorer. An omitted or null
42
- * agent resolves to the default "general" profile, so it is disallowed too.
43
- * Args arrive off the wire, so every field is read defensively.
44
- */
45
- export function findDisallowedSubagentProfile(input) {
46
- const record = input && typeof input === "object" ? input : {};
47
- const requested = Array.isArray(record.tasks)
48
- ? record.tasks.map((task) => task && typeof task === "object" ? task.agent : undefined)
49
- : [record.agent];
50
- for (const profile of requested) {
51
- const name = typeof profile === "string" && profile.trim() ? profile.trim() : "general";
52
- if (name !== PLAN_ALLOWED_SUBAGENT)
53
- return name;
54
- }
55
- return undefined;
56
- }
57
- export default function plan(pi) {
58
- let menuOpen = false;
59
- let pendingCompact;
60
- pi.registerMessageRenderer(PLAN_KICKOFF_MESSAGE_TYPE, renderPlanKickoffMessage);
61
- /**
62
- * Kickoff as a custom message, not a user message: the LLM still receives
63
- * the full kickoff text (custom messages convert to user-role context),
64
- * while the TUI renders the registered collapsed card instead of echoing
65
- * the whole plan again.
66
- */
67
- function sendKickoff(pending) {
68
- pi.sendMessage({
69
- customType: PLAN_KICKOFF_MESSAGE_TYPE,
70
- content: buildKickoffMessage(pending),
71
- display: true,
72
- details: { title: pending.title, planPath: pending.planPath },
73
- }, { triggerTurn: true });
74
- }
75
- pi.registerFlag(PLAN_FLAG_NAME, {
76
- description: "Start in plan mode (read-only planning)",
77
- type: "boolean",
78
- default: false,
79
- });
80
- /**
81
- * Swap to the exploration tool set and return the pre-entry snapshot.
82
- * Snapshot + delta rather than a fixed allowlist: edit/write are dropped,
83
- * the explore tools (grep/find/ls are registered but not active by
84
- * default) and exit_plan are added, and every other active tool — question,
85
- * todo, deepwiki, subagent, ... — stays active.
86
- */
87
- function applyPlanTools() {
88
- const snapshot = pi.getActiveTools();
89
- pi.setActiveTools([
90
- ...new Set([
91
- ...snapshot.filter((name) => !PLAN_BLOCKED_TOOLS.has(name)),
92
- ...PLAN_EXPLORE_TOOLS,
93
- EXIT_PLAN_TOOL_NAME,
94
- ]),
95
- ]);
96
- return snapshot;
97
- }
98
- /**
99
- * Inverse of applyPlanTools: restore the snapshot verbatim. An empty
100
- * snapshot (legacy entries) only drops exit_plan — there is nothing
101
- * recorded to restore to.
102
- */
103
- function restoreTools(toolSnapshot) {
104
- if (toolSnapshot.length > 0) {
105
- pi.setActiveTools([...new Set(toolSnapshot)].filter((name) => name !== EXIT_PLAN_TOOL_NAME));
106
- return;
107
- }
108
- pi.setActiveTools(pi.getActiveTools().filter((name) => name !== EXIT_PLAN_TOOL_NAME));
109
- }
110
- function persist() {
111
- pi.appendEntry(PLAN_ENTRY_TYPE, getPlanState());
112
- }
113
- function updateStatus(ctx) {
114
- const state = getPlanState();
115
- ctx.ui.setStatus(PLAN_STATUS_KEY, state.planning ? ctx.ui.theme.fg("accent", "Plan Mode") : undefined);
116
- }
117
- function enterPlanMode(ctx) {
118
- const state = getPlanState();
119
- if (state.planning)
120
- return;
121
- const toolSnapshot = applyPlanTools();
122
- replacePlanState({ planning: true, toolSnapshot, awaitingCompact: false, planFiles: state.planFiles });
123
- persist();
124
- updateStatus(ctx);
125
- ctx.ui.notify("Plan mode: read-only exploration — edit/write disabled, bash constrained to read-only, subagent limited to explorer.", "info");
126
- }
127
- function exitPlanMode(ctx, notifyText) {
128
- const state = getPlanState();
129
- restoreTools(state.toolSnapshot);
130
- replacePlanState({ ...state, planning: false, awaitingCompact: false, toolSnapshot: [] });
131
- persist();
132
- updateStatus(ctx);
133
- if (notifyText)
134
- ctx.ui.notify(notifyText, "info");
135
- }
136
- /** Save the approved plan, index it in state, and name the session if unnamed. */
137
- function saveApprovedPlan(ctx, params, title) {
138
- const planPath = savePlanFile({
139
- sessionId: ctx.sessionManager.getSessionId(),
140
- title,
141
- plan: params.plan,
142
- cwd: ctx.cwd,
143
- revises: params.revises,
144
- });
145
- const state = getPlanState();
146
- replacePlanState({ ...state, planFiles: [...state.planFiles, planPath] });
147
- if (!pi.getSessionName())
148
- pi.setSessionName(title);
149
- return planPath;
150
- }
151
- /**
152
- * Rebuild state from the current branch and reconcile the live tool set.
153
- * An interrupted compact-and-execute approval (restart, resume, branch
154
- * switch) degrades to the keep-context exit so the approval is honored.
155
- */
156
- function syncFromBranch(ctx, options) {
157
- setActivePlanSession(ctx.sessionManager.getSessionId());
158
- const before = getPlanState();
159
- const replayed = replayPlanFromBranch(ctx);
160
- const next = replayed ?? clonePlanState(EMPTY_PLAN_STATE);
161
- if (!replayed && options?.applyFlag && pi.getFlag(PLAN_FLAG_NAME) === true) {
162
- next.planning = true;
163
- }
164
- let interruptedCompact = false;
165
- if (next.awaitingCompact) {
166
- next.planning = false;
167
- next.awaitingCompact = false;
168
- interruptedCompact = true;
169
- pendingCompact = undefined;
170
- }
171
- if (next.planning) {
172
- // Re-applying is idempotent, but the returned live snapshot is only
173
- // authoritative when the live set was NOT already restricted: on a
174
- // branch switch while planning, re-snapshotting would capture the
175
- // plan-mode set, so the in-memory snapshot wins there.
176
- const liveSnapshot = applyPlanTools();
177
- next.toolSnapshot =
178
- before.planning && before.toolSnapshot.length > 0 ? [...before.toolSnapshot] : liveSnapshot;
179
- }
180
- else {
181
- if (pi.getActiveTools().includes(EXIT_PLAN_TOOL_NAME)) {
182
- restoreTools(before.planning ? before.toolSnapshot : next.toolSnapshot);
183
- }
184
- next.toolSnapshot = [];
185
- }
186
- replacePlanState(next);
187
- if (interruptedCompact) {
188
- persist();
189
- ctx.ui.notify("Pending compaction was interrupted; plan mode exited with context kept.", "warning");
190
- }
191
- updateStatus(ctx);
192
- }
193
- pi.registerTool({
194
- name: EXIT_PLAN_TOOL_NAME,
195
- label: EXIT_PLAN_TOOL_LABEL,
196
- description: EXIT_PLAN_TOOL_DESCRIPTION,
197
- promptSnippet: EXIT_PLAN_PROMPT_SNIPPET,
198
- promptGuidelines: EXIT_PLAN_PROMPT_GUIDELINES,
199
- parameters: ExitPlanParamsSchema,
200
- executionMode: "sequential",
201
- rendersOwnProgress: true,
202
- async execute(_toolCallId, params, signal, _onUpdate, ctx) {
203
- setActivePlanSession(ctx.sessionManager.getSessionId());
204
- const state = getPlanState();
205
- const title = params.title.trim() || "plan";
206
- if (!state.planning || state.awaitingCompact) {
207
- return {
208
- content: [
209
- { type: "text", text: "Plan mode is not awaiting an exit; continue with your current instructions." },
210
- ],
211
- details: { decision: "cancelled", title },
212
- };
213
- }
214
- if (!ctx.hasUI) {
215
- // json/print: no way to ask, so honor the least surprising default —
216
- // exit with context kept. Documented as a headless-mode semantic.
217
- const planPath = saveApprovedPlan(ctx, params, title);
218
- exitPlanMode(ctx);
219
- return {
220
- content: [
221
- {
222
- type: "text",
223
- text: `Plan saved to ${planPath}. No interactive UI, so plan mode exited with context kept (authoritative: write tools are restored). Begin executing the plan now.`,
224
- },
225
- ],
226
- details: { decision: "execute", title, planPath },
227
- };
228
- }
229
- let choice;
230
- menuOpen = true;
231
- try {
232
- choice = await ctx.ui.select(`Approve plan "${title}"?`, EXIT_PLAN_MENU_OPTIONS, {
233
- signal,
234
- subtitle: formatApprovalSubtitle(ctx),
235
- cancelHint: EXIT_PLAN_CANCEL_HINT,
236
- });
237
- }
238
- finally {
239
- menuOpen = false;
240
- }
241
- if (signal?.aborted) {
242
- // Throwing lets Pi mark the result as an error, matching the todo convention.
243
- throw new Error("Plan approval was interrupted; plan mode is still active.");
244
- }
245
- if (choice === MENU_EXECUTE) {
246
- const planPath = saveApprovedPlan(ctx, params, title);
247
- exitPlanMode(ctx, "Plan approved: executing with full context.");
248
- return {
249
- content: [
250
- {
251
- type: "text",
252
- text: `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.`,
253
- },
254
- ],
255
- details: { decision: "execute", title, planPath },
256
- };
257
- }
258
- if (choice === MENU_COMPACT_EXECUTE) {
259
- const planPath = saveApprovedPlan(ctx, params, title);
260
- // Stay in plan mode (tools restricted) until compaction completes;
261
- // terminate ends the run so agent_settled can start the compaction.
262
- replacePlanState({ ...getPlanState(), awaitingCompact: true });
263
- persist();
264
- pendingCompact = { title, plan: params.plan, planPath };
265
- return {
266
- content: [
267
- {
268
- type: "text",
269
- text: `Plan approved and saved to ${planPath}. The context will now be compacted; stop here — execution restarts automatically after compaction.`,
270
- },
271
- ],
272
- details: { decision: "compactAndExecute", title, planPath },
273
- terminate: true,
274
- };
275
- }
276
- // Explicit "Keep planning", Esc, or timeout: the only side-effect-free
277
- // choice, so any dismissal maps here.
278
- let feedback;
279
- if (choice === MENU_KEEP_PLANNING) {
280
- feedback =
281
- ctx.mode === "tui"
282
- ? await ctx.ui.editor("Feedback for the plan (optional):", "")
283
- : await ctx.ui.input("Feedback for the plan (optional):");
284
- }
285
- const trimmedFeedback = feedback?.trim();
286
- return {
287
- content: [
288
- {
289
- type: "text",
290
- text: trimmedFeedback
291
- ? `The user wants to keep planning. Feedback:\n${trimmedFeedback}`
292
- : "The user wants to keep planning. Ask what should change, or keep refining the open points.",
293
- },
294
- ],
295
- details: { decision: "keepPlanning", title },
296
- };
297
- },
298
- renderCall(args, theme, context) {
299
- return renderExitPlanCall(args, theme, context.expanded);
300
- },
301
- renderResult(result, _options, theme) {
302
- return new Text(formatExitPlanResult(result, theme), 0, 0);
303
- },
304
- });
305
- /**
306
- * Plans offered in the /plan panel: this branch's plans first (newest
307
- * first), then the rest of the project's, deduped, bounded. Legacy plans
308
- * from other sessions' directories only appear via planFiles entries.
309
- */
310
- function collectPanelPlanFiles(ctx) {
311
- const state = getPlanState();
312
- const seen = new Set();
313
- const files = [];
314
- for (const path of [...[...state.planFiles].reverse(), ...listProjectPlanFiles(ctx.cwd)]) {
315
- const key = path.replace(/\\/g, "/").toLowerCase();
316
- if (seen.has(key))
317
- continue;
318
- seen.add(key);
319
- files.push(path);
320
- if (files.length >= PLAN_PANEL_MAX_FILES)
321
- break;
322
- }
323
- return files;
324
- }
325
- /** Panel behind /plan while planning: pick a plan file or exit the mode. */
326
- async function openPlanPanel(ctx) {
327
- const files = collectPanelPlanFiles(ctx);
328
- const fileOptions = files.map((path) => ({ label: basename(path), description: shortenPlanPath(path) }));
329
- const options = [{ label: PLAN_PANEL_EXIT, description: PLAN_PANEL_EXIT_DESCRIPTION }, ...fileOptions];
330
- const choice = await ctx.ui.select(PLAN_PANEL_TITLE, options, { cancelHint: PLAN_PANEL_CANCEL_HINT });
331
- if (choice === PLAN_PANEL_EXIT) {
332
- exitPlanMode(ctx, "Plan mode disabled: full tool access restored.");
333
- return;
334
- }
335
- if (!choice)
336
- return; // Esc: keep planning, no side effects.
337
- const selected = files.find((path) => basename(path) === choice);
338
- if (selected)
339
- ctx.ui.pasteToEditor(selected);
340
- }
341
- pi.registerCommand(PLAN_COMMAND_NAME, {
342
- description: "Enter plan mode; while planning, open the plan panel (pick a plan file or exit)",
343
- handler: async (_args, ctx) => {
344
- setActivePlanSession(ctx.sessionManager.getSessionId());
345
- const state = getPlanState();
346
- if (menuOpen) {
347
- ctx.ui.notify("The exit_plan approval menu is open — answer it first.", "warning");
348
- return;
349
- }
350
- if (state.awaitingCompact) {
351
- ctx.ui.notify("Waiting for compaction to finish before leaving plan mode.", "warning");
352
- return;
353
- }
354
- if (!state.planning) {
355
- enterPlanMode(ctx);
356
- return;
357
- }
358
- if (ctx.mode !== "tui") {
359
- // No panel affordances outside the TUI: keep the toggle semantic.
360
- exitPlanMode(ctx, "Plan mode disabled: full tool access restored.");
361
- return;
362
- }
363
- await openPlanPanel(ctx);
364
- },
365
- });
366
- // Defense in depth: the active-tools change lands on the next LLM request,
367
- // so a request already in flight when plan mode toggled can still carry
368
- // blocked tool calls.
369
- pi.on("tool_call", async (event, ctx) => {
370
- setActivePlanSession(ctx.sessionManager.getSessionId());
371
- const state = getPlanState();
372
- if (!state.planning)
373
- return;
374
- if (PLAN_BLOCKED_TOOLS.has(event.toolName)) {
375
- return {
376
- block: true,
377
- reason: `Plan mode: ${event.toolName} is disabled. Keep exploring with read-only tools, or call ${EXIT_PLAN_TOOL_NAME} to submit the plan for approval.`,
378
- };
379
- }
380
- if (event.toolName === PLAN_SUBAGENT_TOOL_NAME) {
381
- const disallowed = findDisallowedSubagentProfile(event.input);
382
- if (disallowed !== undefined) {
383
- return {
384
- block: true,
385
- reason: `Plan mode: subagent profile "${disallowed}" is blocked while planning. Delegate read-only exploration with agent: "${PLAN_ALLOWED_SUBAGENT}" (set it on every task in tasks mode).`,
386
- };
387
- }
388
- }
389
- });
390
- pi.on("before_agent_start", async (event, ctx) => {
391
- setActivePlanSession(ctx.sessionManager.getSessionId());
392
- const state = getPlanState();
393
- if (!state.planning)
394
- return;
395
- return { systemPrompt: `${event.systemPrompt}\n\n${buildPlanModePrompt(state.planFiles)}` };
396
- });
397
- // agent_settled fires once per fully settled run (agent_end can repeat on
398
- // retries and queued continuations), so the approved compaction starts here.
399
- pi.on("agent_settled", async (_event, ctx) => {
400
- setActivePlanSession(ctx.sessionManager.getSessionId());
401
- if (!getPlanState().awaitingCompact || !pendingCompact)
402
- return;
403
- const pending = pendingCompact;
404
- pendingCompact = undefined;
405
- ctx.compact({
406
- customInstructions: `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.`,
407
- onComplete: () => {
408
- exitPlanMode(ctx, "Context compacted; executing the plan.");
409
- sendKickoff(pending);
410
- },
411
- onError: (error) => {
412
- // Never leave the user stuck read-only: degrade to keep-context execution.
413
- exitPlanMode(ctx, `Compaction failed (${error.message}); executing with full context instead.`);
414
- sendKickoff(pending);
415
- },
416
- });
417
- });
418
- pi.on("session_start", async (_event, ctx) => {
419
- syncFromBranch(ctx, { applyFlag: true });
420
- });
421
- pi.on("session_tree", async (_event, ctx) => {
422
- syncFromBranch(ctx);
423
- });
424
- pi.on("session_shutdown", async (_event, ctx) => {
425
- pendingCompact = undefined;
426
- disposePlanSession(ctx.sessionManager.getSessionId());
427
- });
428
- }
429
- //# sourceMappingURL=index.js.map
@@ -1 +0,0 @@
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) snapshots the active tool set and swaps to the\n * exploration set (read/grep/find/ls/bash + exit_plan; edit/write removed,\n * bash prompt-constrained to read-only, subagent guarded to the explorer\n * profile); a tool_call guard covers requests already in flight before the\n * 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 */\n\nimport { basename } from \"node:path\";\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_ALLOWED_SUBAGENT,\n\tPLAN_BLOCKED_TOOLS,\n\tPLAN_COMMAND_NAME,\n\tPLAN_EMBED_MAX_CHARS,\n\tPLAN_ENTRY_TYPE,\n\tPLAN_EXPLORE_TOOLS,\n\tPLAN_FLAG_NAME,\n\tPLAN_KICKOFF_MESSAGE_TYPE,\n\tPLAN_PANEL_CANCEL_HINT,\n\tPLAN_PANEL_EXIT,\n\tPLAN_PANEL_EXIT_DESCRIPTION,\n\tPLAN_PANEL_MAX_FILES,\n\tPLAN_PANEL_TITLE,\n\tPLAN_STATUS_KEY,\n\tPLAN_SUBAGENT_TOOL_NAME,\n} from \"./constants.ts\";\nimport { type ExitPlanDetails, type ExitPlanParams, ExitPlanParamsSchema, type PlanKickoffDetails } 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 { listProjectPlanFiles, savePlanFile } from \"./storage.ts\";\nimport {\n\tformatApprovalSubtitle,\n\tformatExitPlanResult,\n\trenderExitPlanCall,\n\trenderPlanKickoffMessage,\n\tshortenPlanPath,\n} 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\n/**\n * First subagent profile in the call that is not allowed in plan mode, or\n * undefined when every profile is the read-only explorer. An omitted or null\n * agent resolves to the default \"general\" profile, so it is disallowed too.\n * Args arrive off the wire, so every field is read defensively.\n */\nexport function findDisallowedSubagentProfile(input: unknown): string | undefined {\n\tconst record = input && typeof input === \"object\" ? (input as Record<string, unknown>) : {};\n\tconst requested: unknown[] = Array.isArray(record.tasks)\n\t\t? record.tasks.map((task) =>\n\t\t\t\ttask && typeof task === \"object\" ? (task as Record<string, unknown>).agent : undefined,\n\t\t\t)\n\t\t: [record.agent];\n\tfor (const profile of requested) {\n\t\tconst name = typeof profile === \"string\" && profile.trim() ? profile.trim() : \"general\";\n\t\tif (name !== PLAN_ALLOWED_SUBAGENT) return name;\n\t}\n\treturn undefined;\n}\n\nexport default function plan(pi: ExtensionAPI): void {\n\tlet menuOpen = false;\n\tlet pendingCompact: PendingCompact | undefined;\n\n\tpi.registerMessageRenderer<PlanKickoffDetails>(PLAN_KICKOFF_MESSAGE_TYPE, renderPlanKickoffMessage);\n\n\t/**\n\t * Kickoff as a custom message, not a user message: the LLM still receives\n\t * the full kickoff text (custom messages convert to user-role context),\n\t * while the TUI renders the registered collapsed card instead of echoing\n\t * the whole plan again.\n\t */\n\tfunction sendKickoff(pending: PendingCompact): void {\n\t\tpi.sendMessage(\n\t\t\t{\n\t\t\t\tcustomType: PLAN_KICKOFF_MESSAGE_TYPE,\n\t\t\t\tcontent: buildKickoffMessage(pending),\n\t\t\t\tdisplay: true,\n\t\t\t\tdetails: { title: pending.title, planPath: pending.planPath },\n\t\t\t},\n\t\t\t{ triggerTurn: true },\n\t\t);\n\t}\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/**\n\t * Swap to the exploration tool set and return the pre-entry snapshot.\n\t * Snapshot + delta rather than a fixed allowlist: edit/write are dropped,\n\t * the explore tools (grep/find/ls are registered but not active by\n\t * default) and exit_plan are added, and every other active tool — question,\n\t * todo, deepwiki, subagent, ... — stays active.\n\t */\n\tfunction applyPlanTools(): string[] {\n\t\tconst snapshot = pi.getActiveTools();\n\t\tpi.setActiveTools([\n\t\t\t...new Set([\n\t\t\t\t...snapshot.filter((name) => !PLAN_BLOCKED_TOOLS.has(name)),\n\t\t\t\t...PLAN_EXPLORE_TOOLS,\n\t\t\t\tEXIT_PLAN_TOOL_NAME,\n\t\t\t]),\n\t\t]);\n\t\treturn snapshot;\n\t}\n\n\t/**\n\t * Inverse of applyPlanTools: restore the snapshot verbatim. An empty\n\t * snapshot (legacy entries) only drops exit_plan — there is nothing\n\t * recorded to restore to.\n\t */\n\tfunction restoreTools(toolSnapshot: string[]): void {\n\t\tif (toolSnapshot.length > 0) {\n\t\t\tpi.setActiveTools([...new Set(toolSnapshot)].filter((name) => name !== EXIT_PLAN_TOOL_NAME));\n\t\t\treturn;\n\t\t}\n\t\tpi.setActiveTools(pi.getActiveTools().filter((name) => name !== EXIT_PLAN_TOOL_NAME));\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 toolSnapshot = applyPlanTools();\n\t\treplacePlanState({ planning: true, toolSnapshot, awaitingCompact: false, planFiles: state.planFiles });\n\t\tpersist();\n\t\tupdateStatus(ctx);\n\t\tctx.ui.notify(\n\t\t\t\"Plan mode: read-only exploration — edit/write disabled, bash constrained to read-only, subagent limited to explorer.\",\n\t\t\t\"info\",\n\t\t);\n\t}\n\n\tfunction exitPlanMode(ctx: ExtensionContext, notifyText?: string): void {\n\t\tconst state = getPlanState();\n\t\trestoreTools(state.toolSnapshot);\n\t\treplacePlanState({ ...state, planning: false, awaitingCompact: false, toolSnapshot: [] });\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// Re-applying is idempotent, but the returned live snapshot is only\n\t\t\t// authoritative when the live set was NOT already restricted: on a\n\t\t\t// branch switch while planning, re-snapshotting would capture the\n\t\t\t// plan-mode set, so the in-memory snapshot wins there.\n\t\t\tconst liveSnapshot = applyPlanTools();\n\t\t\tnext.toolSnapshot =\n\t\t\t\tbefore.planning && before.toolSnapshot.length > 0 ? [...before.toolSnapshot] : liveSnapshot;\n\t\t} else {\n\t\t\tif (pi.getActiveTools().includes(EXIT_PLAN_TOOL_NAME)) {\n\t\t\t\trestoreTools(before.planning ? before.toolSnapshot : next.toolSnapshot);\n\t\t\t}\n\t\t\tnext.toolSnapshot = [];\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<typeof ExitPlanParamsSchema, ExitPlanDetails>({\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\t\trendersOwnProgress: true,\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\treturn renderExitPlanCall(args, theme, context.expanded);\n\t\t},\n\n\t\trenderResult(result, _options, theme) {\n\t\t\treturn new Text(formatExitPlanResult(result, theme), 0, 0);\n\t\t},\n\t});\n\n\t/**\n\t * Plans offered in the /plan panel: this branch's plans first (newest\n\t * first), then the rest of the project's, deduped, bounded. Legacy plans\n\t * from other sessions' directories only appear via planFiles entries.\n\t */\n\tfunction collectPanelPlanFiles(ctx: ExtensionContext): string[] {\n\t\tconst state = getPlanState();\n\t\tconst seen = new Set<string>();\n\t\tconst files: string[] = [];\n\t\tfor (const path of [...[...state.planFiles].reverse(), ...listProjectPlanFiles(ctx.cwd)]) {\n\t\t\tconst key = path.replace(/\\\\/g, \"/\").toLowerCase();\n\t\t\tif (seen.has(key)) continue;\n\t\t\tseen.add(key);\n\t\t\tfiles.push(path);\n\t\t\tif (files.length >= PLAN_PANEL_MAX_FILES) break;\n\t\t}\n\t\treturn files;\n\t}\n\n\t/** Panel behind /plan while planning: pick a plan file or exit the mode. */\n\tasync function openPlanPanel(ctx: ExtensionContext): Promise<void> {\n\t\tconst files = collectPanelPlanFiles(ctx);\n\t\tconst fileOptions = files.map((path) => ({ label: basename(path), description: shortenPlanPath(path) }));\n\t\tconst options = [{ label: PLAN_PANEL_EXIT, description: PLAN_PANEL_EXIT_DESCRIPTION }, ...fileOptions];\n\t\tconst choice = await ctx.ui.select(PLAN_PANEL_TITLE, options, { cancelHint: PLAN_PANEL_CANCEL_HINT });\n\t\tif (choice === PLAN_PANEL_EXIT) {\n\t\t\texitPlanMode(ctx, \"Plan mode disabled: full tool access restored.\");\n\t\t\treturn;\n\t\t}\n\t\tif (!choice) return; // Esc: keep planning, no side effects.\n\t\tconst selected = files.find((path) => basename(path) === choice);\n\t\tif (selected) ctx.ui.pasteToEditor(selected);\n\t}\n\n\tpi.registerCommand(PLAN_COMMAND_NAME, {\n\t\tdescription: \"Enter plan mode; while planning, open the plan panel (pick a plan file or 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\tenterPlanMode(ctx);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (ctx.mode !== \"tui\") {\n\t\t\t\t// No panel affordances outside the TUI: keep the toggle semantic.\n\t\t\t\texitPlanMode(ctx, \"Plan mode disabled: full tool access restored.\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait openPlanPanel(ctx);\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) return;\n\t\tif (PLAN_BLOCKED_TOOLS.has(event.toolName)) {\n\t\t\treturn {\n\t\t\t\tblock: true,\n\t\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\t};\n\t\t}\n\t\tif (event.toolName === PLAN_SUBAGENT_TOOL_NAME) {\n\t\t\tconst disallowed = findDisallowedSubagentProfile(event.input);\n\t\t\tif (disallowed !== undefined) {\n\t\t\t\treturn {\n\t\t\t\t\tblock: true,\n\t\t\t\t\treason: `Plan mode: subagent profile \"${disallowed}\" is blocked while planning. Delegate read-only exploration with agent: \"${PLAN_ALLOWED_SUBAGENT}\" (set it on every task in tasks mode).`,\n\t\t\t\t};\n\t\t\t}\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\tsendKickoff(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\tsendKickoff(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,20 +0,0 @@
1
- import { type Static, Type } from "typebox";
2
- export declare const ExitPlanParamsSchema: Type.TObject<{
3
- title: Type.TString;
4
- plan: Type.TString;
5
- revises: Type.TOptional<Type.TString>;
6
- }>;
7
- export type ExitPlanParams = Static<typeof ExitPlanParamsSchema>;
8
- export type ExitPlanDecision = "execute" | "compactAndExecute" | "keepPlanning" | "cancelled";
9
- export interface ExitPlanDetails {
10
- decision: ExitPlanDecision;
11
- title: string;
12
- /** Absolute path of the saved plan file. Present for both execute decisions. */
13
- planPath?: string;
14
- }
15
- /** details of the post-compaction kickoff custom message. */
16
- export interface PlanKickoffDetails {
17
- title: string;
18
- planPath: string;
19
- }
20
- //# sourceMappingURL=schema.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../../src/extensions/plan/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAE5C,eAAO,MAAM,oBAAoB;;;;EAY/B,CAAC;AAEH,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAEjE,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,mBAAmB,GAAG,cAAc,GAAG,WAAW,CAAC;AAE9F,MAAM,WAAW,eAAe;IAC/B,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,gFAAgF;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,6DAA6D;AAC7D,MAAM,WAAW,kBAAkB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CACjB","sourcesContent":["import { type Static, Type } from \"typebox\";\n\nexport const ExitPlanParamsSchema = Type.Object({\n\ttitle: Type.String({\n\t\tdescription: \"Short plan title. Used for the plan file name and, when the session is unnamed, the session name.\",\n\t}),\n\tplan: Type.String({\n\t\tdescription: \"The complete plan as markdown: goal, steps, and verification criteria.\",\n\t}),\n\trevises: Type.Optional(\n\t\tType.String({\n\t\t\tdescription: \"File name of a previously saved plan this plan revises, e.g. `01-add-cache.md`.\",\n\t\t}),\n\t),\n});\n\nexport type ExitPlanParams = Static<typeof ExitPlanParamsSchema>;\n\nexport type ExitPlanDecision = \"execute\" | \"compactAndExecute\" | \"keepPlanning\" | \"cancelled\";\n\nexport interface ExitPlanDetails {\n\tdecision: ExitPlanDecision;\n\ttitle: string;\n\t/** Absolute path of the saved plan file. Present for both execute decisions. */\n\tplanPath?: string;\n}\n\n/** details of the post-compaction kickoff custom message. */\nexport interface PlanKickoffDetails {\n\ttitle: string;\n\tplanPath: string;\n}\n"]}
@@ -1,13 +0,0 @@
1
- import { Type } from "typebox";
2
- export const ExitPlanParamsSchema = Type.Object({
3
- title: Type.String({
4
- description: "Short plan title. Used for the plan file name and, when the session is unnamed, the session name.",
5
- }),
6
- plan: Type.String({
7
- description: "The complete plan as markdown: goal, steps, and verification criteria.",
8
- }),
9
- revises: Type.Optional(Type.String({
10
- description: "File name of a previously saved plan this plan revises, e.g. `01-add-cache.md`.",
11
- })),
12
- });
13
- //# sourceMappingURL=schema.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../../src/extensions/plan/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,IAAI,EAAE,MAAM,SAAS,CAAC;AAE5C,MAAM,CAAC,MAAM,oBAAoB,GAAG,IAAI,CAAC,MAAM,CAAC;IAC/C,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC;QAClB,WAAW,EAAE,mGAAmG;KAChH,CAAC;IACF,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC;QACjB,WAAW,EAAE,wEAAwE;KACrF,CAAC;IACF,OAAO,EAAE,IAAI,CAAC,QAAQ,CACrB,IAAI,CAAC,MAAM,CAAC;QACX,WAAW,EAAE,iFAAiF;KAC9F,CAAC,CACF;CACD,CAAC,CAAC","sourcesContent":["import { type Static, Type } from \"typebox\";\n\nexport const ExitPlanParamsSchema = Type.Object({\n\ttitle: Type.String({\n\t\tdescription: \"Short plan title. Used for the plan file name and, when the session is unnamed, the session name.\",\n\t}),\n\tplan: Type.String({\n\t\tdescription: \"The complete plan as markdown: goal, steps, and verification criteria.\",\n\t}),\n\trevises: Type.Optional(\n\t\tType.String({\n\t\t\tdescription: \"File name of a previously saved plan this plan revises, e.g. `01-add-cache.md`.\",\n\t\t}),\n\t),\n});\n\nexport type ExitPlanParams = Static<typeof ExitPlanParamsSchema>;\n\nexport type ExitPlanDecision = \"execute\" | \"compactAndExecute\" | \"keepPlanning\" | \"cancelled\";\n\nexport interface ExitPlanDetails {\n\tdecision: ExitPlanDecision;\n\ttitle: string;\n\t/** Absolute path of the saved plan file. Present for both execute decisions. */\n\tplanPath?: string;\n}\n\n/** details of the post-compaction kickoff custom message. */\nexport interface PlanKickoffDetails {\n\ttitle: string;\n\tplanPath: string;\n}\n"]}
@@ -1,38 +0,0 @@
1
- export interface PlanSessionState {
2
- /** True from entry until the exit is fully applied (including a pending compaction). */
3
- planning: boolean;
4
- /**
5
- * Full active-tool set snapshotted on entry; restored verbatim on exit.
6
- * Empty for legacy entries (pre-snapshot `removedTools` shape) — replay
7
- * re-derives a fresh snapshot for active planning, and an inactive legacy
8
- * entry needs no restore beyond dropping exit_plan.
9
- */
10
- toolSnapshot: string[];
11
- /** A compact-and-execute exit was approved and is waiting for agent_settled + compaction. */
12
- awaitingCompact: boolean;
13
- /** Absolute paths of plan files saved on this conversation branch. */
14
- planFiles: string[];
15
- }
16
- export declare const EMPTY_PLAN_STATE: PlanSessionState;
17
- /** Point the module at a session's state bucket. Call wherever ctx is in hand. */
18
- export declare function setActivePlanSession(sid: string): void;
19
- export declare function getPlanState(): PlanSessionState;
20
- export declare function replacePlanState(next: PlanSessionState): void;
21
- /** Drop in-memory state for a session (call from session_shutdown). */
22
- export declare function disposePlanSession(sid: string): void;
23
- export declare function clonePlanState(source: PlanSessionState): PlanSessionState;
24
- /**
25
- * Accepts both the current shape (`toolSnapshot`) and the legacy one
26
- * (`removedTools`), so branches recorded before the snapshot change replay.
27
- */
28
- export declare function isPlanState(value: unknown): value is PlanSessionState;
29
- /**
30
- * Latest plan-mode entry on the current branch, or undefined when the branch
31
- * has none (lets callers distinguish "never used" from "explicitly exited").
32
- */
33
- export declare function replayPlanFromBranch(ctx: {
34
- sessionManager: {
35
- getBranch(): Iterable<unknown>;
36
- };
37
- }): PlanSessionState | undefined;
38
- //# sourceMappingURL=state.d.ts.map