@gavin-hu/my-pi-agent 0.12.0 → 0.13.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/README.md +28 -21
- package/README.zh-CN.md +30 -22
- package/extensions/bytefly/README.md +139 -67
- package/extensions/bytefly/chrome.ts +7 -2
- package/extensions/bytefly/doctor/register.ts +6 -0
- package/extensions/bytefly/doctor/runtime.ts +36 -6
- package/extensions/bytefly/footer/collect/snapshot.ts +12 -2
- package/extensions/bytefly/footer/config.ts +7 -1
- package/extensions/bytefly/footer/delegation.ts +100 -0
- package/extensions/bytefly/footer/register.ts +13 -2
- package/extensions/bytefly/footer/render/lines.ts +2 -0
- package/extensions/bytefly/footer/render/segments.ts +27 -0
- package/extensions/bytefly/footer/types.ts +11 -0
- package/extensions/bytefly/index.ts +4 -3
- package/extensions/bytefly/notify/config.ts +2 -0
- package/extensions/bytefly/notify/format.ts +19 -1
- package/extensions/bytefly/notify/register.ts +37 -13
- package/extensions/bytefly/notify/types.ts +2 -0
- package/extensions/bytefly/panel/commands.ts +34 -13
- package/extensions/bytefly/panel/settings.ts +98 -16
- package/extensions/bytefly/update/cache.ts +73 -0
- package/extensions/bytefly/update/format.ts +65 -10
- package/extensions/bytefly/update/register.ts +99 -13
- package/extensions/bytefly/update/state.ts +13 -1
- package/extensions/document/README.md +22 -12
- package/extensions/document/config.ts +4 -0
- package/extensions/document/deadline.ts +61 -0
- package/extensions/document/tool.ts +13 -2
- package/extensions/file-browse/README.md +12 -8
- package/extensions/file-browse/client.ts +6 -7
- package/extensions/file-browse/files.ts +0 -4
- package/extensions/file-browse/git.ts +9 -10
- package/extensions/file-browse/html.ts +17 -7
- package/extensions/file-browse/router.ts +24 -15
- package/extensions/goal/README.md +7 -1
- package/extensions/goal/schema.ts +6 -2
- package/extensions/goal/types.ts +6 -2
- package/extensions/job/README.md +16 -12
- package/extensions/job/runtime.ts +23 -6
- package/extensions/job/status.ts +4 -2
- package/extensions/memory/README.md +55 -13
- package/extensions/memory/commands.ts +20 -2
- package/extensions/memory/doctor.ts +137 -0
- package/extensions/memory/format.ts +24 -7
- package/extensions/memory/runtime.ts +49 -0
- package/extensions/memory/store.ts +31 -0
- package/extensions/memory/tui.ts +136 -12
- package/extensions/plan/README.md +60 -29
- package/extensions/plan/commands.ts +67 -8
- package/extensions/plan/index.ts +35 -11
- package/extensions/plan/plan-index.ts +234 -0
- package/extensions/plan/plan-text.ts +95 -0
- package/extensions/plan/plans.ts +90 -1
- package/extensions/plan/review.ts +12 -9
- package/extensions/plan/runtime.ts +72 -27
- package/extensions/plan/steps.ts +35 -25
- package/extensions/plan/todos.ts +24 -2
- package/extensions/plan/tools.ts +124 -24
- package/extensions/plan/tui.ts +7 -28
- package/extensions/plan/types.ts +9 -0
- package/extensions/plan/web-page.ts +2 -2
- package/extensions/plan/web.ts +12 -11
- package/extensions/rewind/README.md +28 -8
- package/extensions/rewind/commands.ts +1 -1
- package/extensions/rewind/format.ts +4 -4
- package/extensions/rewind/git.ts +56 -18
- package/extensions/rewind/index.ts +3 -5
- package/extensions/rewind/path-picker.ts +198 -0
- package/extensions/rewind/paths.ts +128 -0
- package/extensions/rewind/restore.ts +69 -28
- package/extensions/rewind/rewind.ts +80 -3
- package/extensions/rewind/runtime.ts +13 -5
- package/extensions/rewind/snapshot-run.ts +5 -2
- package/extensions/rewind/types.ts +2 -0
- package/extensions/subagent/README.md +7 -1
- package/extensions/subagent/accounting.ts +34 -0
- package/extensions/subagent/index.ts +23 -1
- package/extensions/subagent/stream.ts +2 -0
- package/extensions/subagent/types.ts +2 -0
- package/extensions/watch/README.md +8 -3
- package/extensions/watch/index.ts +19 -0
- package/extensions/web-access/README.md +43 -13
- package/extensions/web-access/charset.ts +58 -0
- package/extensions/web-access/fetch/find.ts +11 -7
- package/extensions/web-access/fetch/format.ts +64 -18
- package/extensions/web-access/fetch/page.ts +5 -1
- package/extensions/web-access/fetch/tool.ts +19 -18
- package/extensions/web-access/http.ts +49 -8
- package/extensions/web-access/search/facts.ts +38 -0
- package/extensions/web-access/search/tool.ts +13 -0
- package/extensions/web-access/text.ts +50 -11
- package/extensions/worktree/README.md +74 -15
- package/extensions/worktree/config.ts +11 -0
- package/extensions/worktree/exit.ts +35 -3
- package/extensions/worktree/gaps.ts +74 -0
- package/extensions/worktree/guard.ts +50 -4
- package/extensions/worktree/handoff.ts +55 -0
- package/extensions/worktree/index.ts +12 -2
- package/extensions/worktree/runtime.ts +20 -0
- package/extensions/worktree/status.ts +15 -2
- package/extensions/worktree/tools.ts +3 -2
- package/lib/README.md +10 -8
- package/lib/events.ts +86 -1
- package/lib/goal-state.ts +95 -0
- package/lib/requirements.ts +282 -62
- package/lib/tui/README.md +91 -0
- package/lib/tui/glyphs.ts +1 -0
- package/lib/web/layout.ts +12 -2
- package/package.json +5 -3
- package/themes/bytefly-dark.json +1 -1
- package/assets/chrome.png +0 -0
- package/assets/file-browse.png +0 -0
- package/assets/job.png +0 -0
- package/assets/plan-review.png +0 -0
- package/assets/question.png +0 -0
- package/assets/rewind.png +0 -0
- package/assets/todo.png +0 -0
- package/assets/tool-renderings.png +0 -0
- package/assets/tools/ask_user_question-cancelled.png +0 -0
- package/assets/tools/ask_user_question.png +0 -0
- package/assets/tools/bash.png +0 -0
- package/assets/tools/edit.png +0 -0
- package/assets/tools/enter_plan_mode.png +0 -0
- package/assets/tools/enter_worktree.png +0 -0
- package/assets/tools/exit_plan_mode.png +0 -0
- package/assets/tools/exit_worktree.png +0 -0
- package/assets/tools/find.png +0 -0
- package/assets/tools/goal.png +0 -0
- package/assets/tools/grep.png +0 -0
- package/assets/tools/job.png +0 -0
- package/assets/tools/list_worktrees.png +0 -0
- package/assets/tools/ls.png +0 -0
- package/assets/tools/memory-long-subject.png +0 -0
- package/assets/tools/memory.png +0 -0
- package/assets/tools/notice.png +0 -0
- package/assets/tools/open_path.png +0 -0
- package/assets/tools/prune_worktrees.png +0 -0
- package/assets/tools/read.png +0 -0
- package/assets/tools/read_doc-running.png +0 -0
- package/assets/tools/read_doc.png +0 -0
- package/assets/tools/run_doctor.png +0 -0
- package/assets/tools/send_wechat.png +0 -0
- package/assets/tools/set_plan_steps.png +0 -0
- package/assets/tools/subagent.png +0 -0
- package/assets/tools/todo.png +0 -0
- package/assets/tools/turn-rule.png +0 -0
- package/assets/tools/user-message.png +0 -0
- package/assets/tools/watch.png +0 -0
- package/assets/tools/web_fetch-rejected.png +0 -0
- package/assets/tools/web_fetch.png +0 -0
- package/assets/tools/web_search.png +0 -0
- package/assets/tools/write.png +0 -0
- package/assets/tools/write_plan.png +0 -0
|
@@ -42,19 +42,28 @@ How the `write_plan` and `exit_plan_mode` calls and their results look in the tr
|
|
|
42
42
|
`<repo-root>/.pi/plans/<YYYY-MM-DD-HHmm>-<slug>.md` (or `<agent-dir>/plans`
|
|
43
43
|
outside a repository). The plans directory is self-ignoring, so plan files
|
|
44
44
|
never show up as untracked files or in `rewind` snapshots.
|
|
45
|
+
- **A discoverable index.** `/plan list` walks the plans directory and shows
|
|
46
|
+
every plan with its title, modified time, and step progress, newest first — in
|
|
47
|
+
the TUI as a docked list where `Enter` opens the plan's review, elsewhere as a
|
|
48
|
+
notice with each plan's path. The directory stays the index: no registry file,
|
|
49
|
+
one bounded read per plan.
|
|
45
50
|
- **A shape that reads.** `write_plan` documents a canonical outline — a
|
|
46
51
|
`# <Title>`, then `## Goal`, `## Context`, `## Approach`, `## Steps`,
|
|
47
52
|
`## Risks`, and `## Verification` — and the model is guided to follow it. This
|
|
48
53
|
is guidance only: any markdown is accepted, stored, and rendered as written.
|
|
49
54
|
- **Trackable steps.** Steps are written as `- [ ]` checkboxes in the `## Steps`
|
|
50
|
-
section; approval seeds them into `todo
|
|
55
|
+
section; approval seeds them into `todo`, with the `## Verification` section as
|
|
56
|
+
the last item so the definition of done travels with the work. A plan with more
|
|
57
|
+
than 50 top-level steps is refused rather than truncated, so approval never
|
|
58
|
+
drops a step you reviewed.
|
|
51
59
|
- **A navigable review.** The review screen splits the body at its headings,
|
|
52
60
|
names the section you are in, and jumps between sections with `n`/`p`.
|
|
53
61
|
- **What changed since the last review.** After a refinement, the review screen
|
|
54
62
|
names how many sections changed, marks the current one in the scroll row, and
|
|
55
63
|
the browser outline dots each changed section, so a re-review does not mean
|
|
56
|
-
re-reading the whole plan. The baseline is
|
|
57
|
-
|
|
64
|
+
re-reading the whole plan. The baseline is one digest per heading section of
|
|
65
|
+
what the previous review showed, kept as a session entry, so a resumed session
|
|
66
|
+
or a `/tree` restore marks only what changed on its own branch.
|
|
58
67
|
- **Kept current.** After approval, `set_plan_steps` flips the checkboxes of
|
|
59
68
|
the steps you name in the file, so the plan mirrors what has actually been
|
|
60
69
|
done. Pass the step's text as `expected` and an update whose step number moved
|
|
@@ -83,19 +92,28 @@ How the `write_plan` and `exit_plan_mode` calls and their results look in the tr
|
|
|
83
92
|
nested sub-bullets are treated as notes. The list is a merge, not a
|
|
84
93
|
replacement: existing items keep their place and status, a step whose text is
|
|
85
94
|
already there is skipped, and the tool's item cap defers the rest instead of
|
|
86
|
-
dropping anything.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
95
|
+
dropping anything. The plan's `## Verification` section follows as the **last**
|
|
96
|
+
todo item, summarized on one line (`Verify: …`) — it is not a plan step, so it is
|
|
97
|
+
ticked in the todo list rather than with `set_plan_steps`, and it never counts
|
|
98
|
+
against the 50-step cap. A plan without a `Steps` heading keeps the older rule —
|
|
99
|
+
the shallowest list in the document — so prose bullets in a plan that follows the
|
|
100
|
+
outline never become todos, and its verification bullets are already covered.
|
|
101
|
+
- **Goal-aware.** While planning, the injected context restates an active
|
|
102
|
+
session goal as one bullet, so the plan is designed against the objective
|
|
103
|
+
instead of beside it. On approval, when the session has no goal at all, the user
|
|
104
|
+
is offered one confirm to record the plan's title as the goal — never replacing
|
|
105
|
+
an existing goal, and with no extra model call.
|
|
106
|
+
- **Branch-aware.** The enabled flag and the review baseline are custom session
|
|
107
|
+
entries, so `/resume` and `/tree` restore the right mode and keep the
|
|
108
|
+
changed-section markers.
|
|
91
109
|
|
|
92
110
|
## Tools
|
|
93
111
|
|
|
94
112
|
| Tool | Active | Purpose |
|
|
95
113
|
|---|---|---|
|
|
96
114
|
| `enter_plan_mode` | normal mode | Ask to enter plan mode; empty schema, confirmed with `ctx.ui.confirm`. |
|
|
97
|
-
| `write_plan` | plan mode | Save the plan: `title` (file-name slug), `content` (full markdown in the documented outline), optional `plan_path` to overwrite a refinement. Returns the absolute path. |
|
|
98
|
-
| `exit_plan_mode` | plan mode | Read the plan file (`plan_path`) and present it for approval. |
|
|
115
|
+
| `write_plan` | plan mode | Save the plan: `title` (file-name slug), `content` (full markdown in the documented outline), optional `plan_path` to overwrite a refinement. Returns the absolute path. A plan with more than 50 top-level steps in its `Steps` section is refused, with the cap and the fix named, and nothing is written. |
|
|
116
|
+
| `exit_plan_mode` | plan mode | Read the plan file (`plan_path`) and present it for approval. Refuses a file whose step list is over the cap, before the review screen. |
|
|
99
117
|
| `set_plan_steps` | normal mode | Keep an approved plan current: set the checkboxes of the given top-level steps (`updates`, each a 1-based `step` in the order `exit_plan_mode` listed, plus `status`, and optionally `expected` — the step's text, so a reordered plan is refused) to `pending` or `completed` in one write. Only the checkboxes change, and a reopened step drops its `[DONE:n]` marker. |
|
|
100
118
|
|
|
101
119
|
### Commands and flags
|
|
@@ -105,6 +123,7 @@ How the `write_plan` and `exit_plan_mode` calls and their results look in the tr
|
|
|
105
123
|
| `enter_plan_mode` tool | the model, before a non-trivial task | `ctx.ui.confirm` |
|
|
106
124
|
| `/plan` | the user | immediate |
|
|
107
125
|
| `/plan <prompt>` | the user, with the task | immediate |
|
|
126
|
+
| `/plan list` | the user, browsing the plans directory | immediate |
|
|
108
127
|
| `/plan review [path]` | the user, reviewing a plan in the browser | immediate |
|
|
109
128
|
| `Ctrl+Alt+P` | the user | immediate |
|
|
110
129
|
| `--plan` | the user, at launch | immediate |
|
|
@@ -143,7 +162,7 @@ actions (`a approve & run` in `success`, `r refine` in `accent`).
|
|
|
143
162
|
mounted and opens an inline editor beneath it (`Enter` submits, an empty buffer
|
|
144
163
|
does nothing, `Esc` returns to the plan); `Esc` keeps planning and tells the
|
|
145
164
|
model the plan was not approved. When the plan changed since the previous review
|
|
146
|
-
|
|
165
|
+
on this branch, a summary line under the title names how many sections changed
|
|
147
166
|
and the scroll row appends `· changed` to the current section. Scroll with `↑`/`↓` or `j`/`k`, `space`/`b` or
|
|
148
167
|
`PgUp`/`PgDn`, `d`/`u`, `g`/`G`, and the mouse wheel. Press `n`/`p` to jump to
|
|
149
168
|
the next or previous heading; the scroll row names the current section when the
|
|
@@ -321,7 +340,7 @@ file and returns them to the model.
|
|
|
321
340
|
links, an active-section highlight, and a per-section comment count; the panel
|
|
322
341
|
above it reports the `Steps` section's progress (`n/m`, the same list approval
|
|
323
342
|
seeds) and total comments. A
|
|
324
|
-
section that changed since the previous review
|
|
343
|
+
section that changed since the previous review on this branch carries a dot
|
|
325
344
|
(its `title` and accessible label say so); an unchanged plan shows none.
|
|
326
345
|
- **Inline threads.** A comment renders directly under the block it anchors to,
|
|
327
346
|
labelled and indented as a thread, so it reads in place and the plan keeps one
|
|
@@ -386,14 +405,15 @@ file and returns them to the model.
|
|
|
386
405
|
Git Bash (`bash`), which must be installed for any shell command to run.
|
|
387
406
|
- The read-only guarantee is a guard rail against accidental writes, not a
|
|
388
407
|
security boundary (see [Security](#security)).
|
|
389
|
-
- The plans directory is the only index
|
|
390
|
-
plan
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
shortened in the todo list and
|
|
396
|
-
|
|
408
|
+
- The plans directory is the only index. `/plan list` walks it on every open and
|
|
409
|
+
keeps no registry, so it sees an external edit or a hand-copied plan
|
|
410
|
+
immediately; it lists a file over the 256 KiB plan limit without reading it,
|
|
411
|
+
and does not index the agent directory's plans while a worktree is active.
|
|
412
|
+
- Approval tracks at most 50 steps: `write_plan` refuses a larger plan, and
|
|
413
|
+
`exit_plan_mode` refuses to present one, so no reviewed step is silently
|
|
414
|
+
dropped. A step longer than 200 characters is shortened in the todo list and
|
|
415
|
+
the approval listing — a display rule only, because the plan file always keeps
|
|
416
|
+
the full text.
|
|
397
417
|
- Browser review needs a local browser: the server is loopback-only, so a remote
|
|
398
418
|
turn cannot drive it.
|
|
399
419
|
- A browser that never connects leaves the review waiting until the user
|
|
@@ -415,9 +435,9 @@ file and returns them to the model.
|
|
|
415
435
|
| Integration point | How |
|
|
416
436
|
|---|---|
|
|
417
437
|
| Tools | `enter_plan_mode`, `write_plan`, `exit_plan_mode`, and `set_plan_steps` registered with `executionMode: "sequential"`. `enter_plan_mode` is active normally with `readOnlyHint: true`; `write_plan` (`readOnlyHint: false`) and `exit_plan_mode` (`readOnlyHint: true`) are `defaultActive: false`. `set_plan_steps` is active normally (`readOnlyHint: false`, `idempotentHint: true`); the read-only policy default-denies it in plan mode. Each returns typed `details` (`EnterPlanModeDetails`, `WritePlanDetails`, `ExitPlanModeDetails`, `SetPlanStepsDetails`), and `ExitPlanModeDetails` carries the review's `annotations` when a browser approval or save supplied comments; no `outputSchema`/`structuredContent`. |
|
|
418
|
-
| Commands and flags | `/plan` (toggle, or enter and send a task), `/plan review [path]` (browser review), `--plan` launch flag, `Ctrl+Alt+P` shortcut. |
|
|
419
|
-
| State | Enabled flag and the tools it hid persisted with `pi.appendEntry("plan-mode", { enabled, removed })
|
|
420
|
-
| Hooks | `tool_call` blocks non-read-only calls and forces `subagent` read-only; `before_agent_start` injects the hidden `[PLAN MODE ACTIVE]` context
|
|
438
|
+
| Commands and flags | `/plan` (toggle, or enter and send a task), `/plan list` (plans index), `/plan review [path]` (browser review), `--plan` launch flag, `Ctrl+Alt+P` shortcut. |
|
|
439
|
+
| State | Enabled flag and the tools it hid persisted with `pi.appendEntry("plan-mode", { enabled, removed })`, and each review's baseline persisted with `pi.appendEntry("plan-baseline", { path, sections })` as one digest per heading section; both rebuilt from `ctx.sessionManager.getBranch()`, so `/resume` and `/tree` restore the mode and the changed markers of the branch they land on. Plan never writes a goal itself: an approval offer records one through `ctx.executeTool("goal", { objective })`. A review that shows the same content appends nothing. The last plan path lives in runtime memory for the footer chip, and a bare `/plan review` falls back to the newest plan file in the directory. |
|
|
440
|
+
| Hooks | `tool_call` blocks non-read-only calls and forces `subagent` read-only; `before_agent_start` injects the hidden `[PLAN MODE ACTIVE]` context, with an active session goal read from the branch through `lib/goal-state.ts`; `context` keeps only the newest plan context while enabled and drops stale ones otherwise. |
|
|
421
441
|
| Lifecycle | `session_start`/`session_tree` restore state; `session_shutdown` clears the footer status and closes any browser review server. |
|
|
422
442
|
|
|
423
443
|
## Design notes
|
|
@@ -456,12 +476,21 @@ file and returns them to the model.
|
|
|
456
476
|
channel; no import from that extension) and the steps are merged into them.
|
|
457
477
|
A rejected seed reports the nested tool's own reason in the approval message
|
|
458
478
|
instead of failing silently.
|
|
479
|
+
- **A cap is enforced where it can still be fixed.** Both refusal points —
|
|
480
|
+
`write_plan` and `exit_plan_mode` — sit before the write and before the review,
|
|
481
|
+
because those are the moments the model can still regroup the steps. A note on
|
|
482
|
+
a successful write would arrive after the file existed and the review had
|
|
483
|
+
already listed every step, which is exactly when the overflow becomes lost
|
|
484
|
+
work.
|
|
459
485
|
- **Changed markers compare what was shown, not revisions on disk.** A review
|
|
460
|
-
result is only meaningful next to the text the reviewer already read, so
|
|
461
|
-
|
|
462
|
-
`
|
|
486
|
+
result is only meaningful next to the text the reviewer already read, so every
|
|
487
|
+
review marks the sections it presented (keyed by resolved path) and
|
|
488
|
+
`plan-text.ts` compares the next plan against those digests. Stored feedback is
|
|
463
489
|
stripped from both sides, so a review comment never reads as an edit, and the
|
|
464
|
-
same helper feeds the terminal summary and the browser outline.
|
|
490
|
+
same helper feeds the terminal summary and the browser outline. The digests are
|
|
491
|
+
a session entry rather than the previous text, so the markers survive a resume
|
|
492
|
+
without copying the plan file into the branch, and the reader and the writer
|
|
493
|
+
share one normalization so a tab cannot read as a change.
|
|
465
494
|
- **The outline guides, it does not enforce.** `write_plan` documents the
|
|
466
495
|
canonical shape in its description and stores whatever markdown it is given;
|
|
467
496
|
no section is required and nothing is rewritten. That keeps the model free to
|
|
@@ -514,12 +543,14 @@ file and returns them to the model.
|
|
|
514
543
|
| `git-bash.ts` | `checkReadOnlyGit`: the pure allowlist that restricts `bash` to read-only git commands. |
|
|
515
544
|
| `plans.ts` | Plan-file store: slug/stamp naming, directory resolution, containment, atomic `write`/`setSteps`/`saveFeedback`, and `read`/`latest`. |
|
|
516
545
|
| `todos.ts` | Approval-time todo merge (pure): the last list replayed from the branch, and the merge of the plan's steps into it with the cap respected. |
|
|
517
|
-
| `steps.ts` | Plan-document structure (pure): the outline, step extraction and the `Steps`-section scope, heading sections,
|
|
518
|
-
| `
|
|
546
|
+
| `steps.ts` | Plan-document structure (pure): the outline, step extraction and the `Steps`-section scope, heading sections, and the checkbox edit. |
|
|
547
|
+
| `plan-text.ts` | The plan text a review shows and compares: `sanitizePlanText`, the one normalization both sides use, the per-section `sectionDigests`, and `changedSections`. |
|
|
548
|
+
| `runtime.ts` | Enabled state, tool gating, persistence (mode and review baseline), control-tool identity, footer status. |
|
|
519
549
|
| `tools.ts` | `enter_plan_mode`, `write_plan`, `exit_plan_mode`, and `set_plan_steps`: schemas, results, transcripts, and the approval-time todo seed. |
|
|
520
550
|
| `review.ts` | The review gate: one decision from the TUI screen, a dialog, or the browser, the shared browser lifecycle, and the "mark what was shown" baseline rule. |
|
|
521
551
|
| `commands.ts` | `/plan` (enter, optional task) and the `/plan review [path]` browser subcommand. |
|
|
522
552
|
| `tui.ts` | `PlanViewComponent` (sectioned Markdown body, changed-section summary and marker, section jump, scroll gauge, approve/refine/browser/keep). |
|
|
553
|
+
| `plan-index.ts` | `/plan list`: the notice text and the docked index screen (title, step progress, age, focused path; Enter reviews). |
|
|
523
554
|
| `blocks.ts` | Source-line block model (fence-aware headings, paragraphs, list items, code, quotes, tables, rules, blanks). |
|
|
524
555
|
| `feedback.ts` | Browser-comment codec: `stripFeedback`, `parseFeedback`, `formatFeedback`, `feedbackSummary`, and payload validation. |
|
|
525
556
|
| `web.ts` | `createPlanWebReview`: the loopback server, the section outline (with changed-section flags) and code highlighting, and the save/approve/cancel/abort session. |
|
|
@@ -2,22 +2,26 @@
|
|
|
2
2
|
* Slash command for the plan-mode extension.
|
|
3
3
|
*
|
|
4
4
|
* `/plan` toggles plan mode, or enters it and sends an optional task;
|
|
5
|
-
* `Ctrl+Alt+P` toggles too. `/plan review [path]`
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* `Ctrl+Alt+P` toggles too. Two subcommands are reserved: `/plan review [path]`
|
|
6
|
+
* opens a plan in the browser (and sends saved comments to the model as a
|
|
7
|
+
* refinement), and `/plan list` shows the plans directory, newest first, with
|
|
8
|
+
* Enter opening a review.
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
11
12
|
import { isRemoteTurn } from "../../lib/interaction.ts";
|
|
12
13
|
import { feedbackSummary } from "./feedback.ts";
|
|
14
|
+
import { PlanIndexComponent, formatPlanIndex } from "./plan-index.ts";
|
|
13
15
|
import { READ_ONLY_SUMMARY } from "./policy.ts";
|
|
14
16
|
import { runBrowserReview } from "./review.ts";
|
|
15
17
|
import type { PlanRuntime } from "./runtime.ts";
|
|
18
|
+
import type { PlanSummary } from "./plans.ts";
|
|
16
19
|
import { createPlanWebReview, type PlanWebReview } from "./web.ts";
|
|
17
20
|
|
|
18
21
|
const ENABLED_NOTICE = `Plan mode enabled — ${READ_ONLY_SUMMARY}.`;
|
|
19
22
|
const DISABLED_NOTICE = "Plan mode disabled — full access restored.";
|
|
20
23
|
const REVIEW_PREFIX = "review";
|
|
24
|
+
const LIST_PREFIX = "list";
|
|
21
25
|
|
|
22
26
|
/** Notice text for the current mode; shared by `/plan` and the `Ctrl+Alt+P` shortcut. */
|
|
23
27
|
export function planModeNotice(enabled: boolean): string {
|
|
@@ -30,6 +34,14 @@ function reviewTarget(args: string): string | undefined {
|
|
|
30
34
|
return target === "" ? undefined : target;
|
|
31
35
|
}
|
|
32
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Whether a command line is the reserved `list` subcommand. Exact match only, so
|
|
39
|
+
* `list the untracked files` stays a task instead of silently listing plans.
|
|
40
|
+
*/
|
|
41
|
+
function isListSubcommand(prompt: string): boolean {
|
|
42
|
+
return prompt.toLowerCase() === LIST_PREFIX;
|
|
43
|
+
}
|
|
44
|
+
|
|
33
45
|
/**
|
|
34
46
|
* Whether a command line is the reserved `review` subcommand. Bare `review`
|
|
35
47
|
* always is; a path after it is, but `review the auth flow` stays a task, so a
|
|
@@ -68,10 +80,10 @@ async function reviewInBrowser(
|
|
|
68
80
|
return;
|
|
69
81
|
}
|
|
70
82
|
|
|
71
|
-
const
|
|
83
|
+
const baseline = runtime.reviewed(file.path);
|
|
72
84
|
const result = await runBrowserReview(web, ctx.cwd, file, {
|
|
73
85
|
canApprove: false,
|
|
74
|
-
|
|
86
|
+
baseline,
|
|
75
87
|
// Mark only once the page is actually open: a failed start must not consume
|
|
76
88
|
// the baseline for a plan nobody saw.
|
|
77
89
|
onOpened: (session) => {
|
|
@@ -100,6 +112,47 @@ async function reviewInBrowser(
|
|
|
100
112
|
else await pi.sendUserMessage(message, { deliverAs: "followUp" });
|
|
101
113
|
}
|
|
102
114
|
|
|
115
|
+
/**
|
|
116
|
+
* `/plan list`: show the plans directory, newest first. The TUI gets a docked
|
|
117
|
+
* list whose Enter opens the focused plan's review; every other mode gets the
|
|
118
|
+
* same facts as a notice, with each plan's path.
|
|
119
|
+
*/
|
|
120
|
+
async function openPlanIndex(
|
|
121
|
+
pi: ExtensionAPI,
|
|
122
|
+
runtime: PlanRuntime,
|
|
123
|
+
web: PlanWebReview,
|
|
124
|
+
ctx: ExtensionContext,
|
|
125
|
+
): Promise<void> {
|
|
126
|
+
const dir = await runtime.plans.dirFor(ctx.cwd);
|
|
127
|
+
const summarise = async (): Promise<Awaited<ReturnType<typeof runtime.plans.index>>> => runtime.plans.index(ctx.cwd);
|
|
128
|
+
const summaries = await summarise();
|
|
129
|
+
|
|
130
|
+
if (ctx.mode !== "tui") {
|
|
131
|
+
ctx.ui.notify(formatPlanIndex(summaries, dir), "info");
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// The review runs after the screen closes, so the input focus is free again.
|
|
136
|
+
let selected: (PlanSummary | undefined) | undefined;
|
|
137
|
+
await ctx.ui.custom<void>((tui, theme, _keybindings, done) => {
|
|
138
|
+
const component = new PlanIndexComponent({
|
|
139
|
+
summaries,
|
|
140
|
+
dir,
|
|
141
|
+
theme,
|
|
142
|
+
onSelect: (summary) => {
|
|
143
|
+
selected = summary;
|
|
144
|
+
done();
|
|
145
|
+
},
|
|
146
|
+
onClose: () => done(),
|
|
147
|
+
reload: summarise,
|
|
148
|
+
requestRender: () => tui.requestRender(),
|
|
149
|
+
viewportRows: () => tui.terminal?.rows,
|
|
150
|
+
});
|
|
151
|
+
return component;
|
|
152
|
+
});
|
|
153
|
+
if (selected) await reviewInBrowser(pi, runtime, web, selected.path, ctx);
|
|
154
|
+
}
|
|
155
|
+
|
|
103
156
|
export function registerCommands(
|
|
104
157
|
pi: ExtensionAPI,
|
|
105
158
|
runtime: PlanRuntime,
|
|
@@ -107,16 +160,22 @@ export function registerCommands(
|
|
|
107
160
|
): void {
|
|
108
161
|
pi.registerCommand("plan", {
|
|
109
162
|
description:
|
|
110
|
-
"Toggle plan mode (read-only exploration), enter it with an optional task, or review
|
|
163
|
+
"Toggle plan mode (read-only exploration), enter it with an optional task, list saved plans, or review one in the browser",
|
|
111
164
|
getArgumentCompletions: (prefix) => {
|
|
112
165
|
const text = prefix.trimStart();
|
|
113
166
|
if (text.includes(" ")) return null;
|
|
114
|
-
|
|
167
|
+
const subcommands = [LIST_PREFIX, REVIEW_PREFIX].filter((name) => name.startsWith(text));
|
|
168
|
+
return subcommands.length > 0 ? subcommands.map((value) => ({ value, label: value })) : null;
|
|
115
169
|
},
|
|
116
170
|
handler: async (args, ctx) => {
|
|
117
171
|
const prompt = args.trim();
|
|
118
172
|
|
|
119
|
-
// `/plan review [path]`
|
|
173
|
+
// `/plan list` and `/plan review [path]` are reserved subcommands;
|
|
174
|
+
// everything else is a task.
|
|
175
|
+
if (isListSubcommand(prompt)) {
|
|
176
|
+
await openPlanIndex(pi, runtime, web, ctx);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
120
179
|
if (isReviewSubcommand(prompt)) {
|
|
121
180
|
await reviewInBrowser(pi, runtime, web, reviewTarget(prompt), ctx);
|
|
122
181
|
return;
|
package/extensions/plan/index.ts
CHANGED
|
@@ -21,7 +21,10 @@ import { Key } from "@earendil-works/pi-tui";
|
|
|
21
21
|
import { fileURLToPath } from "node:url";
|
|
22
22
|
import { hasPathInput } from "./path-guard.ts";
|
|
23
23
|
import { isExtensionEnabled } from "../../lib/env.ts";
|
|
24
|
+
import { sanitizeLine } from "../../lib/format.ts";
|
|
25
|
+
import { lastGoal, type PublishedGoal } from "../../lib/goal-state.ts";
|
|
24
26
|
import { ASK_USER_QUESTION_TOOL, SUBAGENT_READ_ONLY_PARAM, SUBAGENT_TOOL } from "../../lib/tool-names.ts";
|
|
27
|
+
import { clip } from "../../lib/tui/columns.ts";
|
|
25
28
|
import { STATUS_KEYS } from "../../lib/tui/glyphs.ts";
|
|
26
29
|
import { planModeNotice, registerCommands } from "./commands.ts";
|
|
27
30
|
import { checkReadOnlyGit } from "./git-bash.ts";
|
|
@@ -39,15 +42,34 @@ import { createPlanWebReview } from "./web.ts";
|
|
|
39
42
|
/** Marker embedded in the injected prompt and used to filter stale context. */
|
|
40
43
|
export const PLAN_MODE_MARKER = "[PLAN MODE ACTIVE]";
|
|
41
44
|
|
|
42
|
-
/**
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Longest goal text restated here. `goal` injects the full objective every turn
|
|
47
|
+
* regardless of plan mode, so this line is a short pointer; the clip is what keeps
|
|
48
|
+
* the whole context inside the prompt-budget ceiling.
|
|
49
|
+
*/
|
|
50
|
+
const GOAL_CONTEXT_CHARS = 64;
|
|
45
51
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Injected each turn while planning: the facts and the next action, not the tool
|
|
54
|
+
* contracts. An active session goal becomes one extra bullet, so a plan is
|
|
55
|
+
* designed against the objective instead of beside it.
|
|
56
|
+
*/
|
|
57
|
+
export function planModeContext(goal?: PublishedGoal | null): string {
|
|
58
|
+
const lines = [
|
|
59
|
+
PLAN_MODE_MARKER,
|
|
60
|
+
"You are in plan mode: read-only exploration for safe code analysis.",
|
|
61
|
+
"",
|
|
62
|
+
`- Do not modify anything except the plan file: while planning, ${READ_ONLY_SUMMARY}.`,
|
|
63
|
+
"- bash is git-only: read-only commands (git status, git diff, git log) may be chained with && or ;, but pipes, substitution, and redirection are blocked and powershell is unavailable. The cwd is already the project root, so do not prefix commands with cd; use read, grep, find, and ls for files.",
|
|
64
|
+
"- Investigate and design a concrete plan, then save it with write_plan and call exit_plan_mode with the returned plan_path for approval.",
|
|
65
|
+
"- Delegate broad exploration with subagent; while planning it is forced read-only.",
|
|
66
|
+
`- To choose between approaches, ask with ${ASK_USER_QUESTION_TOOL} before finalizing.`,
|
|
67
|
+
];
|
|
68
|
+
if (goal?.status === "active") {
|
|
69
|
+
lines.push(`- Current session goal: ${clip(sanitizeLine(goal.objective), GOAL_CONTEXT_CHARS)}`);
|
|
70
|
+
}
|
|
71
|
+
return lines.join("\n");
|
|
72
|
+
}
|
|
51
73
|
|
|
52
74
|
/** This extension entry file, as Pi records it for our tool registrations. */
|
|
53
75
|
function extensionEntryPath(): string | undefined {
|
|
@@ -158,10 +180,12 @@ export default function planMode(pi: ExtensionAPI): void {
|
|
|
158
180
|
return reason ? { block: true, reason } : undefined;
|
|
159
181
|
});
|
|
160
182
|
|
|
161
|
-
// Tell the model it is planning
|
|
162
|
-
|
|
183
|
+
// Tell the model it is planning, and restate an active goal so the plan is
|
|
184
|
+
// designed against it. Reads the branch, so no extra model call is involved.
|
|
185
|
+
pi.on("before_agent_start", (_event, ctx) => {
|
|
163
186
|
if (!runtime.isEnabled()) return undefined;
|
|
164
|
-
|
|
187
|
+
const goal = lastGoal(ctx.sessionManager.getBranch());
|
|
188
|
+
return { message: { customType: "plan-mode-context", content: planModeContext(goal), display: false } };
|
|
165
189
|
});
|
|
166
190
|
|
|
167
191
|
// Keep stale plan-mode context out of later, non-plan turns (for example after /resume).
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `/plan list` surface: the plan index as a notice, and as a docked list
|
|
3
|
+
* screen.
|
|
4
|
+
*
|
|
5
|
+
* A plan is a file, and the plans directory is its only index, so this module
|
|
6
|
+
* presents what `PlanStore.index` walked: the title, the modified time, and how
|
|
7
|
+
* far its steps got, newest first. Enter on the screen hands the focused plan
|
|
8
|
+
* back to the caller, which reuses the browser review; every other mode gets the
|
|
9
|
+
* same facts as one line per plan, with the path to reopen.
|
|
10
|
+
*
|
|
11
|
+
* Derived state only: the summaries are passed in, a reload replaces them, and a
|
|
12
|
+
* render reads them — like the `/memory` picker.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import { type Component, Key, matchesKey, type TuiMouseEvent, type TuiMouseEventResult } from "@earendil-works/pi-tui";
|
|
17
|
+
import { sanitizeLine, shortenPath } from "../../lib/format.ts";
|
|
18
|
+
import { fitRows, formatRange, isCloseKey, ListCursor, navigateList, scrollList } from "../../lib/tui/cursor.ts";
|
|
19
|
+
import { dockListFrame, screenHint, type ViewportRowsSource } from "../../lib/tui/chrome.ts";
|
|
20
|
+
import { clip } from "../../lib/tui/columns.ts";
|
|
21
|
+
import { emptyState, listRow } from "../../lib/tui/rows.ts";
|
|
22
|
+
import { titleRule } from "../../lib/tui/rules.ts";
|
|
23
|
+
import { paint } from "../../lib/tui/tokens.ts";
|
|
24
|
+
import type { PlanSummary } from "./plans.ts";
|
|
25
|
+
|
|
26
|
+
/** Rows the screen shows when the terminal height is unknown. */
|
|
27
|
+
const DEFAULT_ROWS = 14;
|
|
28
|
+
/** Fixed rows around the list: header, summary, blank, range, detail, hint, blank. */
|
|
29
|
+
const CHROME = 7;
|
|
30
|
+
/** Longest title a row draws before the trailing meta wins the column. */
|
|
31
|
+
const TITLE_WIDTH = 56;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* How long ago a plan was modified: `today`, `3d ago`, `2mo ago`, `1y ago`.
|
|
35
|
+
* Coarse on purpose — a list row compares plans, it does not time them.
|
|
36
|
+
*/
|
|
37
|
+
export function relativeAge(modifiedMs: number, nowMs: number): string {
|
|
38
|
+
const days = Math.floor(Math.max(0, nowMs - modifiedMs) / 86_400_000);
|
|
39
|
+
if (days < 1) return "today";
|
|
40
|
+
if (days < 30) return `${days}d ago`;
|
|
41
|
+
const months = Math.floor(days / 30);
|
|
42
|
+
if (months < 12) return `${months}mo ago`;
|
|
43
|
+
return `${Math.floor(days / 365)}y ago`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** `3/7 steps`, `2/2 steps`, or `steps unknown` for a file the store would not have written. */
|
|
47
|
+
function stepsText(summary: PlanSummary): string {
|
|
48
|
+
if (!summary.steps) return "steps unknown";
|
|
49
|
+
return `${summary.steps.done}/${summary.steps.total} steps`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Local `YYYY-MM-DD HH:mm`, matching the stamp the file name carries. */
|
|
53
|
+
function localTime(ms: number): string {
|
|
54
|
+
const date = new Date(ms);
|
|
55
|
+
const pad = (value: number): string => String(value).padStart(2, "0");
|
|
56
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The non-TUI listing: one line per plan with its title, step progress, modified
|
|
61
|
+
* time, and path, newest first. The directory is named once in the header.
|
|
62
|
+
*/
|
|
63
|
+
export function formatPlanIndex(summaries: readonly PlanSummary[], dir: string): string {
|
|
64
|
+
const where = sanitizeLine(shortenPath(dir));
|
|
65
|
+
if (summaries.length === 0) {
|
|
66
|
+
return `No plans in ${where} yet; write_plan saves one there.`;
|
|
67
|
+
}
|
|
68
|
+
const lines = summaries.map(
|
|
69
|
+
(summary) =>
|
|
70
|
+
` ${sanitizeLine(summary.title)} — ${stepsText(summary)} — ${localTime(summary.modified)} — ${sanitizeLine(summary.relativePath)}`,
|
|
71
|
+
);
|
|
72
|
+
return [`Plans (${summaries.length}) — ${where}`, ...lines].join("\n");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface PlanIndexOptions {
|
|
76
|
+
summaries: readonly PlanSummary[];
|
|
77
|
+
/** Directory the plans live in, for the header. */
|
|
78
|
+
dir: string;
|
|
79
|
+
theme: Theme;
|
|
80
|
+
/** Enter on the focused plan; the caller opens its review after the screen closes. */
|
|
81
|
+
onSelect: (summary: PlanSummary) => void;
|
|
82
|
+
onClose: () => void;
|
|
83
|
+
/** `r`: re-read the directory. Resolves with the new rows. */
|
|
84
|
+
reload: () => Promise<readonly PlanSummary[]>;
|
|
85
|
+
requestRender: () => void;
|
|
86
|
+
viewportRows?: ViewportRowsSource;
|
|
87
|
+
/** Clock for the relative ages; defaults to the wall clock. */
|
|
88
|
+
now?: () => number;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Docked `/plan list` screen: the plans directory, newest first. */
|
|
92
|
+
export class PlanIndexComponent implements Component {
|
|
93
|
+
private readonly cursor: ListCursor;
|
|
94
|
+
private summaries: readonly PlanSummary[];
|
|
95
|
+
private notice: string | undefined;
|
|
96
|
+
private componentFocused = false;
|
|
97
|
+
|
|
98
|
+
constructor(private readonly options: PlanIndexOptions) {
|
|
99
|
+
this.summaries = options.summaries;
|
|
100
|
+
this.cursor = new ListCursor(() => options.requestRender());
|
|
101
|
+
// A screen can take its first key before it is drawn; keep the cursor valid.
|
|
102
|
+
this.cursor.sync(this.summaries.length);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Replace the rows after a reload, keeping the focused plan if it is still there. */
|
|
106
|
+
private setSummaries(next: readonly PlanSummary[]): void {
|
|
107
|
+
this.summaries = next;
|
|
108
|
+
this.cursor.sync(next.length);
|
|
109
|
+
this.cursor.selected = Math.min(this.cursor.selected, Math.max(0, next.length - 1));
|
|
110
|
+
this.requestRender();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private requestRender(): void {
|
|
114
|
+
this.options.requestRender();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private reload(): void {
|
|
118
|
+
void this.options
|
|
119
|
+
.reload()
|
|
120
|
+
.then((next) => {
|
|
121
|
+
this.setSummaries(next);
|
|
122
|
+
this.notice = next.length === 0 ? "No plans yet." : undefined;
|
|
123
|
+
})
|
|
124
|
+
.catch((error: unknown) => {
|
|
125
|
+
this.notice = `Could not read the plans directory: ${error instanceof Error ? error.message : String(error)}`;
|
|
126
|
+
this.requestRender();
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
handleInput(data: string): void {
|
|
131
|
+
if (isCloseKey(data) || data === "q") {
|
|
132
|
+
this.options.onClose();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (this.notice !== undefined) this.notice = undefined;
|
|
136
|
+
|
|
137
|
+
if (navigateList(data, this.cursor, this.summaries.length)) {
|
|
138
|
+
this.requestRender();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
// The hint advertises `g`/`G`, so bind them to the ends.
|
|
142
|
+
if (data === "g" || data === "G") {
|
|
143
|
+
this.cursor.set(data === "g" ? 0 : this.summaries.length - 1, this.summaries.length);
|
|
144
|
+
this.requestRender();
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (data === "r") {
|
|
148
|
+
this.reload();
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (matchesKey(data, Key.enter)) {
|
|
152
|
+
const focused = this.summaries[this.cursor.selected];
|
|
153
|
+
if (focused) this.options.onSelect(focused);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Wheel scrolling moves the selection, matching the other list screens. */
|
|
158
|
+
handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined {
|
|
159
|
+
return scrollList(event, this.cursor, this.summaries.length);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Rows are built per render from the current theme and summaries; nothing is memoized. */
|
|
163
|
+
invalidate(): void {}
|
|
164
|
+
|
|
165
|
+
get focused(): boolean {
|
|
166
|
+
return this.componentFocused;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
set focused(value: boolean) {
|
|
170
|
+
this.componentFocused = value;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private row(summary: PlanSummary, selected: boolean, width: number): string {
|
|
174
|
+
const now = (this.options.now ?? Date.now)();
|
|
175
|
+
const trailing = `${stepsText(summary)} · ${relativeAge(summary.modified, now)}`;
|
|
176
|
+
return listRow(this.options.theme, width, {
|
|
177
|
+
selected,
|
|
178
|
+
label: clip(sanitizeLine(summary.title), TITLE_WIDTH),
|
|
179
|
+
labelRole: selected ? "accent" : "label",
|
|
180
|
+
trailing,
|
|
181
|
+
trailingRole: "meta",
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private summaryLine(width: number): string {
|
|
186
|
+
const { theme } = this.options;
|
|
187
|
+
const count = this.summaries.length;
|
|
188
|
+
const text = `${paint(theme, "accent", "Plans")} ${paint(theme, "meta", `· ${count} ${count === 1 ? "plan" : "plans"}`)}`;
|
|
189
|
+
const where = paint(theme, "faint", ` · ${sanitizeLine(shortenPath(this.options.dir))}`);
|
|
190
|
+
return clip(` ${text}${where}`, width);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private detailLines(width: number): string[] {
|
|
194
|
+
const focused = this.summaries[this.cursor.selected];
|
|
195
|
+
if (!focused) return [];
|
|
196
|
+
const { theme } = this.options;
|
|
197
|
+
const path = paint(theme, "meta", sanitizeLine(focused.relativePath));
|
|
198
|
+
const time = paint(theme, "faint", ` · modified ${localTime(focused.modified)}`);
|
|
199
|
+
return [clip(` ${path}${time}`, width)];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
render(width: number): string[] {
|
|
203
|
+
const { theme } = this.options;
|
|
204
|
+
const w = Math.max(1, width);
|
|
205
|
+
this.cursor.visible = fitRows(this.options.viewportRows, this.summaries.length, CHROME, DEFAULT_ROWS);
|
|
206
|
+
this.cursor.sync(this.summaries.length);
|
|
207
|
+
|
|
208
|
+
const body: string[] = [];
|
|
209
|
+
if (this.summaries.length === 0) {
|
|
210
|
+
body.push(...emptyState(theme, w, { label: "No plans yet. write_plan saves one under .pi/plans." }));
|
|
211
|
+
} else {
|
|
212
|
+
const { start, end } = this.cursor.window(this.summaries.length);
|
|
213
|
+
for (let index = start; index < end; index++) {
|
|
214
|
+
body.push(this.row(this.summaries[index], index === this.cursor.selected, w));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const hint =
|
|
219
|
+
this.notice !== undefined
|
|
220
|
+
? clip(` ${paint(theme, "meta", this.notice)}`, w)
|
|
221
|
+
: screenHint(theme, w, ["Esc close", { text: "Enter review", color: "accent" }, "r reload", "↑/↓", "g/G"]);
|
|
222
|
+
|
|
223
|
+
return dockListFrame({
|
|
224
|
+
header: titleRule(theme, w, "Plan index"),
|
|
225
|
+
summary: this.summaryLine(w),
|
|
226
|
+
body,
|
|
227
|
+
range: this.cursor.clipped(this.summaries.length)
|
|
228
|
+
? formatRange(theme, w, { ...this.cursor.window(this.summaries.length), total: this.summaries.length })
|
|
229
|
+
: undefined,
|
|
230
|
+
detail: this.detailLines(w),
|
|
231
|
+
hint,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|