@gavin-hu/my-pi-agent 0.21.0 → 0.22.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 +46 -31
- package/README.zh-CN.md +40 -26
- package/assets/screens/board.png +0 -0
- package/assets/screens/chrome.png +0 -0
- package/assets/screens/file-browse.png +0 -0
- package/assets/screens/plan-review.png +0 -0
- package/assets/screens/plan.png +0 -0
- package/docs/extensions/README.md +22 -24
- package/docs/extensions/board.md +361 -147
- package/docs/extensions/bytefly.md +152 -111
- package/docs/extensions/check.md +15 -17
- package/docs/extensions/context.md +35 -15
- package/docs/extensions/file-browse.md +30 -31
- package/docs/extensions/file-open.md +9 -11
- package/docs/extensions/file-read.md +12 -14
- package/docs/extensions/goal.md +15 -17
- package/docs/extensions/job.md +54 -42
- package/docs/extensions/memory.md +17 -19
- package/docs/extensions/plan.md +166 -126
- package/docs/extensions/question.md +16 -18
- package/docs/extensions/rewind.md +17 -19
- package/docs/extensions/serve.md +34 -18
- package/docs/extensions/session-bus.md +38 -16
- package/docs/extensions/subagent.md +23 -25
- package/docs/extensions/team.md +72 -33
- package/docs/extensions/todo.md +18 -18
- package/docs/extensions/watch.md +31 -31
- package/docs/extensions/web-access.md +2 -4
- package/docs/extensions/wechat.md +34 -35
- package/docs/extensions/worktree.md +56 -50
- package/extensions/board/commands.ts +56 -15
- package/extensions/board/format.ts +43 -24
- package/extensions/board/index.ts +5 -4
- package/extensions/board/input.ts +4 -9
- package/extensions/board/join.ts +4 -40
- package/extensions/board/lazy-page.ts +40 -0
- package/extensions/board/page.ts +13 -11
- package/extensions/board/result.ts +30 -16
- package/extensions/board/rows.ts +62 -25
- package/extensions/board/schema.ts +42 -10
- package/extensions/board/snapshot.ts +2 -1
- package/extensions/board/tone.ts +22 -0
- package/extensions/board/tools.ts +131 -44
- package/extensions/board/types.ts +36 -32
- package/extensions/board/view/card.ts +229 -0
- package/extensions/board/view/client.ts +33 -15
- package/extensions/board/view/dialog.ts +169 -6
- package/extensions/board/view/empty.ts +47 -0
- package/extensions/board/view/keys.ts +13 -2
- package/extensions/board/view/live.ts +89 -21
- package/extensions/board/view/names.ts +13 -4
- package/extensions/board/view/page.ts +23 -9
- package/extensions/board/view/render.ts +208 -271
- package/extensions/board/view/styles.ts +123 -31
- package/extensions/board/view/team.ts +297 -76
- package/extensions/bytefly/block/fallback.ts +1 -1
- package/extensions/bytefly/doctor/runtime.ts +5 -2
- package/extensions/bytefly/footer/collect/label.ts +60 -0
- package/extensions/bytefly/footer/collect/snapshot.ts +8 -1
- package/extensions/bytefly/footer/config.ts +4 -2
- package/extensions/bytefly/footer/register.ts +33 -4
- package/extensions/bytefly/footer/render/lines.ts +7 -4
- package/extensions/bytefly/footer/render/segments.ts +37 -10
- package/extensions/bytefly/footer/render/status.ts +0 -11
- package/extensions/bytefly/footer/types.ts +7 -0
- package/extensions/bytefly/header/config.ts +1 -1
- package/extensions/bytefly/index.ts +32 -6
- package/extensions/bytefly/mode/editor.ts +115 -0
- package/extensions/bytefly/panel/commands.ts +210 -120
- package/extensions/bytefly/panel/format.ts +4 -4
- package/extensions/bytefly/panel/settings.ts +73 -6
- package/extensions/bytefly/panel/tui/keys.ts +2 -6
- package/extensions/bytefly/update/cache.ts +1 -1
- package/extensions/bytefly/update/register.ts +4 -4
- package/extensions/bytefly/working/config.ts +5 -7
- package/extensions/bytefly/working/format.ts +10 -13
- package/extensions/bytefly/working/register.ts +22 -27
- package/extensions/check/commands.ts +3 -2
- package/extensions/check/{tui.ts → view/check-screens.ts} +14 -19
- package/extensions/context/commands.ts +2 -2
- package/extensions/context/{tui.ts → view/context-report.ts} +51 -19
- package/extensions/file-browse/index.ts +67 -36
- package/extensions/file-read/render.ts +4 -4
- package/extensions/goal/plan-route.ts +2 -2
- package/extensions/job/child.ts +527 -0
- package/extensions/job/clock.ts +142 -0
- package/extensions/job/commands.ts +4 -2
- package/extensions/job/config.ts +8 -1
- package/extensions/job/format.ts +22 -1
- package/extensions/job/log-writer.ts +13 -2
- package/extensions/job/registry.ts +63 -7
- package/extensions/job/render.ts +34 -0
- package/extensions/job/runtime.ts +296 -655
- package/extensions/job/session.ts +132 -19
- package/extensions/job/store.ts +7 -7
- package/extensions/job/tools.ts +3 -3
- package/extensions/job/types.ts +47 -0
- package/extensions/job/{tui.ts → view/jobs-screen.ts} +19 -73
- package/extensions/job/waiters.ts +44 -1
- package/extensions/memory/commands.ts +4 -2
- package/extensions/memory/{tui.ts → view/memory-picker.ts} +12 -12
- package/extensions/plan/commands.ts +12 -9
- package/extensions/plan/format.ts +8 -2
- package/extensions/plan/git-bash.ts +115 -16
- package/extensions/plan/index.ts +53 -52
- package/extensions/plan/plan-index.ts +4 -2
- package/extensions/plan/plans.ts +5 -54
- package/extensions/plan/policy.ts +8 -10
- package/extensions/plan/review-lazy.ts +85 -0
- package/extensions/plan/review.ts +2 -2
- package/extensions/plan/runtime.ts +49 -39
- package/extensions/plan/sections.ts +1 -1
- package/extensions/plan/steps.ts +3 -3
- package/extensions/plan/todos.ts +1 -1
- package/extensions/plan/tools/cancel.ts +27 -62
- package/extensions/plan/tools/enter.ts +55 -81
- package/extensions/plan/tools/exit.ts +221 -238
- package/extensions/plan/tools/refusal.ts +18 -0
- package/extensions/plan/tools/steps.ts +75 -92
- package/extensions/plan/tools/write.ts +77 -120
- package/extensions/plan/tools.ts +188 -30
- package/extensions/plan/tui.ts +5 -5
- package/extensions/plan/types.ts +48 -5
- package/extensions/plan/web-client.ts +3 -0
- package/extensions/plan/web-lazy.ts +53 -0
- package/extensions/plan/web-markdown.ts +9 -67
- package/extensions/plan/web-payload.ts +3 -3
- package/extensions/plan/web-router.ts +18 -9
- package/extensions/plan/web.ts +1 -12
- package/extensions/question/tools.ts +6 -2
- package/extensions/question/{tui.ts → view/question-screen.ts} +11 -11
- package/extensions/rewind/commands/commands.ts +6 -3
- package/extensions/rewind/state/runtime.ts +1 -1
- package/extensions/rewind/{view/status.ts → status.ts} +4 -4
- package/extensions/rewind/view/review-tui.ts +4 -4
- package/extensions/serve/config.ts +10 -8
- package/extensions/serve/index.ts +17 -3
- package/extensions/serve/registry.ts +26 -17
- package/extensions/session-bus/commands.ts +11 -6
- package/extensions/session-bus/join-request.ts +2 -1
- package/extensions/session-bus/mail-watch.ts +26 -17
- package/extensions/session-bus/protocol.ts +15 -3
- package/extensions/session-bus/resolve.ts +5 -7
- package/extensions/session-bus/runtime.ts +40 -12
- package/extensions/session-bus/store.ts +101 -19
- package/extensions/session-bus/types.ts +19 -0
- package/extensions/session-bus/{tui.ts → view/session-list.ts} +16 -16
- package/extensions/subagent/agents.ts +1 -1
- package/extensions/subagent/commands.ts +4 -2
- package/extensions/subagent/format.ts +15 -15
- package/extensions/subagent/index.ts +1 -1
- package/extensions/subagent/render.ts +3 -3
- package/extensions/subagent/{background-tui.ts → view/agents-board.ts} +11 -11
- package/extensions/team/commands.ts +24 -7
- package/extensions/team/dispatch.ts +18 -4
- package/extensions/team/format.ts +17 -2
- package/extensions/team/index.ts +9 -8
- package/extensions/team/schema.ts +6 -5
- package/extensions/team/tools.ts +107 -45
- package/extensions/team/types.ts +3 -2
- package/extensions/todo/commands.ts +4 -2
- package/extensions/todo/format.ts +11 -0
- package/extensions/todo/render.ts +57 -0
- package/extensions/todo/runtime.ts +1 -1
- package/extensions/todo/tools.ts +1 -1
- package/extensions/todo/view/todo-screen.ts +122 -0
- package/extensions/todo/widget.ts +47 -0
- package/extensions/watch/commands.ts +4 -2
- package/extensions/watch/format.ts +36 -7
- package/extensions/watch/render.ts +57 -0
- package/extensions/watch/runtime.ts +27 -22
- package/extensions/watch/tools.ts +4 -4
- package/extensions/watch/{tui.ts → view/watch-screen.ts} +18 -80
- package/extensions/web-access/fetch/tool.ts +12 -4
- package/extensions/wechat/commands/commands.ts +18 -14
- package/extensions/wechat/index.ts +72 -37
- package/extensions/wechat/state/credentials.ts +1 -1
- package/extensions/wechat/state/state.ts +1 -1
- package/extensions/wechat/text/status.ts +112 -0
- package/extensions/wechat/types.ts +2 -2
- package/extensions/wechat/view/status-screen.ts +4 -103
- package/extensions/worktree/commands.ts +11 -9
- package/extensions/worktree/enter.ts +2 -2
- package/extensions/worktree/exit.ts +2 -2
- package/extensions/worktree/index.ts +5 -5
- package/extensions/worktree/screen.ts +3 -3
- package/extensions/worktree/status.ts +2 -2
- package/extensions/worktree/tools.ts +113 -116
- package/lib/README.md +27 -13
- package/lib/board/actions.ts +111 -0
- package/lib/board/artifact.ts +149 -0
- package/lib/board/card.ts +163 -52
- package/lib/board/config.ts +58 -0
- package/lib/board/contract.ts +113 -0
- package/lib/board/join.ts +38 -0
- package/lib/board/lanes.ts +182 -0
- package/lib/board/lead.ts +130 -0
- package/lib/board/limits.ts +78 -0
- package/lib/board/links.ts +31 -10
- package/lib/board/page-pointer.ts +1 -1
- package/lib/board/read.ts +83 -0
- package/lib/board/reason.ts +102 -0
- package/lib/board/round-text.ts +68 -6
- package/lib/board/round.ts +31 -15
- package/lib/board/rules.ts +151 -307
- package/lib/board/seat.ts +35 -0
- package/lib/board/session.ts +136 -54
- package/lib/board/skipped.ts +22 -0
- package/lib/board/steps.ts +487 -0
- package/lib/board/store.ts +87 -30
- package/lib/board/tone.ts +92 -0
- package/lib/board/view.ts +70 -58
- package/{extensions/watch → lib}/child.ts +40 -21
- package/lib/env.ts +65 -7
- package/lib/format.ts +13 -0
- package/lib/http/index.ts +14 -12
- package/lib/http/port.ts +15 -10
- package/lib/lazy.ts +45 -0
- package/lib/plan-entry.ts +39 -10
- package/lib/plans/paths.ts +82 -0
- package/lib/policy.ts +9 -6
- package/lib/report.ts +3 -2
- package/lib/requirements.ts +8 -8
- package/lib/session-label.ts +111 -5
- package/lib/session-roster.ts +43 -1
- package/lib/session-tool.ts +5 -4
- package/lib/stray-fields.ts +7 -2
- package/lib/tool-names.ts +5 -10
- package/lib/tui/README.md +23 -8
- package/lib/tui/chrome.ts +16 -15
- package/lib/tui/cursor.ts +18 -3
- package/lib/tui/glyphs.ts +9 -7
- package/lib/tui/tokens.ts +33 -2
- package/lib/tui/tool.ts +3 -3
- package/lib/web/README.md +21 -0
- package/lib/web/components.ts +5 -1
- package/lib/web/index.ts +1 -0
- package/lib/web/markdown.ts +83 -0
- package/lib/web/markup.ts +2 -1
- package/lib/web/runtime.ts +9 -10
- package/package.json +6 -1
- package/skills/init/SKILL.md +5 -5
- package/skills/next/SKILL.md +3 -5
- package/extensions/board/seat.ts +0 -28
- package/extensions/todo/tui.ts +0 -212
- package/lib/tui/index.ts +0 -34
- package/skills/project-docs/SKILL.md +0 -133
- package/skills/project-docs/references/audit.md +0 -131
- package/skills/project-docs/references/changelog.md +0 -105
- package/skills/project-docs/references/discovery.md +0 -127
- package/skills/project-docs/references/readme.md +0 -80
- package/skills/project-docs/references/release.md +0 -84
- package/skills/project-docs/references/roadmap.md +0 -93
- package/skills/project-docs/scripts/state.ts +0 -316
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { homedir } from "node:os";
|
|
13
|
-
import type
|
|
13
|
+
import { type ExtensionAPI, type ExtensionContext, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
14
14
|
import { SUBAGENT_FINISHED_CHANNEL } from "../../../lib/events.ts";
|
|
15
15
|
import { budgetBody, budgetNotice, budgetState } from "./budget.ts";
|
|
16
16
|
import { createSnapshotReader, type SnapshotReader } from "./collect/snapshot.ts";
|
|
@@ -32,6 +32,30 @@ export function registerFooter(pi: ExtensionAPI, deps: FooterDeps = {}): void {
|
|
|
32
32
|
const delegation = createDelegationLedger();
|
|
33
33
|
pi.events.on(SUBAGENT_FINISHED_CHANNEL, (event) => delegation.note(event));
|
|
34
34
|
|
|
35
|
+
// The label a peer knows this session by, once the follower below reports one; `null` is
|
|
36
|
+
// no record, and the bar keeps Pi's own session name.
|
|
37
|
+
let rosterLabel: string | null = null;
|
|
38
|
+
/** Whether this session's label is being followed: a tree change re-installs, it is not new. */
|
|
39
|
+
let followed = false;
|
|
40
|
+
const followLabel = (ctx: ExtensionContext): void => {
|
|
41
|
+
if (followed) return;
|
|
42
|
+
followed = true;
|
|
43
|
+
// Loaded where it is used: the follower walks the bus's layout on disk and subscribes to
|
|
44
|
+
// its label, and the registration graph is a budget that may only fall.
|
|
45
|
+
void import("./collect/label.ts")
|
|
46
|
+
.then(({ followRosterLabel }) =>
|
|
47
|
+
followRosterLabel({
|
|
48
|
+
pi,
|
|
49
|
+
ctx,
|
|
50
|
+
agentDir: getAgentDir(),
|
|
51
|
+
sink: (label) => {
|
|
52
|
+
rosterLabel = label;
|
|
53
|
+
},
|
|
54
|
+
}),
|
|
55
|
+
)
|
|
56
|
+
.catch(() => {});
|
|
57
|
+
};
|
|
58
|
+
|
|
35
59
|
/** The installed reader and the budget it was priced with, while a footer is up. */
|
|
36
60
|
let budget: { limit: number; read: SnapshotReader } | undefined;
|
|
37
61
|
/** One warning per session: the notice stays in scrollback, and a re-install must not repeat it. */
|
|
@@ -41,8 +65,11 @@ export function registerFooter(pi: ExtensionAPI, deps: FooterDeps = {}): void {
|
|
|
41
65
|
|
|
42
66
|
const install = (ctx: ExtensionContext): void => {
|
|
43
67
|
if (ctx.mode !== "tui") return;
|
|
68
|
+
// Not awaited: a footer is installed synchronously, and the bar draws Pi's name until the
|
|
69
|
+
// label lands — which is what a session with no record shows anyway.
|
|
70
|
+
followLabel(ctx);
|
|
44
71
|
const config = loadByteflyConfig(ctx.cwd);
|
|
45
|
-
const read = createSnapshotReader(config, delegation);
|
|
72
|
+
const read = createSnapshotReader(config, delegation, () => rosterLabel);
|
|
46
73
|
budget = { limit: config.budget, read };
|
|
47
74
|
|
|
48
75
|
ctx.ui.setFooter((tui, _theme, footerData) =>
|
|
@@ -57,9 +84,11 @@ export function registerFooter(pi: ExtensionAPI, deps: FooterDeps = {}): void {
|
|
|
57
84
|
};
|
|
58
85
|
|
|
59
86
|
pi.on("session_start", (_event, ctx) => {
|
|
60
|
-
// A new session in this process starts from zero: the ledger is not branch state,
|
|
61
|
-
// and a budget that was already reported is unreported again.
|
|
87
|
+
// A new session in this process starts from zero: the ledger is not branch state, the
|
|
88
|
+
// label belongs to the session, and a budget that was already reported is unreported again.
|
|
62
89
|
delegation.reset();
|
|
90
|
+
followed = false;
|
|
91
|
+
rosterLabel = null;
|
|
63
92
|
warned = false;
|
|
64
93
|
warnedUnpriced = false;
|
|
65
94
|
install(ctx);
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Turn a `StatusSnapshot` into the two footer lines.
|
|
3
3
|
*
|
|
4
|
-
* Line 1 — state: left `pwd` and the session name, right the alert, the
|
|
5
|
-
* chips, then `⎇ branch · ⑂ worktree · ◉ 4780`. Line 2 — meters: left
|
|
6
|
-
* the context gauge and usage
|
|
4
|
+
* Line 1 — state: left `pwd` and the session name, right the alert, the remaining
|
|
5
|
+
* status chips, then `⎇ branch · ⑂ worktree · ◉ 4780`. Line 2 — meters: left the
|
|
6
|
+
* mode, the context gauge, and the usage; right the model and thinking level.
|
|
7
7
|
*
|
|
8
8
|
* The split is one job per row. The state chips change while a session runs, so
|
|
9
9
|
* they ride the line with slack; the meters are the dense row, and a fact that
|
|
10
|
-
* moves onto the top row is one the bottom row stops dropping to fit.
|
|
10
|
+
* moves onto the top row is one the bottom row stops dropping to fit. The mode
|
|
11
|
+
* leads the meters because it qualifies every number after it.
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
14
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
costSegment,
|
|
20
21
|
delegationSegment,
|
|
21
22
|
modelSegment,
|
|
23
|
+
modeSegment,
|
|
22
24
|
modesSegment,
|
|
23
25
|
portsSegment,
|
|
24
26
|
pwdSegment,
|
|
@@ -54,6 +56,7 @@ export function buildLines(snapshot: StatusSnapshot, theme: Theme, home: string
|
|
|
54
56
|
};
|
|
55
57
|
|
|
56
58
|
const line2Left = defined([
|
|
59
|
+
modeSegment(snapshot, theme),
|
|
57
60
|
contextSegment(snapshot, theme),
|
|
58
61
|
windowSegment(snapshot, theme),
|
|
59
62
|
costSegment(snapshot, theme),
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
thinkingColor,
|
|
25
25
|
truncateLabel,
|
|
26
26
|
} from "./format.ts";
|
|
27
|
-
import { compactStatus
|
|
27
|
+
import { compactStatus } from "./status.ts";
|
|
28
28
|
import type { Segment, StatusSnapshot } from "../types.ts";
|
|
29
29
|
|
|
30
30
|
/** Tertiary text: paths and de-emphasized meters. */
|
|
@@ -55,7 +55,11 @@ export function pwdSegment(snapshot: StatusSnapshot, theme: Theme, home: string
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
export function sessionSegment(snapshot: StatusSnapshot, theme: Theme): Segment | null {
|
|
58
|
-
|
|
58
|
+
// The roster's label first: Pi's session name is what the terminal calls this session, and
|
|
59
|
+
// the label is what a card, a handoff, and `/team` use. No record means no team, so the bar
|
|
60
|
+
// keeps the name the user knows.
|
|
61
|
+
const raw = snapshot.rosterLabel ?? snapshot.sessionName;
|
|
62
|
+
const name = raw ? sanitizeLine(raw) : "";
|
|
59
63
|
if (!name) return null;
|
|
60
64
|
const short = truncateLabel(name, CONFIG.sessionLabelMax);
|
|
61
65
|
// Brackets and a brighter token keep the session name from reading as a
|
|
@@ -151,15 +155,36 @@ export function alertSegment(snapshot: StatusSnapshot, theme: Theme): Segment |
|
|
|
151
155
|
};
|
|
152
156
|
}
|
|
153
157
|
|
|
158
|
+
/**
|
|
159
|
+
* The mode: `≡ plan` while planning, `▶ auto` otherwise.
|
|
160
|
+
*
|
|
161
|
+
* Plan owns the flag, so the presence of its status key is what the bar reads; the
|
|
162
|
+
* mode's word and colour are the bar's own, so neither the plan file name nor a
|
|
163
|
+
* truncated chip can reach it. Planning wears `warn`, the same role the composer's
|
|
164
|
+
* border takes while it is on. It leads the resource line, before the gauge, so the
|
|
165
|
+
* mode is read before how much room is left. A session without `plan` still reads
|
|
166
|
+
* `auto`: that is the mode it is in.
|
|
167
|
+
*/
|
|
168
|
+
export function modeSegment(snapshot: StatusSnapshot, theme: Theme): Segment | null {
|
|
169
|
+
if (snapshot.statuses.has(CONFIG.planStatusKey)) {
|
|
170
|
+
const plan = paint(theme, "warn", `${CONFIG.icons.planMode} ${CONFIG.labels.planMode}`);
|
|
171
|
+
return { id: "mode", weight: 1, droppable: false, separator: "", forms: [plan, compactStatus(plan)] };
|
|
172
|
+
}
|
|
173
|
+
const auto = paint(theme, "accent", `${CONFIG.icons.autoMode} ${CONFIG.labels.autoMode}`);
|
|
174
|
+
return { id: "mode", weight: 1, droppable: false, separator: "", forms: [auto] };
|
|
175
|
+
}
|
|
176
|
+
|
|
154
177
|
export function modesSegment(snapshot: StatusSnapshot, theme: Theme): Segment | null {
|
|
155
178
|
const others = [...snapshot.statuses.entries()]
|
|
156
|
-
.filter(
|
|
179
|
+
.filter(
|
|
180
|
+
([key]) =>
|
|
181
|
+
key !== CONFIG.planStatusKey &&
|
|
182
|
+
key !== CONFIG.worktreeStatusKey &&
|
|
183
|
+
key !== CONFIG.httpStatusKey &&
|
|
184
|
+
!isAlert(key),
|
|
185
|
+
)
|
|
157
186
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
158
|
-
.map(([
|
|
159
|
-
const status = sanitize(value);
|
|
160
|
-
// The plan chip trails the plan file name; the slot shows the mode only.
|
|
161
|
-
return key === CONFIG.planStatusKey ? dropStatusDetail(status) : status;
|
|
162
|
-
})
|
|
187
|
+
.map(([, value]) => sanitize(value))
|
|
163
188
|
.filter(Boolean);
|
|
164
189
|
if (others.length === 0) return null;
|
|
165
190
|
const full = others.join(faint(theme, CONFIG.separators.item));
|
|
@@ -169,7 +194,7 @@ export function modesSegment(snapshot: StatusSnapshot, theme: Theme): Segment |
|
|
|
169
194
|
const icons = others.map((status) => compactStatus(status)).join(" ");
|
|
170
195
|
return {
|
|
171
196
|
id: "statuses",
|
|
172
|
-
// Weight 2, ahead of the branch: a
|
|
197
|
+
// Weight 2, ahead of the branch: a status is stated again by the session, while
|
|
173
198
|
// `⎇` is the only thing on the bar that says where the work will land.
|
|
174
199
|
weight: 2,
|
|
175
200
|
droppable: true,
|
|
@@ -188,7 +213,9 @@ export function contextSegment(snapshot: StatusSnapshot, theme: Theme): Segment
|
|
|
188
213
|
const forms = widths.map((blocks) =>
|
|
189
214
|
blocks > 0 ? `${gaugeBar(theme, { percent, blocks, role, emptyRole: "faint" })} ${percentText}` : percentText,
|
|
190
215
|
);
|
|
191
|
-
|
|
216
|
+
// The mode always leads this segment, so the separator prints on every frame;
|
|
217
|
+
// the gauge's offset is the mode's width, not whether one is present.
|
|
218
|
+
return { id: "context", weight: 1, droppable: false, separator: faint(theme, CONFIG.separators.item), forms };
|
|
192
219
|
}
|
|
193
220
|
|
|
194
221
|
export function windowSegment(snapshot: StatusSnapshot, theme: Theme): Segment | null {
|
|
@@ -29,14 +29,3 @@ export function compactStatus(status: string): string {
|
|
|
29
29
|
const text = tokens.length === 2 && /^\d+$/.test(tokens[1]) ? `${icon}${tokens[1]}` : icon;
|
|
30
30
|
return `${prefix}${text}${suffix}`;
|
|
31
31
|
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Drop a status's trailing ` · detail` while keeping its SGR styling.
|
|
35
|
-
*
|
|
36
|
-
* The `plan-mode` chip carries the plan file name as detail (`≡ plan · name`);
|
|
37
|
-
* the bar shows only the mode. Matching `[^ESC]*` removes the visible suffix
|
|
38
|
-
* without consuming the closing reset, so the chip's color stays balanced.
|
|
39
|
-
*/
|
|
40
|
-
export function dropStatusDetail(status: string): string {
|
|
41
|
-
return status.replace(/ · [^\x1b]*/, "");
|
|
42
|
-
}
|
|
@@ -84,6 +84,13 @@ export interface StatusSnapshot {
|
|
|
84
84
|
cwd: string;
|
|
85
85
|
/** Session name set by the user, `null` when unnamed. */
|
|
86
86
|
sessionName: string | null;
|
|
87
|
+
/**
|
|
88
|
+
* The name peers address this session by, when the bus has recorded one.
|
|
89
|
+
*
|
|
90
|
+
* `null` means no record — bytefly runs without the bus and the bar keeps Pi's own
|
|
91
|
+
* session name, which is all a session that never joined has.
|
|
92
|
+
*/
|
|
93
|
+
rosterLabel: string | null;
|
|
87
94
|
/** Branch name, `null` when not in a repository, `"detached"` on detached HEAD. */
|
|
88
95
|
branch: string | null;
|
|
89
96
|
/** Extension status texts (from `ctx.ui.setStatus`), including `worktree`. */
|
|
@@ -93,7 +93,7 @@ export const HEADER_CONFIG: HeaderConfig = {
|
|
|
93
93
|
{ binding: "app.thinking.toggle", description: "to expand thinking" },
|
|
94
94
|
{ binding: "app.model.select", description: "to select model" },
|
|
95
95
|
{ binding: "app.model.cycleForward", description: "to cycle models" },
|
|
96
|
-
{
|
|
96
|
+
{ raw: "shift+tab", description: "to toggle plan" },
|
|
97
97
|
{ binding: "app.editor.external", description: "for external editor" },
|
|
98
98
|
],
|
|
99
99
|
[
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* bytefly — the package's terminal chrome and diagnostics, composed from
|
|
2
|
+
* bytefly — the package's terminal chrome and diagnostics, composed from twelve
|
|
3
3
|
* feature folders: `header/` (the themed startup mark and version), `footer/`
|
|
4
4
|
* (the two-line, width-adaptive status bar with its price calculation),
|
|
5
5
|
* `message/` (the user-message nameplate), `notice/` (the shared transcript
|
|
6
6
|
* notice renderer), `block/` (the unified tool-block frame), `separator/` (the
|
|
7
|
-
* transcript turn rule), `working/` (the live run line on
|
|
8
|
-
* `panel/` (the unified `/bytefly` screen and its
|
|
7
|
+
* transcript turn rule), `working/` (the live run line on the composer's top
|
|
8
|
+
* border), `panel/` (the unified `/bytefly` screen and its
|
|
9
9
|
* `doctor` report), `doctor/` (the `run_doctor` tool and its session-scoped
|
|
10
|
-
* probe runtime), `notify/` (OS notifications for unattended runs),
|
|
11
|
-
*
|
|
10
|
+
* probe runtime), `notify/` (OS notifications for unattended runs), `mode/`
|
|
11
|
+
* (the editor's Shift+Tab plan-mode toggle and its border), and `update/` (Pi's
|
|
12
|
+
* package list, the host suppression, and the header marker).
|
|
12
13
|
*
|
|
13
14
|
* The header, footer, turn rule, and working line are installed
|
|
14
15
|
* only in interactive (`tui`) sessions, and are always on; the user-message
|
|
@@ -22,7 +23,7 @@
|
|
|
22
23
|
* Load with: pi --extension ./extensions/bytefly
|
|
23
24
|
*/
|
|
24
25
|
|
|
25
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
26
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
26
27
|
import { isExtensionEnabled } from "../../lib/env.ts";
|
|
27
28
|
import { registerToolBlocks } from "./block/register.ts";
|
|
28
29
|
import { registerChrome } from "./chrome.ts";
|
|
@@ -87,4 +88,29 @@ export default function bytefly(pi: ExtensionAPI, deps: ByteflyDeps = {}): void
|
|
|
87
88
|
// the one repaint hook rather than installing a header of its own.
|
|
88
89
|
registerPiRelease(pi, piUpdates, { ...deps.release, onUpdate: () => header.refresh() });
|
|
89
90
|
registerPanel(pi, { doctor, update, discover: deps.extensions?.discover });
|
|
91
|
+
|
|
92
|
+
// Shift+Tab is a chord Pi reserves for the thinking cycle, so the mode key is
|
|
93
|
+
// an editor rather than a shortcut. It loads on the first TUI session and only
|
|
94
|
+
// when `plan` is loaded, so a session without it keeps Pi's key and pays for
|
|
95
|
+
// nothing; the mode module owns that availability check.
|
|
96
|
+
let detachModeKey: (() => void) | undefined;
|
|
97
|
+
let modeEpoch = 0;
|
|
98
|
+
const installModeKey = async (ctx: ExtensionContext): Promise<void> => {
|
|
99
|
+
const epoch = ++modeEpoch;
|
|
100
|
+
detachModeKey?.();
|
|
101
|
+
detachModeKey = undefined;
|
|
102
|
+
if (ctx.mode !== "tui") return;
|
|
103
|
+
const { attachModeKey } = await import("./mode/editor.ts");
|
|
104
|
+
// A shutdown or a newer session may land while the module loads; the stale
|
|
105
|
+
// reply must not install an editor nothing will release.
|
|
106
|
+
if (epoch !== modeEpoch) return;
|
|
107
|
+
detachModeKey = attachModeKey(pi, ctx);
|
|
108
|
+
};
|
|
109
|
+
pi.on("session_start", (_event, ctx) => installModeKey(ctx));
|
|
110
|
+
pi.on("session_tree", (_event, ctx) => installModeKey(ctx));
|
|
111
|
+
pi.on("session_shutdown", () => {
|
|
112
|
+
modeEpoch++;
|
|
113
|
+
detachModeKey?.();
|
|
114
|
+
detachModeKey = undefined;
|
|
115
|
+
});
|
|
90
116
|
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Shift+Tab chord at the main editor: the plan-mode toggle and the border
|
|
3
|
+
* that says which mode the composer is in, with the live working line riding it.
|
|
4
|
+
*
|
|
5
|
+
* Pi reserves `app.thinking.cycle` on Shift+Tab, so an extension shortcut for the
|
|
6
|
+
* key is refused. The editor is the narrower place to own it: an overlay or a
|
|
7
|
+
* picker has focus instead, so a dialog keeps its own Shift+Tab. Attaching also
|
|
8
|
+
* unsets the built-in cycle binding, because a key that acts and a key the help
|
|
9
|
+
* still claims are two different answers to the same press.
|
|
10
|
+
*
|
|
11
|
+
* Replacing Pi's editor means opting back into what that editor did:
|
|
12
|
+
* `embedWorkingStatus` is what makes the host hand the working indicator over for
|
|
13
|
+
* the top border, as Pi's own editor asks for. Without it the indicator falls back
|
|
14
|
+
* to a row above the composer, and the border would carry the mode colour alone.
|
|
15
|
+
*
|
|
16
|
+
* The mode is plan's to own; this module only asks over the shared channel and
|
|
17
|
+
* reads the branch for the border. Attach once per session and release with the
|
|
18
|
+
* returned cleanup, which reverts only the editor and binding it still owns.
|
|
19
|
+
* Without `plan` there is nothing to toggle, so attach does nothing and Pi keeps
|
|
20
|
+
* its thinking-cycle chord.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
CustomEditor,
|
|
25
|
+
type ExtensionAPI,
|
|
26
|
+
type ExtensionContext,
|
|
27
|
+
type KeybindingsManager as AppKeybindingsManager,
|
|
28
|
+
type Theme,
|
|
29
|
+
} from "@earendil-works/pi-coding-agent";
|
|
30
|
+
import { type EditorTheme, getKeybindings, type KeybindingsManager, type TUI } from "@earendil-works/pi-tui";
|
|
31
|
+
import { isPlanExtensionLoaded, requestPlanToggle } from "../../../lib/plan-entry.ts";
|
|
32
|
+
import { isPlanModeEnabled } from "../../../lib/plan-state.ts";
|
|
33
|
+
import { isShiftTab } from "../../../lib/tui/cursor.ts";
|
|
34
|
+
import { paint } from "../../../lib/tui/tokens.ts";
|
|
35
|
+
|
|
36
|
+
/** What the editor reads on every input and frame. */
|
|
37
|
+
interface ModeEditorOptions {
|
|
38
|
+
/** Ask the mode's owner to flip it. */
|
|
39
|
+
onToggle: () => void;
|
|
40
|
+
/** The branch is the truth, whoever toggled it. */
|
|
41
|
+
isPlanning: () => boolean;
|
|
42
|
+
/** The role theme, read each frame so a theme change repaints. */
|
|
43
|
+
theme: () => Theme;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The composer with Shift+Tab bound to plan mode and its border to the mode's own colour. */
|
|
47
|
+
export class ModeEditor extends CustomEditor {
|
|
48
|
+
constructor(
|
|
49
|
+
tui: TUI,
|
|
50
|
+
theme: EditorTheme,
|
|
51
|
+
keybindings: KeybindingsManager,
|
|
52
|
+
private readonly options: ModeEditorOptions,
|
|
53
|
+
) {
|
|
54
|
+
// Pi's manager carries the resolved keybindings config on top of the TUI one;
|
|
55
|
+
// the editor only calls `matches()`, which both provide. The option is the same
|
|
56
|
+
// one Pi's own editor sets, so the working line lands on this border instead of
|
|
57
|
+
// in a row of its own above the composer.
|
|
58
|
+
super(tui, theme, keybindings as unknown as AppKeybindingsManager, { embedWorkingStatus: true });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
override handleInput(data: string): void {
|
|
62
|
+
if (isShiftTab(data)) {
|
|
63
|
+
this.options.onToggle();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
super.handleInput(data);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
override render(width: number): string[] {
|
|
70
|
+
// The two roles the footer's mode segment draws, so the bar and the composer agree.
|
|
71
|
+
const role = this.options.isPlanning() ? "warn" : "accent";
|
|
72
|
+
this.borderColor = (text) => paint(this.options.theme(), role, text);
|
|
73
|
+
return super.render(width);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The binding the editor takes over; Pi's default chord for the thinking cycle. */
|
|
78
|
+
const THINKING_CYCLE = "app.thinking.cycle";
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Install the mode editor and take Shift+Tab from the thinking cycle, or return
|
|
82
|
+
* `undefined` when the `plan` extension is not loaded.
|
|
83
|
+
*
|
|
84
|
+
* Returns an idempotent cleanup. It clears the editor only while ours is the
|
|
85
|
+
* installed factory, and restores the binding only while the empty override it
|
|
86
|
+
* wrote is still there, so a later extension's editor or binding is never undone.
|
|
87
|
+
*/
|
|
88
|
+
export function attachModeKey(
|
|
89
|
+
pi: Pick<ExtensionAPI, "events" | "getAllTools">,
|
|
90
|
+
ctx: ExtensionContext,
|
|
91
|
+
): (() => void) | undefined {
|
|
92
|
+
if (!isPlanExtensionLoaded(pi)) return undefined;
|
|
93
|
+
const factory = (tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) =>
|
|
94
|
+
new ModeEditor(tui, theme, keybindings, {
|
|
95
|
+
onToggle: () => {
|
|
96
|
+
void requestPlanToggle(pi, ctx);
|
|
97
|
+
},
|
|
98
|
+
isPlanning: () => isPlanModeEnabled(ctx.sessionManager.getBranch()),
|
|
99
|
+
theme: () => ctx.ui.theme,
|
|
100
|
+
});
|
|
101
|
+
const bindings = getKeybindings();
|
|
102
|
+
const previous = bindings.getUserBindings()[THINKING_CYCLE];
|
|
103
|
+
ctx.ui.setEditorComponent(factory);
|
|
104
|
+
bindings.setUserBindings({ ...bindings.getUserBindings(), [THINKING_CYCLE]: [] });
|
|
105
|
+
|
|
106
|
+
return () => {
|
|
107
|
+
if (ctx.ui.getEditorComponent() === factory) ctx.ui.setEditorComponent(undefined);
|
|
108
|
+
const current = bindings.getUserBindings()[THINKING_CYCLE];
|
|
109
|
+
if (!Array.isArray(current) || current.length > 0) return;
|
|
110
|
+
const restored = { ...bindings.getUserBindings() };
|
|
111
|
+
if (previous === undefined) delete restored[THINKING_CYCLE];
|
|
112
|
+
else restored[THINKING_CYCLE] = previous;
|
|
113
|
+
bindings.setUserBindings(restored);
|
|
114
|
+
};
|
|
115
|
+
}
|