@gavin-hu/my-pi-agent 0.15.1 → 0.17.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 +22 -13
- package/README.zh-CN.md +19 -11
- package/extensions/bytefly/README.md +255 -200
- package/extensions/bytefly/footer/collect/entries.ts +1 -6
- package/extensions/bytefly/footer/collect/snapshot.ts +23 -9
- package/extensions/bytefly/footer/price/price.ts +12 -8
- package/extensions/bytefly/footer/register.ts +13 -1
- package/extensions/bytefly/footer/types.ts +11 -0
- package/extensions/bytefly/footer/unpriced.ts +36 -0
- package/extensions/bytefly/header/component.ts +15 -29
- package/extensions/bytefly/header/identity.ts +71 -0
- package/extensions/bytefly/header/register.ts +9 -3
- package/extensions/bytefly/index.ts +13 -5
- package/extensions/bytefly/message/format.ts +20 -13
- package/extensions/bytefly/message/register.ts +8 -7
- package/extensions/bytefly/notice/component.ts +6 -4
- package/extensions/bytefly/notice/config.ts +16 -2
- package/extensions/bytefly/panel/commands.ts +46 -25
- package/extensions/bytefly/panel/format.ts +52 -22
- package/extensions/bytefly/panel/join.ts +36 -15
- package/extensions/bytefly/panel/settings.ts +72 -392
- package/extensions/bytefly/panel/skills.ts +51 -0
- package/extensions/bytefly/panel/tui/entries.ts +16 -14
- package/extensions/bytefly/panel/tui/extensions-view.ts +7 -58
- package/extensions/bytefly/panel/tui/frame.ts +11 -3
- package/extensions/bytefly/panel/tui/list-view.ts +72 -0
- package/extensions/bytefly/panel/tui/nav.ts +71 -3
- package/extensions/bytefly/panel/tui/settings-section.ts +250 -0
- package/extensions/bytefly/panel/tui/settings-view.ts +8 -12
- package/extensions/bytefly/panel/tui/skills-view.ts +50 -0
- package/extensions/bytefly/panel/tui.ts +223 -333
- package/extensions/bytefly/panel/types.ts +26 -8
- package/extensions/bytefly/picker/discovery.ts +32 -27
- package/extensions/bytefly/picker/resources.ts +22 -10
- package/extensions/bytefly/picker/runtime.ts +62 -34
- package/extensions/bytefly/picker/toggle.ts +16 -10
- package/extensions/bytefly/picker/types.ts +31 -13
- package/extensions/bytefly/update/format.ts +66 -8
- package/extensions/bytefly/update/patch.ts +66 -0
- package/extensions/bytefly/update/register.ts +4 -11
- package/extensions/bytefly/update/release.ts +65 -0
- package/extensions/bytefly/update/state.ts +14 -0
- package/extensions/bytefly/update/suppress.ts +47 -29
- package/extensions/bytefly/update/types.ts +15 -5
- package/extensions/bytefly/working/register.ts +2 -8
- package/extensions/check/tools.ts +2 -11
- package/extensions/check/tui.ts +2 -0
- package/extensions/context/README.md +172 -0
- package/extensions/context/collect.ts +53 -0
- package/extensions/context/commands.ts +100 -0
- package/extensions/context/format.ts +84 -0
- package/extensions/context/index.ts +16 -0
- package/extensions/context/report.ts +332 -0
- package/extensions/context/tui.ts +284 -0
- package/extensions/context/types.ts +124 -0
- package/extensions/file-browse/README.md +67 -19
- package/extensions/file-browse/client.ts +16 -463
- package/extensions/file-browse/config.ts +8 -0
- package/extensions/file-browse/html.ts +9 -7
- package/extensions/file-browse/index.ts +17 -2
- package/extensions/file-browse/live-client.ts +13 -13
- package/extensions/file-browse/live.ts +44 -10
- package/extensions/file-browse/markdown.ts +3 -10
- package/extensions/file-browse/palette.ts +224 -0
- package/extensions/file-browse/raw-html.ts +3 -10
- package/extensions/file-browse/router.ts +2 -2
- package/extensions/file-browse/server.ts +13 -2
- package/extensions/file-browse/styles.ts +8 -3
- package/extensions/file-browse/tree.ts +381 -0
- package/extensions/file-open/README.md +1 -2
- package/extensions/{document → file-read}/README.md +76 -57
- package/extensions/{document → file-read}/config.ts +12 -9
- package/extensions/{document → file-read}/extract/doc.ts +2 -2
- package/extensions/{document → file-read}/extract/docx.ts +3 -3
- package/extensions/{document → file-read}/extract/file.ts +9 -9
- package/extensions/{document → file-read}/extract/odt.ts +10 -8
- package/extensions/{document → file-read}/extract/pdf.ts +3 -3
- package/extensions/{document → file-read}/extract/rtf.ts +3 -3
- package/extensions/{document → file-read}/extract/xlsx.ts +4 -4
- package/extensions/{document → file-read}/formats.ts +11 -11
- package/extensions/file-read/index.ts +17 -0
- package/extensions/{document → file-read}/paging.ts +15 -13
- package/extensions/{document → file-read}/render.ts +17 -13
- package/extensions/{document → file-read}/schema.ts +7 -7
- package/extensions/{document → file-read}/tool.ts +36 -28
- package/extensions/goal/tools.ts +16 -25
- package/extensions/job/commands.ts +32 -46
- package/extensions/job/index.ts +20 -45
- package/extensions/job/runtime.ts +6 -8
- package/extensions/job/tools.ts +58 -103
- package/extensions/memory/commands.ts +30 -44
- package/extensions/memory/tools.ts +10 -20
- package/extensions/memory/tui.ts +5 -1
- package/extensions/plan/README.md +88 -33
- package/extensions/plan/commands.ts +19 -4
- package/extensions/plan/config.ts +27 -0
- package/extensions/plan/plan-index.ts +3 -1
- package/extensions/plan/plans.ts +31 -13
- package/extensions/plan/review.ts +34 -14
- package/extensions/plan/runtime.ts +3 -1
- package/extensions/plan/steps.ts +43 -14
- package/extensions/plan/tools/cancel.ts +79 -0
- package/extensions/plan/tools/enter.ts +16 -17
- package/extensions/plan/tools/exit.ts +60 -50
- package/extensions/plan/tools/steps.ts +13 -26
- package/extensions/plan/tools/write.ts +30 -38
- package/extensions/plan/tools.ts +5 -2
- package/extensions/plan/tui.ts +9 -7
- package/extensions/plan/types.ts +8 -0
- package/extensions/plan/web-chrome.ts +101 -0
- package/extensions/plan/web-client.ts +98 -311
- package/extensions/plan/web-markdown.ts +222 -0
- package/extensions/plan/web-page.ts +28 -2
- package/extensions/plan/web-router.ts +2 -2
- package/extensions/plan/web.ts +21 -12
- package/extensions/question/tools.ts +5 -4
- package/extensions/rewind/checklist.ts +4 -0
- package/extensions/rewind/review-tui.ts +3 -1
- package/extensions/rewind/rewind-tui.ts +4 -0
- package/extensions/subagent/README.md +25 -12
- package/extensions/subagent/background.ts +1 -7
- package/extensions/subagent/format.ts +72 -25
- package/extensions/subagent/index.ts +21 -47
- package/extensions/subagent/orchestrate.ts +18 -41
- package/extensions/subagent/render.ts +29 -42
- package/extensions/subagent/stream.ts +24 -18
- package/extensions/subagent/types.ts +7 -0
- package/extensions/todo/tools.ts +15 -20
- package/extensions/todo/tui.ts +5 -1
- package/extensions/watch/README.md +5 -5
- package/extensions/watch/commands.ts +33 -47
- package/extensions/watch/index.ts +10 -35
- package/extensions/watch/runtime.ts +5 -7
- package/extensions/watch/tools.ts +38 -77
- package/extensions/web-access/README.md +3 -3
- package/extensions/web-access/search/search.ts +9 -18
- package/extensions/wechat/README.md +40 -18
- package/extensions/wechat/bridge.ts +38 -40
- package/extensions/wechat/client.ts +1 -4
- package/extensions/wechat/commands.ts +1 -1
- package/extensions/wechat/interaction.ts +25 -4
- package/extensions/wechat/login-screen.ts +2 -3
- package/extensions/wechat/notices.ts +7 -5
- package/extensions/wechat/prompt.ts +19 -4
- package/extensions/wechat/qrcode.ts +9 -9
- package/extensions/wechat/slash.ts +14 -0
- package/extensions/wechat/status-screen.ts +2 -2
- package/extensions/wechat/timer.ts +9 -30
- package/extensions/wechat/tools.ts +7 -18
- package/extensions/worktree/README.md +48 -12
- package/extensions/worktree/commands.ts +109 -65
- package/extensions/worktree/enter.ts +1 -1
- package/extensions/worktree/exit.ts +11 -1
- package/extensions/worktree/lifecycle.ts +9 -1
- package/extensions/worktree/screen.ts +263 -0
- package/extensions/worktree/status.ts +84 -38
- package/lib/README.md +15 -11
- package/lib/config.ts +12 -0
- package/lib/escape.ts +15 -2
- package/lib/format.ts +6 -2
- package/lib/interaction.ts +18 -1
- package/lib/pdf.ts +2 -2
- package/lib/report.ts +75 -2
- package/lib/requirements.ts +2 -2
- package/lib/screen-session.ts +85 -0
- package/lib/timer.ts +61 -0
- package/lib/tool-result.ts +54 -4
- package/lib/tui/README.md +19 -11
- package/lib/tui/chrome.ts +27 -6
- package/lib/tui/columns.ts +33 -8
- package/lib/tui/dock-screen.ts +6 -2
- package/lib/tui/gauge.ts +9 -5
- package/lib/tui/rows.ts +15 -6
- package/lib/tui/rules.ts +4 -8
- package/lib/tui/tool.ts +55 -2
- package/lib/usage.ts +26 -0
- package/lib/web/README.md +131 -0
- package/lib/web/base.ts +19 -2
- package/lib/web/components.ts +10 -6
- package/lib/web/layout.ts +2 -0
- package/lib/web/markup.ts +2 -1
- package/lib/web/runtime.ts +103 -25
- package/lib/web/tokens.ts +23 -7
- package/package.json +10 -4
- package/skills/changelog/SKILL.md +148 -0
- package/skills/next/SKILL.md +184 -0
- package/extensions/document/index.ts +0 -17
- /package/extensions/{document → file-read}/deadline.ts +0 -0
- /package/extensions/{document → file-read}/extract/vendor.d.ts +0 -0
|
@@ -20,6 +20,31 @@ How the `list_worktrees` call and its result look in the transcript — the call
|
|
|
20
20
|
|
|
21
21
|
<img src="../../assets/tools/list_worktrees.png" alt="The list_worktrees tool's call and result, drawn by the generic fallback" width="100%">
|
|
22
22
|
|
|
23
|
+
## The `/worktree` screen
|
|
24
|
+
|
|
25
|
+
<img src="../../assets/worktree.png" alt="The /worktree screen: a Worktrees title rule with a `4 worktrees · 1 dirty · 1 ahead` summary, the current dark-mode row carrying its ahead count, a dirty row, a merged row, and a stale-locked row, the focused row's path and base with the Not isolated gap under it, and the key hints" width="100%">
|
|
26
|
+
|
|
27
|
+
`/worktree` on its own opens a list screen: one row per managed worktree with its
|
|
28
|
+
state (current, dirty, ahead, behind, merged, locked, last used), the active
|
|
29
|
+
worktree's base and isolation gaps under the focused row, and a detail pane for
|
|
30
|
+
the selected one. Outside a terminal — print, RPC, JSON — the same facts print as
|
|
31
|
+
text instead, which is also what the model's `list_worktrees` reads.
|
|
32
|
+
|
|
33
|
+
| Key | |
|
|
34
|
+
|---|---|
|
|
35
|
+
| `j` / `k`, arrows | move |
|
|
36
|
+
| `g` / `G` | first / last row |
|
|
37
|
+
| Enter, `l` | open the selected worktree's detail |
|
|
38
|
+
| `e` | enter the selected worktree |
|
|
39
|
+
| `x` | exit the active worktree |
|
|
40
|
+
| `p` | prune eligible worktrees in place |
|
|
41
|
+
| Esc, `q` | close |
|
|
42
|
+
|
|
43
|
+
`e` and `x` close the screen before they act, because an exit asks through the
|
|
44
|
+
normal prompt and a prompt cannot open inside a mounted screen. `p` never asks,
|
|
45
|
+
so it prunes without closing and reports the result in the footer. In plan mode
|
|
46
|
+
the screen still opens and every verb refuses with a notice.
|
|
47
|
+
|
|
23
48
|
## What it does
|
|
24
49
|
|
|
25
50
|
- `enter_worktree` creates `<repo>/.pi/worktrees/<name>` on branch
|
|
@@ -50,18 +75,21 @@ How the `list_worktrees` call and its result look in the transcript — the call
|
|
|
50
75
|
|
|
51
76
|
| | |
|
|
52
77
|
|---|---|
|
|
53
|
-
| `/worktree` |
|
|
78
|
+
| `/worktree` | the list screen: managed worktrees, the active one, and its gaps |
|
|
54
79
|
| `/worktree enter [name]` | enter a worktree (a name may be a PR/MR reference) |
|
|
55
|
-
| `/worktree exit
|
|
80
|
+
| `/worktree exit` | exit a worktree |
|
|
56
81
|
| `/worktree prune` | remove clean, unused, old managed worktrees |
|
|
57
82
|
| `--worktree <name>` | enter at session start |
|
|
58
83
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
84
|
+
Exit asks whether to keep or remove, and `worktree.onExit` answers without
|
|
85
|
+
asking: a clean worktree this session created is removed, one holding work or
|
|
86
|
+
entered by path asks, and only a worktree with no work loses its branch. There
|
|
87
|
+
are no flags to disagree with either. Declining a confirmation (entering an
|
|
88
|
+
outside path, exiting a worktree with work) is reported as normal feedback, not
|
|
89
|
+
an error. An argument a subcommand does not take prints the usage line instead of
|
|
90
|
+
being ignored.
|
|
63
91
|
|
|
64
|
-
|
|
92
|
+
`list_worktrees` (and a headless `/worktree`) reports the active worktree, any
|
|
65
93
|
override that is not re-rooted, what isolation does not cover, and the managed
|
|
66
94
|
worktrees:
|
|
67
95
|
|
|
@@ -261,7 +289,7 @@ use, falling back to directory mtime for a worktree with no record.
|
|
|
261
289
|
| Registration | The default export calls `registerWorktree`: root-tool overrides, the `--worktree` flag, lifecycle listeners, model tools, and the `/worktree` command. Nothing long-lived starts at load. |
|
|
262
290
|
| Tools | `enter_worktree`, `exit_worktree`, `prune_worktrees`, `list_worktrees` via `registerTool`. All `executionMode: "sequential"`. Annotations: `enter_worktree` destructive + open-world; `exit_worktree`/`prune_worktrees` destructive; `list_worktrees` read-only. No `outputSchema`; structured state is returned in tool-result `details` — `exit_worktree` carries `{ state, removed, handoff }`, where `handoff` is `{ branch, base, ahead, merged }`. |
|
|
263
291
|
| Built-in overrides | `read`/`write`/`edit`/`bash`/`grep`/`find`/`ls` are re-registered under the same names with `defaultActive: false`; each spreads the built-in definition and replaces only `execute`. `skipOverrides` is applied at load time. `powershell` is deliberately not overridden: it cannot be re-rooted without a guard, so the `tool_call` handler refuses it (see `guard.blockPowershell`). |
|
|
264
|
-
| Commands | `/worktree` via `registerCommand`, with argument completions for
|
|
292
|
+
| Commands | `/worktree` via `registerCommand`, with argument completions for `enter`, `exit`, and `prune`. The bare command mounts `screen.ts` in a terminal and prints the status text otherwise; `enter`/`exit`/`prune` refuse while plan mode is read-only. |
|
|
265
293
|
| Flags | `--worktree` via `registerFlag`. |
|
|
266
294
|
| Events | `session_start` (bind inherited env, restore or refuse recorded state, read the flag), `session_shutdown` (release the lock, clear status), `before_agent_start` (set the prompt cwd and worktree section), `tool_call` (guard file tools and `bash`, refuse `powershell`, and count a command it lets through unverified). |
|
|
267
295
|
| State | The binding is a custom session entry (`worktree`) written with `pi.appendEntry` and rebuilt from `ctx.sessionManager.getBranch()` on `session_start`. `.pi/worktrees/index.json` is a disposable managed-record cache, reconciled against `git worktree list`. Child processes inherit `PI_WORKTREE_*` env. |
|
|
@@ -291,6 +319,11 @@ use, falling back to directory mtime for a worktree with no record.
|
|
|
291
319
|
provenance and usage in a disposable index. A *Binding* is this session's
|
|
292
320
|
active root, a session entry that drives `getRoot()` and is replayable on the
|
|
293
321
|
branch.
|
|
322
|
+
- **One snapshot, two surfaces.** `worktreeSnapshot` reads the active binding, the
|
|
323
|
+
gaps, and every managed worktree once; `formatWorktreeStatus` renders it as text
|
|
324
|
+
and `screen.ts` renders the same rows as the list. A tool result and the screen
|
|
325
|
+
that disagree about a worktree would be a second source of truth, so there is
|
|
326
|
+
one read and two renderings.
|
|
294
327
|
- **Mutable root in one place.** Pi loads one extension factory per runtime, so
|
|
295
328
|
`runtime.ts` owns the single active worktree, the config cache, and the
|
|
296
329
|
process-environment hook; lifecycle, tools, and commands stay stateless
|
|
@@ -327,12 +360,13 @@ use, falling back to directory mtime for a worktree with no record.
|
|
|
327
360
|
| `enter.ts` / `exit.ts` / `prune.ts` | The lifecycle phases |
|
|
328
361
|
| `handoff.ts` | The `Handoff:` block an exit leaves: the branch's lead, whether the base contains it, and the merge or PR command. |
|
|
329
362
|
| `tools.ts` / `commands.ts` | Model tools / slash commands |
|
|
363
|
+
| `screen.ts` | The `/worktree` list screen: rows, keys, detail pane, and verbs |
|
|
330
364
|
| `root-tools.ts` | Load-time built-in overrides bound to the active root |
|
|
331
365
|
| `git.ts` | Git plumbing (shared runner + read helpers) |
|
|
332
366
|
| `guard.ts` | Path, shell, and git-redirect isolation checks, plus the `powershell` refusal |
|
|
333
367
|
| `gaps.ts` | The isolation gaps a session carries, and how they read (pure) |
|
|
334
368
|
| `include.ts` | `.worktreeinclude` matcher and copy |
|
|
335
|
-
| `config.ts` / `state.ts` / `status.ts` | Config, session entry shape, status text |
|
|
369
|
+
| `config.ts` / `state.ts` / `status.ts` | Config, session entry shape, status text and the shared snapshot |
|
|
336
370
|
| `registry.ts` | Disposable managed-worktree index + derived status |
|
|
337
371
|
|
|
338
372
|
## Testing
|
|
@@ -344,6 +378,8 @@ bun run test:watch
|
|
|
344
378
|
```
|
|
345
379
|
|
|
346
380
|
The suite covers the pure logic (`include.ts` gitignore matcher, `guard.ts` path
|
|
347
|
-
and command checks, `parsePrReference`)
|
|
348
|
-
|
|
349
|
-
|
|
381
|
+
and command checks, `parsePrReference`), drives the `/worktree` screen against a
|
|
382
|
+
snapshot (`ui.test.ts`: rows, marks, verbs, and width bounds), and drives the
|
|
383
|
+
extension end-to-end against real temporary git repositories through a mocked
|
|
384
|
+
`pi` API: enter/exit, isolation guards, borrowed worktrees, prune, and
|
|
385
|
+
inactive-override reporting.
|
|
@@ -1,22 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Slash commands for the worktree extension.
|
|
3
3
|
*
|
|
4
|
-
* One `/worktree` command keeps the surface small
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* One `/worktree` command keeps the surface small: the bare command is the
|
|
5
|
+
* worktree screen (the status text when the session is not a terminal), and three
|
|
6
|
+
* verbs do the lifecycle.
|
|
7
|
+
*
|
|
8
|
+
* /worktree the worktree screen
|
|
9
|
+
* /worktree enter [name] create or re-enter a worktree (name may be a PR/MR ref)
|
|
10
|
+
* /worktree exit leave; worktree.onExit and the prompt decide the fate
|
|
8
11
|
* /worktree prune remove clean, unused, old managed worktrees
|
|
12
|
+
*
|
|
13
|
+
* There are no flags: the exit prompt already offers keep or remove and the
|
|
14
|
+
* config sets the default, so a flag could only disagree with one of the two.
|
|
9
15
|
*/
|
|
10
16
|
|
|
11
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
17
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
18
|
+
import { messageOf } from "../../lib/errors.ts";
|
|
19
|
+
import { isPlanModeEnabled } from "../../lib/plan-state.ts";
|
|
20
|
+
import { withRailsSuppressed } from "../../lib/rails.ts";
|
|
12
21
|
import {
|
|
13
22
|
enterWorktree,
|
|
14
23
|
exitWorktree,
|
|
24
|
+
formatWorktreeStatus,
|
|
15
25
|
isWorktreeCancelled,
|
|
16
26
|
pruneWorktrees,
|
|
17
27
|
worktreeLabel,
|
|
18
|
-
|
|
28
|
+
worktreeSnapshot,
|
|
19
29
|
} from "./lifecycle.ts";
|
|
30
|
+
import { type WorktreeAction, WorktreeListComponent } from "./screen.ts";
|
|
20
31
|
|
|
21
32
|
interface Completion {
|
|
22
33
|
value: string;
|
|
@@ -25,83 +36,116 @@ interface Completion {
|
|
|
25
36
|
}
|
|
26
37
|
|
|
27
38
|
const SUBCOMMANDS: Completion[] = [
|
|
28
|
-
{ value: "status", label: "status", description: "Show the current worktree and list managed worktrees" },
|
|
29
39
|
{ value: "enter", label: "enter", description: "Create or enter a worktree: enter [name]" },
|
|
30
|
-
{ value: "exit", label: "exit", description: "Return to the main checkout
|
|
40
|
+
{ value: "exit", label: "exit", description: "Return to the main checkout" },
|
|
31
41
|
{ value: "prune", label: "prune", description: "Remove clean, unused managed worktrees older than pruneAfterDays" },
|
|
32
42
|
];
|
|
33
43
|
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
const USAGE = "Usage: /worktree [enter [name]|exit|prune]";
|
|
45
|
+
|
|
46
|
+
/** Refuse a lifecycle change while plan mode is read-only, as `/check` does. */
|
|
47
|
+
function refuseInPlanMode(ctx: ExtensionContext): boolean {
|
|
48
|
+
if (!isPlanModeEnabled(ctx.sessionManager.getBranch())) return false;
|
|
49
|
+
ctx.ui.notify("Plan mode is read-only. Approve the plan before changing worktrees.", "warning");
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function runEnter(pi: ExtensionAPI, ctx: ExtensionContext, name: string | undefined): Promise<void> {
|
|
54
|
+
if (refuseInPlanMode(ctx)) return;
|
|
55
|
+
try {
|
|
56
|
+
const { state } = await enterWorktree(pi, ctx, { name });
|
|
57
|
+
ctx.ui.notify(`Entered worktree ${worktreeLabel(state)}\n${state.path}`, "info");
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (isWorktreeCancelled(error)) {
|
|
60
|
+
ctx.ui.notify(messageOf(error), "info");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
ctx.ui.notify(`Enter failed: ${messageOf(error)}`, "error");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function runExit(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
|
|
68
|
+
if (refuseInPlanMode(ctx)) return;
|
|
69
|
+
try {
|
|
70
|
+
const result = await exitWorktree(pi, ctx, {});
|
|
71
|
+
ctx.ui.notify(result.output, "info");
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (isWorktreeCancelled(error)) {
|
|
74
|
+
ctx.ui.notify(messageOf(error), "info");
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
ctx.ui.notify(`Exit failed: ${messageOf(error)}`, "error");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
42
80
|
|
|
43
|
-
|
|
81
|
+
async function runPrune(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
|
|
82
|
+
if (refuseInPlanMode(ctx)) return;
|
|
83
|
+
ctx.ui.notify(await pruneWorktrees(pi, ctx), "info");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The bare command: the list screen in a terminal, the status text elsewhere. */
|
|
87
|
+
async function showWorktrees(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
|
|
88
|
+
const snapshot = await worktreeSnapshot(pi, ctx);
|
|
89
|
+
if (ctx.mode !== "tui") {
|
|
90
|
+
ctx.ui.notify(formatWorktreeStatus(snapshot), "info");
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
// The screen closes before an enter or exit runs: both change the session
|
|
94
|
+
// binding, and `exitWorktree` asks through `ctx.ui.select`, which cannot nest
|
|
95
|
+
// inside a mounted screen. Prune runs in place, so its verb stays on the screen.
|
|
96
|
+
const action = await withRailsSuppressed(pi, () =>
|
|
97
|
+
ctx.ui.custom<WorktreeAction | undefined>((tui, theme, _keybindings, done) => {
|
|
98
|
+
return new WorktreeListComponent({
|
|
99
|
+
snapshot,
|
|
100
|
+
theme,
|
|
101
|
+
requestRender: () => tui.requestRender(),
|
|
102
|
+
onDone: (next) => done(next),
|
|
103
|
+
reload: () => worktreeSnapshot(pi, ctx),
|
|
104
|
+
prune: () => pruneWorktrees(pi, ctx),
|
|
105
|
+
readOnly: isPlanModeEnabled(ctx.sessionManager.getBranch()),
|
|
106
|
+
viewportRowsSource: () => tui.terminal?.rows,
|
|
107
|
+
});
|
|
108
|
+
}),
|
|
109
|
+
);
|
|
110
|
+
if (!action) return;
|
|
111
|
+
if (action.kind === "exit") await runExit(pi, ctx);
|
|
112
|
+
else await runEnter(pi, ctx, action.name);
|
|
113
|
+
}
|
|
44
114
|
|
|
45
115
|
export function registerCommands(pi: ExtensionAPI): void {
|
|
46
116
|
pi.registerCommand("worktree", {
|
|
47
|
-
description: "Manage git worktrees:
|
|
117
|
+
description: "Manage git worktrees: the worktree screen, or enter, exit, and prune",
|
|
48
118
|
getArgumentCompletions: (prefix) => {
|
|
49
|
-
const
|
|
50
|
-
if (
|
|
119
|
+
const text = prefix.trimStart();
|
|
120
|
+
if (text.includes(" ")) return null;
|
|
121
|
+
const items = SUBCOMMANDS.filter((item) => item.value.startsWith(text));
|
|
122
|
+
return items.length > 0 ? items.map((item) => ({ ...item, value: `${item.value} ` })) : null;
|
|
123
|
+
},
|
|
124
|
+
handler: async (args, ctx) => {
|
|
125
|
+
const [sub = "", ...rest] = args.trim().split(/\s+/).filter(Boolean);
|
|
51
126
|
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
return
|
|
127
|
+
if (sub === "") {
|
|
128
|
+
await showWorktrees(pi, ctx);
|
|
129
|
+
return;
|
|
55
130
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
131
|
+
// Only `enter` takes an argument. Anything else is a typo, and a typo that
|
|
132
|
+
// is silently ignored is how a command does something the user did not ask for.
|
|
133
|
+
const allowed = sub === "enter" ? 1 : 0;
|
|
134
|
+
if (rest.length > allowed) {
|
|
135
|
+
ctx.ui.notify(`Unexpected argument "${rest[allowed]}".\n${USAGE}`, "error");
|
|
136
|
+
return;
|
|
60
137
|
}
|
|
61
138
|
|
|
62
|
-
return null;
|
|
63
|
-
},
|
|
64
|
-
handler: async (args, ctx) => {
|
|
65
|
-
const [sub = "status", ...rest] = args.trim().split(/\s+/).filter(Boolean);
|
|
66
|
-
|
|
67
139
|
switch (sub) {
|
|
68
|
-
case "
|
|
69
|
-
|
|
140
|
+
case "enter":
|
|
141
|
+
await runEnter(pi, ctx, rest[0]);
|
|
70
142
|
return;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
try {
|
|
74
|
-
const { state } = await enterWorktree(pi, ctx, { name: rest[0] });
|
|
75
|
-
ctx.ui.notify(`Entered worktree ${worktreeLabel(state)}\n${state.path}`, "info");
|
|
76
|
-
} catch (error) {
|
|
77
|
-
if (isWorktreeCancelled(error)) {
|
|
78
|
-
ctx.ui.notify(error.message, "info");
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
ctx.ui.notify(`Enter failed: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
82
|
-
}
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
case "exit": {
|
|
87
|
-
const remove = rest.includes("--remove") ? true : rest.includes("--keep") ? false : undefined;
|
|
88
|
-
try {
|
|
89
|
-
const result = await exitWorktree(pi, ctx, { remove });
|
|
90
|
-
ctx.ui.notify(result.output, "info");
|
|
91
|
-
} catch (error) {
|
|
92
|
-
if (isWorktreeCancelled(error)) {
|
|
93
|
-
ctx.ui.notify(error.message, "info");
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
ctx.ui.notify(`Exit failed: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
97
|
-
}
|
|
143
|
+
case "exit":
|
|
144
|
+
await runExit(pi, ctx);
|
|
98
145
|
return;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
146
|
case "prune":
|
|
102
|
-
|
|
147
|
+
await runPrune(pi, ctx);
|
|
103
148
|
return;
|
|
104
|
-
|
|
105
149
|
default:
|
|
106
150
|
ctx.ui.notify(`Unknown worktree subcommand "${sub}".\n${USAGE}`, "error");
|
|
107
151
|
}
|
|
@@ -110,7 +110,7 @@ export async function enterWorktree(
|
|
|
110
110
|
}
|
|
111
111
|
if (current) {
|
|
112
112
|
throw new Error(
|
|
113
|
-
`Already working in a worktree (${worktreeLabel(current)}).
|
|
113
|
+
`Already working in a worktree (${worktreeLabel(current)}). Run /worktree exit, or call exit_worktree, before entering another.`,
|
|
114
114
|
);
|
|
115
115
|
}
|
|
116
116
|
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
lockWorktree,
|
|
17
17
|
refExists,
|
|
18
18
|
unlockWorktree,
|
|
19
|
+
worktreePath,
|
|
19
20
|
worktreeRemove,
|
|
20
21
|
} from "./git.ts";
|
|
21
22
|
import { handoffLines, type Handoff } from "./handoff.ts";
|
|
@@ -163,7 +164,16 @@ export async function exitWorktree(
|
|
|
163
164
|
}
|
|
164
165
|
} else {
|
|
165
166
|
lines.push(`Kept worktree ${state.path}`);
|
|
166
|
-
if (state.branch)
|
|
167
|
+
if (state.branch) {
|
|
168
|
+
// Name entry only reaches a managed worktree; a path the user pointed at is
|
|
169
|
+
// re-entered through the tool.
|
|
170
|
+
const label = worktreeLabel(state);
|
|
171
|
+
lines.push(
|
|
172
|
+
worktreePath(state.repoRoot, config, label) === state.path
|
|
173
|
+
? `Re-enter with /worktree enter ${label}`
|
|
174
|
+
: `Re-enter with enter_worktree (path: ${state.path}); /worktree enter only manages ${config.dir}`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
167
177
|
touchRecord(state.repoRoot, config, state.path);
|
|
168
178
|
}
|
|
169
179
|
|
|
@@ -11,4 +11,12 @@ export { enterWorktree, type EnterOptions } from "./enter.ts";
|
|
|
11
11
|
export { exitWorktree, type ExitOptions } from "./exit.ts";
|
|
12
12
|
export { pruneWorktrees } from "./prune.ts";
|
|
13
13
|
export { findInactiveOverrides } from "./root-tools.ts";
|
|
14
|
-
export {
|
|
14
|
+
export {
|
|
15
|
+
formatWorktreeStatus,
|
|
16
|
+
refusalMessage,
|
|
17
|
+
stateSummary,
|
|
18
|
+
worktreeFacts,
|
|
19
|
+
worktreeLabel,
|
|
20
|
+
worktreeSnapshot,
|
|
21
|
+
worktreeStatus,
|
|
22
|
+
} from "./status.ts";
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `/worktree` list screen: one row per managed worktree, the active one
|
|
3
|
+
* marked, with a detail pane and three verbs.
|
|
4
|
+
*
|
|
5
|
+
* The frame, navigation, y/N confirmation, and pane come from the shared
|
|
6
|
+
* `DockListScreen`, so this module owns only the worktree vocabulary. Rows come
|
|
7
|
+
* from the same `worktreeSnapshot` the text status renders, so the screen and the
|
|
8
|
+
* `list_worktrees` tool cannot disagree about a worktree.
|
|
9
|
+
*
|
|
10
|
+
* Every verb but `p` closes the screen and hands the action back to the command:
|
|
11
|
+
* entering and exiting change the session binding and `exitWorktree` asks through
|
|
12
|
+
* `ctx.ui.select`, which cannot nest inside a mount. `p` never prompts, so it
|
|
13
|
+
* runs in place and refreshes the rows.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { basename } from "node:path";
|
|
17
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
18
|
+
import { messageOf } from "../../lib/errors.ts";
|
|
19
|
+
import type { ScreenHint, ViewportRowsSource } from "../../lib/tui/chrome.ts";
|
|
20
|
+
import { DockListScreen, type DetailContents } from "../../lib/tui/dock-screen.ts";
|
|
21
|
+
import { GLYPHS } from "../../lib/tui/glyphs.ts";
|
|
22
|
+
import { statusRow } from "../../lib/tui/rows.ts";
|
|
23
|
+
import { paint, type TuiRole } from "../../lib/tui/tokens.ts";
|
|
24
|
+
import { gapLines } from "./gaps.ts";
|
|
25
|
+
import { handoffLines } from "./handoff.ts";
|
|
26
|
+
import type { CheckoutStatus } from "./registry.ts";
|
|
27
|
+
import { relativeDay, statusMarks, type WorktreeSnapshot, worktreeLabel } from "./status.ts";
|
|
28
|
+
|
|
29
|
+
/** What a verb asks the command to do once the screen is gone. */
|
|
30
|
+
export type WorktreeAction = { kind: "enter"; name: string } | { kind: "exit" };
|
|
31
|
+
|
|
32
|
+
/** One row: a managed worktree, or the active one when this session does not manage it. */
|
|
33
|
+
export interface WorktreeRow {
|
|
34
|
+
/** Canonical path, and the row's stable id. */
|
|
35
|
+
path: string;
|
|
36
|
+
/** Basename of the path: the name `enter` takes back. */
|
|
37
|
+
name: string;
|
|
38
|
+
branch: string | undefined;
|
|
39
|
+
/** Whether this row is the session's active worktree. */
|
|
40
|
+
current: boolean;
|
|
41
|
+
/** What git says about it; absent for a worktree entered by path. */
|
|
42
|
+
status: CheckoutStatus | undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The rows for a snapshot, in git's order, with the active worktree always present. */
|
|
46
|
+
export function worktreeRows(snapshot: WorktreeSnapshot): WorktreeRow[] {
|
|
47
|
+
const rows: WorktreeRow[] = snapshot.managed.map((item) => ({
|
|
48
|
+
path: item.entry.path,
|
|
49
|
+
name: basename(item.entry.path),
|
|
50
|
+
branch: item.entry.branch,
|
|
51
|
+
current: snapshot.active?.path === item.entry.path,
|
|
52
|
+
status: item.status,
|
|
53
|
+
}));
|
|
54
|
+
const active = snapshot.active;
|
|
55
|
+
// A worktree entered by path is not in the managed list, but a reader looking
|
|
56
|
+
// for "where am I" must still see it.
|
|
57
|
+
if (active && !rows.some((row) => row.path === active.path)) {
|
|
58
|
+
rows.unshift({
|
|
59
|
+
path: active.path,
|
|
60
|
+
name: worktreeLabel(active),
|
|
61
|
+
branch: active.branch,
|
|
62
|
+
current: true,
|
|
63
|
+
status: undefined,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return rows;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The glyph and its role: a missing checkout is the error, dirty work the warning. */
|
|
70
|
+
function rowGlyph(status: CheckoutStatus | undefined): { char: string; role: TuiRole } {
|
|
71
|
+
if (!status) return { char: GLYPHS.worktree, role: "info" };
|
|
72
|
+
if (status.state === "missing") return { char: GLYPHS.cross, role: "bad" };
|
|
73
|
+
if (status.state === "dirty") return { char: GLYPHS.warn, role: "warn" };
|
|
74
|
+
if (status.merged) return { char: GLYPHS.check, role: "ok" };
|
|
75
|
+
return { char: GLYPHS.branch, role: "info" };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The row's right-hand text: its marks, or `clean` when it has none. */
|
|
79
|
+
function rowMarks(row: WorktreeRow): string {
|
|
80
|
+
const marks = row.status ? statusMarks(row.status, row.current) : ["current"];
|
|
81
|
+
if (marks.length === 0) marks.push("clean");
|
|
82
|
+
return marks.join(" ");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface WorktreeScreenOptions {
|
|
86
|
+
/** The rows to open with; the command has already read them. */
|
|
87
|
+
snapshot: WorktreeSnapshot;
|
|
88
|
+
theme: Theme;
|
|
89
|
+
requestRender(): void;
|
|
90
|
+
/** Close the screen; `undefined` when the user only closed it. */
|
|
91
|
+
onDone(action: WorktreeAction | undefined): void;
|
|
92
|
+
/** Read the rows again after a verb changed them. */
|
|
93
|
+
reload(): Promise<WorktreeSnapshot>;
|
|
94
|
+
/** Run a prune in place and return its report. */
|
|
95
|
+
prune(): Promise<string>;
|
|
96
|
+
/** Plan mode: the screen still shows the list, but no verb may change it. */
|
|
97
|
+
readOnly: boolean;
|
|
98
|
+
viewportRowsSource?: ViewportRowsSource;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The `/worktree` screen. */
|
|
102
|
+
export class WorktreeListComponent extends DockListScreen<WorktreeRow> {
|
|
103
|
+
private view: WorktreeSnapshot;
|
|
104
|
+
private pruning = false;
|
|
105
|
+
|
|
106
|
+
constructor(private readonly deps: WorktreeScreenOptions) {
|
|
107
|
+
super({
|
|
108
|
+
theme: deps.theme,
|
|
109
|
+
title: "Worktrees",
|
|
110
|
+
detailTitle: "Worktree",
|
|
111
|
+
emptyList: "No managed worktrees. /worktree enter <name> creates one.",
|
|
112
|
+
emptyDetail: "Nothing recorded for this worktree.",
|
|
113
|
+
requestRender: deps.requestRender,
|
|
114
|
+
onClose: () => deps.onDone(undefined),
|
|
115
|
+
viewportRowsSource: deps.viewportRowsSource,
|
|
116
|
+
});
|
|
117
|
+
this.view = deps.snapshot;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
protected rows(): readonly WorktreeRow[] {
|
|
121
|
+
return worktreeRows(this.view);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
protected idOf(row: WorktreeRow): string {
|
|
125
|
+
return row.path;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
protected rowLine(row: WorktreeRow, width: number, selected: boolean): string {
|
|
129
|
+
const glyph = rowGlyph(row.status);
|
|
130
|
+
return statusRow(this.theme, width, {
|
|
131
|
+
selected,
|
|
132
|
+
glyph: glyph.char,
|
|
133
|
+
glyphRole: glyph.role,
|
|
134
|
+
label: row.name,
|
|
135
|
+
labelRole: selected ? "accent" : "label",
|
|
136
|
+
trailing: rowMarks(row),
|
|
137
|
+
trailingRole: row.current ? "accent" : "faint",
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
protected detailBlock(row: WorktreeRow): string[] {
|
|
142
|
+
const lines = [`${row.path} ${row.branch ?? "(detached)"}`];
|
|
143
|
+
const active = this.view.active;
|
|
144
|
+
// The gaps are the same rules `/worktree` status and `doctor` report: a gap
|
|
145
|
+
// the user cannot see is a gap they cannot weigh.
|
|
146
|
+
if (row.current && active) {
|
|
147
|
+
lines.push(`base: ${active.baseRef} (${active.baseRefMode}) main: ${active.repoRoot}`);
|
|
148
|
+
if (this.view.gaps.length > 0) lines.push("Not isolated:", ...gapLines(this.view.gaps));
|
|
149
|
+
}
|
|
150
|
+
return lines;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
protected summaryCells(rows: readonly WorktreeRow[]): string[] {
|
|
154
|
+
const cells = [paint(this.theme, "meta", `${rows.length} worktree${rows.length === 1 ? "" : "s"}`)];
|
|
155
|
+
const dirty = rows.filter((row) => row.status?.state === "dirty").length;
|
|
156
|
+
if (dirty > 0) cells.push(paint(this.theme, "warn", `${dirty} dirty`));
|
|
157
|
+
const ahead = rows.filter((row) => (row.status?.ahead ?? 0) > 0).length;
|
|
158
|
+
if (ahead > 0) cells.push(paint(this.theme, "accent", `${ahead} ahead`));
|
|
159
|
+
const inactive = this.view.inactiveOverrides;
|
|
160
|
+
if (inactive.length > 0) cells.push(paint(this.theme, "warn", `not isolated: ${inactive.join(", ")}`));
|
|
161
|
+
return cells;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
protected listHints(): ScreenHint[] {
|
|
165
|
+
return [
|
|
166
|
+
"Esc close",
|
|
167
|
+
"↑/↓ or j/k select",
|
|
168
|
+
{ text: "Enter or l detail", color: "accent" },
|
|
169
|
+
{ text: "e enter", color: "accent" },
|
|
170
|
+
{ text: "x exit", color: "warning" },
|
|
171
|
+
{ text: "p prune", color: "warning" },
|
|
172
|
+
];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
protected detailHints(): ScreenHint[] {
|
|
176
|
+
return ["Esc back", "↑/↓ scroll", "g/G ends"];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
protected detailFor(row: WorktreeRow): DetailContents {
|
|
180
|
+
const lines = [`path ${row.path}`, `branch ${row.branch ?? "(detached)"}`];
|
|
181
|
+
const status = row.status;
|
|
182
|
+
if (!status) {
|
|
183
|
+
lines.push("state entered by path; this session does not manage it");
|
|
184
|
+
} else {
|
|
185
|
+
if (status.head) lines.push(`head ${status.head.slice(0, 7)}`);
|
|
186
|
+
const bits = [status.state === "dirty" ? `dirty (${status.changed} changed)` : "clean"];
|
|
187
|
+
if (status.ahead > 0) bits.push(`${status.ahead} ahead`);
|
|
188
|
+
if (status.behind > 0) bits.push(`${status.behind} behind`);
|
|
189
|
+
if (status.merged) bits.push("merged");
|
|
190
|
+
lines.push(`state ${bits.join(", ")}`);
|
|
191
|
+
if (status.locked) {
|
|
192
|
+
lines.push(`lock ${status.staleLock ? "stale (owner is gone)" : "held by a live session"}`);
|
|
193
|
+
}
|
|
194
|
+
if (status.lastUsedAt) lines.push(`used ${relativeDay(status.lastUsedAt)}`);
|
|
195
|
+
// The handoff is read, not performed: a merge lands in the main checkout.
|
|
196
|
+
if (status.ahead > 0 && row.branch) {
|
|
197
|
+
lines.push(
|
|
198
|
+
"",
|
|
199
|
+
...handoffLines({
|
|
200
|
+
branch: row.branch,
|
|
201
|
+
base: this.view.baseBranch ?? null,
|
|
202
|
+
ahead: status.ahead,
|
|
203
|
+
merged: status.merged,
|
|
204
|
+
}),
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return { title: row.name, lines, subtitle: row.branch ?? row.path };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
protected handleVerb(key: string, row: WorktreeRow | undefined): void {
|
|
212
|
+
if (key !== "e" && key !== "x" && key !== "p") return;
|
|
213
|
+
if (this.deps.readOnly) {
|
|
214
|
+
this.notify("Plan mode is read-only. Approve the plan before changing worktrees.");
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (key === "e") {
|
|
218
|
+
if (!row) {
|
|
219
|
+
this.notify("No worktree to enter.");
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (row.current) {
|
|
223
|
+
this.notify(`${row.name} is already the current worktree.`);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (this.view.active) {
|
|
227
|
+
this.notify(`Still in ${worktreeLabel(this.view.active)}; press x to exit first.`);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
this.deps.onDone({ kind: "enter", name: row.name });
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (key === "x") {
|
|
234
|
+
if (!this.view.active) {
|
|
235
|
+
this.notify("Not in a worktree.");
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
this.deps.onDone({ kind: "exit" });
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (this.pruning) {
|
|
242
|
+
this.notify("A prune is already running.");
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
this.pruning = true;
|
|
246
|
+
this.notify("Pruning…");
|
|
247
|
+
void this.pruneInPlace();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Prune is the one verb that never prompts, so it runs without closing the screen. */
|
|
251
|
+
private async pruneInPlace(): Promise<void> {
|
|
252
|
+
try {
|
|
253
|
+
const report = await this.deps.prune();
|
|
254
|
+
this.view = await this.deps.reload();
|
|
255
|
+
this.resort();
|
|
256
|
+
this.notify(report.split("\n")[0] ?? "Pruned.");
|
|
257
|
+
} catch (error) {
|
|
258
|
+
this.notify(`Prune failed: ${messageOf(error)}`);
|
|
259
|
+
} finally {
|
|
260
|
+
this.pruning = false;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|