pi-better-harness 0.27.1 → 0.27.2
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/node_modules/pi-better-plan/README.md +36 -74
- package/node_modules/pi-better-plan/REFERENCE.md +111 -0
- package/node_modules/pi-better-plan/package.json +3 -2
- package/node_modules/pi-better-plan/src/index.ts +85 -29
- package/node_modules/pi-better-plan/src/plan-presentation.ts +32 -2
- package/node_modules/pi-better-plan/src/plan-render.ts +2 -2
- package/node_modules/pi-better-plan/src/plan-state.ts +6 -2
- package/node_modules/pi-better-plan/src/plan-widget.ts +41 -0
- package/node_modules/pi-better-plan/src/types.ts +2 -1
- package/node_modules/pi-better-plan/src/workflow-plan.ts +8 -4
- package/package.json +2 -2
|
@@ -1,104 +1,66 @@
|
|
|
1
1
|
# pi-better-plan
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
See what Pi is working on, what is done, and what comes next.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
## What It Does
|
|
8
|
-
|
|
9
|
-
- Gives models one `update_plan` interface for checklist updates, workflow binding, and workflow transitions, plus `get_plan` for inspection.
|
|
10
|
-
- Supports dependency edges (`id` and `dependsOn`) so independent ready steps can run concurrently.
|
|
11
|
-
- Shows the complete checklist of completed, active, pending, and blocked steps above the editor.
|
|
12
|
-
- Persists plan state and display preferences on the active Pi session branch.
|
|
13
|
-
- Keeps a completed plan visible for 30 seconds, then clears it automatically.
|
|
14
|
-
- Opens the complete plan with `/plan`.
|
|
5
|
+
## Install
|
|
15
6
|
|
|
16
|
-
|
|
7
|
+
```sh
|
|
8
|
+
pi install npm:pi-better-plan
|
|
9
|
+
```
|
|
17
10
|
|
|
18
|
-
##
|
|
11
|
+
## Using the plan
|
|
19
12
|
|
|
20
|
-
|
|
13
|
+
Ask Pi to make a plan for your task. The checklist appears above the editor and updates as Pi works.
|
|
21
14
|
|
|
22
|
-
|
|
23
|
-
the active subagent delegation mode. Without `pi-better-subagents`, the plan uses
|
|
24
|
-
adaptive guidance. Manual keeps planned work in the foreground unless the user or
|
|
25
|
-
an active workflow explicitly requires delegation. Adaptive delegates substantial
|
|
26
|
-
independent work when useful. Coordinator consults current catalog roles and
|
|
27
|
-
delegates nontrivial role-owned work while retaining integration and final
|
|
28
|
-
verification in the foreground.
|
|
15
|
+
By default, you see up to five steps around the current work. The view starts at the beginning and moves toward the end as work progresses. Ellipses show how many steps are hidden; the progress count always covers the whole plan.
|
|
29
16
|
|
|
30
|
-
|
|
31
|
-
when both are actually underway. Use steps for distinct deliverables, not
|
|
32
|
-
individual worker processes; worker tools and the background-work navigator own
|
|
33
|
-
run status. Complete verification and the plan only after every relevant
|
|
34
|
-
delegated task is terminal and its result or failure has been inspected and
|
|
35
|
-
integrated.
|
|
17
|
+
Click the plan to expand or collapse it in Pi's fullscreen mode. In regular terminal mode, use `/plan expand` or `/plan collapse`. For the full plan with details, run `/plan`.
|
|
36
18
|
|
|
37
|
-
|
|
19
|
+
Rows show `✓` for done, `●` for active, `○` for pending, and `!` for blocked. Pi marks steps complete explicitly; the count is not an estimate of effort.
|
|
38
20
|
|
|
39
|
-
|
|
21
|
+
Your plan and display settings survive reload and follow the session branch. A completed checklist clears after 30 seconds. A released workflow plan stays visible for reference, but cannot be edited until the workflow resumes.
|
|
40
22
|
|
|
41
|
-
|
|
23
|
+
## Examples
|
|
42
24
|
|
|
43
|
-
|
|
44
|
-
{ "workflow": {
|
|
45
|
-
"path": "/absolute/project/.resolve-issues/rush/run-1/task-plan.json",
|
|
46
|
-
"revision": 12
|
|
47
|
-
} }
|
|
48
|
-
```
|
|
25
|
+
These images show the same eight-step plan, rendered by the actual widget with demonstration data.
|
|
49
26
|
|
|
50
|
-
|
|
27
|
+
### Just started
|
|
51
28
|
|
|
52
|
-
|
|
29
|
+
The first five steps are visible.
|
|
53
30
|
|
|
54
|
-
|
|
55
|
-
{ "workflow": {
|
|
56
|
-
"event": "unit-validated",
|
|
57
|
-
"revision": 12,
|
|
58
|
-
"changes": [
|
|
59
|
-
{ "id": "1201", "set": { "stage": "done", "status": "in-flight", "worker": null, "headSha": "abc123" } },
|
|
60
|
-
{ "id": "C1", "set": { "status": "combining" } }
|
|
61
|
-
],
|
|
62
|
-
"profiling": { "outcome": "succeeded", "wallMs": 540000 }
|
|
63
|
-
} }
|
|
64
|
-
```
|
|
31
|
+

|
|
65
32
|
|
|
66
|
-
|
|
33
|
+
### In the middle
|
|
67
34
|
|
|
68
|
-
|
|
35
|
+
The active step has nearby context, with hidden steps counted on both sides.
|
|
69
36
|
|
|
70
|
-
|
|
37
|
+

|
|
71
38
|
|
|
72
|
-
|
|
39
|
+
### Near the end
|
|
73
40
|
|
|
74
|
-
|
|
41
|
+
The view shifts to the last five steps.
|
|
75
42
|
|
|
76
|
-
|
|
43
|
+

|
|
77
44
|
|
|
78
|
-
|
|
45
|
+
### Expanded
|
|
79
46
|
|
|
80
|
-
|
|
47
|
+
See every step. Click again or run `/plan collapse` to return to the compact view.
|
|
81
48
|
|
|
82
|
-
|
|
83
|
-
pi install npm:pi-better-plan
|
|
84
|
-
```
|
|
49
|
+

|
|
85
50
|
|
|
86
51
|
## Commands
|
|
87
52
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
```
|
|
53
|
+
| Command | What it does |
|
|
54
|
+
| --- | --- |
|
|
55
|
+
| `/plan` | Open the full plan. Use Up/Down to select a step and Escape to return. |
|
|
56
|
+
| `/plan expand` | Show all steps above the editor. |
|
|
57
|
+
| `/plan collapse` | Return to the five-step view. |
|
|
58
|
+
| `/plan hide` | Hide the plan without deleting it. |
|
|
59
|
+
| `/plan show` | Show it again. |
|
|
60
|
+
| `/plan clear` | Remove the displayed plan. |
|
|
97
61
|
|
|
98
|
-
|
|
62
|
+
## For developers
|
|
99
63
|
|
|
100
|
-
|
|
64
|
+
Pi uses `update_plan` to create or update a plan and `get_plan` to inspect it. Plans can include dependencies and work running in parallel.
|
|
101
65
|
|
|
102
|
-
|
|
103
|
-
npm run verify
|
|
104
|
-
```
|
|
66
|
+
See the [technical reference](./REFERENCE.md) for workflow integration, delegation rules, display settings, tests, and screenshot generation.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Plan reference
|
|
2
|
+
|
|
3
|
+
Technical details for workflow authors and contributors. For installation, screenshots, and everyday use, see the [README](./README.md).
|
|
4
|
+
|
|
5
|
+
## Coordinating Delegated Work
|
|
6
|
+
|
|
7
|
+
Use the plan as the foreground coordinator's milestone ledger. Delegate independent, sufficiently substantial work early with subagents, and use background tasks for long-running processes or repeated checks. Keep doing unblocked foreground work after launch; do not poll workers.
|
|
8
|
+
|
|
9
|
+
Before the first implementation milestone, check for independent work and follow
|
|
10
|
+
the active subagent delegation mode. Without `pi-better-subagents`, the plan uses
|
|
11
|
+
adaptive guidance. Manual keeps planned work in the foreground unless the user or
|
|
12
|
+
an active workflow explicitly requires delegation. Adaptive delegates substantial
|
|
13
|
+
independent work when useful. Coordinator consults current catalog roles and
|
|
14
|
+
delegates nontrivial role-owned work while retaining integration and final
|
|
15
|
+
verification in the foreground.
|
|
16
|
+
|
|
17
|
+
Independent foreground and delegated milestones may both be `in_progress` only
|
|
18
|
+
when both are actually underway. Use steps for distinct deliverables, not
|
|
19
|
+
individual worker processes; worker tools and the background-work navigator own
|
|
20
|
+
run status. Complete verification and the plan only after every relevant
|
|
21
|
+
delegated task is terminal and its result or failure has been inspected and
|
|
22
|
+
integrated.
|
|
23
|
+
|
|
24
|
+
For a DAG, assign stable ids to prerequisite steps and list those ids in dependent steps' `dependsOn`. Dependencies must exist in the same plan; cycles and starting or completing a step before its prerequisites are complete are rejected. `get_plan` reports pending steps whose prerequisites are complete as ready. Plans without edges keep their existing behavior.
|
|
25
|
+
|
|
26
|
+
When an explicitly invoked skill declares `workflow-role: coordinator` in its metadata, that skill's task plan takes precedence. The generic checklist stays persisted but is hidden, and `update_plan` refuses generic checklist updates until workflow ownership is released. `/plan` never opens a stale generic checklist during workflow ownership.
|
|
27
|
+
|
|
28
|
+
For any workflow using the shared task-plan contract (including `resolve-issues` and `rush-issues`), bind its plan through `update_plan` with the absolute `.resolve-issues/rush/<run-id>/task-plan.json` path and its persisted `planRevision`:
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{ "workflow": {
|
|
32
|
+
"path": "/absolute/project/.resolve-issues/rush/run-1/task-plan.json",
|
|
33
|
+
"revision": 12
|
|
34
|
+
} }
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
This binds the run to the session and shows its fleet stages and units in the widget, `/plan`, and `get_plan`. It does not rewrite the plan, increment its revision, or append a profiling event. The binding survives Pi session resume. Ownership release keeps the last bound plan visible as a read-only handoff; a generic checklist update or `/plan clear` replaces or dismisses it. Repeat this form to reload an externally saved checkpoint at its exact revision. `sync_workflow_plan` has been removed; migrate its old `{path, revision}` arguments into `update_plan`'s `workflow` object.
|
|
38
|
+
|
|
39
|
+
After binding, record every transition with `update_plan` and a `workflow` object instead of editing `task-plan.json` or the profiling log by hand:
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{ "workflow": {
|
|
43
|
+
"event": "unit-validated",
|
|
44
|
+
"revision": 12,
|
|
45
|
+
"changes": [
|
|
46
|
+
{ "id": "1201", "set": { "stage": "done", "status": "in-flight", "worker": null, "headSha": "abc123" } },
|
|
47
|
+
{ "id": "C1", "set": { "status": "combining" } }
|
|
48
|
+
],
|
|
49
|
+
"profiling": { "outcome": "succeeded", "wallMs": 540000 }
|
|
50
|
+
} }
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
You may include `path` and `revision` alongside `event` and `changes` in the first call to bind and save a transition together. A rejected transition does not activate a new binding. Every successful binding or transition requests a TUI refresh; there is no separate synchronization tool to call afterward.
|
|
54
|
+
|
|
55
|
+
Each issue row shows where it is between code and delivery: `●` while it is being built, `◐ implemented` once `stage` is `done` with a `headSha`, `◑ review passed` while `reviewedHead` equals `headSha`, and `✓` only at `status: succeeded`, when the change has landed on the target branch by a merged PR or a direct push. Only `✓` rows count as complete.
|
|
56
|
+
|
|
57
|
+
`changes` addresses units, components, and fleet stages by id (a change without an id sets run-level fields); all changes in one call are one transition. For a scope change, a change with `target` `unit` or `component` and an `add` row appends a new row; existing rows cannot be removed or renamed. The tool checks ids, dependencies (including cycles), worker slots, and status/stage values against the shared issue-resolution contract (a fleet status of `n/a` is saved as `not-applicable`), refuses a stale `revision`, appends an optional `decision`, then saves the plan atomically with `planRevision + 1` and a new `updatedAt`, and appends one profiling event with the same revision to the log the run already uses (`profiling/run.jsonl` or `profiling.jsonl`). A rejected update changes nothing. The event is appended before the plan is renamed into place, so a crash between the two can leave the log one revision ahead; the next update notices, reuses that revision, and marks its event with `logAheadRevision`.
|
|
58
|
+
|
|
59
|
+
The binding records the active workflow's name, which is shown in the plan view. A different active workflow cannot read or update that binding; a new invocation invalidates it. After ownership release, the prior binding remains available for read-only inspection but cannot authorize writes. The skill name is not an allowlist: explicit binding opts a workflow into the shared schema and transition rules, while path confinement to `.resolve-issues/rush/<run-id>/task-plan.json`, run identity, size limits, and revision checks still apply. Workflows using other schemas or storage locations retain their own planning and must not bind incompatible state.
|
|
60
|
+
|
|
61
|
+
## Plan Display
|
|
62
|
+
|
|
63
|
+
Native and workflow-synced plans share a `plan` heading, completion counts, aligned identifiers and titles, and status colors. Completed rows use `✓`, active rows `●`, pending rows `○`, and blocked rows `!`. Active and blocked rows also carry text labels; failed workflow rows use `×` and `failed` rather than an active indicator.
|
|
64
|
+
|
|
65
|
+
Workflow identity, revision, and fleet progress sit on a secondary line that wraps on narrow terminals. `/plan` uses the same styling and shows workflow stage, raw status, worker, dependencies, and notes beneath each issue. Use ↑/↓ to move the selection and Escape or ← to return. The passive widget never captures editor arrow keys.
|
|
66
|
+
|
|
67
|
+
The folded widget shows at most five steps. Active steps take priority, with nearby steps filling the window; if more than five are active, the last five in plan order are shown. Early progress shows the head, late or completed progress shows the tail. Ellipses count omitted rows, while summary counts always cover the entire plan. Expanding reveals all steps without the detailed notes from `/plan`.
|
|
68
|
+
|
|
69
|
+
In Pi's fullscreen TUI, a plain left click on the visible plan toggles expansion without taking editor focus. Regular terminal mode keeps mouse events for terminal selection and scrollback; use `/plan expand` or `/plan collapse` there. Expansion and display preferences survive reload and follow the active session branch.
|
|
70
|
+
|
|
71
|
+
## Commands
|
|
72
|
+
|
|
73
|
+
```text
|
|
74
|
+
/plan
|
|
75
|
+
/plan clear
|
|
76
|
+
/plan hide
|
|
77
|
+
/plan show
|
|
78
|
+
/plan expand
|
|
79
|
+
/plan collapse
|
|
80
|
+
/plan pin auto
|
|
81
|
+
/plan pin on
|
|
82
|
+
/plan pin off
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`pin auto` and `pin on` currently use the compact widget until Pi exposes a reserved right-rail extension interface. A floating overlay is intentionally not used as a substitute because it would cover transcript content.
|
|
86
|
+
|
|
87
|
+
## Development
|
|
88
|
+
|
|
89
|
+
```sh
|
|
90
|
+
npm run verify
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
From the repository root, run the real terminal journey with tmux:
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
node --import tsx --import ./scripts/isolate-registry.mjs --test scripts/plan-navigation.tui.e2e.test.mjs
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Regenerate only the example screenshots, then check the gallery assets:
|
|
100
|
+
|
|
101
|
+
```sh
|
|
102
|
+
npm run gallery:render -- --plan-examples
|
|
103
|
+
npm run gallery:check
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
To also exercise fullscreen mouse dispatch on a mouse-capable Pi build:
|
|
107
|
+
|
|
108
|
+
```sh
|
|
109
|
+
PI_PLAN_TUI_CLI=/absolute/path/to/pi PI_PLAN_TUI_MOUSE=1 \
|
|
110
|
+
node --import tsx --import ./scripts/isolate-registry.mjs --test scripts/plan-navigation.tui.e2e.test.mjs
|
|
111
|
+
```
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-better-plan",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Structured execution plans with persistent progress for Pi.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "src/index.ts",
|
|
8
8
|
"files": [
|
|
9
9
|
"src",
|
|
10
|
-
"README.md"
|
|
10
|
+
"README.md",
|
|
11
|
+
"REFERENCE.md"
|
|
11
12
|
],
|
|
12
13
|
"keywords": [
|
|
13
14
|
"pi-package",
|
|
@@ -13,6 +13,8 @@ import {
|
|
|
13
13
|
replacePlan,
|
|
14
14
|
} from "./plan-state.js";
|
|
15
15
|
import { createFullPlanComponent, renderCompactPlan } from "./plan-render.js";
|
|
16
|
+
import { createPlanWidget } from "./plan-widget.js";
|
|
17
|
+
import { COMPACT_PLAN_ROWS } from "./plan-presentation.js";
|
|
16
18
|
import {
|
|
17
19
|
createRushPlanComponent, readRushPlan, renderRushPlan, workflowBinding,
|
|
18
20
|
WORKFLOW_PLAN_ENTRY, type RushPlan, type WorkflowPlanBinding,
|
|
@@ -85,6 +87,8 @@ const PLAN_ACTIONS: readonly AutocompleteItem[] = [
|
|
|
85
87
|
{ value: "clear", label: "clear", description: "Remove the current plan" },
|
|
86
88
|
{ value: "hide", label: "hide", description: "Hide the plan widget" },
|
|
87
89
|
{ value: "show", label: "show", description: "Restore automatic plan display" },
|
|
90
|
+
{ value: "expand", label: "expand", description: "Show all steps in the plan widget" },
|
|
91
|
+
{ value: "collapse", label: "collapse", description: "Show up to five relevant steps" },
|
|
88
92
|
{ value: "pin auto", label: "pin auto", description: "Use automatic plan pinning" },
|
|
89
93
|
{ value: "pin on", label: "pin on", description: "Keep the plan pinned" },
|
|
90
94
|
{ value: "pin off", label: "pin off", description: "Keep the plan unpinned" },
|
|
@@ -119,6 +123,7 @@ function workflowPlanOwner(ctx: ExtensionContext): string | null {
|
|
|
119
123
|
export default function planExtension(pi: ExtensionAPI): void {
|
|
120
124
|
let currentPlan: PlanSnapshot | null = null;
|
|
121
125
|
let displayMode: PlanDisplayMode = "auto";
|
|
126
|
+
let expanded = false;
|
|
122
127
|
let displayedWorkflowOwner: string | null = null;
|
|
123
128
|
let rushBinding: WorkflowPlanBinding | null = null;
|
|
124
129
|
let rushPlan: RushPlan | null = null;
|
|
@@ -128,16 +133,26 @@ export default function planExtension(pi: ExtensionAPI): void {
|
|
|
128
133
|
const unsubscribeWorkflow = pi.events.on("pi-better-workflow:changed", (owner) => {
|
|
129
134
|
displayedWorkflowOwner = owner && typeof owner === "object" && "name" in owner && typeof owner.name === "string"
|
|
130
135
|
? owner.name : null;
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
136
|
+
if (displayedWorkflowOwner) {
|
|
137
|
+
rushBinding = null;
|
|
138
|
+
rushPlan = null;
|
|
139
|
+
rushError = null;
|
|
140
|
+
}
|
|
134
141
|
refreshWidget?.(true);
|
|
135
142
|
});
|
|
136
143
|
|
|
137
144
|
const refresh = (force = false): void => refreshWidget?.(force);
|
|
138
145
|
|
|
139
146
|
const loadRushPlan = (ctx: ExtensionContext): RushPlan | null => {
|
|
140
|
-
|
|
147
|
+
const owner = workflowPlanOwner(ctx);
|
|
148
|
+
const binding = workflowBinding(ctx.sessionManager.getBranch());
|
|
149
|
+
displayedWorkflowOwner = owner;
|
|
150
|
+
rushBinding = binding && (!owner || binding.owner === owner) ? binding : null;
|
|
151
|
+
if (!rushBinding) {
|
|
152
|
+
rushPlan = null;
|
|
153
|
+
rushError = null;
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
141
156
|
try {
|
|
142
157
|
const plan = readRushPlan(rushBinding.path, ctx.cwd);
|
|
143
158
|
if (plan.runId !== rushBinding.runId) throw new Error("Rush run identity changed.");
|
|
@@ -151,17 +166,24 @@ export default function planExtension(pi: ExtensionAPI): void {
|
|
|
151
166
|
return rushPlan;
|
|
152
167
|
};
|
|
153
168
|
|
|
154
|
-
const displayWorkflowPlan = (plan: RushPlan, binding: WorkflowPlanBinding): void => {
|
|
169
|
+
const displayWorkflowPlan = (plan: RushPlan, binding: WorkflowPlanBinding, readOnly = false): void => {
|
|
155
170
|
if (!rushBinding || rushBinding.owner !== binding.owner || rushBinding.path !== binding.path || rushBinding.runId !== binding.runId) {
|
|
156
171
|
pi.appendEntry(WORKFLOW_PLAN_ENTRY, { version: 1, kind: "set", ...binding });
|
|
157
172
|
}
|
|
158
|
-
displayedWorkflowOwner = binding.owner;
|
|
173
|
+
displayedWorkflowOwner = readOnly ? null : binding.owner;
|
|
159
174
|
rushBinding = binding;
|
|
160
175
|
rushPlan = plan;
|
|
161
176
|
rushError = null;
|
|
162
177
|
refresh(true);
|
|
163
178
|
};
|
|
164
179
|
|
|
180
|
+
const clearWorkflowPlan = (): void => {
|
|
181
|
+
if (rushBinding) pi.appendEntry(WORKFLOW_PLAN_ENTRY, { version: 1, kind: "clear" });
|
|
182
|
+
rushBinding = null;
|
|
183
|
+
rushPlan = null;
|
|
184
|
+
rushError = null;
|
|
185
|
+
};
|
|
186
|
+
|
|
165
187
|
const cancelCompletedPlanClear = (): void => {
|
|
166
188
|
if (completedPlanClearTimer) clearTimeout(completedPlanClearTimer);
|
|
167
189
|
completedPlanClearTimer = undefined;
|
|
@@ -196,17 +218,19 @@ export default function planExtension(pi: ExtensionAPI): void {
|
|
|
196
218
|
currentPlan = state.plan;
|
|
197
219
|
displayedWorkflowOwner = workflowPlanOwner(ctx);
|
|
198
220
|
const binding = workflowBinding(ctx.sessionManager.getBranch());
|
|
199
|
-
rushBinding = binding
|
|
221
|
+
rushBinding = binding && (!displayedWorkflowOwner || binding.owner === displayedWorkflowOwner) ? binding : null;
|
|
200
222
|
rushPlan = null;
|
|
201
223
|
rushError = null;
|
|
202
224
|
if (rushBinding) loadRushPlan(ctx);
|
|
203
225
|
displayMode = state.displayMode;
|
|
226
|
+
expanded = state.expanded;
|
|
204
227
|
if (ctx.hasUI) ctx.ui.setStatus(LEGACY_PLAN_NAV_STATUS_KEY, undefined);
|
|
205
228
|
refresh(true);
|
|
206
229
|
scheduleCompletedPlanClear();
|
|
207
230
|
};
|
|
208
231
|
|
|
209
232
|
const persistPlan = (plan: PlanSnapshot): void => {
|
|
233
|
+
clearWorkflowPlan();
|
|
210
234
|
currentPlan = plan;
|
|
211
235
|
pi.appendEntry(EXTENSION_NAME, planSetEntry(plan));
|
|
212
236
|
refresh(true);
|
|
@@ -219,6 +243,12 @@ export default function planExtension(pi: ExtensionAPI): void {
|
|
|
219
243
|
refresh(true);
|
|
220
244
|
};
|
|
221
245
|
|
|
246
|
+
const setExpanded = (value: boolean): void => {
|
|
247
|
+
expanded = value;
|
|
248
|
+
pi.appendEntry(EXTENSION_NAME, { version: 1, kind: "expansion", expanded, at: Math.floor(Date.now() / 1_000) });
|
|
249
|
+
refresh(true);
|
|
250
|
+
};
|
|
251
|
+
|
|
222
252
|
const installWidget = (ctx: ExtensionContext): void => {
|
|
223
253
|
if (!ctx.hasUI) return;
|
|
224
254
|
ctx.ui.setWidget(
|
|
@@ -226,20 +256,26 @@ export default function planExtension(pi: ExtensionAPI): void {
|
|
|
226
256
|
(tui, theme) => {
|
|
227
257
|
const localRefresh = (force = false): void => tui.requestRender(force);
|
|
228
258
|
refreshWidget = localRefresh;
|
|
229
|
-
|
|
230
|
-
|
|
259
|
+
const widget = createPlanWidget(
|
|
260
|
+
(width: number): string[] => {
|
|
231
261
|
if (displayMode === "hidden" || displayedWorkflowOwner !== workflowPlanOwner(ctx)) return [];
|
|
232
|
-
if (displayedWorkflowOwner) {
|
|
233
|
-
if (rushBinding
|
|
262
|
+
if (displayedWorkflowOwner || rushBinding) {
|
|
263
|
+
if (!rushBinding || (displayedWorkflowOwner && rushBinding.owner !== displayedWorkflowOwner) ||
|
|
264
|
+
!workflowBinding(ctx.sessionManager.getBranch())) return [];
|
|
234
265
|
const fg = typeof theme?.fg === "function"
|
|
235
266
|
? (color: string, value: string) => theme.fg(color as never, value)
|
|
236
267
|
: undefined;
|
|
237
|
-
return rushPlan ?
|
|
268
|
+
return rushPlan ? renderRushPlan(rushPlan, width, false, fg, rushBinding.owner, !displayedWorkflowOwner, expanded) : [];
|
|
238
269
|
}
|
|
239
270
|
if (!currentPlan) return [];
|
|
240
|
-
return
|
|
271
|
+
return renderCompactPlan(currentPlan, width, theme as never, expanded);
|
|
241
272
|
},
|
|
242
|
-
|
|
273
|
+
() => (rushBinding ? rushPlan?.issues.length ?? 0 : currentPlan?.steps.length ?? 0) > COMPACT_PLAN_ROWS,
|
|
274
|
+
() => expanded,
|
|
275
|
+
() => setExpanded(!expanded),
|
|
276
|
+
);
|
|
277
|
+
return {
|
|
278
|
+
...widget,
|
|
243
279
|
dispose() {
|
|
244
280
|
if (refreshWidget === localRefresh) refreshWidget = undefined;
|
|
245
281
|
},
|
|
@@ -251,21 +287,23 @@ export default function planExtension(pi: ExtensionAPI): void {
|
|
|
251
287
|
|
|
252
288
|
const showFullPlan = async (ctx: ExtensionContext): Promise<void> => {
|
|
253
289
|
const owner = workflowPlanOwner(ctx);
|
|
254
|
-
|
|
290
|
+
const binding = workflowBinding(ctx.sessionManager.getBranch());
|
|
291
|
+
if (owner || binding) {
|
|
255
292
|
const plan = loadRushPlan(ctx);
|
|
293
|
+
const planOwner = owner ?? binding!.owner;
|
|
256
294
|
if (!plan) {
|
|
257
|
-
ctx.ui.notify(`${owner
|
|
295
|
+
ctx.ui.notify(`${planOwner}${owner ? " plan" : " read-only handoff"} unavailable: ${rushError ?? "not bound"}`, "warning");
|
|
258
296
|
return;
|
|
259
297
|
}
|
|
260
298
|
if (ctx.mode !== "tui") {
|
|
261
|
-
ctx.ui.notify(renderRushPlan(plan, 160, true, undefined, owner).join("\n"), "info");
|
|
299
|
+
ctx.ui.notify(renderRushPlan(plan, 160, true, undefined, planOwner, !owner).join("\n"), "info");
|
|
262
300
|
return;
|
|
263
301
|
}
|
|
264
302
|
await ctx.ui.custom<void>((tui, theme, _keys, done) => {
|
|
265
303
|
const fg = typeof theme?.fg === "function"
|
|
266
304
|
? (color: string, value: string) => theme.fg(color as never, value)
|
|
267
305
|
: undefined;
|
|
268
|
-
const component = createRushPlanComponent(plan, () => done(), fg, owner);
|
|
306
|
+
const component = createRushPlanComponent(plan, () => done(), fg, planOwner, !owner);
|
|
269
307
|
return { render: (width) => component.render(width), handleInput(data) {
|
|
270
308
|
component.handleInput?.(data);
|
|
271
309
|
tui.requestRender();
|
|
@@ -315,16 +353,26 @@ export default function planExtension(pi: ExtensionAPI): void {
|
|
|
315
353
|
const owner = workflowPlanOwner(ctx);
|
|
316
354
|
const input = params as { explanation?: string; plan?: PlanStepInput[]; workflow?: WorkflowPlanInput };
|
|
317
355
|
if (input.workflow !== undefined) {
|
|
318
|
-
|
|
356
|
+
const savedBinding = workflowBinding(ctx.sessionManager.getBranch());
|
|
357
|
+
if (!owner && (input.workflow.event !== undefined || input.workflow.changes !== undefined ||
|
|
358
|
+
input.workflow.decision !== undefined || input.workflow.profiling !== undefined || !savedBinding)) {
|
|
359
|
+
throw new Error(savedBinding
|
|
360
|
+
? `${savedBinding.owner} plan is a read-only handoff. Reinvoke /skill:${savedBinding.owner} before updating its workflow plan.`
|
|
361
|
+
: "No workflow owns the task plan; send plan instead of workflow.");
|
|
362
|
+
}
|
|
319
363
|
if (input.plan !== undefined) throw new Error("Send either plan or workflow, not both.");
|
|
320
364
|
const { path, ...transition } = input.workflow;
|
|
321
365
|
if (transition.event === undefined && (transition.changes !== undefined || transition.decision !== undefined || transition.profiling !== undefined)) {
|
|
322
366
|
throw new Error("workflow.event is required when sending changes, decision, or profiling.");
|
|
323
367
|
}
|
|
324
|
-
let binding =
|
|
368
|
+
let binding = savedBinding && (!owner || savedBinding.owner === owner) ? savedBinding : null;
|
|
325
369
|
if (path !== undefined) {
|
|
370
|
+
if (!owner && path !== binding?.path) {
|
|
371
|
+
throw new Error(`${binding!.owner} plan is a read-only handoff; only its bound path can be reloaded. Reinvoke /skill:${binding!.owner} to bind a workflow plan.`);
|
|
372
|
+
}
|
|
326
373
|
if (transition.revision === undefined) throw new Error("workflow.revision is required with workflow.path.");
|
|
327
374
|
const plan = readRushPlan(path, ctx.cwd);
|
|
375
|
+
if (!owner && plan.runId !== binding!.runId) throw new Error("Rush run identity changed.");
|
|
328
376
|
if (plan.planRevision !== transition.revision) {
|
|
329
377
|
if (transition.event === undefined) {
|
|
330
378
|
rushPlan = null;
|
|
@@ -333,10 +381,10 @@ export default function planExtension(pi: ExtensionAPI): void {
|
|
|
333
381
|
}
|
|
334
382
|
throw new Error(`Rush plan revision mismatch: expected ${transition.revision}, found ${plan.planRevision}.`);
|
|
335
383
|
}
|
|
336
|
-
binding = { owner, path, runId: plan.runId };
|
|
384
|
+
binding = { owner: owner ?? binding!.owner, path, runId: plan.runId };
|
|
337
385
|
if (transition.event === undefined) {
|
|
338
|
-
displayWorkflowPlan(plan, binding);
|
|
339
|
-
return { content: [{ type: "text", text: `Showing ${owner} rev ${plan.planRevision}: ${plan.issues.length} units
|
|
386
|
+
displayWorkflowPlan(plan, binding, !owner);
|
|
387
|
+
return { content: [{ type: "text", text: `Showing ${binding.owner} rev ${plan.planRevision}: ${plan.issues.length} units.${owner ? "" : " Read-only handoff."}` }], details: { ok: true, runId: plan.runId, revision: plan.planRevision, ...(!owner ? { readOnly: true } : {}) } };
|
|
340
388
|
}
|
|
341
389
|
}
|
|
342
390
|
if (!binding) throw new Error(`No ${owner} plan is bound; send workflow.path and workflow.revision to update_plan first.`);
|
|
@@ -390,13 +438,16 @@ export default function planExtension(pi: ExtensionAPI): void {
|
|
|
390
438
|
parameters: Type.Object({}),
|
|
391
439
|
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
392
440
|
const owner = workflowPlanOwner(ctx);
|
|
393
|
-
|
|
441
|
+
const binding = workflowBinding(ctx.sessionManager.getBranch());
|
|
442
|
+
if (owner || binding) {
|
|
394
443
|
const workflowPlan = loadRushPlan(ctx);
|
|
444
|
+
const planOwner = owner ?? binding!.owner;
|
|
395
445
|
return {
|
|
396
446
|
content: [{ type: "text", text: workflowPlan
|
|
397
|
-
? renderRushPlan(workflowPlan, 160, true, undefined, owner).join("\n")
|
|
398
|
-
: `${owner} owns the task plan; ${rushError ?? "consult its persisted workflow state"}.`
|
|
399
|
-
|
|
447
|
+
? renderRushPlan(workflowPlan, 160, true, undefined, planOwner, !owner).join("\n")
|
|
448
|
+
: owner ? `${owner} owns the task plan; ${rushError ?? "consult its persisted workflow state"}.`
|
|
449
|
+
: `${planOwner} read-only handoff unavailable: ${rushError ?? "not bound"}.` }],
|
|
450
|
+
details: { hasPlan: !!workflowPlan, plan: workflowPlan, progress: null, workflowOwner: planOwner, ...(!owner ? { readOnly: true } : {}) },
|
|
400
451
|
};
|
|
401
452
|
}
|
|
402
453
|
if (!currentPlan) {
|
|
@@ -424,6 +475,7 @@ export default function planExtension(pi: ExtensionAPI): void {
|
|
|
424
475
|
ctx.ui.notify("The workflow owns its plan; /plan clear cannot change it.", "warning");
|
|
425
476
|
return;
|
|
426
477
|
}
|
|
478
|
+
clearWorkflowPlan();
|
|
427
479
|
clearPlan();
|
|
428
480
|
ctx.ui.notify("Plan cleared.", "info");
|
|
429
481
|
return;
|
|
@@ -436,13 +488,17 @@ export default function planExtension(pi: ExtensionAPI): void {
|
|
|
436
488
|
setDisplayMode("auto");
|
|
437
489
|
return;
|
|
438
490
|
}
|
|
491
|
+
if (input === "expand" || input === "collapse") {
|
|
492
|
+
setExpanded(input === "expand");
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
439
495
|
const pinMode = input.match(/^pin\s+(auto|on|off)$/)?.[1] as PlanDisplayMode | undefined;
|
|
440
496
|
if (pinMode) {
|
|
441
497
|
setDisplayMode(pinMode);
|
|
442
498
|
ctx.ui.notify(`Plan pin mode: ${pinMode}.`, "info");
|
|
443
499
|
return;
|
|
444
500
|
}
|
|
445
|
-
ctx.ui.notify("Usage: /plan [clear|hide|show|pin auto|pin on|pin off]", "warning");
|
|
501
|
+
ctx.ui.notify("Usage: /plan [clear|hide|show|expand|collapse|pin auto|pin on|pin off]", "warning");
|
|
446
502
|
},
|
|
447
503
|
});
|
|
448
504
|
|
|
@@ -454,7 +510,7 @@ export default function planExtension(pi: ExtensionAPI): void {
|
|
|
454
510
|
restore(ctx);
|
|
455
511
|
});
|
|
456
512
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
457
|
-
if (workflowPlanOwner(ctx)) return;
|
|
513
|
+
if (workflowPlanOwner(ctx) || workflowBinding(ctx.sessionManager.getBranch())) return;
|
|
458
514
|
if (!currentPlan) return;
|
|
459
515
|
return {
|
|
460
516
|
systemPrompt: `${event.systemPrompt}\n\n${planPrompt(currentPlan, activeDelegationMode(pi))}`,
|
|
@@ -22,6 +22,24 @@ export interface PlanPresentation {
|
|
|
22
22
|
|
|
23
23
|
export const plainPlanTheme: PlanRenderTheme = { fg: (_color, value) => value };
|
|
24
24
|
|
|
25
|
+
export const COMPACT_PLAN_ROWS = 5;
|
|
26
|
+
|
|
27
|
+
function compactRowIndices(rows: PlanRow[]): number[] {
|
|
28
|
+
if (rows.length <= COMPACT_PLAN_ROWS) return rows.map((_, index) => index);
|
|
29
|
+
let active = rows.flatMap((row, index) =>
|
|
30
|
+
["in_progress", "implemented", "reviewed"].includes(row.status) ? [index] : []);
|
|
31
|
+
if (!active.length) active = rows.flatMap((row, index) =>
|
|
32
|
+
row.status === "blocked" || row.status === "failed" ? [index] : []);
|
|
33
|
+
const firstPending = rows.findIndex((row) => row.status === "pending");
|
|
34
|
+
const anchor = active.at(-1) ?? (firstPending < 0 ? rows.length - 1 : firstPending);
|
|
35
|
+
const start = Math.max(0, Math.min(anchor - 2, rows.length - COMPACT_PLAN_ROWS));
|
|
36
|
+
const indices = new Set(active.slice(-COMPACT_PLAN_ROWS));
|
|
37
|
+
for (let index = start; index < start + COMPACT_PLAN_ROWS && indices.size < COMPACT_PLAN_ROWS; index++) {
|
|
38
|
+
indices.add(index);
|
|
39
|
+
}
|
|
40
|
+
return [...indices].sort((a, b) => a - b);
|
|
41
|
+
}
|
|
42
|
+
|
|
25
43
|
export function statusStyle(status: DisplayStatus): { glyph: string; color: string; label: string } {
|
|
26
44
|
switch (status) {
|
|
27
45
|
case "completed": return { glyph: "✓", color: "success", label: "" };
|
|
@@ -43,7 +61,7 @@ export function planText(value: string): string {
|
|
|
43
61
|
|
|
44
62
|
export function renderPlanPresentation(
|
|
45
63
|
plan: PlanPresentation, width: number, theme: PlanRenderTheme,
|
|
46
|
-
full = false, selectedIndex = -1,
|
|
64
|
+
full = false, selectedIndex = -1, expanded = false,
|
|
47
65
|
): string[] {
|
|
48
66
|
const size = Math.max(1, Math.floor(width));
|
|
49
67
|
const count = (status: DisplayStatus) => plan.rows.filter((row) => row.status === status).length;
|
|
@@ -65,7 +83,15 @@ export function renderPlanPresentation(
|
|
|
65
83
|
const labelWidth = Math.min(12, Math.max(4, ...plan.rows.map((row) => visibleWidth(planText(row.label)))));
|
|
66
84
|
const statusWidth = Math.min(24, Math.max(0, ...plan.rows.map((row) =>
|
|
67
85
|
visibleWidth(planText(row.statusLabel ?? statusStyle(row.status).label)))));
|
|
68
|
-
|
|
86
|
+
const indices = full || expanded ? plan.rows.map((_, index) => index) : compactRowIndices(plan.rows);
|
|
87
|
+
let previous = -1;
|
|
88
|
+
for (const index of indices) {
|
|
89
|
+
if (index > previous + 1) {
|
|
90
|
+
const omitted = index - previous - 1;
|
|
91
|
+
lines.push(theme.fg("dim", ` ... ${omitted} ${previous < 0 ? "earlier " : ""}${omitted === 1 ? "step" : "steps"}`));
|
|
92
|
+
}
|
|
93
|
+
previous = index;
|
|
94
|
+
const row = plan.rows[index]!;
|
|
69
95
|
const style = statusStyle(row.status);
|
|
70
96
|
const label = truncateToWidth(planText(row.label), labelWidth);
|
|
71
97
|
const prefix = (index === selectedIndex ? theme.fg("accent", "› ") : " ") +
|
|
@@ -94,6 +120,10 @@ export function renderPlanPresentation(
|
|
|
94
120
|
}
|
|
95
121
|
}
|
|
96
122
|
}
|
|
123
|
+
if (previous < plan.rows.length - 1) {
|
|
124
|
+
const omitted = plan.rows.length - previous - 1;
|
|
125
|
+
lines.push(theme.fg("dim", ` ... ${omitted} more ${omitted === 1 ? "step" : "steps"}`));
|
|
126
|
+
}
|
|
97
127
|
if (full) lines.push("", theme.fg("dim", "↑↓ navigate · esc / ← back"));
|
|
98
128
|
return lines.map((line) => truncateToWidth(line, size));
|
|
99
129
|
}
|
|
@@ -15,8 +15,8 @@ function presentPlan(plan: PlanSnapshot): PlanPresentation {
|
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export function renderCompactPlan(plan: PlanSnapshot, width: number, theme: PlanRenderTheme): string[] {
|
|
19
|
-
return renderPlanPresentation(presentPlan(plan), width, theme);
|
|
18
|
+
export function renderCompactPlan(plan: PlanSnapshot, width: number, theme: PlanRenderTheme, expanded = false): string[] {
|
|
19
|
+
return renderPlanPresentation(presentPlan(plan), width, theme, false, -1, expanded);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export function renderFullPlan(plan: PlanSnapshot, width: number, theme: PlanRenderTheme, selectedIndex = -1): string[] {
|
|
@@ -16,6 +16,7 @@ interface SessionEntryLike {
|
|
|
16
16
|
export interface ReconstructedPlanState {
|
|
17
17
|
plan: PlanSnapshot | null;
|
|
18
18
|
displayMode: PlanDisplayMode;
|
|
19
|
+
expanded: boolean;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
function nowSeconds(): number {
|
|
@@ -176,13 +177,15 @@ export function planDisplayEntry(mode: PlanDisplayMode, at = nowSeconds()): Plan
|
|
|
176
177
|
export function reconstructPlanState(entries: Iterable<SessionEntryLike>): ReconstructedPlanState {
|
|
177
178
|
let plan: PlanSnapshot | null = null;
|
|
178
179
|
let displayMode: PlanDisplayMode = "auto";
|
|
180
|
+
let expanded = false;
|
|
179
181
|
for (const entry of entries) {
|
|
180
182
|
if (entry.type !== "custom" || entry.customType !== EXTENSION_NAME || !isPlanEntry(entry.data)) continue;
|
|
181
183
|
if (entry.data.kind === "set") plan = entry.data.plan;
|
|
182
184
|
else if (entry.data.kind === "clear") plan = null;
|
|
183
|
-
else displayMode = entry.data.mode;
|
|
185
|
+
else if (entry.data.kind === "display") displayMode = entry.data.mode;
|
|
186
|
+
else expanded = entry.data.expanded;
|
|
184
187
|
}
|
|
185
|
-
return { plan, displayMode };
|
|
188
|
+
return { plan, displayMode, expanded };
|
|
186
189
|
}
|
|
187
190
|
|
|
188
191
|
function isPlanEntry(value: unknown): value is PlanEntry {
|
|
@@ -190,6 +193,7 @@ function isPlanEntry(value: unknown): value is PlanEntry {
|
|
|
190
193
|
const candidate = value as Partial<PlanEntry>;
|
|
191
194
|
if (candidate.version !== 1) return false;
|
|
192
195
|
if (candidate.kind === "clear") return true;
|
|
196
|
+
if (candidate.kind === "expansion") return typeof candidate.expanded === "boolean";
|
|
193
197
|
if (candidate.kind === "display") {
|
|
194
198
|
return candidate.mode === "auto" || candidate.mode === "on" || candidate.mode === "off" || candidate.mode === "hidden";
|
|
195
199
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Component } from "@earendil-works/pi-tui";
|
|
2
|
+
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
|
|
4
|
+
// Structural typing keeps keyboard-only Pi versions compatible with the fullscreen mouse API.
|
|
5
|
+
interface PlanMouseEvent {
|
|
6
|
+
type: string;
|
|
7
|
+
button: string;
|
|
8
|
+
x: number;
|
|
9
|
+
y: number;
|
|
10
|
+
width: number;
|
|
11
|
+
shift?: boolean;
|
|
12
|
+
alt?: boolean;
|
|
13
|
+
ctrl?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function createPlanWidget(
|
|
17
|
+
render: (width: number) => string[],
|
|
18
|
+
isFoldable: () => boolean,
|
|
19
|
+
isExpanded: () => boolean,
|
|
20
|
+
toggle: () => void,
|
|
21
|
+
): Component & { handleMouse(event: PlanMouseEvent): { handled: true; render: boolean } | undefined } {
|
|
22
|
+
let lines: string[] = [];
|
|
23
|
+
return {
|
|
24
|
+
render(width) {
|
|
25
|
+
lines = render(width);
|
|
26
|
+
if (lines.length && isFoldable()) {
|
|
27
|
+
lines = [truncateToWidth(`${isExpanded() ? "▾" : "▸"} ${lines[0]}`, width), ...lines.slice(1)];
|
|
28
|
+
}
|
|
29
|
+
return lines.length ? ["", ...lines] : [];
|
|
30
|
+
},
|
|
31
|
+
handleMouse(event) {
|
|
32
|
+
if (!isFoldable() || (event.type !== "click" && event.type !== "press") || event.button !== "left" || event.shift || event.alt || event.ctrl ||
|
|
33
|
+
event.x < 0 || event.x >= event.width || event.y < 1 || event.y > lines.length) return undefined;
|
|
34
|
+
// Pi synthesizes click only for a component that handled the initial press.
|
|
35
|
+
if (event.type === "press") return { handled: true, render: false };
|
|
36
|
+
toggle();
|
|
37
|
+
return { handled: true, render: true };
|
|
38
|
+
},
|
|
39
|
+
invalidate() { lines = []; },
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -39,4 +39,5 @@ export interface PlanProgress {
|
|
|
39
39
|
export type PlanEntry =
|
|
40
40
|
| { version: 1; kind: "set"; plan: PlanSnapshot; at: number }
|
|
41
41
|
| { version: 1; kind: "clear"; at: number }
|
|
42
|
-
| { version: 1; kind: "display"; mode: PlanDisplayMode; at: number }
|
|
42
|
+
| { version: 1; kind: "display"; mode: PlanDisplayMode; at: number }
|
|
43
|
+
| { version: 1; kind: "expansion"; expanded: boolean; at: number };
|
|
@@ -113,7 +113,7 @@ export function workflowBinding(entries: Iterable<{ type: string; customType?: s
|
|
|
113
113
|
for (const entry of entries) {
|
|
114
114
|
if (entry.type === "custom" && entry.customType === "pi-better-workflow") {
|
|
115
115
|
const data = entry.data;
|
|
116
|
-
if (isRecord(data) && data.version === 1 &&
|
|
116
|
+
if (isRecord(data) && data.version === 1 && data.kind === "set") binding = null;
|
|
117
117
|
}
|
|
118
118
|
if (entry.type !== "custom" || entry.customType !== WORKFLOW_PLAN_ENTRY) continue;
|
|
119
119
|
const data = entry.data;
|
|
@@ -148,7 +148,7 @@ function unitStatus(unit: RushUnit): DisplayStatus {
|
|
|
148
148
|
return unit.reviewedHead === unit.headSha ? "reviewed" : "implemented";
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
function presentRushPlan(plan: RushPlan, owner: string): PlanPresentation {
|
|
151
|
+
function presentRushPlan(plan: RushPlan, owner: string, readOnly = false): PlanPresentation {
|
|
152
152
|
// The warehouse canary only applies to data work; hide it otherwise.
|
|
153
153
|
const stages = ["explore", "implement", "review", "ci", ...(plan.warehouseCanaryRequired ? ["canary"] : [])];
|
|
154
154
|
const fleet = stages.map((stage) => {
|
|
@@ -158,6 +158,7 @@ function presentRushPlan(plan: RushPlan, owner: string): PlanPresentation {
|
|
|
158
158
|
return { color: style.color, text: ` ${stage} ${style.glyph}${display === "unknown" ? ` ${planText(status)}` : ""}` };
|
|
159
159
|
});
|
|
160
160
|
const metadata = [[{ color: "dim", text: `${planText(owner)} · rev ${plan.planRevision} ·` }, ...fleet]];
|
|
161
|
+
if (readOnly) metadata.push([{ color: "warning", text: "read-only handoff" }]);
|
|
161
162
|
if (plan.spec?.title) metadata.push([{ color: "dim", text: plan.spec.title }]);
|
|
162
163
|
return {
|
|
163
164
|
metadata,
|
|
@@ -180,9 +181,11 @@ export function renderRushPlan(
|
|
|
180
181
|
full = false,
|
|
181
182
|
fg?: (color: string, value: string) => string,
|
|
182
183
|
owner = "rush-issues",
|
|
184
|
+
readOnly = false,
|
|
185
|
+
expanded = false,
|
|
183
186
|
): string[] {
|
|
184
187
|
const theme = fg ? { fg } : plainPlanTheme;
|
|
185
|
-
return renderPlanPresentation(presentRushPlan(plan, owner), width, theme, full);
|
|
188
|
+
return renderPlanPresentation(presentRushPlan(plan, owner, readOnly), width, theme, full, -1, expanded);
|
|
186
189
|
}
|
|
187
190
|
|
|
188
191
|
export function createRushPlanComponent(
|
|
@@ -190,7 +193,8 @@ export function createRushPlanComponent(
|
|
|
190
193
|
onClose: () => void,
|
|
191
194
|
fg?: (color: string, value: string) => string,
|
|
192
195
|
owner = "rush-issues",
|
|
196
|
+
readOnly = false,
|
|
193
197
|
): Component {
|
|
194
198
|
const theme = fg ? { fg } : plainPlanTheme;
|
|
195
|
-
return createPlanPresentationComponent(presentRushPlan(plan, owner), theme, onClose);
|
|
199
|
+
return createPlanPresentationComponent(presentRushPlan(plan, owner, readOnly), theme, onClose);
|
|
196
200
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-better-harness",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.2",
|
|
4
4
|
"description": "Pi extension bundle for a write sandbox, subagents, background tasks, SSH, goals, and structured plans.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"pi-better-background-tasks": "0.8.0",
|
|
57
57
|
"pi-better-goal": "0.8.2",
|
|
58
|
-
"pi-better-plan": "0.
|
|
58
|
+
"pi-better-plan": "0.8.0",
|
|
59
59
|
"pi-better-sandbox": "0.12.0",
|
|
60
60
|
"pi-better-ssh": "0.1.2",
|
|
61
61
|
"pi-better-subagents": "0.17.0",
|