@hank-warren/pi-plan-mode 1.4.0 → 1.6.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 +25 -0
- package/package.json +2 -2
- package/src/command.ts +1 -1
- package/src/completion-tool.ts +11 -6
- package/src/extension-runtime.ts +0 -11
- package/src/fresh-implementation.ts +2 -2
- package/src/lifecycle.ts +79 -0
- package/src/plan-export.ts +4 -4
- package/src/plan-file.ts +1 -5
- package/src/plan-launch-menu.ts +1 -1
- package/src/plan-mode.ts +174 -213
- package/src/presentation.ts +54 -12
- package/src/prompt.ts +26 -6
- package/src/question-tool.ts +26 -10
- package/src/settings-menu.ts +6 -48
- package/src/settings-watch.ts +60 -0
- package/src/settings.ts +12 -69
- package/src/state.ts +0 -44
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# @hank-warren/pi-plan-mode
|
|
2
2
|
|
|
3
|
+
## 1.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- d0c46a5: Drop two legacy paths that were past their delete-by date:
|
|
8
|
+
|
|
9
|
+
- The one-shot repair of a thinking level left raised by a pre-1.3.0 session, and the "thinkingLevel is no longer used" row in the settings menu. An unknown `thinkingLevel` key in `pi-plan-mode.json` is still preserved verbatim on save.
|
|
10
|
+
- The `plan-mode.json` settings fallback and its "Using legacy…" / "ignored because…" notices. Only `$PI_CODING_AGENT_DIR/pi-plan-mode.json` is read now; a host still on the old filename gets defaults and should rename the file.
|
|
11
|
+
|
|
12
|
+
Internally, the settings watcher and the menu/workflow lifecycle moved into their own modules and the state transitions share one helper; nothing else about `/plan` changed. `engines.node` now states Pi's own floor, `>=22.19.0`.
|
|
13
|
+
|
|
14
|
+
## 1.5.0
|
|
15
|
+
|
|
16
|
+
### Minor Changes
|
|
17
|
+
|
|
18
|
+
- c079c51: Keep the completed plan out of model context, stage the Plan tools, and fail honestly when a plan cannot be saved.
|
|
19
|
+
|
|
20
|
+
The completed-plan card is a display-only session entry rendered through `registerEntryRenderer` instead of a message, so the plan stays visible and restorable in the transcript while never entering model context or compaction. `plan_mode_complete` returns a one-line `Plan saved to <path>.` pointer; the durable file remains the handoff.
|
|
21
|
+
|
|
22
|
+
`plan_mode_complete` now writes the file first and throws when the write fails, rather than reporting success and returning `undefined`. Prior state stays intact and the call is retryable.
|
|
23
|
+
|
|
24
|
+
`plan_mode_complete` and the `plan_mode_question` fallback activate when Plan mode is entered or restored, so a session that never plans does not carry their schemas. Ownership of `ask_user_question` is resolved by package directory and read back from the host rather than assumed from the write, and every reconcile is announced on an event, so the fallback no longer depends on hook order between packages. A headless run has no legitimate question tool, so the prompt and the finalize steer switch to a plain-text variant instead of naming a tool that both packages strip.
|
|
25
|
+
|
|
26
|
+
`AbortSignal` is wired through the question tool with exactly-once cleanup.
|
|
27
|
+
|
|
3
28
|
## 1.4.0
|
|
4
29
|
|
|
5
30
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hank-warren/pi-plan-mode",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Plan mode for Pi: research and design with a durable plan file that survives compaction.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
},
|
|
23
23
|
"homepage": "https://github.com/hank-warren/pi-extensions/tree/main/packages/pi-plan-mode#readme",
|
|
24
24
|
"engines": {
|
|
25
|
-
"node": ">=
|
|
25
|
+
"node": ">=22.19.0"
|
|
26
26
|
},
|
|
27
27
|
"pi": {
|
|
28
28
|
"extensions": [
|
package/src/command.ts
CHANGED
package/src/completion-tool.ts
CHANGED
|
@@ -2,10 +2,10 @@ import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import { Markdown } from "@earendil-works/pi-tui";
|
|
3
3
|
|
|
4
4
|
export const PLAN_MODE_COMPLETE_TOOL_NAME = "plan_mode_complete";
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
const PLAN_MODE_COMPLETE_VERSION = 1;
|
|
6
|
+
const PLAN_MODE_MAX_CHARS = 50_000;
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
type PlanModeCompletionDetails = {
|
|
9
9
|
version: typeof PLAN_MODE_COMPLETE_VERSION;
|
|
10
10
|
source: typeof PLAN_MODE_COMPLETE_TOOL_NAME;
|
|
11
11
|
plan: string;
|
|
@@ -43,7 +43,7 @@ export function normalizePlanModeCompletion(input: unknown): NormalizePlanModeCo
|
|
|
43
43
|
return { ok: true, plan };
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
function planFromCompletionDetails(value: unknown) {
|
|
47
47
|
if (!isRecord(value)) return undefined;
|
|
48
48
|
if (
|
|
49
49
|
value.version !== PLAN_MODE_COMPLETE_VERSION ||
|
|
@@ -57,7 +57,12 @@ export function planFromCompletionDetails(value: unknown) {
|
|
|
57
57
|
|
|
58
58
|
export function planModeCompleted(plan: string, planPath?: string) {
|
|
59
59
|
return {
|
|
60
|
-
content: [
|
|
60
|
+
content: [
|
|
61
|
+
{
|
|
62
|
+
type: "text" as const,
|
|
63
|
+
text: planPath ? `Plan saved to ${planPath}.` : "Plan saved.",
|
|
64
|
+
},
|
|
65
|
+
],
|
|
61
66
|
details: {
|
|
62
67
|
version: PLAN_MODE_COMPLETE_VERSION,
|
|
63
68
|
source: PLAN_MODE_COMPLETE_TOOL_NAME,
|
|
@@ -73,7 +78,7 @@ type PlanModeCompletionRenderResult = {
|
|
|
73
78
|
details?: unknown;
|
|
74
79
|
};
|
|
75
80
|
|
|
76
|
-
|
|
81
|
+
function planModeCompletionMarkdown(result: PlanModeCompletionRenderResult) {
|
|
77
82
|
const content = result.content
|
|
78
83
|
.filter((block) => block.type === "text" && typeof block.text === "string")
|
|
79
84
|
.map((block) => block.text)
|
package/src/extension-runtime.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type { LegacyThinkingCapture } from "./state.js";
|
|
3
2
|
|
|
4
3
|
type AgentSettledHandler = (event: unknown, ctx: ExtensionContext) => unknown;
|
|
5
4
|
|
|
@@ -11,16 +10,6 @@ export function onAgentSettled(pi: ExtensionAPI, handler: AgentSettledHandler) {
|
|
|
11
10
|
).on("agent_settled", handler);
|
|
12
11
|
}
|
|
13
12
|
|
|
14
|
-
/**
|
|
15
|
-
* Only the one-shot migration that undoes a pre-1.3.0 thinking-level change
|
|
16
|
-
* still calls this. Plan mode never sets the thinking level otherwise.
|
|
17
|
-
*
|
|
18
|
-
* legacy: delete in 1.4.0
|
|
19
|
-
*/
|
|
20
|
-
export function setPlanThinkingLevel(pi: ExtensionAPI, level: LegacyThinkingCapture["previous"]) {
|
|
21
|
-
(pi.setThinkingLevel as unknown as (level: string) => void)(level);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
13
|
export function isStaleExtensionContextError(error: unknown) {
|
|
25
14
|
return (
|
|
26
15
|
error instanceof Error &&
|
|
@@ -5,7 +5,7 @@ import type { PlanModeState } from "./state.js";
|
|
|
5
5
|
type NewSessionOptions = Exclude<Parameters<ExtensionCommandContext["newSession"]>[0], undefined>;
|
|
6
6
|
type ReplacementContext = Parameters<NonNullable<NewSessionOptions["withSession"]>>[0];
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
interface FreshImplementationRequest {
|
|
9
9
|
plan: string;
|
|
10
10
|
planPath: string;
|
|
11
11
|
stateEntryType: string;
|
|
@@ -18,7 +18,7 @@ interface FreshImplementationFromStateOptions {
|
|
|
18
18
|
stateEntryType: string;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
type FreshImplementationResult =
|
|
22
22
|
| { kind: "started" }
|
|
23
23
|
| { kind: "cancelled" }
|
|
24
24
|
| { kind: "partial" }
|
package/src/lifecycle.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A scope captured when deferred work starts: the signal that work should race
|
|
3
|
+
* against, and the question "is what I was started for still the current
|
|
4
|
+
* thing?".
|
|
5
|
+
*/
|
|
6
|
+
export interface LifecycleScope {
|
|
7
|
+
readonly signal: AbortSignal;
|
|
8
|
+
isCurrent(): boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Two nested generations decide whether deferred Plan-mode work may still act.
|
|
13
|
+
*
|
|
14
|
+
* The session generation moves when Pi replaces or shuts down the session: a
|
|
15
|
+
* menu, a settings reload, or a question left waiting from the previous session
|
|
16
|
+
* must never write to the new one. The workflow generation moves on every
|
|
17
|
+
* enter/exit/implement, so a menu opened against one plan cannot act after the
|
|
18
|
+
* user has moved on — while a settings reload, which belongs to the session
|
|
19
|
+
* rather than to a plan, is deliberately left alone by it.
|
|
20
|
+
*
|
|
21
|
+
* The abort signal is the second half of the same rule: it stops work that is
|
|
22
|
+
* already blocked on the UI, where a generation check would never be reached.
|
|
23
|
+
*/
|
|
24
|
+
export function createLifecycle() {
|
|
25
|
+
let sessionGeneration = 0;
|
|
26
|
+
let workflowGeneration = 0;
|
|
27
|
+
let controller = new AbortController();
|
|
28
|
+
|
|
29
|
+
const sessionScope = (): LifecycleScope => {
|
|
30
|
+
const session = sessionGeneration;
|
|
31
|
+
const active = controller;
|
|
32
|
+
return {
|
|
33
|
+
signal: active.signal,
|
|
34
|
+
isCurrent: () => session === sessionGeneration && !active.signal.aborted,
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Ends the current session: everything captured before this call goes stale
|
|
40
|
+
* and everything waiting on the signal is aborted with `reason`. The aborted
|
|
41
|
+
* signal stays in place, so anything captured *after* it is stale too —
|
|
42
|
+
* which is what a shut-down session wants: there is no next session to be
|
|
43
|
+
* current for, and a menu opened in that window must refuse to run.
|
|
44
|
+
*/
|
|
45
|
+
const endSession = (reason: string) => {
|
|
46
|
+
sessionGeneration += 1;
|
|
47
|
+
controller.abort(new DOMException(reason, "AbortError"));
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
/** The live session signal, for composing with a caller's own. */
|
|
52
|
+
get signal() {
|
|
53
|
+
return controller.signal;
|
|
54
|
+
},
|
|
55
|
+
endSession,
|
|
56
|
+
/**
|
|
57
|
+
* Ends the current session and opens the next one, whose scope is
|
|
58
|
+
* returned: work started from here races against a fresh signal.
|
|
59
|
+
*/
|
|
60
|
+
nextSession(reason: string): LifecycleScope {
|
|
61
|
+
endSession(reason);
|
|
62
|
+
controller = new AbortController();
|
|
63
|
+
return sessionScope();
|
|
64
|
+
},
|
|
65
|
+
/** Supersedes menus and prompts opened against the previous plan state. */
|
|
66
|
+
nextWorkflow() {
|
|
67
|
+
workflowGeneration += 1;
|
|
68
|
+
},
|
|
69
|
+
/** The scope for menu-scale work: stale as soon as either generation moves. */
|
|
70
|
+
capture(): LifecycleScope {
|
|
71
|
+
const session = sessionScope();
|
|
72
|
+
const workflow = workflowGeneration;
|
|
73
|
+
return {
|
|
74
|
+
signal: session.signal,
|
|
75
|
+
isCurrent: () => session.isCurrent() && workflow === workflowGeneration,
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
package/src/plan-export.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type { PlanModeState } from "./state.js";
|
|
|
8
8
|
|
|
9
9
|
export { DEFAULT_PLAN_EXPORT_PATH };
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
interface PlanExportResult {
|
|
12
12
|
path: string;
|
|
13
13
|
}
|
|
14
14
|
|
|
@@ -17,7 +17,7 @@ export interface PlanExportDestination {
|
|
|
17
17
|
resolvedPath: string;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
interface PlanExportLifecycle {
|
|
21
21
|
signal: AbortSignal;
|
|
22
22
|
isCurrent(): boolean;
|
|
23
23
|
getState?(): PlanModeState;
|
|
@@ -74,7 +74,7 @@ export async function exportStoredPlan(
|
|
|
74
74
|
return true;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
async function exportPlanToFile(
|
|
78
78
|
plan: string,
|
|
79
79
|
requestedPath: string | undefined,
|
|
80
80
|
cwd: string,
|
|
@@ -109,7 +109,7 @@ export function planExportDestination(defaultPath: string, cwd: string): PlanExp
|
|
|
109
109
|
};
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
|
|
112
|
+
function resolvePlanExportPath(
|
|
113
113
|
requestedPath: string | undefined,
|
|
114
114
|
cwd: string,
|
|
115
115
|
defaultPath = DEFAULT_PLAN_EXPORT_PATH,
|
package/src/plan-file.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { mkdir, open, rename, rm, unlink, writeFile } from "node:fs/promises";
|
|
|
4
4
|
import { basename, dirname, join } from "node:path";
|
|
5
5
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
const PLANS_DIRECTORY = "plans";
|
|
8
8
|
const MAX_PLAN_BYTES = 1024 * 1024;
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -79,7 +79,3 @@ export async function readPlanFile(path: string): Promise<string | undefined> {
|
|
|
79
79
|
export async function deletePlanFile(path: string): Promise<void> {
|
|
80
80
|
await unlink(path).catch(() => undefined);
|
|
81
81
|
}
|
|
82
|
-
|
|
83
|
-
export async function planFileExists(path: string): Promise<boolean> {
|
|
84
|
-
return (await readPlanFile(path)) !== undefined;
|
|
85
|
-
}
|
package/src/plan-launch-menu.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { defineMenu, runMenu } from "@narumitw/pi-tui-kit";
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
interface PlanLaunchMenuOptions {
|
|
5
5
|
statusText: string;
|
|
6
6
|
signal: AbortSignal;
|
|
7
7
|
isCurrent(): boolean;
|