@gavin-hu/my-pi-agent 0.18.0 → 0.20.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 +66 -52
- package/README.zh-CN.md +56 -46
- package/docs/extensions/README.md +34 -0
- package/docs/extensions/board.md +507 -0
- package/{extensions/bytefly/README.md → docs/extensions/bytefly.md} +774 -574
- package/{extensions/check/README.md → docs/extensions/check.md} +83 -68
- package/{extensions/context/README.md → docs/extensions/context.md} +40 -23
- package/docs/extensions/file-browse.md +561 -0
- package/{extensions/file-open/README.md → docs/extensions/file-open.md} +31 -23
- package/{extensions/file-read/README.md → docs/extensions/file-read.md} +47 -55
- package/{extensions/goal/README.md → docs/extensions/goal.md} +115 -131
- package/{extensions/job/README.md → docs/extensions/job.md} +62 -52
- package/{extensions/memory/README.md → docs/extensions/memory.md} +71 -60
- package/{extensions/plan/README.md → docs/extensions/plan.md} +440 -322
- package/{extensions/question/README.md → docs/extensions/question.md} +54 -31
- package/{extensions/rewind/README.md → docs/extensions/rewind.md} +115 -122
- package/docs/extensions/session-bus.md +298 -0
- package/{extensions/subagent/README.md → docs/extensions/subagent.md} +165 -118
- package/docs/extensions/team.md +254 -0
- package/{extensions/todo/README.md → docs/extensions/todo.md} +85 -88
- package/{extensions/watch/README.md → docs/extensions/watch.md} +54 -36
- package/{extensions/web-access/README.md → docs/extensions/web-access.md} +93 -50
- package/{extensions/wechat/README.md → docs/extensions/wechat.md} +260 -163
- package/{extensions/worktree/README.md → docs/extensions/worktree.md} +137 -79
- package/extensions/board/commands.ts +105 -0
- package/extensions/board/config.ts +36 -0
- package/extensions/board/format.ts +170 -0
- package/extensions/board/index.ts +40 -0
- package/extensions/board/input.ts +54 -0
- package/extensions/board/join.ts +42 -0
- package/extensions/board/page.ts +388 -0
- package/extensions/board/result.ts +108 -0
- package/extensions/board/rows.ts +48 -0
- package/extensions/board/schema.ts +90 -0
- package/extensions/board/seat.ts +28 -0
- package/extensions/board/snapshot.ts +42 -0
- package/extensions/board/tools.ts +444 -0
- package/extensions/board/transport/router.ts +91 -0
- package/extensions/board/transport/server.ts +138 -0
- package/extensions/board/types.ts +121 -0
- package/extensions/board/view/client.ts +51 -0
- package/extensions/board/view/dialog.ts +43 -0
- package/extensions/board/view/keys.ts +125 -0
- package/extensions/board/view/live.ts +231 -0
- package/extensions/board/view/names.ts +91 -0
- package/extensions/board/view/page.ts +118 -0
- package/extensions/board/view/render.ts +519 -0
- package/extensions/board/view/styles.ts +228 -0
- package/extensions/board/view/team.ts +327 -0
- package/extensions/bytefly/footer/budget.ts +6 -5
- package/extensions/bytefly/footer/collect/entries.ts +12 -4
- package/extensions/bytefly/footer/collect/snapshot.ts +8 -17
- package/extensions/bytefly/footer/config.ts +19 -8
- package/extensions/bytefly/footer/price/price.ts +30 -8
- package/extensions/bytefly/footer/register.ts +5 -5
- package/extensions/bytefly/footer/render/format.ts +18 -0
- package/extensions/bytefly/footer/render/lines.ts +23 -7
- package/extensions/bytefly/footer/render/segments.ts +117 -31
- package/extensions/bytefly/footer/types.ts +29 -5
- package/extensions/bytefly/footer/unpriced.ts +42 -17
- package/extensions/bytefly/panel/commands.ts +67 -7
- package/extensions/bytefly/panel/filter.ts +68 -0
- package/extensions/bytefly/panel/format.ts +16 -15
- package/extensions/bytefly/panel/join.ts +56 -13
- package/extensions/bytefly/panel/prices.ts +43 -0
- package/extensions/bytefly/panel/settings-runtime.ts +0 -6
- package/extensions/bytefly/panel/settings.ts +75 -43
- package/extensions/bytefly/panel/tui/entries.ts +26 -8
- package/extensions/bytefly/panel/tui/frame.ts +130 -93
- package/extensions/bytefly/panel/tui/keys.ts +352 -0
- package/extensions/bytefly/panel/tui/list-view.ts +25 -46
- package/extensions/bytefly/panel/tui/panes.ts +76 -0
- package/extensions/bytefly/panel/tui/section-view.ts +126 -0
- package/extensions/bytefly/panel/tui/settings-section.ts +54 -125
- package/extensions/bytefly/panel/tui/settings-view.ts +30 -131
- package/extensions/bytefly/panel/tui/side-pane.ts +224 -0
- package/extensions/bytefly/panel/tui.ts +306 -259
- package/extensions/bytefly/picker/discovery.ts +0 -2
- package/extensions/bytefly/picker/types.ts +0 -2
- package/extensions/bytefly/update/check.ts +39 -0
- package/extensions/bytefly/update/format.ts +15 -0
- package/extensions/bytefly/update/register.ts +73 -34
- package/extensions/bytefly/update/state.ts +6 -3
- package/extensions/check/commands.ts +2 -1
- package/extensions/context/tui.ts +1 -1
- package/extensions/file-browse/{config.ts → config/config.ts} +1 -1
- package/extensions/file-browse/files/classify.ts +136 -0
- package/extensions/file-browse/files/files.ts +12 -0
- package/extensions/file-browse/files/index.ts +90 -0
- package/extensions/file-browse/files/list.ts +261 -0
- package/extensions/file-browse/{paths.ts → files/paths.ts} +1 -1
- package/extensions/file-browse/files/tree.ts +101 -0
- package/extensions/file-browse/{git.ts → git/git.ts} +10 -3
- package/extensions/file-browse/index.ts +4 -4
- package/extensions/file-browse/{live.ts → live/live.ts} +20 -69
- package/extensions/file-browse/{router.ts → transport/router.ts} +21 -17
- package/extensions/file-browse/transport/server.ts +110 -0
- package/extensions/file-browse/{chrome.ts → view/chrome.ts} +4 -4
- package/extensions/file-browse/{client.ts → view/client.ts} +1 -1
- package/extensions/file-browse/{diff-html.ts → view/diff-html.ts} +3 -3
- package/extensions/file-browse/view/emit.ts +51 -0
- package/extensions/file-browse/{html.ts → view/html.ts} +19 -18
- package/extensions/file-browse/{icons.ts → view/icons.ts} +1 -1
- package/extensions/file-browse/{markdown.ts → view/markdown.ts} +18 -20
- package/extensions/file-browse/{raw-html.ts → view/raw-html.ts} +19 -14
- package/extensions/file-browse/{styles.ts → view/styles.ts} +29 -5
- package/extensions/file-browse/{tree.ts → view/tree.ts} +1 -1
- package/extensions/file-browse/{urls.ts → view/urls.ts} +1 -1
- package/extensions/file-read/schema.ts +2 -1
- package/extensions/job/runtime.ts +4 -4
- package/extensions/memory/store.ts +12 -5
- package/extensions/plan/blocks.ts +23 -2
- package/extensions/plan/commands.ts +21 -16
- package/extensions/plan/feedback.ts +185 -52
- package/extensions/plan/format.ts +10 -3
- package/extensions/plan/index.ts +23 -5
- package/extensions/plan/plan-files.ts +88 -0
- package/extensions/plan/plan-index.ts +43 -9
- package/extensions/plan/plan-text.ts +33 -23
- package/extensions/plan/plans.ts +27 -34
- package/extensions/plan/policy.ts +30 -17
- package/extensions/plan/review.ts +3 -27
- package/extensions/plan/runtime.ts +52 -11
- package/extensions/plan/sections.ts +112 -0
- package/extensions/plan/steps.ts +3 -22
- package/extensions/plan/todos.ts +51 -3
- package/extensions/plan/tools/exit.ts +20 -95
- package/extensions/plan/tools/todo-seed.ts +4 -2
- package/extensions/plan/tools/write.ts +12 -1
- package/extensions/plan/tui.ts +5 -34
- package/extensions/plan/types.ts +15 -8
- package/extensions/plan/web-blocks.ts +89 -0
- package/extensions/plan/web-chrome.ts +3 -5
- package/extensions/plan/web-client.ts +179 -330
- package/extensions/plan/web-collab.ts +3 -3
- package/extensions/plan/web-comments.ts +18 -8
- package/extensions/plan/web-composer.ts +80 -0
- package/extensions/plan/web-draft.ts +40 -0
- package/extensions/plan/web-keys.ts +134 -0
- package/extensions/plan/web-outline.ts +192 -0
- package/extensions/plan/web-page.ts +4 -8
- package/extensions/plan/web-plan.ts +150 -0
- package/extensions/plan/web-router.ts +3 -58
- package/extensions/plan/web-settle.ts +194 -0
- package/extensions/plan/web-share-session.ts +24 -30
- package/extensions/plan/web-streams.ts +14 -14
- package/extensions/plan/web-tokens.ts +2 -2
- package/extensions/plan/web.ts +66 -120
- package/extensions/rewind/{commands.ts → commands/commands.ts} +5 -5
- package/extensions/rewind/{review.ts → commands/review.ts} +15 -14
- package/extensions/rewind/{rewind.ts → commands/rewind.ts} +6 -6
- package/extensions/rewind/{config.ts → config/config.ts} +1 -1
- package/extensions/rewind/index.ts +3 -3
- package/extensions/rewind/{policy.ts → state/policy.ts} +4 -4
- package/extensions/rewind/{runtime.ts → state/runtime.ts} +11 -11
- package/extensions/rewind/{snapshot-run.ts → state/snapshot-run.ts} +1 -1
- package/extensions/rewind/{timeline.ts → state/timeline.ts} +2 -2
- package/extensions/rewind/{format.ts → text/format.ts} +3 -3
- package/extensions/rewind/{git.ts → transport/git.ts} +2 -2
- package/extensions/rewind/{restore.ts → transport/restore.ts} +2 -2
- package/extensions/rewind/{snapshot.ts → transport/snapshot.ts} +1 -1
- package/extensions/rewind/{store.ts → transport/store.ts} +1 -1
- package/extensions/rewind/{checklist.ts → view/checklist.ts} +6 -6
- package/extensions/rewind/{hunk-picker.ts → view/hunk-picker.ts} +8 -8
- package/extensions/rewind/{path-picker.ts → view/path-picker.ts} +7 -7
- package/extensions/rewind/{review-tui.ts → view/review-tui.ts} +11 -11
- package/extensions/rewind/{rewind-tui.ts → view/rewind-tui.ts} +17 -11
- package/extensions/rewind/{status.ts → view/status.ts} +4 -4
- package/extensions/session-bus/commands.ts +17 -69
- package/extensions/session-bus/format.ts +22 -45
- package/extensions/session-bus/index.ts +13 -7
- package/extensions/session-bus/join-request.ts +40 -0
- package/extensions/session-bus/mail-watch.ts +134 -0
- package/extensions/session-bus/notice.ts +32 -4
- package/{lib/claims → extensions/session-bus}/overlap.ts +11 -18
- package/extensions/session-bus/protocol.ts +19 -49
- package/extensions/session-bus/resolve.ts +67 -0
- package/extensions/session-bus/result.ts +16 -51
- package/extensions/session-bus/rows.ts +11 -5
- package/extensions/session-bus/runtime.ts +220 -267
- package/extensions/session-bus/store.ts +55 -7
- package/extensions/session-bus/tools.ts +31 -66
- package/extensions/session-bus/tui.ts +5 -5
- package/extensions/session-bus/types.ts +23 -14
- package/extensions/subagent/agents.ts +9 -8
- package/extensions/subagent/commands.ts +2 -1
- package/extensions/subagent/invocation.ts +4 -2
- package/extensions/team/commands.ts +145 -0
- package/extensions/team/dispatch.ts +133 -0
- package/extensions/team/format.ts +119 -0
- package/extensions/team/index.ts +34 -0
- package/extensions/team/schema.ts +29 -0
- package/extensions/team/tools.ts +267 -0
- package/extensions/team/types.ts +65 -0
- package/extensions/todo/index.ts +4 -1
- package/extensions/todo/nudge.ts +8 -2
- package/extensions/web-access/fetch/page.ts +4 -1
- package/extensions/web-access/fetch/structured.ts +447 -0
- package/extensions/web-access/fetch/tool.ts +2 -1
- package/extensions/web-access/search/tool.ts +2 -1
- package/extensions/wechat/bridge.ts +106 -251
- package/extensions/wechat/{commands.ts → commands/commands.ts} +26 -18
- package/extensions/wechat/{doctor.ts → commands/doctor.ts} +5 -5
- package/extensions/wechat/{history.ts → commands/history.ts} +1 -1
- package/extensions/wechat/{login.ts → commands/login.ts} +3 -3
- package/extensions/wechat/{slash.ts → commands/slash.ts} +38 -2
- package/extensions/wechat/dialog/channels.ts +136 -0
- package/extensions/wechat/{interaction.ts → dialog/interaction.ts} +37 -37
- package/extensions/wechat/{prompt.ts → dialog/prompt.ts} +2 -2
- package/extensions/wechat/{remote-ui.ts → dialog/remote-ui.ts} +1 -1
- package/extensions/wechat/index.ts +10 -10
- package/extensions/wechat/intake.ts +245 -0
- package/extensions/wechat/{admission.ts → peer/admission.ts} +2 -2
- package/extensions/wechat/{peer-session.ts → peer/peer-session.ts} +2 -2
- package/extensions/wechat/{peer-worker.ts → peer/peer-worker.ts} +100 -22
- package/extensions/wechat/{presence.ts → peer/presence.ts} +8 -7
- package/extensions/wechat/{reply-pipeline.ts → peer/reply-pipeline.ts} +1 -1
- package/extensions/wechat/{credentials.ts → state/credentials.ts} +2 -2
- package/extensions/wechat/{state.ts → state/state.ts} +14 -2
- package/extensions/wechat/{format.ts → text/format.ts} +3 -3
- package/extensions/wechat/{notices.ts → text/notices.ts} +8 -0
- package/extensions/wechat/tools.ts +29 -12
- package/extensions/wechat/{client.ts → transport/client.ts} +26 -2
- package/extensions/wechat/{deliver.ts → transport/deliver.ts} +72 -28
- package/extensions/wechat/{inbound.ts → transport/inbound.ts} +5 -5
- package/extensions/wechat/{media.ts → transport/media.ts} +11 -5
- package/extensions/wechat/transport/outbound.ts +142 -0
- package/extensions/wechat/{poll.ts → transport/poll.ts} +3 -3
- package/extensions/wechat/types.ts +10 -1
- package/extensions/wechat/{login-screen.ts → view/login-screen.ts} +4 -4
- package/extensions/wechat/{status-screen.ts → view/status-screen.ts} +31 -26
- package/extensions/worktree/commands.ts +24 -17
- package/extensions/worktree/exit.ts +12 -6
- package/extensions/worktree/git.ts +22 -14
- package/extensions/worktree/guard.ts +18 -290
- package/extensions/worktree/handoff.ts +10 -2
- package/extensions/worktree/index.ts +2 -1
- package/extensions/worktree/lifecycle.ts +2 -0
- package/extensions/worktree/registry.ts +20 -11
- package/extensions/worktree/root-tools.ts +13 -3
- package/extensions/worktree/runtime.ts +8 -2
- package/extensions/worktree/screen.ts +15 -8
- package/extensions/worktree/shell.ts +303 -0
- package/extensions/worktree/state.ts +1 -1
- package/extensions/worktree/status.ts +42 -9
- package/extensions/worktree/tools.ts +19 -13
- package/lib/README.md +11 -8
- package/lib/board/card.ts +479 -0
- package/lib/board/links.ts +141 -0
- package/lib/board/page-pointer.ts +310 -0
- package/lib/board/round-text.ts +81 -0
- package/lib/board/round.ts +94 -0
- package/lib/board/rules.ts +437 -0
- package/lib/board/session.ts +105 -0
- package/lib/board/store.ts +497 -0
- package/lib/board/view.ts +234 -0
- package/lib/errors.ts +12 -4
- package/lib/format.ts +35 -0
- package/lib/http/guard.ts +112 -0
- package/lib/http/index.ts +16 -5
- package/lib/http/serve.ts +237 -0
- package/lib/http/server.ts +12 -10
- package/lib/http/sink.ts +135 -0
- package/lib/http/token.ts +38 -0
- package/lib/plan-entry.ts +11 -0
- package/lib/policy.ts +25 -2
- package/lib/requirements.ts +6 -7
- package/lib/session-label.ts +64 -0
- package/lib/session-roster.ts +201 -0
- package/lib/session-tool.ts +184 -0
- package/lib/spawn.ts +37 -3
- package/lib/stray-fields.ts +45 -0
- package/lib/tool-names.ts +38 -2
- package/lib/tui/glyphs.ts +3 -2
- package/lib/web/README.md +57 -4
- package/lib/web/client-runtime.ts +123 -0
- package/lib/web/components.ts +5 -0
- package/lib/web/index.ts +4 -2
- package/lib/web/markup.ts +88 -1
- package/lib/web/names.ts +42 -0
- package/lib/web/runtime.ts +7 -1
- package/lib/web/tokens.ts +25 -2
- package/package.json +26 -14
- package/skills/next/SKILL.md +4 -3
- package/skills/project-docs/SKILL.md +133 -0
- package/skills/project-docs/references/audit.md +131 -0
- package/skills/project-docs/references/changelog.md +105 -0
- package/skills/project-docs/references/discovery.md +127 -0
- package/skills/project-docs/references/readme.md +80 -0
- package/skills/project-docs/references/release.md +84 -0
- package/skills/project-docs/references/roadmap.md +93 -0
- package/skills/project-docs/scripts/state.ts +316 -0
- package/skills/refactor/SKILL.md +100 -125
- package/skills/refactor/references/handoff.md +35 -0
- package/skills/refactor/references/signals.md +54 -0
- package/skills/refactor/scripts/inventory.ts +587 -0
- package/extensions/bytefly/panel/tui/extensions-view.ts +0 -40
- package/extensions/bytefly/panel/tui/skills-view.ts +0 -50
- package/extensions/file-browse/README.md +0 -506
- package/extensions/file-browse/files.ts +0 -564
- package/extensions/file-browse/server.ts +0 -141
- package/extensions/job/process.ts +0 -35
- package/extensions/plan/split.ts +0 -87
- package/extensions/session-bus/README.md +0 -247
- package/extensions/session-bus/team.ts +0 -102
- package/extensions/wechat/outbound.ts +0 -100
- package/lib/plans/sections.ts +0 -172
- package/lib/plans/setting.ts +0 -52
- package/lib/plans/split.ts +0 -215
- package/lib/plans/store.ts +0 -19
- package/lib/plans/team.ts +0 -151
- package/skills/changelog/SKILL.md +0 -148
- package/skills/parallel-dev/SKILL.md +0 -150
- /package/extensions/file-browse/{rev.ts → live/rev.ts} +0 -0
- /package/extensions/file-browse/{diff.ts → text/diff.ts} +0 -0
- /package/extensions/file-browse/{fuzzy.ts → text/fuzzy.ts} +0 -0
- /package/extensions/file-browse/{live-client.ts → view/live-client.ts} +0 -0
- /package/extensions/file-browse/{palette.ts → view/palette.ts} +0 -0
- /package/extensions/rewind/{patch.ts → text/patch.ts} +0 -0
- /package/extensions/rewind/{paths.ts → text/paths.ts} +0 -0
- /package/extensions/rewind/{queue.ts → transport/queue.ts} +0 -0
- /package/extensions/rewind/{temp-index.ts → transport/temp-index.ts} +0 -0
- /package/extensions/wechat/{lock.ts → state/lock.ts} +0 -0
- /package/extensions/wechat/{channel.ts → text/channel.ts} +0 -0
|
@@ -12,11 +12,24 @@
|
|
|
12
12
|
* 5. the un-re-rooted `powershell` tool, which has no guard of its own.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import {
|
|
16
|
-
import { isAbsolute, join, resolve } from "node:path";
|
|
15
|
+
import { isAbsolute, resolve } from "node:path";
|
|
17
16
|
import { isInside, isInsideReal, realPathOfNearest } from "../../lib/path.ts";
|
|
18
|
-
import {
|
|
17
|
+
import { WORKSPACE_READERS } from "../../lib/policy.ts";
|
|
19
18
|
import type { WorktreeConfig } from "./config.ts";
|
|
19
|
+
import {
|
|
20
|
+
COMMAND_WRAPPERS,
|
|
21
|
+
isAssignment,
|
|
22
|
+
isUnverifiable,
|
|
23
|
+
pathEscapes,
|
|
24
|
+
redirectionTargets,
|
|
25
|
+
splitSegments,
|
|
26
|
+
type ShellWord,
|
|
27
|
+
tokenize,
|
|
28
|
+
} from "./shell.ts";
|
|
29
|
+
|
|
30
|
+
// The reader's public names, so a caller keeps importing the guards from here.
|
|
31
|
+
export { isUnverifiable };
|
|
32
|
+
export type { Redirect, ShellWord } from "./shell.ts";
|
|
20
33
|
|
|
21
34
|
export interface GuardBlock {
|
|
22
35
|
block: true;
|
|
@@ -33,14 +46,11 @@ export function resolveUnder(root: string, path: string | undefined): string {
|
|
|
33
46
|
// File tools (check 1)
|
|
34
47
|
// ---------------------------------------------------------------------------
|
|
35
48
|
|
|
49
|
+
/** The file tools and the one argument that carries their path. */
|
|
36
50
|
const PATH_FIELD: Record<string, string> = {
|
|
37
|
-
|
|
38
|
-
read_doc: "path",
|
|
51
|
+
...Object.fromEntries(WORKSPACE_READERS.map((name): [string, string] => [name, "path"])),
|
|
39
52
|
write: "path",
|
|
40
53
|
edit: "path",
|
|
41
|
-
grep: "path",
|
|
42
|
-
find: "path",
|
|
43
|
-
ls: "path",
|
|
44
54
|
};
|
|
45
55
|
|
|
46
56
|
const WRITE_TOOLS = new Set(["write", "edit"]);
|
|
@@ -82,281 +92,6 @@ export function guardFileTool(
|
|
|
82
92
|
return undefined;
|
|
83
93
|
}
|
|
84
94
|
|
|
85
|
-
// ---------------------------------------------------------------------------
|
|
86
|
-
// Shell commands (checks 2-4)
|
|
87
|
-
// ---------------------------------------------------------------------------
|
|
88
|
-
|
|
89
|
-
/** One shell word, with whether it began inside a quote (a quoted literal). */
|
|
90
|
-
interface ShellWord {
|
|
91
|
-
text: string;
|
|
92
|
-
leadingQuoted: boolean;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** Split a command on unquoted `;`, `&`, `|`, and newlines, preserving quotes. */
|
|
96
|
-
function splitSegments(command: string): string[] {
|
|
97
|
-
const segments: string[] = [];
|
|
98
|
-
let current = "";
|
|
99
|
-
let quote: string | null = null;
|
|
100
|
-
for (let i = 0; i < command.length; i++) {
|
|
101
|
-
const ch = command[i];
|
|
102
|
-
if (quote) {
|
|
103
|
-
current += ch;
|
|
104
|
-
if (ch === "\\" && quote === '"' && i + 1 < command.length) {
|
|
105
|
-
const next = command[i + 1];
|
|
106
|
-
if (isShellEscapable(next)) current += command[++i];
|
|
107
|
-
} else if (ch === quote) {
|
|
108
|
-
quote = null;
|
|
109
|
-
}
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
|
-
if (ch === "'" || ch === '"') {
|
|
113
|
-
quote = ch;
|
|
114
|
-
current += ch;
|
|
115
|
-
} else if (ch === ";" || ch === "\n") {
|
|
116
|
-
segments.push(current);
|
|
117
|
-
current = "";
|
|
118
|
-
} else if (ch === "&") {
|
|
119
|
-
const prev = command[i - 1];
|
|
120
|
-
const next = command[i + 1];
|
|
121
|
-
if (next === "&") {
|
|
122
|
-
// `&&` is a command separator.
|
|
123
|
-
segments.push(current);
|
|
124
|
-
current = "";
|
|
125
|
-
i++;
|
|
126
|
-
} else if (prev === ">" || prev === "<" || next === ">") {
|
|
127
|
-
// `>&`, `<&`, `&>`: redirection, not a separator.
|
|
128
|
-
current += ch;
|
|
129
|
-
} else {
|
|
130
|
-
// Background job `&`.
|
|
131
|
-
segments.push(current);
|
|
132
|
-
current = "";
|
|
133
|
-
}
|
|
134
|
-
} else if (ch === "|") {
|
|
135
|
-
if (command[i + 1] === "&") i++; // `|&` is still a pipe separator.
|
|
136
|
-
segments.push(current);
|
|
137
|
-
current = "";
|
|
138
|
-
} else {
|
|
139
|
-
current += ch;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
segments.push(current);
|
|
143
|
-
return segments;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/** Tokenize a shell segment into words, removing quotes and tracking quoted leads. */
|
|
147
|
-
function tokenize(segment: string): ShellWord[] {
|
|
148
|
-
const words: ShellWord[] = [];
|
|
149
|
-
let current = "";
|
|
150
|
-
let leadingQuoted = false;
|
|
151
|
-
let started = false;
|
|
152
|
-
let quote: string | null = null;
|
|
153
|
-
const push = () => {
|
|
154
|
-
if (started) words.push({ text: current, leadingQuoted });
|
|
155
|
-
current = "";
|
|
156
|
-
leadingQuoted = false;
|
|
157
|
-
started = false;
|
|
158
|
-
};
|
|
159
|
-
for (let i = 0; i < segment.length; i++) {
|
|
160
|
-
const ch = segment[i];
|
|
161
|
-
if (quote) {
|
|
162
|
-
if (ch === "\\" && quote === '"' && i + 1 < segment.length) {
|
|
163
|
-
const next = segment[i + 1];
|
|
164
|
-
if (isShellEscapable(next)) current += segment[++i];
|
|
165
|
-
else current += ch;
|
|
166
|
-
} else if (ch === quote) {
|
|
167
|
-
quote = null;
|
|
168
|
-
} else {
|
|
169
|
-
current += ch;
|
|
170
|
-
}
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
if (ch === "'" || ch === '"') {
|
|
174
|
-
if (!started) {
|
|
175
|
-
started = true;
|
|
176
|
-
leadingQuoted = true;
|
|
177
|
-
}
|
|
178
|
-
quote = ch;
|
|
179
|
-
} else if (ch === " " || ch === "\t") {
|
|
180
|
-
push();
|
|
181
|
-
} else if (ch === "\\" && i + 1 < segment.length) {
|
|
182
|
-
started = true;
|
|
183
|
-
const next = segment[i + 1];
|
|
184
|
-
if (isShellEscapable(next)) current += segment[++i];
|
|
185
|
-
else current += ch;
|
|
186
|
-
} else {
|
|
187
|
-
started = true;
|
|
188
|
-
current += ch;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
push();
|
|
192
|
-
return words;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Expand `~` and environment variables so a path that only leaves the worktree
|
|
197
|
-
* after shell expansion is still caught. `unresolved` marks a value that cannot
|
|
198
|
-
* be expanded statically (unknown variable, `$`, backtick, `~user`); callers
|
|
199
|
-
* treat it as escaping, because guessing could allow a bypass.
|
|
200
|
-
*/
|
|
201
|
-
function expandShellPath(raw: string): { path?: string; unresolved: boolean } {
|
|
202
|
-
let value = raw;
|
|
203
|
-
if (value === "~") value = homedir();
|
|
204
|
-
else if (value.startsWith("~/")) value = join(homedir(), value.slice(2));
|
|
205
|
-
else if (value.startsWith("~")) return { unresolved: true };
|
|
206
|
-
|
|
207
|
-
let unresolved = false;
|
|
208
|
-
value = value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g, (match, braced, plain) => {
|
|
209
|
-
const name = braced ?? plain;
|
|
210
|
-
const env = process.env[name];
|
|
211
|
-
if (env === undefined) {
|
|
212
|
-
unresolved = true;
|
|
213
|
-
return match;
|
|
214
|
-
}
|
|
215
|
-
return env;
|
|
216
|
-
});
|
|
217
|
-
if (value.includes("$") || value.includes("`")) unresolved = true;
|
|
218
|
-
return { path: value, unresolved };
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/** Whether a shell path argument leaves the worktree once expanded. */
|
|
222
|
-
function pathEscapes(root: string, raw: string): boolean {
|
|
223
|
-
const expanded = expandShellPath(raw);
|
|
224
|
-
if (expanded.unresolved || !expanded.path) return true;
|
|
225
|
-
return !isInside(root, resolve(root, expanded.path));
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Constructs whose text does not spell out the git command that runs. Exported
|
|
230
|
-
* so the `tool_call` handler counts exactly the commands this check would have
|
|
231
|
-
* refused, instead of guessing at a second list.
|
|
232
|
-
*/
|
|
233
|
-
export function isUnverifiable(command: string): boolean {
|
|
234
|
-
return /\$\(|`|\$\{!|\beval\b|\b(?:sh|bash|zsh|dash|ksh)\s+-c\b/.test(command);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// ---------------------------------------------------------------------------
|
|
238
|
-
// Shell redirections (check 1b)
|
|
239
|
-
// ---------------------------------------------------------------------------
|
|
240
|
-
|
|
241
|
-
interface Redirect {
|
|
242
|
-
target: string;
|
|
243
|
-
/** True for output/read-write redirections; false for input-only. */
|
|
244
|
-
write: boolean;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/** Read a shell word starting at `start`, skipping leading blanks, unquoting the result. */
|
|
248
|
-
function readShellWord(text: string, start: number): { raw: string; end: number } {
|
|
249
|
-
let i = start;
|
|
250
|
-
while (i < text.length && (text[i] === " " || text[i] === "\t")) i++;
|
|
251
|
-
let raw = "";
|
|
252
|
-
let quote: string | null = null;
|
|
253
|
-
while (i < text.length) {
|
|
254
|
-
const ch = text[i];
|
|
255
|
-
if (quote) {
|
|
256
|
-
if (ch === "\\" && quote === '"' && i + 1 < text.length) {
|
|
257
|
-
const next = text[i + 1];
|
|
258
|
-
if (isShellEscapable(next)) {
|
|
259
|
-
raw += next;
|
|
260
|
-
i += 2;
|
|
261
|
-
} else {
|
|
262
|
-
raw += ch;
|
|
263
|
-
i += 1;
|
|
264
|
-
}
|
|
265
|
-
continue;
|
|
266
|
-
}
|
|
267
|
-
if (ch === quote) {
|
|
268
|
-
quote = null;
|
|
269
|
-
i++;
|
|
270
|
-
continue;
|
|
271
|
-
}
|
|
272
|
-
raw += ch;
|
|
273
|
-
i++;
|
|
274
|
-
continue;
|
|
275
|
-
}
|
|
276
|
-
if (ch === "'" || ch === '"') {
|
|
277
|
-
quote = ch;
|
|
278
|
-
i++;
|
|
279
|
-
continue;
|
|
280
|
-
}
|
|
281
|
-
if (ch === "\\" && i + 1 < text.length) {
|
|
282
|
-
const next = text[i + 1];
|
|
283
|
-
if (isShellEscapable(next)) {
|
|
284
|
-
raw += next;
|
|
285
|
-
i += 2;
|
|
286
|
-
} else {
|
|
287
|
-
raw += ch;
|
|
288
|
-
i += 1;
|
|
289
|
-
}
|
|
290
|
-
continue;
|
|
291
|
-
}
|
|
292
|
-
if (ch === " " || ch === "\t") break;
|
|
293
|
-
raw += ch;
|
|
294
|
-
i++;
|
|
295
|
-
}
|
|
296
|
-
return { raw, end: i };
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/**
|
|
300
|
-
* File redirection targets in one shell segment. File-descriptor duplications
|
|
301
|
-
* (`2>&1`), heredocs (`<<EOF`), and here-strings (`<<<x`) are skipped because
|
|
302
|
-
* their operand is not a path.
|
|
303
|
-
*/
|
|
304
|
-
function redirectionTargets(segment: string): Redirect[] {
|
|
305
|
-
const redirects: Redirect[] = [];
|
|
306
|
-
let quote: string | null = null;
|
|
307
|
-
for (let i = 0; i < segment.length; i++) {
|
|
308
|
-
const ch = segment[i];
|
|
309
|
-
if (quote) {
|
|
310
|
-
if (ch === "\\" && quote === '"' && i + 1 < segment.length) {
|
|
311
|
-
i++;
|
|
312
|
-
continue;
|
|
313
|
-
}
|
|
314
|
-
if (ch === quote) quote = null;
|
|
315
|
-
continue;
|
|
316
|
-
}
|
|
317
|
-
if (ch === "'" || ch === '"') {
|
|
318
|
-
quote = ch;
|
|
319
|
-
continue;
|
|
320
|
-
}
|
|
321
|
-
if (ch === "\\") {
|
|
322
|
-
i++;
|
|
323
|
-
continue;
|
|
324
|
-
}
|
|
325
|
-
if (ch !== ">" && ch !== "<") continue;
|
|
326
|
-
|
|
327
|
-
const two = segment.slice(i, i + 2);
|
|
328
|
-
const three = segment.slice(i, i + 3);
|
|
329
|
-
if (three === "<<<") {
|
|
330
|
-
i += 2;
|
|
331
|
-
continue;
|
|
332
|
-
}
|
|
333
|
-
if (two === "<<") {
|
|
334
|
-
// Heredoc delimiter: not a path.
|
|
335
|
-
i += 1;
|
|
336
|
-
continue;
|
|
337
|
-
}
|
|
338
|
-
if (two === ">&" || two === "<&") {
|
|
339
|
-
// File-descriptor duplication (`2>&1`, `>&-`) is not a path, but bash
|
|
340
|
-
// treats `>&file` as `&>file` (stdout+stderr to a file).
|
|
341
|
-
const operand = readShellWord(segment, i + 2);
|
|
342
|
-
if (operand.raw === "" || /^\d+$/.test(operand.raw) || operand.raw === "-") {
|
|
343
|
-
i += 1;
|
|
344
|
-
continue;
|
|
345
|
-
}
|
|
346
|
-
redirects.push({ target: operand.raw, write: ch === ">" });
|
|
347
|
-
i = operand.end - 1;
|
|
348
|
-
continue;
|
|
349
|
-
}
|
|
350
|
-
// `>`, `>>`, `>|`, `<`, `<>`: the next word is a path.
|
|
351
|
-
const write = ch === ">" || two === "<>";
|
|
352
|
-
if (two === ">>" || two === ">|") i += 1;
|
|
353
|
-
const word = readShellWord(segment, i + 1);
|
|
354
|
-
if (word.raw) redirects.push({ target: word.raw, write });
|
|
355
|
-
i = word.end - 1;
|
|
356
|
-
}
|
|
357
|
-
return redirects;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
95
|
/** Refuse an output/input redirection whose target leaves the worktree. */
|
|
361
96
|
function redirectionEscapes(root: string, command: string, config: WorktreeConfig): string | undefined {
|
|
362
97
|
const checkWrites = config.guard.blockFileEscapes;
|
|
@@ -383,13 +118,6 @@ function isNullDevice(raw: string): boolean {
|
|
|
383
118
|
return raw.replace(/^(['"])(.*)\1$/, "$2") === "/dev/null";
|
|
384
119
|
}
|
|
385
120
|
|
|
386
|
-
/** Commands that wrap another command, so a following assignment is still a prefix. */
|
|
387
|
-
const COMMAND_WRAPPERS = new Set(["sudo", "doas", "env", "command", "builtin", "exec", "nohup", "nice", "time"]);
|
|
388
|
-
|
|
389
|
-
function isAssignment(word: string): boolean {
|
|
390
|
-
return /^[A-Za-z_][A-Za-z0-9_]*=/.test(word);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
121
|
/** A leading `GIT_DIR=`/`GIT_WORK_TREE=` assignment that points outside. */
|
|
394
122
|
function envGitRedirect(root: string, words: ShellWord[]): string | undefined {
|
|
395
123
|
for (let i = 0; i < words.length; i++) {
|
|
@@ -20,7 +20,12 @@ export interface Handoff {
|
|
|
20
20
|
base: string | null;
|
|
21
21
|
/** Commits on the branch that the base does not contain. */
|
|
22
22
|
ahead: number;
|
|
23
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* Commits the branch has made since the commit it was created at, when that was
|
|
25
|
+
* recorded. Absent for a worktree this package did not create.
|
|
26
|
+
*/
|
|
27
|
+
commits?: number;
|
|
28
|
+
/** Whether the work landed: every commit is in the base, and there were commits. */
|
|
24
29
|
merged: boolean;
|
|
25
30
|
}
|
|
26
31
|
|
|
@@ -43,7 +48,10 @@ export function handoffLines(handoff: Handoff): string[] {
|
|
|
43
48
|
];
|
|
44
49
|
}
|
|
45
50
|
if (handoff.merged || handoff.ahead === 0) {
|
|
46
|
-
|
|
51
|
+
// "Merged" is a claim that work landed; a branch with no commits of its own never had
|
|
52
|
+
// any to land, and saying so is what stops an empty worktree reading as finished.
|
|
53
|
+
const landed = handoff.merged && handoff.commits !== 0;
|
|
54
|
+
const state = landed ? `is merged into ${handoff.base}` : `has no commits beyond ${handoff.base}`;
|
|
47
55
|
return [`Handoff: ${handoff.branch} ${state} — nothing to merge.`];
|
|
48
56
|
}
|
|
49
57
|
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
isWorktreeCancelled,
|
|
35
35
|
refusalMessage,
|
|
36
36
|
worktreeFacts,
|
|
37
|
+
enteredWorktree,
|
|
37
38
|
worktreeLabel,
|
|
38
39
|
} from "./lifecycle.ts";
|
|
39
40
|
import { registerCommands } from "./commands.ts";
|
|
@@ -160,7 +161,7 @@ export function registerWorktree(pi: ExtensionAPI, { entryPath, createBuiltin }:
|
|
|
160
161
|
const name = flag.trim() === "true" ? undefined : flag.trim();
|
|
161
162
|
try {
|
|
162
163
|
const { state } = await enterWorktree(pi, ctx, { name });
|
|
163
|
-
ctx.ui.notify(
|
|
164
|
+
ctx.ui.notify(enteredWorktree(state), "info");
|
|
164
165
|
} catch (error) {
|
|
165
166
|
if (isWorktreeCancelled(error)) {
|
|
166
167
|
ctx.ui.notify(error instanceof Error ? error.message : String(error), "info");
|
|
@@ -12,6 +12,8 @@ export { exitWorktree, type ExitOptions } from "./exit.ts";
|
|
|
12
12
|
export { pruneWorktrees } from "./prune.ts";
|
|
13
13
|
export { findInactiveOverrides } from "./root-tools.ts";
|
|
14
14
|
export {
|
|
15
|
+
enteredWorktree,
|
|
16
|
+
exitedWorktree,
|
|
15
17
|
formatWorktreeStatus,
|
|
16
18
|
refusalMessage,
|
|
17
19
|
stateSummary,
|
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
* is a disposable cache: a missing or corrupt file loads as empty, writes are
|
|
7
7
|
* best-effort, and reads reconcile against `git worktree list`.
|
|
8
8
|
*
|
|
9
|
-
* `statusOf` is the single derived view
|
|
10
|
-
*
|
|
9
|
+
* `statusOf` is the single derived view the status listing reads, so the text and the
|
|
10
|
+
* screen do not each re-derive dirty/ahead/behind/merged state; exit reads the one fact it
|
|
11
|
+
* needs (`commitsAheadOf`) without it.
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
14
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
@@ -15,14 +16,7 @@ import { dirname, join } from "node:path";
|
|
|
15
16
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
16
17
|
import { realPathOfNearest } from "../../lib/path.ts";
|
|
17
18
|
import type { WorktreeConfig } from "./config.ts";
|
|
18
|
-
import {
|
|
19
|
-
commitsAhead,
|
|
20
|
-
commitsBehind,
|
|
21
|
-
isBranchMerged,
|
|
22
|
-
isStaleLock,
|
|
23
|
-
type ManagedWorktree,
|
|
24
|
-
statusEntries,
|
|
25
|
-
} from "./git.ts";
|
|
19
|
+
import { commitsAhead, commitsBehind, isStaleLock, type ManagedWorktree, statusEntries } from "./git.ts";
|
|
26
20
|
|
|
27
21
|
const REGISTRY_VERSION = 1;
|
|
28
22
|
|
|
@@ -53,8 +47,16 @@ export interface CheckoutStatus {
|
|
|
53
47
|
state: "clean" | "dirty" | "missing";
|
|
54
48
|
/** Number of changed files (porcelain entries). */
|
|
55
49
|
changed: number;
|
|
50
|
+
/**
|
|
51
|
+
* Commits the branch has made since the commit it was created at.
|
|
52
|
+
*
|
|
53
|
+
* Absent when that commit was never recorded — a worktree the user pointed us at —
|
|
54
|
+
* because git keeps no fork point of its own to ask.
|
|
55
|
+
*/
|
|
56
|
+
commits?: number;
|
|
56
57
|
ahead: number;
|
|
57
58
|
behind: number;
|
|
59
|
+
/** Whether the work landed: every commit is in the base, and there were commits. */
|
|
58
60
|
merged: boolean;
|
|
59
61
|
locked: boolean;
|
|
60
62
|
staleLock: boolean;
|
|
@@ -218,10 +220,17 @@ export async function statusOf(
|
|
|
218
220
|
const baseRef = options.baseRef;
|
|
219
221
|
base.state = dirty.length > 0 ? "dirty" : "clean";
|
|
220
222
|
base.changed = dirty.length;
|
|
223
|
+
// The commit the worktree was created at is the only fork point git can be asked about:
|
|
224
|
+
// once a branch is merged, it and a branch that never committed are both ancestors of
|
|
225
|
+
// the base, so `ahead` reports 0 for each and cannot tell them apart. This can.
|
|
226
|
+
const since = options.record?.base.commit;
|
|
227
|
+
if (since) base.commits = await commitsAhead(pi, path, since);
|
|
221
228
|
if (baseRef) {
|
|
222
229
|
base.ahead = await commitsAhead(pi, path, baseRef);
|
|
223
230
|
base.behind = await commitsBehind(pi, path, baseRef);
|
|
224
|
-
|
|
231
|
+
// `merged` claims the work landed, so it needs commits that could have landed: a
|
|
232
|
+
// branch with none is an empty checkout, not a finished one.
|
|
233
|
+
base.merged = base.ahead === 0 && base.commits !== 0;
|
|
225
234
|
}
|
|
226
235
|
return base;
|
|
227
236
|
}
|
|
@@ -35,14 +35,24 @@ import { realPathOfNearest } from "../../lib/path.ts";
|
|
|
35
35
|
export const ROOT_TOOL_NAMES = ["read", "write", "edit", "bash", "grep", "find", "ls"] as const;
|
|
36
36
|
export type RootToolName = (typeof ROOT_TOOL_NAMES)[number];
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
/**
|
|
39
|
+
* A built-in tool definition, as the seven factories return it.
|
|
40
|
+
*
|
|
41
|
+
* Pi's own three defaults, spelled out rather than left implicit: `renderCall`'s arguments
|
|
42
|
+
* are contravariant in the parameter schema and its context in the detail type, so the
|
|
43
|
+
* seven concrete definitions do not widen to one `ToolDefinition<TSchema, unknown, ...>`
|
|
44
|
+
* — and neither does the test seam that replaces them with a stub. Erasing all three is
|
|
45
|
+
* the only alias that accepts both, which is what makes this line Pi's escape hatch
|
|
46
|
+
* rather than a missing type.
|
|
47
|
+
*/
|
|
48
|
+
type BuiltinToolDef = ToolDefinition<any, any, any>;
|
|
39
49
|
|
|
40
50
|
export interface FactoryOptions {
|
|
41
51
|
read?: { autoResizeImages?: boolean };
|
|
42
52
|
bash?: { commandPrefix?: string; shellPath?: string };
|
|
43
53
|
}
|
|
44
54
|
|
|
45
|
-
function createBuiltin(name: RootToolName, cwd: string, options: FactoryOptions):
|
|
55
|
+
function createBuiltin(name: RootToolName, cwd: string, options: FactoryOptions): BuiltinToolDef {
|
|
46
56
|
switch (name) {
|
|
47
57
|
case "read":
|
|
48
58
|
return createReadToolDefinition(cwd, options.read);
|
|
@@ -62,7 +72,7 @@ function createBuiltin(name: RootToolName, cwd: string, options: FactoryOptions)
|
|
|
62
72
|
}
|
|
63
73
|
|
|
64
74
|
/** Host built-in factory. Injectable so tests avoid `mock.module`. */
|
|
65
|
-
export type BuiltinFactory = (name: RootToolName, cwd: string, options: FactoryOptions) =>
|
|
75
|
+
export type BuiltinFactory = (name: RootToolName, cwd: string, options: FactoryOptions) => BuiltinToolDef;
|
|
66
76
|
|
|
67
77
|
export interface RootToolDeps {
|
|
68
78
|
/** Override the host built-in factories. Tests inject a stub definition. */
|
|
@@ -21,8 +21,14 @@ export const ENV_BRANCH = "PI_WORKTREE_BRANCH";
|
|
|
21
21
|
/** Main checkout a borrowed worktree forked from, so a child knows its `repoRoot`. */
|
|
22
22
|
export const ENV_MAIN = "PI_WORKTREE_MAIN";
|
|
23
23
|
|
|
24
|
-
/**
|
|
25
|
-
|
|
24
|
+
/**
|
|
25
|
+
* The channel `publishWorktree` announces on, for another extension that follows the active
|
|
26
|
+
* root without polling; `docs/extensions/worktree.md` names it as the contract.
|
|
27
|
+
*
|
|
28
|
+
* Not exported: the channel is an event name on `pi.events`, so what crosses the boundary
|
|
29
|
+
* is the string a listener subscribes to, and the constant is this module's own.
|
|
30
|
+
*/
|
|
31
|
+
const WORKTREE_EVENT_CHANNEL = "worktree:changed";
|
|
26
32
|
|
|
27
33
|
let active: WorktreeState | null = null;
|
|
28
34
|
/** Directory to use when not isolated (the session cwd at startup). */
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
*
|
|
5
5
|
* The frame, navigation, y/N confirmation, and pane come from the shared
|
|
6
6
|
* `DockListScreen`, so this module owns only the worktree vocabulary. Rows come
|
|
7
|
-
* from the same `worktreeSnapshot` the text status renders
|
|
8
|
-
* `list_worktrees` tool cannot disagree
|
|
7
|
+
* from the same `worktreeSnapshot` the text status renders and their marks from the
|
|
8
|
+
* same `statusMarks`, so the screen and the `list_worktrees` tool cannot disagree
|
|
9
|
+
* about a worktree.
|
|
9
10
|
*
|
|
10
11
|
* Every verb but `p` closes the screen and hands the action back to the command:
|
|
11
12
|
* entering and exiting change the session binding and `exitWorktree` asks through
|
|
@@ -16,6 +17,7 @@
|
|
|
16
17
|
import { basename } from "node:path";
|
|
17
18
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
18
19
|
import { messageOf } from "../../lib/errors.ts";
|
|
20
|
+
import { planModeRefusal } from "../../lib/plan-entry.ts";
|
|
19
21
|
import type { ScreenHint, ViewportRowsSource } from "../../lib/tui/chrome.ts";
|
|
20
22
|
import { DockListScreen, type DetailContents } from "../../lib/tui/dock-screen.ts";
|
|
21
23
|
import { GLYPHS } from "../../lib/tui/glyphs.ts";
|
|
@@ -75,11 +77,9 @@ function rowGlyph(status: CheckoutStatus | undefined): { char: string; role: Tui
|
|
|
75
77
|
return { char: GLYPHS.branch, role: "info" };
|
|
76
78
|
}
|
|
77
79
|
|
|
78
|
-
/** The row's right-hand text: its marks,
|
|
80
|
+
/** The row's right-hand text: its marks, worded once in `statusMarks`. */
|
|
79
81
|
function rowMarks(row: WorktreeRow): string {
|
|
80
|
-
|
|
81
|
-
if (marks.length === 0) marks.push("clean");
|
|
82
|
-
return marks.join(" ");
|
|
82
|
+
return (row.status ? statusMarks(row.status, row.current) : ["current"]).join(" ");
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
export interface WorktreeScreenOptions {
|
|
@@ -186,7 +186,13 @@ export class WorktreeListComponent extends DockListScreen<WorktreeRow> {
|
|
|
186
186
|
const bits = [status.state === "dirty" ? `dirty (${status.changed} changed)` : "clean"];
|
|
187
187
|
if (status.ahead > 0) bits.push(`${status.ahead} ahead`);
|
|
188
188
|
if (status.behind > 0) bits.push(`${status.behind} behind`);
|
|
189
|
-
|
|
189
|
+
// The row says it too, and for the same reason: a merged mark is a claim that work
|
|
190
|
+
// landed, which a worktree with no commits of its own cannot make.
|
|
191
|
+
if (status.commits === 0) bits.push("no commits");
|
|
192
|
+
else if (status.merged) {
|
|
193
|
+
if (status.commits !== undefined) bits.push(`${status.commits} commits`);
|
|
194
|
+
bits.push("merged");
|
|
195
|
+
}
|
|
190
196
|
lines.push(`state ${bits.join(", ")}`);
|
|
191
197
|
if (status.locked) {
|
|
192
198
|
lines.push(`lock ${status.staleLock ? "stale (owner is gone)" : "held by a live session"}`);
|
|
@@ -200,6 +206,7 @@ export class WorktreeListComponent extends DockListScreen<WorktreeRow> {
|
|
|
200
206
|
branch: row.branch,
|
|
201
207
|
base: this.view.baseBranch ?? null,
|
|
202
208
|
ahead: status.ahead,
|
|
209
|
+
commits: status.commits,
|
|
203
210
|
merged: status.merged,
|
|
204
211
|
}),
|
|
205
212
|
);
|
|
@@ -211,7 +218,7 @@ export class WorktreeListComponent extends DockListScreen<WorktreeRow> {
|
|
|
211
218
|
protected handleVerb(key: string, row: WorktreeRow | undefined): void {
|
|
212
219
|
if (key !== "e" && key !== "x" && key !== "p") return;
|
|
213
220
|
if (this.deps.readOnly) {
|
|
214
|
-
this.notify("
|
|
221
|
+
this.notify(planModeRefusal("changing worktrees"));
|
|
215
222
|
return;
|
|
216
223
|
}
|
|
217
224
|
if (key === "e") {
|