pi-crew 0.11.0 → 0.11.1
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/CHANGELOG.md +116 -0
- package/README.md +0 -1
- package/dist/index.mjs +29936 -29118
- package/package.json +1 -1
- package/schema.json +10 -0
- package/skills/real-test-pi-crew/REPORT-TEMPLATE.md +2 -0
- package/skills/real-test-pi-crew/SKILL.md +181 -1
- package/src/config/config-validation.ts +0 -2
- package/src/config/defaults.ts +0 -1
- package/src/config/types.ts +0 -5
- package/src/extension/crew-vibes/config.ts +0 -21
- package/src/extension/crew-vibes/index.ts +0 -2
- package/src/extension/crew-vibes/render.ts +1 -50
- package/src/extension/help.ts +0 -2
- package/src/extension/register.ts +7 -2
- package/src/extension/registration/commands/manage.ts +5 -4
- package/src/extension/registration/commands/shared.ts +0 -1
- package/src/extension/registration/subagent-tools.ts +16 -6
- package/src/extension/registration/team-tool.ts +10 -3
- package/src/extension/registration/terminal-status-wiring.ts +172 -0
- package/src/extension/registration/viewers.ts +6 -0
- package/src/extension/team-tool/handle-settings.ts +14 -2
- package/src/runtime/task-display.ts +1 -1
- package/src/schema/config-schema.ts +4 -0
- package/src/ui/adaptive-card.ts +65 -0
- package/src/ui/agents-jobs-browser.ts +70 -64
- package/src/ui/card-colors.ts +36 -7
- package/src/ui/dashboard-panes/agents-pane.ts +45 -13
- package/src/ui/dashboard-panes/cancellation-pane.ts +0 -42
- package/src/ui/dashboard-panes/health-pane.ts +7 -5
- package/src/ui/dashboard-panes/mailbox-pane.ts +22 -6
- package/src/ui/dashboard-panes/metrics-pane.ts +15 -7
- package/src/ui/dashboard-panes/pane-theme.ts +21 -0
- package/src/ui/dashboard-panes/plan-pane.ts +63 -30
- package/src/ui/dashboard-panes/progress-pane.ts +3 -2
- package/src/ui/dashboard-panes/schedules-pane.ts +44 -21
- package/src/ui/dashboard-panes/transcript-pane.ts +11 -5
- package/src/ui/dwf-phase-display.ts +3 -20
- package/src/ui/format-helpers.ts +22 -0
- package/src/ui/inline-panel/crew-editor.ts +13 -3
- package/src/ui/inline-panel/index.ts +60 -4
- package/src/ui/keybinding-map.ts +230 -32
- package/src/ui/live-conversation-overlay.ts +176 -46
- package/src/ui/live-run-sidebar.ts +134 -55
- package/src/ui/mascot.ts +32 -16
- package/src/ui/overlays/agent-picker-overlay.ts +81 -26
- package/src/ui/overlays/confirm-overlay.ts +55 -29
- package/src/ui/overlays/help-overlay.ts +108 -53
- package/src/ui/overlays/mailbox-compose-overlay.ts +89 -50
- package/src/ui/overlays/mailbox-detail-overlay.ts +137 -57
- package/src/ui/powerbar-publisher.ts +0 -1
- package/src/ui/rail.ts +333 -0
- package/src/ui/run-dashboard.ts +95 -74
- package/src/ui/settings-overlay.ts +81 -39
- package/src/ui/terminal-status.ts +7 -1
- package/src/ui/theme-adapter.ts +0 -45
- package/src/ui/theme-discovery.ts +12 -6
- package/src/ui/tool-progress-formatter.ts +126 -8
- package/src/ui/tool-renderers/brief-mode.ts +8 -66
- package/src/ui/tool-renderers/index.ts +372 -521
- package/src/ui/transcript-viewer.ts +30 -12
- package/src/ui/widget/index.ts +32 -52
- package/src/ui/widget/task-list.ts +64 -32
- package/src/ui/widget/widget-formatters.ts +3 -402
- package/src/ui/widget/widget-model.ts +28 -7
- package/src/ui/widget/widget-renderer.ts +201 -128
- package/src/ui/widget/widget-types.ts +0 -2
- package/src/utils/visual.ts +0 -4
- package/src/extension/crew-vibes/figures.ts +0 -22
- package/src/extension/crew-vibes/font-detect.ts +0 -71
- package/src/ui/dynamic-border.ts +0 -35
- package/src/ui/loaders.ts +0 -6
- package/src/ui/overlay-stack.ts +0 -148
|
@@ -1,52 +1,51 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* K-1 — dashboard keybinding cheatsheet overlay.
|
|
2
|
+
* K-1 — dashboard keybinding cheatsheet overlay (RAIL design language, M4/E1).
|
|
3
3
|
*
|
|
4
4
|
* Toggled by `?` (bound in keybinding-map.ts). Renders the dashboard's
|
|
5
|
-
* keybindings grouped by scope (general / navigation / panes /
|
|
6
|
-
* mailbox / health) directly from `DASHBOARD_KEYS`, so
|
|
7
|
-
* place is reflected here automatically.
|
|
5
|
+
* keybindings grouped by scope (general / navigation / panes / schedules /
|
|
6
|
+
* plan / run actions / mailbox / health) directly from `DASHBOARD_KEYS`, so
|
|
7
|
+
* adding a key in one place is reflected here automatically.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
* so the overlay's border column aligns with the dashboard's stable-height
|
|
11
|
-
* blank-padding rows (preserves the "stable overlay height" strength — the
|
|
12
|
-
* blank rows injected by the dashboard's pad/trim must land in the same
|
|
13
|
-
* column as this overlay's `│` borders).
|
|
9
|
+
* Grammar (docs/UI-DESIGN-SYSTEM.md §2.E):
|
|
14
10
|
*
|
|
15
|
-
*
|
|
11
|
+
* ┏ HELP ▸ dashboard
|
|
12
|
+
* ┣ GENERAL
|
|
13
|
+
* ┃ Q/Esc close dashboard Enter/S/Tab/Space open run status
|
|
14
|
+
* ┗ ? toggle · Esc dismiss
|
|
15
|
+
*
|
|
16
|
+
* The legacy rounded box (`╭─╮│╰─╯├─┤`) is retired: the overlay is a canopy +
|
|
17
|
+
* `┣ SECTION` group headers + `┃` rows. Each group title is a RAIL section
|
|
18
|
+
* (replacing the old accent-only row), and the always-visible column key/token
|
|
19
|
+
* pair now goes through the SHARED `keyToken()` from `src/ui/rail.ts`.
|
|
20
|
+
*
|
|
21
|
+
* Why that mattered: the old file-local `keyToken` had no `case "\t"` branch, so
|
|
22
|
+
* `DASHBOARD_KEYS.select`'s raw tab byte was rendered verbatim into the table
|
|
23
|
+
* and knocked the row's alignment out by one column (audit §4). The shared
|
|
24
|
+
* tokenizer maps it to `Tab` (and `Esc`/`↑/↓`/`PgUp`/`Space` consistently).
|
|
25
|
+
*
|
|
26
|
+
* Width contract: `render(width)` — every line is built through `railLine`,
|
|
27
|
+
* which owns the `glyph + space` separator and never emits a line wider than
|
|
28
|
+
* the render width. Nothing is baked at build time.
|
|
16
29
|
*/
|
|
17
30
|
|
|
18
|
-
import { pad, truncate } from "../../utils/visual.ts";
|
|
31
|
+
import { pad, truncate, visibleWidth } from "../../utils/visual.ts";
|
|
19
32
|
import { DASHBOARD_KEYS } from "../keybinding-map.ts";
|
|
20
|
-
import {
|
|
33
|
+
import { canopyLine, formatHint, keyToken, RAIL, railLine, sectionLine } from "../rail.ts";
|
|
21
34
|
import { asCrewTheme, type CrewTheme } from "../theme-adapter.ts";
|
|
22
35
|
|
|
23
|
-
/** Translate a raw key sequence into a readable token for the cheatsheet.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
case "\u001b":
|
|
27
|
-
return "Esc";
|
|
28
|
-
case "\u001b[A":
|
|
29
|
-
return "↑";
|
|
30
|
-
case "\u001b[B":
|
|
31
|
-
return "↓";
|
|
32
|
-
case "\r":
|
|
33
|
-
case "\n":
|
|
34
|
-
return "Enter";
|
|
35
|
-
default:
|
|
36
|
-
return key;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
36
|
+
/** Translate a raw key sequence into a readable token for the cheatsheet.
|
|
37
|
+
* Delegates to the SHARED `keyToken` (rail.ts) so the cheatsheet, the footer
|
|
38
|
+
* hints and the tool cards can never drift apart. */
|
|
40
39
|
function keyList(keys: readonly string[]): string {
|
|
41
|
-
return [...keys
|
|
40
|
+
return [...new Set(keys.map((key) => keyToken(key)))].join("/");
|
|
42
41
|
}
|
|
43
42
|
|
|
44
|
-
interface HelpEntry {
|
|
43
|
+
export interface HelpEntry {
|
|
45
44
|
readonly keys: string;
|
|
46
45
|
readonly label: string;
|
|
47
46
|
}
|
|
48
47
|
|
|
49
|
-
interface HelpGroup {
|
|
48
|
+
export interface HelpGroup {
|
|
50
49
|
readonly title: string;
|
|
51
50
|
readonly entries: readonly HelpEntry[];
|
|
52
51
|
}
|
|
@@ -62,7 +61,24 @@ const ROOT_LABELS: Record<string, string> = {
|
|
|
62
61
|
transcript: "transcript",
|
|
63
62
|
liveConversation: "live conv",
|
|
64
63
|
reload: "reload",
|
|
65
|
-
|
|
64
|
+
// NOTE: `progressToggle` deliberately has no label here — the `p` binding
|
|
65
|
+
// had no visible effect (P1-5, phantom key) and is being removed from
|
|
66
|
+
// keybinding-map.ts. Entries derive from Object.entries(DASHBOARD_KEYS.root),
|
|
67
|
+
// so the row disappears entirely once that key is gone.
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const SCHEDULES_LABELS: Record<string, string> = {
|
|
71
|
+
toggle: "toggle",
|
|
72
|
+
runNow: "run now",
|
|
73
|
+
details: "details",
|
|
74
|
+
delete: "delete",
|
|
75
|
+
refresh: "refresh",
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const PLAN_LABELS: Record<string, string> = {
|
|
79
|
+
approve: "approve",
|
|
80
|
+
deny: "deny",
|
|
81
|
+
diff: "diff",
|
|
66
82
|
};
|
|
67
83
|
|
|
68
84
|
const MAILBOX_LABELS: Record<string, string> = {
|
|
@@ -88,6 +104,16 @@ function buildHelpGroups(): HelpGroup[] {
|
|
|
88
104
|
keys: keyList(keys),
|
|
89
105
|
label: ROOT_LABELS[name] ?? name,
|
|
90
106
|
}));
|
|
107
|
+
// P1-14: pane 8 (schedules) and pane 7 (plan) have pane-scoped action keys
|
|
108
|
+
// that were missing from this cheatsheet entirely.
|
|
109
|
+
const scheduleEntries: HelpEntry[] = Object.entries(DASHBOARD_KEYS.schedules).map(([name, keys]) => ({
|
|
110
|
+
keys: keyList(keys),
|
|
111
|
+
label: SCHEDULES_LABELS[name] ?? name,
|
|
112
|
+
}));
|
|
113
|
+
const planEntries: HelpEntry[] = Object.entries(DASHBOARD_KEYS.plan).map(([name, keys]) => ({
|
|
114
|
+
keys: keyList(keys),
|
|
115
|
+
label: PLAN_LABELS[name] ?? name,
|
|
116
|
+
}));
|
|
91
117
|
const mailboxEntries: HelpEntry[] = Object.entries(DASHBOARD_KEYS.mailbox)
|
|
92
118
|
.filter(([name]) => name !== "openDetail")
|
|
93
119
|
.map(([name, keys]) => ({
|
|
@@ -117,12 +143,18 @@ function buildHelpGroups(): HelpGroup[] {
|
|
|
117
143
|
title: "Navigation",
|
|
118
144
|
entries: [
|
|
119
145
|
{
|
|
120
|
-
|
|
146
|
+
// Union of the up/down aliases, deduped through the shared
|
|
147
|
+
// `keyToken` (`k`/`up` and `j`/`down` collapse to one `↑/↓`).
|
|
148
|
+
keys: [
|
|
149
|
+
...new Set([...DASHBOARD_KEYS.navigation.up, ...DASHBOARD_KEYS.navigation.down].map((key) => keyToken(key))),
|
|
150
|
+
].join("/"),
|
|
121
151
|
label: "move selection",
|
|
122
152
|
},
|
|
123
153
|
],
|
|
124
154
|
},
|
|
125
155
|
{ title: "Panes", entries: paneEntries },
|
|
156
|
+
{ title: "Schedules (pane 8)", entries: scheduleEntries },
|
|
157
|
+
{ title: "Plan (pane 7)", entries: planEntries },
|
|
126
158
|
{ title: "Run actions", entries: rootEntries },
|
|
127
159
|
{ title: "Mailbox (pane 3)", entries: mailboxEntries },
|
|
128
160
|
{
|
|
@@ -138,6 +170,21 @@ function buildHelpGroups(): HelpGroup[] {
|
|
|
138
170
|
];
|
|
139
171
|
}
|
|
140
172
|
|
|
173
|
+
/** Test seam (mirrors `__test__openPane` / `__resetInlinePanelForTest`): the
|
|
174
|
+
* generated groups, so tests can assert keys/labels without parsing the
|
|
175
|
+
* rendered surface. The overlay itself renders through `buildHelpGroups()`. */
|
|
176
|
+
export function __test__buildHelpGroups(): HelpGroup[] {
|
|
177
|
+
return buildHelpGroups();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Width of the key column: the widest rendered token list, capped at 20 so a
|
|
181
|
+
* future key set cannot push the labels off the row. Derived from the data —
|
|
182
|
+
* never a hand-tuned constant that silently misaligns when a key is added. */
|
|
183
|
+
function keyColumnWidth(groups: readonly HelpGroup[]): number {
|
|
184
|
+
const widths = groups.flatMap((group) => group.entries.map((entry) => visibleWidth(entry.keys)));
|
|
185
|
+
return Math.min(20, Math.max(1, ...widths));
|
|
186
|
+
}
|
|
187
|
+
|
|
141
188
|
export class HelpOverlay {
|
|
142
189
|
private readonly theme: CrewTheme;
|
|
143
190
|
|
|
@@ -150,28 +197,36 @@ export class HelpOverlay {
|
|
|
150
197
|
}
|
|
151
198
|
|
|
152
199
|
render(width: number): string[] {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
const
|
|
157
|
-
const
|
|
158
|
-
const
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
const bot = fg("border", `╰${bar}╯`);
|
|
162
|
-
const keyCol = 9;
|
|
163
|
-
const lines: string[] = [top, row(`${fg("accent", "pi-crew dashboard")} ${fg("dim", "— key reference (press ? to close)")}`), mid];
|
|
164
|
-
for (const group of buildHelpGroups()) {
|
|
165
|
-
lines.push(row(fg("accent", group.title)));
|
|
200
|
+
if (width < 6) return [];
|
|
201
|
+
const theme = this.theme;
|
|
202
|
+
const budget = width - 2;
|
|
203
|
+
const groups = buildHelpGroups();
|
|
204
|
+
const keyCol = keyColumnWidth(groups);
|
|
205
|
+
const lines: string[] = [canopyLine({ word: "HELP", subject: "dashboard", theme, budget })];
|
|
206
|
+
for (const group of groups) {
|
|
207
|
+
lines.push(sectionLine({ name: group.title, theme, budget }));
|
|
166
208
|
for (let i = 0; i < group.entries.length; i += 2) {
|
|
167
209
|
const pair = group.entries.slice(i, i + 2);
|
|
168
|
-
const cell = (entry: HelpEntry) =>
|
|
169
|
-
|
|
210
|
+
const cell = (entry: HelpEntry) =>
|
|
211
|
+
`${theme.bold(pad(truncate(entry.keys, keyCol), keyCol))} ${theme.fg("dim", truncate(entry.label, 16))}`;
|
|
212
|
+
lines.push(railLine(RAIL.body, "border", truncate(pair.map(cell).join(" "), budget), theme, budget));
|
|
170
213
|
}
|
|
171
214
|
}
|
|
172
|
-
lines.push(
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
215
|
+
lines.push(
|
|
216
|
+
railLine(
|
|
217
|
+
RAIL.close,
|
|
218
|
+
"border",
|
|
219
|
+
theme.fg(
|
|
220
|
+
"dim",
|
|
221
|
+
formatHint([
|
|
222
|
+
["?", "toggle"],
|
|
223
|
+
["escape", "dismiss"],
|
|
224
|
+
]),
|
|
225
|
+
),
|
|
226
|
+
theme,
|
|
227
|
+
budget,
|
|
228
|
+
),
|
|
229
|
+
);
|
|
230
|
+
return lines;
|
|
176
231
|
}
|
|
177
232
|
}
|
|
@@ -1,6 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mailbox compose overlay — RAIL design language (M4/E1, 2026-09-16).
|
|
3
|
+
*
|
|
4
|
+
* ┏ COMPOSE ▸ mailbox
|
|
5
|
+
* ┃ › from: operator │ Preview
|
|
6
|
+
* ┃ › to: leader │ # Title
|
|
7
|
+
* ┃ › body: hello │ • item
|
|
8
|
+
* ┃ › taskId: │
|
|
9
|
+
* ┃ › [ ] Send to outbox │
|
|
10
|
+
* ┗ P close preview · Tab cycle · Enter submit · Esc discard
|
|
11
|
+
*
|
|
12
|
+
* Frame: canopy + `┃` body + `┗` close cap. When the preview is on, the body is
|
|
13
|
+
* a genuine two-column layout and `│` is the inner COLUMN separator (§2.D); the
|
|
14
|
+
* canopy/hint rows are NOT part of the split any more, so the hint keeps its
|
|
15
|
+
* full width instead of being cropped to 60% of the overlay.
|
|
16
|
+
*
|
|
17
|
+
* Keys/behaviour are unchanged (same `overlay:*` dispatch, same free-text
|
|
18
|
+
* passthrough, same validation errors, same discard confirmation). Hints come
|
|
19
|
+
* from `formatHint` (discard LAST, keys through `keyToken`).
|
|
20
|
+
*/
|
|
21
|
+
|
|
1
22
|
import type { MailboxDirection } from "../../state/coordination/mailbox.ts";
|
|
2
|
-
import { pad, truncate } from "../../utils/visual.ts";
|
|
3
|
-
import {
|
|
23
|
+
import { pad, sanitizeLine, truncate } from "../../utils/visual.ts";
|
|
24
|
+
import { overlayActionForKey } from "../keybinding-map.ts";
|
|
25
|
+
import { CURSOR, canopyLine, formatHint, RAIL, railLine, railRaw } from "../rail.ts";
|
|
4
26
|
import { asCrewTheme, type CrewTheme } from "../theme-adapter.ts";
|
|
5
27
|
import { ConfirmOverlay } from "./confirm-overlay.ts";
|
|
6
28
|
import { renderComposePreview } from "./mailbox-compose-preview.ts";
|
|
@@ -49,36 +71,53 @@ export class MailboxComposeOverlay {
|
|
|
49
71
|
|
|
50
72
|
render(width: number): string[] {
|
|
51
73
|
if (this.confirm) return this.confirm.render(width);
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
this.preview
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
74
|
+
if (width < 10) return [];
|
|
75
|
+
const theme = this.theme;
|
|
76
|
+
const budget = width - 2;
|
|
77
|
+
const hint = formatHint([
|
|
78
|
+
["P", this.preview ? "close preview" : "preview"],
|
|
79
|
+
["tab", "cycle"],
|
|
80
|
+
["enter", "submit"],
|
|
81
|
+
["escape", "discard"],
|
|
82
|
+
]);
|
|
83
|
+
const lines: string[] = [canopyLine({ word: "COMPOSE", subject: "mailbox", theme, budget })];
|
|
84
|
+
if (this.error) lines.push(railLine(RAIL.body, "error", truncate(this.error, budget), theme, budget));
|
|
85
|
+
|
|
86
|
+
const formBudget = this.preview ? Math.max(10, Math.floor(budget * 0.6)) : budget;
|
|
87
|
+
const previewBudget = Math.max(8, budget - formBudget - 3);
|
|
88
|
+
const formRows = [
|
|
89
|
+
this.fieldLine("from", formBudget),
|
|
90
|
+
this.fieldLine("to", formBudget),
|
|
91
|
+
this.fieldLine("body", formBudget),
|
|
92
|
+
this.fieldLine("taskId", formBudget),
|
|
93
|
+
`${this.activeField === 4 ? CURSOR : " "} [${this.fields.direction === "outbox" ? "x" : " "}] Send to outbox`,
|
|
65
94
|
];
|
|
66
|
-
if (!this.preview)
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
95
|
+
if (!this.preview) {
|
|
96
|
+
for (const row of formRows) lines.push(railLine(RAIL.body, "border", truncate(row, budget), theme, budget));
|
|
97
|
+
} else {
|
|
98
|
+
const previewLines = renderComposePreview(this.fields.body, previewBudget, theme);
|
|
99
|
+
const max = Math.max(formRows.length, previewLines.length);
|
|
100
|
+
for (let index = 0; index < max; index += 1) {
|
|
101
|
+
const left = pad(truncate(formRows[index] ?? "", formBudget), formBudget);
|
|
102
|
+
const right = truncate(previewLines[index] ?? "", previewBudget);
|
|
103
|
+
// No separator when the preview column has no row at this height:
|
|
104
|
+
// a `│` with nothing to its right reads as a frame edge.
|
|
105
|
+
lines.push(railRaw(RAIL.body, "border", right ? `${left} ${theme.fg("dim", "│")} ${right}` : left, theme));
|
|
106
|
+
}
|
|
74
107
|
}
|
|
75
|
-
|
|
108
|
+
lines.push(railLine(RAIL.close, "border", theme.fg("dim", hint), theme, budget));
|
|
109
|
+
return lines;
|
|
76
110
|
}
|
|
77
111
|
|
|
78
112
|
private fieldLine(field: Exclude<FieldName, "direction">, width: number): string {
|
|
79
113
|
const active = FIELD_ORDER[this.activeField] === field;
|
|
80
114
|
const label = field === "taskId" ? "taskId" : field;
|
|
81
|
-
|
|
115
|
+
// `sanitizeLine`: a pasted / pre-filled value can carry a raw newline or
|
|
116
|
+
// TAB — printing it verbatim would break the rail row into several lines
|
|
117
|
+
// (the field is a single-line cell; multi-line content belongs to the
|
|
118
|
+
// preview column).
|
|
119
|
+
const value = sanitizeLine(this.fields[field] ?? "");
|
|
120
|
+
return `${active ? CURSOR : " "} ${label}: ${truncate(value, Math.max(8, width - label.length - 5))}`;
|
|
82
121
|
}
|
|
83
122
|
|
|
84
123
|
private activeName(): FieldName {
|
|
@@ -151,32 +190,32 @@ export class MailboxComposeOverlay {
|
|
|
151
190
|
this.confirm.handleInput(data);
|
|
152
191
|
return;
|
|
153
192
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
else this.activeField = (this.activeField + 1) % FIELD_ORDER.length;
|
|
178
|
-
return;
|
|
193
|
+
// M2-1: keys come from the central `overlay:*` keyspace (remappable via
|
|
194
|
+
// `.crew/config.json` → keybindings["overlay:mailbox-compose:<action>"]).
|
|
195
|
+
switch (overlayActionForKey("mailbox-compose", data)) {
|
|
196
|
+
case "cancel":
|
|
197
|
+
this.cancel();
|
|
198
|
+
return;
|
|
199
|
+
case "preview":
|
|
200
|
+
this.preview = !this.preview;
|
|
201
|
+
return;
|
|
202
|
+
case "nextField":
|
|
203
|
+
this.activeField = (this.activeField + 1) % FIELD_ORDER.length;
|
|
204
|
+
return;
|
|
205
|
+
case "space":
|
|
206
|
+
if (this.activeName() === "direction") this.fields.direction = this.fields.direction === "inbox" ? "outbox" : "inbox";
|
|
207
|
+
else this.appendText(data);
|
|
208
|
+
return;
|
|
209
|
+
case "backspace":
|
|
210
|
+
this.backspace();
|
|
211
|
+
return;
|
|
212
|
+
case "submit":
|
|
213
|
+
if (this.activeName() === "body" || this.fields.body.trim()) this.submit();
|
|
214
|
+
else this.activeField = (this.activeField + 1) % FIELD_ORDER.length;
|
|
215
|
+
return;
|
|
179
216
|
}
|
|
217
|
+
// Free-text passthrough (unchanged): any other printable char is typed
|
|
218
|
+
// into the active field.
|
|
180
219
|
if (data.length === 1 && data >= " ") this.appendText(data);
|
|
181
220
|
}
|
|
182
221
|
}
|
|
@@ -1,7 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mailbox detail overlay — RAIL design language (M4/E1, 2026-09-16).
|
|
3
|
+
*
|
|
4
|
+
* ┏ MAILBOX ▸ 1a2b3c4d
|
|
5
|
+
* ┃ Inbox │ Outbox
|
|
6
|
+
* ┃ ›✓ lead ▸ one: ping │ !
|
|
7
|
+
* ┃ ▼ 3 below
|
|
8
|
+
* ┣ MESSAGE
|
|
9
|
+
* ┃ lead ▸ one (one) · pending
|
|
10
|
+
* ┗ Tab side · ↑/↓ select · Enter expand · … ···· Esc close
|
|
11
|
+
*
|
|
12
|
+
* The `│` in the body is a genuine inner COLUMN separator of the two-column
|
|
13
|
+
* mailbox table (§2.D) — it is not a frame edge. The frame is canopy + `┃`
|
|
14
|
+
* body + `┗` close cap; the legacy title/hint header rows, the `───` inline
|
|
15
|
+
* rule before the expanded message and the hand-typed hint string are gone.
|
|
16
|
+
*
|
|
17
|
+
* Hints: built by `formatHint` (close LAST) and laid out with `railLeaders` so
|
|
18
|
+
* the `Esc close` segment stays visible on a narrow overlay instead of being
|
|
19
|
+
* truncated away with the long action list.
|
|
20
|
+
*
|
|
21
|
+
* Every disk-sourced field is guarded (`?? "?"`): mailbox JSON is not
|
|
22
|
+
* schema-validated at read time.
|
|
23
|
+
*/
|
|
24
|
+
|
|
1
25
|
import { type MailboxMessage, readDeliveryState, readMailbox } from "../../state/coordination/mailbox.ts";
|
|
2
26
|
import { loadRunManifestById } from "../../state/stores/state-store.ts";
|
|
3
27
|
import { pad, truncate } from "../../utils/visual.ts";
|
|
4
|
-
import {
|
|
28
|
+
import { overlayActionForKey } from "../keybinding-map.ts";
|
|
29
|
+
import { ACTIVE, CURSOR, canopyLine, formatHint, overflowHint, RAIL, railLeaders, railLine, sectionLine, shortId } from "../rail.ts";
|
|
5
30
|
import { asCrewTheme, type CrewTheme } from "../theme-adapter.ts";
|
|
6
31
|
|
|
7
32
|
export type MailboxAction =
|
|
@@ -11,6 +36,9 @@ export type MailboxAction =
|
|
|
11
36
|
| { type: "ackAll" }
|
|
12
37
|
| { type: "close" };
|
|
13
38
|
|
|
39
|
+
/** Rows shown per column before the overflow hint takes over. */
|
|
40
|
+
const MAX_ROWS = 12;
|
|
41
|
+
|
|
14
42
|
export class MailboxDetailOverlay {
|
|
15
43
|
private readonly runId: string;
|
|
16
44
|
private readonly cwd: string;
|
|
@@ -84,72 +112,124 @@ export class MailboxDetailOverlay {
|
|
|
84
112
|
this.refresh();
|
|
85
113
|
this.needsRefresh = false;
|
|
86
114
|
}
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
115
|
+
if (width < 10) return [];
|
|
116
|
+
const theme = this.theme;
|
|
117
|
+
const budget = width - 2;
|
|
118
|
+
const lines: string[] = [canopyLine({ word: "MAILBOX", subject: shortId(this.runId), theme, budget })];
|
|
119
|
+
|
|
120
|
+
// Two-column body (inbox ↔ outbox): `│` is the inner column separator.
|
|
121
|
+
const col = Math.max(10, Math.floor((budget - 3) / 2));
|
|
122
|
+
const rightCol = Math.max(1, budget - col - 3);
|
|
123
|
+
const twoCol = (left: string, right: string) =>
|
|
124
|
+
`${pad(truncate(left, col), col)} ${theme.fg("dim", "│")} ${truncate(right, rightCol)}`;
|
|
125
|
+
lines.push(railLine(RAIL.body, "border", twoCol(theme.bold("Inbox"), theme.bold("Outbox")), theme, budget));
|
|
126
|
+
const total = Math.max(this.inbox.length, this.outbox.length);
|
|
127
|
+
const shown = Math.min(total, MAX_ROWS);
|
|
128
|
+
for (let index = 0; index < shown; index += 1) {
|
|
129
|
+
lines.push(
|
|
130
|
+
railLine(
|
|
131
|
+
RAIL.body,
|
|
132
|
+
"border",
|
|
133
|
+
twoCol(this.row(this.inbox[index], "inbox", index, col), this.row(this.outbox[index], "outbox", index, col)),
|
|
134
|
+
theme,
|
|
135
|
+
budget,
|
|
136
|
+
),
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
if (total > shown) {
|
|
140
|
+
lines.push(railLine(RAIL.body, "border", overflowHint(0, total - shown, theme), theme, budget));
|
|
97
141
|
}
|
|
142
|
+
|
|
98
143
|
const selected = this.selectedMessage();
|
|
99
144
|
if (this.expanded && selected) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
lines.push(
|
|
145
|
+
const from = selected.from ?? "?";
|
|
146
|
+
const to = selected.to ?? "?";
|
|
147
|
+
lines.push(sectionLine({ name: "Message", theme, budget }));
|
|
148
|
+
lines.push(
|
|
149
|
+
railLine(
|
|
150
|
+
RAIL.body,
|
|
151
|
+
"border",
|
|
152
|
+
truncate(
|
|
153
|
+
`${from} ${ACTIVE} ${to}${selected.taskId ? ` (${selected.taskId})` : ""} · ${selected.status ?? "?"}`,
|
|
154
|
+
budget,
|
|
155
|
+
),
|
|
156
|
+
theme,
|
|
157
|
+
budget,
|
|
158
|
+
),
|
|
159
|
+
);
|
|
160
|
+
for (const line of (selected.body ?? "").split(/\r?\n/)) {
|
|
161
|
+
lines.push(railLine(RAIL.body, "border", truncate(line, budget), theme, budget));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (!this.inbox.length && !this.outbox.length) {
|
|
166
|
+
lines.push(railLine(RAIL.body, "border", theme.fg("dim", "Mailbox is empty."), theme, budget));
|
|
103
167
|
}
|
|
104
|
-
|
|
105
|
-
|
|
168
|
+
|
|
169
|
+
const actions = formatHint([
|
|
170
|
+
["tab", "side"],
|
|
171
|
+
[["up", "down"], "select"],
|
|
172
|
+
["enter", "expand"],
|
|
173
|
+
["A", "ack"],
|
|
174
|
+
["N", "nudge"],
|
|
175
|
+
["C", "compose"],
|
|
176
|
+
["X", "ack all"],
|
|
177
|
+
]);
|
|
178
|
+
const close = formatHint([["escape", "close"]]);
|
|
179
|
+
lines.push(
|
|
180
|
+
railLine(RAIL.close, "border", railLeaders(theme.fg("dim", actions), theme.fg("dim", close), budget, theme), theme, budget),
|
|
181
|
+
);
|
|
182
|
+
return lines;
|
|
106
183
|
}
|
|
107
184
|
|
|
108
185
|
private row(message: MailboxMessage | undefined, side: "inbox" | "outbox", index: number, width: number): string {
|
|
109
|
-
if (!message) return
|
|
110
|
-
const marker = this.side === side && this.selected === index ?
|
|
111
|
-
const
|
|
112
|
-
|
|
186
|
+
if (!message) return "";
|
|
187
|
+
const marker = this.side === side && this.selected === index ? CURSOR : " ";
|
|
188
|
+
const acknowledged = message.status === "acknowledged";
|
|
189
|
+
const status = this.theme.fg(acknowledged ? "success" : "warning", acknowledged ? "✓" : "!");
|
|
190
|
+
const from = message.from ?? "?";
|
|
191
|
+
const to = message.to ?? "?";
|
|
192
|
+
const body = (message.body ?? "").replace(/\s+/g, " ");
|
|
193
|
+
return truncate(`${marker}${status} ${from} ${ACTIVE} ${to}: ${body}`, width);
|
|
113
194
|
}
|
|
114
195
|
|
|
115
196
|
handleInput(data: string): void {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
197
|
+
// M2-1: keys come from the central `overlay:*` keyspace (remappable via
|
|
198
|
+
// `.crew/config.json` → keybindings["overlay:mailbox-detail:<action>"]).
|
|
199
|
+
switch (overlayActionForKey("mailbox-detail", data)) {
|
|
200
|
+
case "close":
|
|
201
|
+
this.done({ type: "close" });
|
|
202
|
+
return;
|
|
203
|
+
case "toggleSide":
|
|
204
|
+
this.side = this.side === "inbox" ? "outbox" : "inbox";
|
|
205
|
+
this.selected = Math.min(this.selected, Math.max(0, this.current().length - 1));
|
|
206
|
+
return;
|
|
207
|
+
case "up":
|
|
208
|
+
this.selected = Math.max(0, this.selected - 1);
|
|
209
|
+
return;
|
|
210
|
+
case "down":
|
|
211
|
+
this.selected = Math.min(Math.max(0, this.current().length - 1), this.selected + 1);
|
|
212
|
+
return;
|
|
213
|
+
case "toggleDetail":
|
|
214
|
+
this.expanded = !this.expanded;
|
|
215
|
+
return;
|
|
216
|
+
case "ack": {
|
|
217
|
+
const message = this.selectedMessage();
|
|
218
|
+
if (message) this.done({ type: "ack", messageId: message.id });
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
case "nudge":
|
|
222
|
+
this.done({
|
|
223
|
+
type: "nudge",
|
|
224
|
+
agentId: this.selectedMessage()?.taskId,
|
|
225
|
+
});
|
|
226
|
+
return;
|
|
227
|
+
case "compose":
|
|
228
|
+
this.done({ type: "compose" });
|
|
229
|
+
return;
|
|
230
|
+
case "ackAll":
|
|
231
|
+
this.done({ type: "ackAll" });
|
|
232
|
+
return;
|
|
152
233
|
}
|
|
153
|
-
if (data === "X") this.done({ type: "ackAll" });
|
|
154
234
|
}
|
|
155
235
|
}
|
|
@@ -271,7 +271,6 @@ class PowerbarPublisher {
|
|
|
271
271
|
workspaceId?: string,
|
|
272
272
|
): void {
|
|
273
273
|
if (config?.powerbar === false) return;
|
|
274
|
-
const useStatusFallback = !hasPowerbarConsumer(events);
|
|
275
274
|
// P3 (#10): prefer an explicit workspaceId arg; otherwise self-derive
|
|
276
275
|
// from the ctx every caller already passes (ExtensionContext exposes
|
|
277
276
|
// sessionManager). Back-compat: when neither is present, no filtering.
|