@astralyn/pi 0.82.6 → 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.
- package/CHANGELOG.md +61 -5099
- package/README.md +12 -111
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +9 -2
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/credential-print.d.ts +23 -0
- package/dist/cli/credential-print.d.ts.map +1 -0
- package/dist/cli/credential-print.js +116 -0
- package/dist/cli/credential-print.js.map +1 -0
- package/dist/cli/session-picker.d.ts.map +1 -1
- package/dist/cli/session-picker.js +11 -13
- package/dist/cli/session-picker.js.map +1 -1
- package/dist/core/agent-session-runtime.d.ts.map +1 -1
- package/dist/core/agent-session-runtime.js +3 -0
- package/dist/core/agent-session-runtime.js.map +1 -1
- package/dist/core/agent-session.d.ts +1 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +13 -6
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +1 -0
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +7 -0
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +11 -0
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/footer-data-provider.d.ts +10 -0
- package/dist/core/footer-data-provider.d.ts.map +1 -1
- package/dist/core/footer-data-provider.js +1 -1
- package/dist/core/footer-data-provider.js.map +1 -1
- package/dist/core/model-runtime.d.ts +2 -0
- package/dist/core/model-runtime.d.ts.map +1 -1
- package/dist/core/model-runtime.js.map +1 -1
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +13 -6
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/resource-loader.d.ts +15 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +70 -9
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +2 -2
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/tools/bash.d.ts +1 -5
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +16 -41
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/edit.d.ts.map +1 -1
- package/dist/core/tools/edit.js +42 -11
- package/dist/core/tools/edit.js.map +1 -1
- package/dist/core/tools/find.d.ts.map +1 -1
- package/dist/core/tools/find.js +40 -11
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +17 -8
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/ls.d.ts.map +1 -1
- package/dist/core/tools/ls.js +3 -3
- package/dist/core/tools/ls.js.map +1 -1
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +4 -4
- package/dist/core/tools/read.js.map +1 -1
- package/dist/core/tools/render-utils.d.ts +7 -0
- package/dist/core/tools/render-utils.d.ts.map +1 -1
- package/dist/core/tools/render-utils.js +11 -0
- package/dist/core/tools/render-utils.js.map +1 -1
- package/dist/core/tools/write.d.ts.map +1 -1
- package/dist/core/tools/write.js +2 -3
- package/dist/core/tools/write.js.map +1 -1
- package/dist/core/trust-manager.d.ts.map +1 -1
- package/dist/core/trust-manager.js +48 -9
- package/dist/core/trust-manager.js.map +1 -1
- package/dist/extensions/deepwiki/render.d.ts.map +1 -1
- package/dist/extensions/deepwiki/render.js +34 -22
- package/dist/extensions/deepwiki/render.js.map +1 -1
- package/dist/extensions/index.d.ts.map +1 -1
- package/dist/extensions/index.js +0 -2
- package/dist/extensions/index.js.map +1 -1
- package/dist/extensions/llama/provider.d.ts.map +1 -1
- package/dist/extensions/llama/provider.js +1 -1
- package/dist/extensions/llama/provider.js.map +1 -1
- package/dist/extensions/question/dialog.d.ts.map +1 -1
- package/dist/extensions/question/dialog.js +29 -10
- package/dist/extensions/question/dialog.js.map +1 -1
- package/dist/extensions/question/index.d.ts.map +1 -1
- package/dist/extensions/question/index.js +7 -2
- package/dist/extensions/question/index.js.map +1 -1
- package/dist/extensions/question/render.d.ts +4 -8
- package/dist/extensions/question/render.d.ts.map +1 -1
- package/dist/extensions/question/render.js +202 -29
- package/dist/extensions/question/render.js.map +1 -1
- package/dist/extensions/question/results.d.ts.map +1 -1
- package/dist/extensions/question/results.js +1 -0
- package/dist/extensions/question/results.js.map +1 -1
- package/dist/extensions/question/schema.d.ts.map +1 -1
- package/dist/extensions/question/schema.js +2 -2
- package/dist/extensions/question/schema.js.map +1 -1
- package/dist/extensions/question/types.d.ts +1 -0
- package/dist/extensions/question/types.d.ts.map +1 -1
- package/dist/extensions/question/types.js +1 -1
- package/dist/extensions/question/types.js.map +1 -1
- package/dist/extensions/router/stream.d.ts +1 -0
- package/dist/extensions/router/stream.d.ts.map +1 -1
- package/dist/extensions/router/stream.js +44 -13
- package/dist/extensions/router/stream.js.map +1 -1
- package/dist/extensions/subagent/constants.d.ts +1 -0
- package/dist/extensions/subagent/constants.d.ts.map +1 -1
- package/dist/extensions/subagent/constants.js +1 -0
- package/dist/extensions/subagent/constants.js.map +1 -1
- package/dist/extensions/subagent/index.d.ts.map +1 -1
- package/dist/extensions/subagent/index.js +2 -0
- package/dist/extensions/subagent/index.js.map +1 -1
- package/dist/extensions/subagent/render.d.ts.map +1 -1
- package/dist/extensions/subagent/render.js +121 -48
- package/dist/extensions/subagent/render.js.map +1 -1
- package/dist/extensions/subagent/retry.d.ts +17 -0
- package/dist/extensions/subagent/retry.d.ts.map +1 -0
- package/dist/extensions/subagent/retry.js +41 -0
- package/dist/extensions/subagent/retry.js.map +1 -0
- package/dist/extensions/subagent/runner.d.ts.map +1 -1
- package/dist/extensions/subagent/runner.js +33 -5
- package/dist/extensions/subagent/runner.js.map +1 -1
- package/dist/extensions/subagent/sdk-runner.d.ts +6 -0
- package/dist/extensions/subagent/sdk-runner.d.ts.map +1 -1
- package/dist/extensions/subagent/sdk-runner.js +46 -24
- package/dist/extensions/subagent/sdk-runner.js.map +1 -1
- package/dist/extensions/subagent/types.d.ts +7 -0
- package/dist/extensions/subagent/types.d.ts.map +1 -1
- package/dist/extensions/subagent/types.js.map +1 -1
- package/dist/extensions/todo/index.d.ts.map +1 -1
- package/dist/extensions/todo/index.js +1 -1
- package/dist/extensions/todo/index.js.map +1 -1
- package/dist/extensions/todo/state.d.ts.map +1 -1
- package/dist/extensions/todo/state.js +10 -2
- package/dist/extensions/todo/state.js.map +1 -1
- package/dist/extensions/todo/view.d.ts +1 -1
- package/dist/extensions/todo/view.d.ts.map +1 -1
- package/dist/extensions/todo/view.js +179 -34
- package/dist/extensions/todo/view.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +43 -0
- package/dist/main.js.map +1 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +2 -1
- package/dist/migrations.js.map +1 -1
- package/dist/modes/interactive/components/armin.d.ts.map +1 -1
- package/dist/modes/interactive/components/armin.js +5 -0
- package/dist/modes/interactive/components/armin.js.map +1 -1
- package/dist/modes/interactive/components/bash-execution.d.ts +1 -0
- package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/bash-execution.js +4 -2
- package/dist/modes/interactive/components/bash-execution.js.map +1 -1
- package/dist/modes/interactive/components/countdown-timer.d.ts +13 -6
- package/dist/modes/interactive/components/countdown-timer.d.ts.map +1 -1
- package/dist/modes/interactive/components/countdown-timer.js +54 -17
- package/dist/modes/interactive/components/countdown-timer.js.map +1 -1
- package/dist/modes/interactive/components/extension-selector.d.ts +1 -1
- package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-selector.js +10 -6
- package/dist/modes/interactive/components/extension-selector.js.map +1 -1
- package/dist/modes/interactive/components/keybinding-hints.d.ts +11 -1
- package/dist/modes/interactive/components/keybinding-hints.d.ts.map +1 -1
- package/dist/modes/interactive/components/keybinding-hints.js +56 -0
- package/dist/modes/interactive/components/keybinding-hints.js.map +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +4 -1
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/components/session-selector.d.ts +3 -0
- package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/session-selector.js +52 -2
- package/dist/modes/interactive/components/session-selector.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts +10 -0
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +133 -18
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/components/tool-group.d.ts +1 -0
- package/dist/modes/interactive/components/tool-group.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-group.js +5 -0
- package/dist/modes/interactive/components/tool-group.js.map +1 -1
- package/dist/modes/interactive/components/tree-selector.d.ts +10 -1
- package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/tree-selector.js +55 -2
- package/dist/modes/interactive/components/tree-selector.js.map +1 -1
- package/dist/modes/interactive/external-editor.d.ts.map +1 -1
- package/dist/modes/interactive/external-editor.js +40 -3
- package/dist/modes/interactive/external-editor.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +5 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +92 -24
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +3 -2
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +13 -0
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/utils/paths.d.ts +5 -0
- package/dist/utils/paths.d.ts.map +1 -1
- package/dist/utils/paths.js +7 -0
- package/dist/utils/paths.js.map +1 -1
- package/docs/bundled/README.md +13 -15
- package/docs/bundled/extensions/deepwiki.md +1 -1
- package/docs/bundled/extensions/question.md +7 -5
- package/docs/bundled/extensions/router.md +3 -4
- package/docs/bundled/extensions/statusline.md +0 -1
- package/docs/bundled/extensions/subagent.md +4 -3
- package/docs/bundled/extensions/todo.md +1 -1
- package/docs/bundled/themes.md +1 -1
- package/docs/bundled/tool-presentation.md +17 -16
- package/docs/compaction.md +25 -30
- package/docs/custom-provider.md +298 -583
- package/docs/docs.json +0 -8
- package/docs/extensions.md +132 -121
- package/docs/index.md +18 -74
- package/docs/json.md +20 -14
- package/docs/models.md +193 -151
- package/docs/providers.md +2 -1
- package/docs/quickstart.md +3 -3
- package/docs/rpc.md +166 -124
- package/docs/sdk.md +119 -178
- package/docs/security.md +1 -1
- package/docs/session-format.md +139 -119
- package/docs/skills.md +0 -1
- package/docs/themes.md +2 -2
- package/docs/tui.md +411 -549
- package/docs/usage.md +3 -11
- package/examples/README.md +6 -1
- package/examples/extensions/README.md +13 -33
- package/examples/extensions/custom-provider-anthropic/index.ts +9 -3
- package/examples/extensions/dynamic-resources/dynamic.json +1 -1
- package/examples/extensions/truncated-tool.ts +2 -2
- package/examples/sdk/12-full-control.ts +2 -0
- package/npm-shrinkwrap.json +25 -25
- package/package.json +5 -5
- package/dist/extensions/plan/constants.d.ts +0 -40
- package/dist/extensions/plan/constants.d.ts.map +0 -1
- package/dist/extensions/plan/constants.js +0 -69
- package/dist/extensions/plan/constants.js.map +0 -1
- package/dist/extensions/plan/index.d.ts +0 -3
- package/dist/extensions/plan/index.d.ts.map +0 -1
- package/dist/extensions/plan/index.js +0 -323
- package/dist/extensions/plan/index.js.map +0 -1
- package/dist/extensions/plan/schema.d.ts +0 -15
- package/dist/extensions/plan/schema.d.ts.map +0 -1
- package/dist/extensions/plan/schema.js +0 -13
- package/dist/extensions/plan/schema.js.map +0 -1
- package/dist/extensions/plan/state.d.ts +0 -29
- package/dist/extensions/plan/state.d.ts.map +0 -1
- package/dist/extensions/plan/state.js +0 -74
- package/dist/extensions/plan/state.js.map +0 -1
- package/dist/extensions/plan/storage.d.ts +0 -18
- package/dist/extensions/plan/storage.d.ts.map +0 -1
- package/dist/extensions/plan/storage.js +0 -58
- package/dist/extensions/plan/storage.js.map +0 -1
- package/dist/extensions/plan/view.d.ts +0 -36
- package/dist/extensions/plan/view.d.ts.map +0 -1
- package/dist/extensions/plan/view.js +0 -74
- package/dist/extensions/plan/view.js.map +0 -1
- package/docs/bundled/extensions/README.md +0 -19
- package/docs/bundled/extensions/plan.md +0 -33
- package/examples/extensions/plan-mode/README.md +0 -66
- package/examples/extensions/plan-mode/index.ts +0 -390
- package/examples/extensions/plan-mode/utils.ts +0 -168
|
@@ -1,323 +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) 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";
|
|
21
|
-
import { ExitPlanParamsSchema } from "./schema.js";
|
|
22
|
-
import { clonePlanState, disposePlanSession, EMPTY_PLAN_STATE, getPlanState, replacePlanState, replayPlanFromBranch, setActivePlanSession, } from "./state.js";
|
|
23
|
-
import { savePlanFile } from "./storage.js";
|
|
24
|
-
import { formatApprovalSubtitle, formatExitPlanCall, formatExitPlanResult } from "./view.js";
|
|
25
|
-
function buildKickoffMessage(pending) {
|
|
26
|
-
const truncated = pending.plan.length > PLAN_EMBED_MAX_CHARS;
|
|
27
|
-
const body = truncated
|
|
28
|
-
? `${pending.plan.slice(0, PLAN_EMBED_MAX_CHARS)}\n\n[Plan truncated — read the full plan at ${pending.planPath}]`
|
|
29
|
-
: pending.plan;
|
|
30
|
-
return `Execute the approved plan "${pending.title}" (saved at ${pending.planPath}).
|
|
31
|
-
|
|
32
|
-
${body}
|
|
33
|
-
|
|
34
|
-
Start now. Track multi-step work with the todo tool, and report back instead of improvising if the plan turns out to need revision.`;
|
|
35
|
-
}
|
|
36
|
-
export default function plan(pi) {
|
|
37
|
-
let menuOpen = false;
|
|
38
|
-
let pendingCompact;
|
|
39
|
-
pi.registerFlag(PLAN_FLAG_NAME, {
|
|
40
|
-
description: "Start in plan mode (read-only planning)",
|
|
41
|
-
type: "boolean",
|
|
42
|
-
default: false,
|
|
43
|
-
});
|
|
44
|
-
/** Remove blocked tools, activate exit_plan, and return what was actually removed. */
|
|
45
|
-
function applyPlanTools() {
|
|
46
|
-
const active = pi.getActiveTools();
|
|
47
|
-
const removed = active.filter((name) => PLAN_BLOCKED_TOOLS.has(name));
|
|
48
|
-
pi.setActiveTools([...new Set([...active.filter((name) => !PLAN_BLOCKED_TOOLS.has(name)), EXIT_PLAN_TOOL_NAME])]);
|
|
49
|
-
return removed;
|
|
50
|
-
}
|
|
51
|
-
/** Inverse of applyPlanTools: drop exit_plan, re-add exactly what was removed. */
|
|
52
|
-
function restoreTools(removedTools) {
|
|
53
|
-
const active = pi.getActiveTools().filter((name) => name !== EXIT_PLAN_TOOL_NAME);
|
|
54
|
-
pi.setActiveTools([...new Set([...active, ...removedTools])]);
|
|
55
|
-
}
|
|
56
|
-
function persist() {
|
|
57
|
-
pi.appendEntry(PLAN_ENTRY_TYPE, getPlanState());
|
|
58
|
-
}
|
|
59
|
-
function updateStatus(ctx) {
|
|
60
|
-
const state = getPlanState();
|
|
61
|
-
ctx.ui.setStatus(PLAN_STATUS_KEY, state.planning ? ctx.ui.theme.fg("accent", "Plan Mode") : undefined);
|
|
62
|
-
}
|
|
63
|
-
function enterPlanMode(ctx) {
|
|
64
|
-
const state = getPlanState();
|
|
65
|
-
if (state.planning)
|
|
66
|
-
return;
|
|
67
|
-
const removedTools = applyPlanTools();
|
|
68
|
-
replacePlanState({ planning: true, removedTools, awaitingCompact: false, planFiles: state.planFiles });
|
|
69
|
-
persist();
|
|
70
|
-
updateStatus(ctx);
|
|
71
|
-
ctx.ui.notify("Plan mode enabled: edit, write, bash, and subagent are disabled.", "info");
|
|
72
|
-
}
|
|
73
|
-
function exitPlanMode(ctx, notifyText) {
|
|
74
|
-
const state = getPlanState();
|
|
75
|
-
restoreTools(state.removedTools);
|
|
76
|
-
replacePlanState({ ...state, planning: false, awaitingCompact: false, removedTools: [] });
|
|
77
|
-
persist();
|
|
78
|
-
updateStatus(ctx);
|
|
79
|
-
if (notifyText)
|
|
80
|
-
ctx.ui.notify(notifyText, "info");
|
|
81
|
-
}
|
|
82
|
-
/** Save the approved plan, index it in state, and name the session if unnamed. */
|
|
83
|
-
function saveApprovedPlan(ctx, params, title) {
|
|
84
|
-
const planPath = savePlanFile({
|
|
85
|
-
sessionId: ctx.sessionManager.getSessionId(),
|
|
86
|
-
title,
|
|
87
|
-
plan: params.plan,
|
|
88
|
-
cwd: ctx.cwd,
|
|
89
|
-
revises: params.revises,
|
|
90
|
-
});
|
|
91
|
-
const state = getPlanState();
|
|
92
|
-
replacePlanState({ ...state, planFiles: [...state.planFiles, planPath] });
|
|
93
|
-
if (!pi.getSessionName())
|
|
94
|
-
pi.setSessionName(title);
|
|
95
|
-
return planPath;
|
|
96
|
-
}
|
|
97
|
-
/**
|
|
98
|
-
* Rebuild state from the current branch and reconcile the live tool set.
|
|
99
|
-
* An interrupted compact-and-execute approval (restart, resume, branch
|
|
100
|
-
* switch) degrades to the keep-context exit so the approval is honored.
|
|
101
|
-
*/
|
|
102
|
-
function syncFromBranch(ctx, options) {
|
|
103
|
-
setActivePlanSession(ctx.sessionManager.getSessionId());
|
|
104
|
-
const before = getPlanState();
|
|
105
|
-
const replayed = replayPlanFromBranch(ctx);
|
|
106
|
-
const next = replayed ?? clonePlanState(EMPTY_PLAN_STATE);
|
|
107
|
-
if (!replayed && options?.applyFlag && pi.getFlag(PLAN_FLAG_NAME) === true) {
|
|
108
|
-
next.planning = true;
|
|
109
|
-
}
|
|
110
|
-
let interruptedCompact = false;
|
|
111
|
-
if (next.awaitingCompact) {
|
|
112
|
-
next.planning = false;
|
|
113
|
-
next.awaitingCompact = false;
|
|
114
|
-
interruptedCompact = true;
|
|
115
|
-
pendingCompact = undefined;
|
|
116
|
-
}
|
|
117
|
-
if (next.planning) {
|
|
118
|
-
// The fresh removal set is authoritative for the eventual restore:
|
|
119
|
-
// the live tool set may differ from what the entry recorded.
|
|
120
|
-
next.removedTools = applyPlanTools();
|
|
121
|
-
}
|
|
122
|
-
else {
|
|
123
|
-
if (pi.getActiveTools().includes(EXIT_PLAN_TOOL_NAME)) {
|
|
124
|
-
restoreTools(before.planning ? before.removedTools : next.removedTools);
|
|
125
|
-
}
|
|
126
|
-
next.removedTools = [];
|
|
127
|
-
}
|
|
128
|
-
replacePlanState(next);
|
|
129
|
-
if (interruptedCompact) {
|
|
130
|
-
persist();
|
|
131
|
-
ctx.ui.notify("Pending compaction was interrupted; plan mode exited with context kept.", "warning");
|
|
132
|
-
}
|
|
133
|
-
updateStatus(ctx);
|
|
134
|
-
}
|
|
135
|
-
pi.registerTool({
|
|
136
|
-
name: EXIT_PLAN_TOOL_NAME,
|
|
137
|
-
label: EXIT_PLAN_TOOL_LABEL,
|
|
138
|
-
description: EXIT_PLAN_TOOL_DESCRIPTION,
|
|
139
|
-
promptSnippet: EXIT_PLAN_PROMPT_SNIPPET,
|
|
140
|
-
promptGuidelines: EXIT_PLAN_PROMPT_GUIDELINES,
|
|
141
|
-
parameters: ExitPlanParamsSchema,
|
|
142
|
-
executionMode: "sequential",
|
|
143
|
-
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
144
|
-
setActivePlanSession(ctx.sessionManager.getSessionId());
|
|
145
|
-
const state = getPlanState();
|
|
146
|
-
const title = params.title.trim() || "plan";
|
|
147
|
-
if (!state.planning || state.awaitingCompact) {
|
|
148
|
-
return {
|
|
149
|
-
content: [
|
|
150
|
-
{ type: "text", text: "Plan mode is not awaiting an exit; continue with your current instructions." },
|
|
151
|
-
],
|
|
152
|
-
details: { decision: "cancelled", title },
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
if (!ctx.hasUI) {
|
|
156
|
-
// json/print: no way to ask, so honor the least surprising default —
|
|
157
|
-
// exit with context kept. Documented as a headless-mode semantic.
|
|
158
|
-
const planPath = saveApprovedPlan(ctx, params, title);
|
|
159
|
-
exitPlanMode(ctx);
|
|
160
|
-
return {
|
|
161
|
-
content: [
|
|
162
|
-
{
|
|
163
|
-
type: "text",
|
|
164
|
-
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.`,
|
|
165
|
-
},
|
|
166
|
-
],
|
|
167
|
-
details: { decision: "execute", title, planPath },
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
let choice;
|
|
171
|
-
menuOpen = true;
|
|
172
|
-
try {
|
|
173
|
-
choice = await ctx.ui.select(`Approve plan "${title}"?`, EXIT_PLAN_MENU_OPTIONS, {
|
|
174
|
-
signal,
|
|
175
|
-
subtitle: formatApprovalSubtitle(ctx),
|
|
176
|
-
cancelHint: EXIT_PLAN_CANCEL_HINT,
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
finally {
|
|
180
|
-
menuOpen = false;
|
|
181
|
-
}
|
|
182
|
-
if (signal?.aborted) {
|
|
183
|
-
// Throwing lets Pi mark the result as an error, matching the todo convention.
|
|
184
|
-
throw new Error("Plan approval was interrupted; plan mode is still active.");
|
|
185
|
-
}
|
|
186
|
-
if (choice === MENU_EXECUTE) {
|
|
187
|
-
const planPath = saveApprovedPlan(ctx, params, title);
|
|
188
|
-
exitPlanMode(ctx, "Plan approved: executing with full context.");
|
|
189
|
-
return {
|
|
190
|
-
content: [
|
|
191
|
-
{
|
|
192
|
-
type: "text",
|
|
193
|
-
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.`,
|
|
194
|
-
},
|
|
195
|
-
],
|
|
196
|
-
details: { decision: "execute", title, planPath },
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
if (choice === MENU_COMPACT_EXECUTE) {
|
|
200
|
-
const planPath = saveApprovedPlan(ctx, params, title);
|
|
201
|
-
// Stay in plan mode (tools restricted) until compaction completes;
|
|
202
|
-
// terminate ends the run so agent_settled can start the compaction.
|
|
203
|
-
replacePlanState({ ...getPlanState(), awaitingCompact: true });
|
|
204
|
-
persist();
|
|
205
|
-
pendingCompact = { title, plan: params.plan, planPath };
|
|
206
|
-
return {
|
|
207
|
-
content: [
|
|
208
|
-
{
|
|
209
|
-
type: "text",
|
|
210
|
-
text: `Plan approved and saved to ${planPath}. The context will now be compacted; stop here — execution restarts automatically after compaction.`,
|
|
211
|
-
},
|
|
212
|
-
],
|
|
213
|
-
details: { decision: "compactAndExecute", title, planPath },
|
|
214
|
-
terminate: true,
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
// Explicit "Keep planning", Esc, or timeout: the only side-effect-free
|
|
218
|
-
// choice, so any dismissal maps here.
|
|
219
|
-
let feedback;
|
|
220
|
-
if (choice === MENU_KEEP_PLANNING) {
|
|
221
|
-
feedback =
|
|
222
|
-
ctx.mode === "tui"
|
|
223
|
-
? await ctx.ui.editor("Feedback for the plan (optional):", "")
|
|
224
|
-
: await ctx.ui.input("Feedback for the plan (optional):");
|
|
225
|
-
}
|
|
226
|
-
const trimmedFeedback = feedback?.trim();
|
|
227
|
-
return {
|
|
228
|
-
content: [
|
|
229
|
-
{
|
|
230
|
-
type: "text",
|
|
231
|
-
text: trimmedFeedback
|
|
232
|
-
? `The user wants to keep planning. Feedback:\n${trimmedFeedback}`
|
|
233
|
-
: "The user wants to keep planning. Ask what should change, or keep refining the open points.",
|
|
234
|
-
},
|
|
235
|
-
],
|
|
236
|
-
details: { decision: "keepPlanning", title },
|
|
237
|
-
};
|
|
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
|
-
},
|
|
249
|
-
});
|
|
250
|
-
pi.registerCommand(PLAN_COMMAND_NAME, {
|
|
251
|
-
description: "Toggle plan mode (read-only planning with user-approved exit)",
|
|
252
|
-
handler: async (_args, ctx) => {
|
|
253
|
-
setActivePlanSession(ctx.sessionManager.getSessionId());
|
|
254
|
-
const state = getPlanState();
|
|
255
|
-
if (menuOpen) {
|
|
256
|
-
ctx.ui.notify("The exit_plan approval menu is open — answer it first.", "warning");
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
if (state.awaitingCompact) {
|
|
260
|
-
ctx.ui.notify("Waiting for compaction to finish before leaving plan mode.", "warning");
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
if (state.planning) {
|
|
264
|
-
exitPlanMode(ctx, "Plan mode disabled: full tool access restored.");
|
|
265
|
-
}
|
|
266
|
-
else {
|
|
267
|
-
enterPlanMode(ctx);
|
|
268
|
-
}
|
|
269
|
-
},
|
|
270
|
-
});
|
|
271
|
-
// Defense in depth: the active-tools change lands on the next LLM request,
|
|
272
|
-
// so a request already in flight when plan mode toggled can still carry
|
|
273
|
-
// blocked tool calls.
|
|
274
|
-
pi.on("tool_call", async (event, ctx) => {
|
|
275
|
-
setActivePlanSession(ctx.sessionManager.getSessionId());
|
|
276
|
-
const state = getPlanState();
|
|
277
|
-
if (!state.planning || !PLAN_BLOCKED_TOOLS.has(event.toolName))
|
|
278
|
-
return;
|
|
279
|
-
return {
|
|
280
|
-
block: true,
|
|
281
|
-
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.`,
|
|
282
|
-
};
|
|
283
|
-
});
|
|
284
|
-
pi.on("before_agent_start", async (event, ctx) => {
|
|
285
|
-
setActivePlanSession(ctx.sessionManager.getSessionId());
|
|
286
|
-
const state = getPlanState();
|
|
287
|
-
if (!state.planning)
|
|
288
|
-
return;
|
|
289
|
-
return { systemPrompt: `${event.systemPrompt}\n\n${buildPlanModePrompt(state.planFiles)}` };
|
|
290
|
-
});
|
|
291
|
-
// agent_settled fires once per fully settled run (agent_end can repeat on
|
|
292
|
-
// retries and queued continuations), so the approved compaction starts here.
|
|
293
|
-
pi.on("agent_settled", async (_event, ctx) => {
|
|
294
|
-
setActivePlanSession(ctx.sessionManager.getSessionId());
|
|
295
|
-
if (!getPlanState().awaitingCompact || !pendingCompact)
|
|
296
|
-
return;
|
|
297
|
-
const pending = pendingCompact;
|
|
298
|
-
pendingCompact = undefined;
|
|
299
|
-
ctx.compact({
|
|
300
|
-
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.`,
|
|
301
|
-
onComplete: () => {
|
|
302
|
-
exitPlanMode(ctx, "Context compacted; executing the plan.");
|
|
303
|
-
pi.sendUserMessage(buildKickoffMessage(pending));
|
|
304
|
-
},
|
|
305
|
-
onError: (error) => {
|
|
306
|
-
// Never leave the user stuck read-only: degrade to keep-context execution.
|
|
307
|
-
exitPlanMode(ctx, `Compaction failed (${error.message}); executing with full context instead.`);
|
|
308
|
-
pi.sendUserMessage(buildKickoffMessage(pending));
|
|
309
|
-
},
|
|
310
|
-
});
|
|
311
|
-
});
|
|
312
|
-
pi.on("session_start", async (_event, ctx) => {
|
|
313
|
-
syncFromBranch(ctx, { applyFlag: true });
|
|
314
|
-
});
|
|
315
|
-
pi.on("session_tree", async (_event, ctx) => {
|
|
316
|
-
syncFromBranch(ctx);
|
|
317
|
-
});
|
|
318
|
-
pi.on("session_shutdown", async (_event, ctx) => {
|
|
319
|
-
pendingCompact = undefined;
|
|
320
|
-
disposePlanSession(ctx.sessionManager.getSessionId());
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
//# 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) 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,15 +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
|
-
//# 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","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"]}
|
|
@@ -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"]}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
export interface PlanSessionState {
|
|
2
|
-
/** True from entry until the exit is fully applied (including a pending compaction). */
|
|
3
|
-
planning: boolean;
|
|
4
|
-
/** Tools actually removed on entry; restored verbatim on exit. */
|
|
5
|
-
removedTools: string[];
|
|
6
|
-
/** A compact-and-execute exit was approved and is waiting for agent_settled + compaction. */
|
|
7
|
-
awaitingCompact: boolean;
|
|
8
|
-
/** Absolute paths of plan files saved on this conversation branch. */
|
|
9
|
-
planFiles: string[];
|
|
10
|
-
}
|
|
11
|
-
export declare const EMPTY_PLAN_STATE: PlanSessionState;
|
|
12
|
-
/** Point the module at a session's state bucket. Call wherever ctx is in hand. */
|
|
13
|
-
export declare function setActivePlanSession(sid: string): void;
|
|
14
|
-
export declare function getPlanState(): PlanSessionState;
|
|
15
|
-
export declare function replacePlanState(next: PlanSessionState): void;
|
|
16
|
-
/** Drop in-memory state for a session (call from session_shutdown). */
|
|
17
|
-
export declare function disposePlanSession(sid: string): void;
|
|
18
|
-
export declare function clonePlanState(source: PlanSessionState): PlanSessionState;
|
|
19
|
-
export declare function isPlanState(value: unknown): value is PlanSessionState;
|
|
20
|
-
/**
|
|
21
|
-
* Latest plan-mode entry on the current branch, or undefined when the branch
|
|
22
|
-
* has none (lets callers distinguish "never used" from "explicitly exited").
|
|
23
|
-
*/
|
|
24
|
-
export declare function replayPlanFromBranch(ctx: {
|
|
25
|
-
sessionManager: {
|
|
26
|
-
getBranch(): Iterable<unknown>;
|
|
27
|
-
};
|
|
28
|
-
}): PlanSessionState | undefined;
|
|
29
|
-
//# sourceMappingURL=state.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../../src/extensions/plan/state.ts"],"names":[],"mappings":"AAeA,MAAM,WAAW,gBAAgB;IAChC,wFAAwF;IACxF,QAAQ,EAAE,OAAO,CAAC;IAClB,kEAAkE;IAClE,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,6FAA6F;IAC7F,eAAe,EAAE,OAAO,CAAC;IACzB,sEAAsE;IACtE,SAAS,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,eAAO,MAAM,gBAAgB,EAAE,gBAK9B,CAAC;AAKF,kFAAkF;AAClF,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAEtD;AAED,wBAAgB,YAAY,IAAI,gBAAgB,CAE/C;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,gBAAgB,GAAG,IAAI,CAE7D;AAED,uEAAuE;AACvE,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAIpD;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,gBAAgB,GAAG,gBAAgB,CAOzE;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,gBAAgB,CASrE;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE;IACzC,cAAc,EAAE;QAAE,SAAS,IAAI,QAAQ,CAAC,OAAO,CAAC,CAAA;KAAE,CAAC;CACnD,GAAG,gBAAgB,GAAG,SAAS,CAS/B","sourcesContent":["/**\n * plan/state.ts — per-session plan-mode state, persisted as custom entries.\n *\n * State is conversation-backed like the todo extension: every transition is\n * appended as a `plan-mode` custom entry (never sent to the LLM), and lifecycle\n * handlers replay the current branch tail → head to restore. Custom entries are\n * copied on fork/clone, so `planFiles` keeps pointing at the right plan files\n * even though the forked session gets a new session id.\n *\n * State is keyed per session id: one Pi process can host several sessions over\n * its lifetime (resume, /tree switches), so callers re-point the active bucket\n * wherever ctx is in hand.\n */\nimport { PLAN_ENTRY_TYPE } from \"./constants.ts\";\n\nexport interface PlanSessionState {\n\t/** True from entry until the exit is fully applied (including a pending compaction). */\n\tplanning: boolean;\n\t/** Tools actually removed on entry; restored verbatim on exit. */\n\tremovedTools: string[];\n\t/** A compact-and-execute exit was approved and is waiting for agent_settled + compaction. */\n\tawaitingCompact: boolean;\n\t/** Absolute paths of plan files saved on this conversation branch. */\n\tplanFiles: string[];\n}\n\nexport const EMPTY_PLAN_STATE: PlanSessionState = {\n\tplanning: false,\n\tremovedTools: [],\n\tawaitingCompact: false,\n\tplanFiles: [],\n};\n\nconst states = new Map<string, PlanSessionState>();\nlet activeSid = \"\";\n\n/** Point the module at a session's state bucket. Call wherever ctx is in hand. */\nexport function setActivePlanSession(sid: string): void {\n\tactiveSid = sid;\n}\n\nexport function getPlanState(): PlanSessionState {\n\treturn states.get(activeSid) ?? clonePlanState(EMPTY_PLAN_STATE);\n}\n\nexport function replacePlanState(next: PlanSessionState): void {\n\tstates.set(activeSid, clonePlanState(next));\n}\n\n/** Drop in-memory state for a session (call from session_shutdown). */\nexport function disposePlanSession(sid: string): void {\n\tif (!sid) return;\n\tstates.delete(sid);\n\tif (activeSid === sid) activeSid = \"\";\n}\n\nexport function clonePlanState(source: PlanSessionState): PlanSessionState {\n\treturn {\n\t\tplanning: source.planning,\n\t\tremovedTools: [...source.removedTools],\n\t\tawaitingCompact: source.awaitingCompact,\n\t\tplanFiles: [...source.planFiles],\n\t};\n}\n\nexport function isPlanState(value: unknown): value is PlanSessionState {\n\tif (!value || typeof value !== \"object\") return false;\n\tconst record = value as Record<string, unknown>;\n\treturn (\n\t\ttypeof record.planning === \"boolean\" &&\n\t\ttypeof record.awaitingCompact === \"boolean\" &&\n\t\tArray.isArray(record.removedTools) &&\n\t\tArray.isArray(record.planFiles)\n\t);\n}\n\n/**\n * Latest plan-mode entry on the current branch, or undefined when the branch\n * has none (lets callers distinguish \"never used\" from \"explicitly exited\").\n */\nexport function replayPlanFromBranch(ctx: {\n\tsessionManager: { getBranch(): Iterable<unknown> };\n}): PlanSessionState | undefined {\n\tconst branch = Array.from(ctx.sessionManager.getBranch());\n\tfor (let i = branch.length - 1; i >= 0; i--) {\n\t\tconst item = branch[i] as { type?: string; customType?: string; data?: unknown };\n\t\tif (item.type !== \"custom\" || item.customType !== PLAN_ENTRY_TYPE) continue;\n\t\tif (!isPlanState(item.data)) continue;\n\t\treturn clonePlanState(item.data);\n\t}\n\treturn undefined;\n}\n"]}
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* plan/state.ts — per-session plan-mode state, persisted as custom entries.
|
|
3
|
-
*
|
|
4
|
-
* State is conversation-backed like the todo extension: every transition is
|
|
5
|
-
* appended as a `plan-mode` custom entry (never sent to the LLM), and lifecycle
|
|
6
|
-
* handlers replay the current branch tail → head to restore. Custom entries are
|
|
7
|
-
* copied on fork/clone, so `planFiles` keeps pointing at the right plan files
|
|
8
|
-
* even though the forked session gets a new session id.
|
|
9
|
-
*
|
|
10
|
-
* State is keyed per session id: one Pi process can host several sessions over
|
|
11
|
-
* its lifetime (resume, /tree switches), so callers re-point the active bucket
|
|
12
|
-
* wherever ctx is in hand.
|
|
13
|
-
*/
|
|
14
|
-
import { PLAN_ENTRY_TYPE } from "./constants.js";
|
|
15
|
-
export const EMPTY_PLAN_STATE = {
|
|
16
|
-
planning: false,
|
|
17
|
-
removedTools: [],
|
|
18
|
-
awaitingCompact: false,
|
|
19
|
-
planFiles: [],
|
|
20
|
-
};
|
|
21
|
-
const states = new Map();
|
|
22
|
-
let activeSid = "";
|
|
23
|
-
/** Point the module at a session's state bucket. Call wherever ctx is in hand. */
|
|
24
|
-
export function setActivePlanSession(sid) {
|
|
25
|
-
activeSid = sid;
|
|
26
|
-
}
|
|
27
|
-
export function getPlanState() {
|
|
28
|
-
return states.get(activeSid) ?? clonePlanState(EMPTY_PLAN_STATE);
|
|
29
|
-
}
|
|
30
|
-
export function replacePlanState(next) {
|
|
31
|
-
states.set(activeSid, clonePlanState(next));
|
|
32
|
-
}
|
|
33
|
-
/** Drop in-memory state for a session (call from session_shutdown). */
|
|
34
|
-
export function disposePlanSession(sid) {
|
|
35
|
-
if (!sid)
|
|
36
|
-
return;
|
|
37
|
-
states.delete(sid);
|
|
38
|
-
if (activeSid === sid)
|
|
39
|
-
activeSid = "";
|
|
40
|
-
}
|
|
41
|
-
export function clonePlanState(source) {
|
|
42
|
-
return {
|
|
43
|
-
planning: source.planning,
|
|
44
|
-
removedTools: [...source.removedTools],
|
|
45
|
-
awaitingCompact: source.awaitingCompact,
|
|
46
|
-
planFiles: [...source.planFiles],
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
export function isPlanState(value) {
|
|
50
|
-
if (!value || typeof value !== "object")
|
|
51
|
-
return false;
|
|
52
|
-
const record = value;
|
|
53
|
-
return (typeof record.planning === "boolean" &&
|
|
54
|
-
typeof record.awaitingCompact === "boolean" &&
|
|
55
|
-
Array.isArray(record.removedTools) &&
|
|
56
|
-
Array.isArray(record.planFiles));
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* Latest plan-mode entry on the current branch, or undefined when the branch
|
|
60
|
-
* has none (lets callers distinguish "never used" from "explicitly exited").
|
|
61
|
-
*/
|
|
62
|
-
export function replayPlanFromBranch(ctx) {
|
|
63
|
-
const branch = Array.from(ctx.sessionManager.getBranch());
|
|
64
|
-
for (let i = branch.length - 1; i >= 0; i--) {
|
|
65
|
-
const item = branch[i];
|
|
66
|
-
if (item.type !== "custom" || item.customType !== PLAN_ENTRY_TYPE)
|
|
67
|
-
continue;
|
|
68
|
-
if (!isPlanState(item.data))
|
|
69
|
-
continue;
|
|
70
|
-
return clonePlanState(item.data);
|
|
71
|
-
}
|
|
72
|
-
return undefined;
|
|
73
|
-
}
|
|
74
|
-
//# sourceMappingURL=state.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"state.js","sourceRoot":"","sources":["../../../src/extensions/plan/state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAajD,MAAM,CAAC,MAAM,gBAAgB,GAAqB;IACjD,QAAQ,EAAE,KAAK;IACf,YAAY,EAAE,EAAE;IAChB,eAAe,EAAE,KAAK;IACtB,SAAS,EAAE,EAAE;CACb,CAAC;AAEF,MAAM,MAAM,GAAG,IAAI,GAAG,EAA4B,CAAC;AACnD,IAAI,SAAS,GAAG,EAAE,CAAC;AAEnB,kFAAkF;AAClF,MAAM,UAAU,oBAAoB,CAAC,GAAW,EAAQ;IACvD,SAAS,GAAG,GAAG,CAAC;AAAA,CAChB;AAED,MAAM,UAAU,YAAY,GAAqB;IAChD,OAAO,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,cAAc,CAAC,gBAAgB,CAAC,CAAC;AAAA,CACjE;AAED,MAAM,UAAU,gBAAgB,CAAC,IAAsB,EAAQ;IAC9D,MAAM,CAAC,GAAG,CAAC,SAAS,EAAE,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC;AAAA,CAC5C;AAED,uEAAuE;AACvE,MAAM,UAAU,kBAAkB,CAAC,GAAW,EAAQ;IACrD,IAAI,CAAC,GAAG;QAAE,OAAO;IACjB,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACnB,IAAI,SAAS,KAAK,GAAG;QAAE,SAAS,GAAG,EAAE,CAAC;AAAA,CACtC;AAED,MAAM,UAAU,cAAc,CAAC,MAAwB,EAAoB;IAC1E,OAAO;QACN,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,YAAY,EAAE,CAAC,GAAG,MAAM,CAAC,YAAY,CAAC;QACtC,eAAe,EAAE,MAAM,CAAC,eAAe;QACvC,SAAS,EAAE,CAAC,GAAG,MAAM,CAAC,SAAS,CAAC;KAChC,CAAC;AAAA,CACF;AAED,MAAM,UAAU,WAAW,CAAC,KAAc,EAA6B;IACtE,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IACtD,MAAM,MAAM,GAAG,KAAgC,CAAC;IAChD,OAAO,CACN,OAAO,MAAM,CAAC,QAAQ,KAAK,SAAS;QACpC,OAAO,MAAM,CAAC,eAAe,KAAK,SAAS;QAC3C,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,YAAY,CAAC;QAClC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,CAC/B,CAAC;AAAA,CACF;AAED;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAAC,GAEpC,EAAgC;IAChC,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,cAAc,CAAC,SAAS,EAAE,CAAC,CAAC;IAC1D,KAAK,IAAI,CAAC,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC7C,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,CAA2D,CAAC;QACjF,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,UAAU,KAAK,eAAe;YAAE,SAAS;QAC5E,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,SAAS;QACtC,OAAO,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAClC,CAAC;IACD,OAAO,SAAS,CAAC;AAAA,CACjB","sourcesContent":["/**\n * plan/state.ts — per-session plan-mode state, persisted as custom entries.\n *\n * State is conversation-backed like the todo extension: every transition is\n * appended as a `plan-mode` custom entry (never sent to the LLM), and lifecycle\n * handlers replay the current branch tail → head to restore. Custom entries are\n * copied on fork/clone, so `planFiles` keeps pointing at the right plan files\n * even though the forked session gets a new session id.\n *\n * State is keyed per session id: one Pi process can host several sessions over\n * its lifetime (resume, /tree switches), so callers re-point the active bucket\n * wherever ctx is in hand.\n */\nimport { PLAN_ENTRY_TYPE } from \"./constants.ts\";\n\nexport interface PlanSessionState {\n\t/** True from entry until the exit is fully applied (including a pending compaction). */\n\tplanning: boolean;\n\t/** Tools actually removed on entry; restored verbatim on exit. */\n\tremovedTools: string[];\n\t/** A compact-and-execute exit was approved and is waiting for agent_settled + compaction. */\n\tawaitingCompact: boolean;\n\t/** Absolute paths of plan files saved on this conversation branch. */\n\tplanFiles: string[];\n}\n\nexport const EMPTY_PLAN_STATE: PlanSessionState = {\n\tplanning: false,\n\tremovedTools: [],\n\tawaitingCompact: false,\n\tplanFiles: [],\n};\n\nconst states = new Map<string, PlanSessionState>();\nlet activeSid = \"\";\n\n/** Point the module at a session's state bucket. Call wherever ctx is in hand. */\nexport function setActivePlanSession(sid: string): void {\n\tactiveSid = sid;\n}\n\nexport function getPlanState(): PlanSessionState {\n\treturn states.get(activeSid) ?? clonePlanState(EMPTY_PLAN_STATE);\n}\n\nexport function replacePlanState(next: PlanSessionState): void {\n\tstates.set(activeSid, clonePlanState(next));\n}\n\n/** Drop in-memory state for a session (call from session_shutdown). */\nexport function disposePlanSession(sid: string): void {\n\tif (!sid) return;\n\tstates.delete(sid);\n\tif (activeSid === sid) activeSid = \"\";\n}\n\nexport function clonePlanState(source: PlanSessionState): PlanSessionState {\n\treturn {\n\t\tplanning: source.planning,\n\t\tremovedTools: [...source.removedTools],\n\t\tawaitingCompact: source.awaitingCompact,\n\t\tplanFiles: [...source.planFiles],\n\t};\n}\n\nexport function isPlanState(value: unknown): value is PlanSessionState {\n\tif (!value || typeof value !== \"object\") return false;\n\tconst record = value as Record<string, unknown>;\n\treturn (\n\t\ttypeof record.planning === \"boolean\" &&\n\t\ttypeof record.awaitingCompact === \"boolean\" &&\n\t\tArray.isArray(record.removedTools) &&\n\t\tArray.isArray(record.planFiles)\n\t);\n}\n\n/**\n * Latest plan-mode entry on the current branch, or undefined when the branch\n * has none (lets callers distinguish \"never used\" from \"explicitly exited\").\n */\nexport function replayPlanFromBranch(ctx: {\n\tsessionManager: { getBranch(): Iterable<unknown> };\n}): PlanSessionState | undefined {\n\tconst branch = Array.from(ctx.sessionManager.getBranch());\n\tfor (let i = branch.length - 1; i >= 0; i--) {\n\t\tconst item = branch[i] as { type?: string; customType?: string; data?: unknown };\n\t\tif (item.type !== \"custom\" || item.customType !== PLAN_ENTRY_TYPE) continue;\n\t\tif (!isPlanState(item.data)) continue;\n\t\treturn clonePlanState(item.data);\n\t}\n\treturn undefined;\n}\n"]}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
export interface SavePlanOptions {
|
|
2
|
-
sessionId: string;
|
|
3
|
-
title: string;
|
|
4
|
-
plan: string;
|
|
5
|
-
cwd: string;
|
|
6
|
-
revises?: string;
|
|
7
|
-
}
|
|
8
|
-
export declare function getPlansDir(sessionId: string): string;
|
|
9
|
-
/** File-name slug from a plan title: lowercase, ascii-ish, bounded. */
|
|
10
|
-
export declare function slugifyTitle(title: string): string;
|
|
11
|
-
/** Next NN based on existing `NN-*.md` names; gaps never reuse numbers. */
|
|
12
|
-
export declare function nextPlanNumber(existingNames: string[]): number;
|
|
13
|
-
export declare function buildPlanDocument(options: SavePlanOptions & {
|
|
14
|
-
createdIso: string;
|
|
15
|
-
}): string;
|
|
16
|
-
/** Write the plan to disk and return its absolute path. */
|
|
17
|
-
export declare function savePlanFile(options: SavePlanOptions): string;
|
|
18
|
-
//# sourceMappingURL=storage.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../../src/extensions/plan/storage.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,eAAe;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAErD;AAED,uEAAuE;AACvE,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQlD;AAED,2EAA2E;AAC3E,wBAAgB,cAAc,CAAC,aAAa,EAAE,MAAM,EAAE,GAAG,MAAM,CAO9D;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,eAAe,GAAG;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAW3F;AAED,2DAA2D;AAC3D,wBAAgB,YAAY,CAAC,OAAO,EAAE,eAAe,GAAG,MAAM,CAQ7D","sourcesContent":["/**\n * plan/storage.ts — plan file layout and writing.\n *\n * Plans live under `<agentDir>/plans/<sessionId>/NN-<slug>.md`, append-only\n * with a monotonically increasing NN. The directory is only the initial\n * storage location — the authoritative index is the `planFiles` list in the\n * session's plan-mode entries, which follows fork/clone.\n */\nimport { mkdirSync, readdirSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { getAgentDir } from \"../../config.ts\";\n\nexport interface SavePlanOptions {\n\tsessionId: string;\n\ttitle: string;\n\tplan: string;\n\tcwd: string;\n\trevises?: string;\n}\n\nexport function getPlansDir(sessionId: string): string {\n\treturn join(getAgentDir(), \"plans\", sessionId);\n}\n\n/** File-name slug from a plan title: lowercase, ascii-ish, bounded. */\nexport function slugifyTitle(title: string): string {\n\tconst slug = title\n\t\t.toLowerCase()\n\t\t.replace(/[^a-z0-9]+/g, \"-\")\n\t\t.replace(/^-+|-+$/g, \"\")\n\t\t.slice(0, 40)\n\t\t.replace(/-+$/, \"\");\n\treturn slug || \"plan\";\n}\n\n/** Next NN based on existing `NN-*.md` names; gaps never reuse numbers. */\nexport function nextPlanNumber(existingNames: string[]): number {\n\tlet max = 0;\n\tfor (const name of existingNames) {\n\t\tconst match = /^(\\d+)-/.exec(name);\n\t\tif (match) max = Math.max(max, Number(match[1]));\n\t}\n\treturn max + 1;\n}\n\nexport function buildPlanDocument(options: SavePlanOptions & { createdIso: string }): string {\n\tconst lines = [\n\t\t\"---\",\n\t\t`title: ${JSON.stringify(options.title)}`,\n\t\t`created: ${options.createdIso}`,\n\t\t`cwd: ${JSON.stringify(options.cwd)}`,\n\t\t`session: ${options.sessionId}`,\n\t];\n\tif (options.revises) lines.push(`revises: ${JSON.stringify(options.revises)}`);\n\tlines.push(\"---\", \"\", options.plan.trimEnd(), \"\");\n\treturn lines.join(\"\\n\");\n}\n\n/** Write the plan to disk and return its absolute path. */\nexport function savePlanFile(options: SavePlanOptions): string {\n\tconst dir = getPlansDir(options.sessionId);\n\tmkdirSync(dir, { recursive: true });\n\tconst number = nextPlanNumber(readdirSync(dir));\n\tconst fileName = `${String(number).padStart(2, \"0\")}-${slugifyTitle(options.title)}.md`;\n\tconst path = join(dir, fileName);\n\twriteFileSync(path, buildPlanDocument({ ...options, createdIso: new Date().toISOString() }), \"utf-8\");\n\treturn path;\n}\n"]}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* plan/storage.ts — plan file layout and writing.
|
|
3
|
-
*
|
|
4
|
-
* Plans live under `<agentDir>/plans/<sessionId>/NN-<slug>.md`, append-only
|
|
5
|
-
* with a monotonically increasing NN. The directory is only the initial
|
|
6
|
-
* storage location — the authoritative index is the `planFiles` list in the
|
|
7
|
-
* session's plan-mode entries, which follows fork/clone.
|
|
8
|
-
*/
|
|
9
|
-
import { mkdirSync, readdirSync, writeFileSync } from "node:fs";
|
|
10
|
-
import { join } from "node:path";
|
|
11
|
-
import { getAgentDir } from "../../config.js";
|
|
12
|
-
export function getPlansDir(sessionId) {
|
|
13
|
-
return join(getAgentDir(), "plans", sessionId);
|
|
14
|
-
}
|
|
15
|
-
/** File-name slug from a plan title: lowercase, ascii-ish, bounded. */
|
|
16
|
-
export function slugifyTitle(title) {
|
|
17
|
-
const slug = title
|
|
18
|
-
.toLowerCase()
|
|
19
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
20
|
-
.replace(/^-+|-+$/g, "")
|
|
21
|
-
.slice(0, 40)
|
|
22
|
-
.replace(/-+$/, "");
|
|
23
|
-
return slug || "plan";
|
|
24
|
-
}
|
|
25
|
-
/** Next NN based on existing `NN-*.md` names; gaps never reuse numbers. */
|
|
26
|
-
export function nextPlanNumber(existingNames) {
|
|
27
|
-
let max = 0;
|
|
28
|
-
for (const name of existingNames) {
|
|
29
|
-
const match = /^(\d+)-/.exec(name);
|
|
30
|
-
if (match)
|
|
31
|
-
max = Math.max(max, Number(match[1]));
|
|
32
|
-
}
|
|
33
|
-
return max + 1;
|
|
34
|
-
}
|
|
35
|
-
export function buildPlanDocument(options) {
|
|
36
|
-
const lines = [
|
|
37
|
-
"---",
|
|
38
|
-
`title: ${JSON.stringify(options.title)}`,
|
|
39
|
-
`created: ${options.createdIso}`,
|
|
40
|
-
`cwd: ${JSON.stringify(options.cwd)}`,
|
|
41
|
-
`session: ${options.sessionId}`,
|
|
42
|
-
];
|
|
43
|
-
if (options.revises)
|
|
44
|
-
lines.push(`revises: ${JSON.stringify(options.revises)}`);
|
|
45
|
-
lines.push("---", "", options.plan.trimEnd(), "");
|
|
46
|
-
return lines.join("\n");
|
|
47
|
-
}
|
|
48
|
-
/** Write the plan to disk and return its absolute path. */
|
|
49
|
-
export function savePlanFile(options) {
|
|
50
|
-
const dir = getPlansDir(options.sessionId);
|
|
51
|
-
mkdirSync(dir, { recursive: true });
|
|
52
|
-
const number = nextPlanNumber(readdirSync(dir));
|
|
53
|
-
const fileName = `${String(number).padStart(2, "0")}-${slugifyTitle(options.title)}.md`;
|
|
54
|
-
const path = join(dir, fileName);
|
|
55
|
-
writeFileSync(path, buildPlanDocument({ ...options, createdIso: new Date().toISOString() }), "utf-8");
|
|
56
|
-
return path;
|
|
57
|
-
}
|
|
58
|
-
//# sourceMappingURL=storage.js.map
|