49 KiB
Phase 8 — Operator Experience: Interactive Mailbox, Health Pane, Smart Notifications
Tiếp nối tự nhiên của Phase 7 (UI Optimization). Mục tiêu: biến dashboard từ "viewer" thành "operator console" — actions thực hiện được trực tiếp từ UI, không phải toggle CLI. Path X chosen (Phase 8 = Theme A, Phase 9 = Theme B+C Observability+Reliability deferred).
Open Questions Resolution (Q1-Q6 đã chốt — xem Section 7 chi tiết):
- Q1=(b) Có preview compose pane | Q2=(c) Sink JSONL khi
telemetry.enabled| Q3=(b) Cross-day quiet-hours wrap - Q4=(c) Full action menu R/K/D trên health pane | Q5=(c) Confirm chỉ destructive | Q6=(a) ESC discard + confirm-if-long guard
0. Implementation Status
- 8.0 Foundation: keybinding contract + action dispatcher + RunActionResult shape + ConfirmOverlay primitive
- 8.1.A Mailbox detail overlay (passive list view, no actions yet)
- 8.1.B Mailbox ack action (hotkey
Atrên message đang chọn) - 8.1.C Mailbox nudge action (hotkey
N+ agent picker) - 8.1.D Mailbox compose action (hotkey
C+ form overlay) — Q6: ESC discard + confirm-if-long (>50 chars) - 8.1.E Mailbox compose preview pane (key
Ptoggle, render markdown read-only) — Q1 - 8.1.F Mailbox ackAll destructive action (hotkey
Shift+X) — Q5: requires confirm overlay - 8.2.A Heartbeat aggregator (
heartbeat-aggregator.ts) - 8.2.B Health pane (pane index
5) trong dashboard - 8.2.C Auto-recovery prompt (stuck worker > N minutes → toast + confirm) — throttled 5min/run
- 8.2.D Health pane action menu —
Rrecovery (foreground only),Kkill stale workers,Ddiagnostic export — Q4 - 8.3.A Notification router (severity classifier + dedup window)
- 8.3.B Notification quiet-hours (cross-day wrap parser) + batching config — Q3
- 8.3.C Toast badge counter trong widget/powerbar (đếm số notification chưa ack)
- 8.3.D Notification JSONL sink rotate 7 ngày, gated bởi
telemetry.enabled— Q2 - 8.4 Wire
register.ts+commands.ts - 8.5 Tests: unit + integration
1. Roadmap-Level Decisions
| # | Decision | Chosen | Rationale |
|---|---|---|---|
| D1 | Mailbox actions chạy trực tiếp hay dispatch về team API? | Dispatch qua handleTeamTool({action:"api", config:{operation:...}}) |
Tận dụng API hiện có (ack-message, send-message, nudge-agent); zero state-machine duplication; locks/events được giữ nguyên |
| D2 | Overlay form vs inline edit? | Overlay form (modal-like, anchor center) | Dashboard sidebar quá hẹp cho text input; overlay tách biệt focus; ESC dễ cancel |
| D3 | Health pane là pane mới (5) hay tab trong progress? |
Pane mới 5 |
Tránh pollute progress pane; cho user toggle độc lập; consistent với existing 1-4 |
| D4 | Notification sink: optional opt-in hay default-on? | Default-on khi telemetry.enabled !== false (Q2=c) |
Đồng nhất pattern Phase 6 telemetry; debug-friendly; user opt-out qua telemetry config chung. Path: <crewRoot>/state/notifications/{YYYY-MM-DD}.jsonl, rotate 7 ngày |
| D5 | Quiet-hours format + cross-day? | HH:MM-HH:MM trong config local timezone, support cross-day wrap (Q3=b) | Single range "22:00-07:00" parser tự nhận diện wrap-around; intuitive vs multi-range array |
| D6 | Compose-form fields scope? | Phase 8: from/to/body/taskId + preview pane (Q1=b) | Preview key P toggle render markdown read-only; thread/attachment defer Phase 9 |
| D7 | Action mới có break keybinding cũ? | No — phím mới: A/N/C/P/Shift+X (mailbox), R/K/D (health), H/X (notification); phím hiện hành (s/u/a/i/d/m/e/o/v/r/p/1-4/k/j) giữ nguyên (lowercase r vẫn = reload root, uppercase R = recovery in health pane only) |
Backward-compat; context-scoped uppercase |
| D8 | Mailbox detail panel: inline expand hay separate overlay? | Separate overlay (mở khi nhấn Enter trên pane mailbox) | Pane chính giữ nguyên density; overlay scrollable |
| D9 | Health pane action mode: prompt-only vs full menu? | Full action menu (Q4=c): R recovery (foreground-only), K kill stale workers, D diagnostic export |
Operator power-user toolkit; async runs R/K disabled with hint; D cực hữu ích cho bug report |
| D10 | Foundation 8.0: tách RunActionDispatcher hay inline? | Tách module src/ui/run-action-dispatcher.ts |
Reuse cho overlay con; dễ test; không bloat dashboard |
| D11 | Compose ESC behavior? | Discard + confirm-if-long (Q6=a) | ESC không lưu draft; nếu body > 50 ký tự → confirm overlay Y=discard, N=continue editing; defer draft persistence Phase 9 |
| D12 | Confirm overlay: per-action ad-hoc hay reusable primitive? | Reusable primitive src/ui/overlays/confirm-overlay.ts |
Q5=c destructive (ackAll/recovery/diagnostic-export-with-secrets) cần consistent UX; reuse cho mọi confirm |
| D13 | Auto-recovery throttle window? | 5 phút/run/condition-type | Tránh notification storm khi run dead lâu; recovery_dead_workers riêng biệt với recovery_missing_heartbeat |
| D14 | Diagnostic export D format & destination? |
JSON + redact secrets vào <crewRoot>/artifacts/{runId}/diagnostic-{timestamp}.json |
Self-contained snapshot (manifest + tasks + recent events + heartbeat summary); confirm before write nếu artifact-dir đã có file diag cũ < 1 phút |
| D15 | Preview pane render scope (Q1=b)? | Read-only markdown render: bold/italic/code-block/list — no images/links | Đủ cho operator đọc nội dung trước khi gửi; không cần markdown engine đầy đủ; reuse từ existing transcript-viewer markdown helper nếu có |
2. Phase Breakdown
Phase 8.0 — Foundation (2 dev-day, +0.5 cho ConfirmOverlay)
File mới:
src/ui/run-action-dispatcher.ts— wrapper gọihandleTeamToolvớirunId+operation, normalize result thành{ ok, message, data }.src/ui/keybinding-map.ts— central registry mappingdata(raw stdin) → action name; exportKEY_RESERVEDđể overlay con check conflict.src/ui/overlays/confirm-overlay.ts— (Q5) reusable confirm primitive, anchor center, auto-focusN(safe default), Y/Enter=confirm, N/ESC=cancel. ~80 LOC.
Sửa:
src/ui/run-dashboard.ts— refactorhandleInputdùngkeybinding-map; không thay đổi behavior cũ.
Skeleton:
// run-action-dispatcher.ts
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
import { handleTeamTool } from "../extension/team-tool.ts";
export interface RunActionResult {
ok: boolean;
message: string;
data?: unknown;
}
export async function dispatchMailboxAck(ctx: ExtensionContext, runId: string, messageId: string): Promise<RunActionResult> {
try {
const r = await handleTeamTool({ action: "api", runId, config: { operation: "ack-message", messageId } }, ctx);
return { ok: r.metadata?.status === "ok", message: r.text, data: r };
} catch (error) {
return { ok: false, message: error instanceof Error ? error.message : String(error) };
}
}
export async function dispatchMailboxNudge(ctx: ExtensionContext, runId: string, agentId: string, message: string): Promise<RunActionResult> { /* ... */ }
export async function dispatchMailboxCompose(ctx: ExtensionContext, runId: string, payload: { from: string; to: string; body: string; taskId?: string; direction: "inbox" | "outbox" }): Promise<RunActionResult> { /* ... */ }
export async function dispatchMailboxAckAll(ctx: ExtensionContext, runId: string): Promise<RunActionResult> { /* read-mailbox → loop ack-message */ }
export async function dispatchHealthRecovery(ctx: ExtensionContext, runId: string): Promise<RunActionResult> { /* foreground-interrupt API */ }
export async function dispatchKillStaleWorkers(ctx: ExtensionContext, runId: string): Promise<RunActionResult> { /* mark dead heartbeats; emit event */ }
export async function dispatchDiagnosticExport(ctx: ExtensionContext, runId: string): Promise<RunActionResult> { /* read-manifest + list-tasks + read-events limit=200 + heartbeat summary → write artifact */ }
// keybinding-map.ts (Q4 + Q5 expanded)
export const DASHBOARD_KEYS = {
close: ["q", "\u001b"],
select: ["\r", "\n", "s"],
pane: { agents: ["1"], progress: ["2"], mailbox: ["3"], output: ["4"], health: ["5"] },
// Mailbox detail overlay context
mailbox: { ack: ["A"], nudge: ["N"], compose: ["C"], preview: ["P"], ackAll: ["X"], openDetail: ["\r", "\n"] },
// Health pane context (Q4=c full menu)
health: { recovery: ["R"], killStale: ["K"], diagnosticExport: ["D"] },
// Notification context
notification: { dismissAll: ["H"] }, // 'H' for Hush
} as const;
// confirm-overlay.ts
export interface ConfirmOptions {
title: string;
body?: string;
dangerLevel?: "low" | "medium" | "high"; // colors theme accent
defaultAction?: "confirm" | "cancel"; // default "cancel"
}
export class ConfirmOverlay {
constructor(private opts: ConfirmOptions, private done: (confirmed: boolean) => void, private theme: unknown) {}
render(width: number): string[] { /* anchor-center box, dim Y/N hint */ }
handleInput(data: string): void {
if (data === "y" || data === "Y" || data === "\r" || data === "\n") return this.done(true);
if (data === "n" || data === "N" || data === "\u001b" || data === "q") return this.done(false);
}
}
Tests:
test/unit/run-action-dispatcher.test.ts(7 test cases — 4 mailbox dispatchers + 3 health dispatchers, mockhandleTeamTool).test/unit/confirm-overlay.test.ts(4 cases: render, Y confirms, N cancels, default cancel safety).
Phase 8.1 — Mailbox Interactivity
8.1.A Mailbox detail overlay (1 dev-day)
File mới:
src/ui/overlays/mailbox-detail-overlay.ts— classMailboxDetailOverlayimplement Pi UI custom widget; render 2-column (inbox | outbox); ↑/↓ select; Enter expand body; ESC/q close.
Cập nhật:
src/ui/dashboard-panes/mailbox-pane.ts— line cuối cùng đổi từ "use /team-api ..." thành"Press Enter on mailbox pane to open detail (A=ack, N=nudge, C=compose)".src/ui/run-dashboard.ts— khiactivePane === "mailbox"và user nhấn Enter, return{action: "mailbox-detail"}thay vì close.src/extension/registration/commands.ts— handleselection.action === "mailbox-detail"→ mởMailboxDetailOverlayquactx.ui.custom.
Skeleton:
export class MailboxDetailOverlay {
private inbox: MailboxMessage[] = [];
private outbox: MailboxMessage[] = [];
private selected = 0;
private side: "inbox" | "outbox" = "inbox";
constructor(private opts: { runId: string; cwd: string; ctx: ExtensionContext; done: (sel?: MailboxAction) => void; theme: unknown }) {
this.refresh();
}
private refresh(): void { /* read mailbox via team api */ }
render(width: number): string[] { /* 2-col layout, highlight selected */ }
handleInput(data: string): void { /* arrow nav, A/N/C dispatch via this.opts.done */ }
}
export interface MailboxAction {
type: "ack" | "nudge" | "compose" | "reply";
messageId?: string;
agentId?: string;
}
Tests: test/unit/mailbox-detail-overlay.test.ts — 4 cases (render empty, render with items, key navigation, action dispatch).
8.1.B Ack action (0.75 dev-day)
Logic: trong MailboxDetailOverlay.handleInput, key A (uppercase, để tránh conflict với a=artifacts ở dashboard root) → done({type:"ack", messageId: selectedMessage.id}).
Update commands.ts: sau khi overlay close, nếu action.type === "ack" → call dispatchMailboxAck(ctx, runId, action.messageId!) → toast result.
Acceptance: ack thành công → mailbox pane re-render với attention count giảm trong < 250ms (snapshot cache invalidate khi crew.mailbox.acknowledged event).
8.1.C Nudge action (0.75 dev-day)
Logic: key N → mở agent picker overlay (reuse pattern từ existing LiveRunSidebar); chọn xong → message input → dispatch dispatchMailboxNudge.
File mới: src/ui/overlays/agent-picker-overlay.ts (nhỏ, 80-120 LOC).
Acceptance: nudge → crew.mailbox.message event fire → snapshot invalidate → mailbox pane attention count tăng đúng.
8.1.D Compose form (1.25 dev-day)
File mới: src/ui/overlays/mailbox-compose-overlay.ts — form 4 field (from/to/body/taskId), Tab navigation, Enter submit, ESC cancel.
Behavior chi tiết (Q6=a):
- Tab/Shift+Tab: cycle giữa các field.
- Body multi-line: Ctrl+Enter → newline; Enter trên field body với content non-empty → submit.
- ESC khi body ≤ 50 ký tự → discard immediately, close overlay.
- ESC khi body > 50 ký tự → mở
ConfirmOverlayvới title"Discard draft?"body"Body has N chars. Y=discard, N=continue editing". Cancel default = continue editing (safe). - Submit validate: body required (non-whitespace), to required, from default
"operator"if empty. - Direction toggle: Tab vào checkbox
[ ] Send to outbox→ Space toggle.
Logic dispatch: dispatchMailboxCompose với direction từ checkbox (default "inbox" — operator gửi vào inbox của run).
Tests: test/unit/mailbox-compose-overlay.test.ts — 8 cases (render, tab nav, ESC short discard, ESC long → confirm overlay, confirm overlay cancel = stay editing, confirm overlay confirm = discard, Enter submit, validation empty body, validation empty to).
8.1.E Compose preview pane (0.75 dev-day) — Q1=b
File mới: src/ui/overlays/mailbox-compose-preview.ts — read-only render markdown của body field hiện tại; share state với mailbox-compose-overlay.ts.
Layout: compose overlay split horizontal khi preview active — 60% form / 40% preview pane (pane render markdown read-only, không cho focus).
Render scope (D15): bold (**), italic (*), code-block (```), inline code (`), unordered list (-), numbered list (1.), heading (#/##/###). Skip images/links (out of scope; render link text only).
Behavior:
- Key
Ptoggle preview on/off (state in compose overlay). - Preview cập nhật real-time khi body thay đổi (debounce 100ms để tránh re-render mỗi keystroke).
- Khi preview active, header help line update:
"P close preview · Tab cycle · Enter submit · ESC discard".
Skeleton:
// mailbox-compose-preview.ts
export function renderComposePreview(body: string, width: number, theme: CrewTheme): string[] {
const tokens = tokenizeMarkdown(body); // simple tokenizer ~80 LOC
return tokens.flatMap((t) => renderToken(t, width, theme));
}
function tokenizeMarkdown(body: string): MdToken[] { /* line-by-line scan */ }
type MdToken = { type: "heading" | "code-block" | "list-item" | "paragraph"; level?: number; text: string };
Tests: test/unit/mailbox-compose-preview.test.ts — 6 cases (plain text, bold/italic, code block, list, heading, mixed content).
8.1.F Mailbox ackAll (0.5 dev-day) — Q5=c destructive
Logic: trong MailboxDetailOverlay.handleInput, key Shift+X (raw stdin "X" uppercase) → mở ConfirmOverlay:
- Title:
"Acknowledge all N unread messages?" - Body:
"This cannot be undone. Y=ack all, N=cancel." - DangerLevel:
"medium".
Confirm Y → dispatchMailboxAckAll(ctx, runId) (dispatcher loop ack-message từng id) → toast result "Acknowledged N messages.".
Acceptance: ackAll trong run với 10 unread → all marked acknowledged trong < 2s; mailbox pane attention → 0; emit 10x crew.mailbox.acknowledged event.
Tests: test/unit/mailbox-detail-overlay.test.ts thêm 3 cases (Shift+X opens confirm, confirm Y dispatches loop, confirm N stays).
Phase 8.2 — Health Pane & Recovery
8.2.A Heartbeat aggregator (1 dev-day)
File mới: src/ui/heartbeat-aggregator.ts
export interface HeartbeatSummary {
runId: string;
totalTasks: number;
healthy: number; // alive=true, lastSeenAt < threshold
stale: number; // lastSeenAt > stale threshold (default 60s)
dead: number; // lastSeenAt > dead threshold (default 5min) hoặc alive=false
missing: number; // task running nhưng no heartbeat record
worstStaleMs: number;
}
export function summarizeHeartbeats(snapshot: RunUiSnapshot, opts?: { staleMs?: number; deadMs?: number; now?: number }): HeartbeatSummary { /* ... */ }
Tests: test/unit/heartbeat-aggregator.test.ts — 6 cases (all healthy, mixed, all dead, missing record, custom threshold, edge lastSeenAt=now).
8.2.B Health pane (0.75 dev-day)
File mới: src/ui/dashboard-panes/health-pane.ts
export function renderHealthPane(snapshot: RunUiSnapshot | undefined, opts?: { staleMs?: number; deadMs?: number; isForeground?: boolean }): string[] {
if (!snapshot) return ["Health pane: snapshot unavailable"];
const summary = summarizeHeartbeats(snapshot, opts);
const lines: string[] = [
`Health: ${summary.healthy}/${summary.totalTasks} healthy · stale=${summary.stale} · dead=${summary.dead} · missing=${summary.missing}`,
];
if (summary.worstStaleMs > 0) lines.push(`Worst stale: ${Math.round(summary.worstStaleMs / 1000)}s ago`);
// Q4=c: show full action menu hint
const actionHints: string[] = [];
if ((summary.dead > 0 || summary.missing > 0) && opts?.isForeground !== false) actionHints.push("R recovery");
if (summary.dead > 0 || summary.stale > 0) actionHints.push("K kill stale");
actionHints.push("D diagnostic export");
if (actionHints.length > 0) lines.push(`Actions: ${actionHints.join(" · ")}`);
if (summary.dead > 0 && opts?.isForeground === false) lines.push("(Async run: R/K disabled — use kill <pid> manually)");
return lines;
}
Update run-dashboard.ts:
- Thêm
"health"vào typePane. - Key
5→activePane = "health". - Switch case render
renderHealthPanevớiisForegroundtừselectedRun.async ? false : true. - Trong
handleInput: nếuactivePane === "health":R→ emit{action: "health-recovery", runId}(handler sẽ check foreground + ConfirmOverlay).K→ emit{action: "health-kill-stale", runId}(handler ConfirmOverlay if dead > 5).D→ emit{action: "health-diagnostic-export", runId}(handler check existing diag < 1min → confirm overwrite).
- Header help line update:
"1 agents 2 progress 3 mailbox 4 output 5 health • s/u/a/i actions • R/K/D health".
Tests: test/unit/health-pane.test.ts — 6 cases (no snapshot, all healthy → only D hint, dead foreground → R+K+D, dead async → only D + warning, mixed states, foreground false hint visible).
8.2.C Auto-recovery toast (0.5 dev-day) — Q4 simplified
Logic: RenderScheduler.tick callback (đã có) gọi summarizeHeartbeats; nếu dead > 0 hoặc missing > 0 lần đầu → fire toast qua notification-router (8.3.A) với severity "warning":
- Title:
"Run {runId} has {N} dead workers". - Body:
"Open dashboard → 5 health → R recovery / K kill stale / D diagnostic".
Throttle (D13): dedup id = recovery_dead_workers_${runId} — router dedup 5 phút/run/condition-type. Riêng recovery_missing_heartbeat có id khác để alert song song nếu cả hai cùng xảy ra.
Tests: test/integration/health-recovery.test.ts — simulate stale heartbeat, verify single toast emitted; emit lần 2 trong window → drop; emit lần 2 sau 5min → fire lại.
8.2.D Health action handlers (1.5 dev-day) — Q4=c full menu
Update src/extension/registration/commands.ts: handle 3 new actions từ dashboard:
// pseudo-code
if (selection.action === "health-recovery") {
const run = manifestCache.get(selection.runId);
if (run?.async) { ctx.ui.notify("Recovery only available for foreground runs.", "warning"); return; }
const confirmed = await openConfirmOverlay(ctx, { title: "Interrupt foreground run?", body: "Tasks will be marked failed. Y=interrupt, N=cancel.", dangerLevel: "high" });
if (!confirmed) return;
const r = await dispatchHealthRecovery(ctx, selection.runId);
ctx.ui.notify(r.message, r.ok ? "info" : "error");
}
if (selection.action === "health-kill-stale") {
const summary = summarizeHeartbeats(snapshotCache.get(selection.runId)!);
if (summary.dead + summary.stale > 5) {
const confirmed = await openConfirmOverlay(ctx, { title: `Kill ${summary.dead + summary.stale} stale workers?`, dangerLevel: "medium" });
if (!confirmed) return;
}
const r = await dispatchKillStaleWorkers(ctx, selection.runId);
ctx.ui.notify(r.message, r.ok ? "info" : "error");
}
if (selection.action === "health-diagnostic-export") {
// D14: check existing diag in last 1min
const diagDir = path.join(run.artifactsRoot, "diagnostic");
const recentDiag = listRecentDiagnostic(diagDir, 60_000);
if (recentDiag) {
const confirmed = await openConfirmOverlay(ctx, { title: "Recent diagnostic exists", body: `File ${recentDiag} created < 1min ago. Overwrite?`, defaultAction: "cancel" });
if (!confirmed) return;
}
const r = await dispatchDiagnosticExport(ctx, selection.runId);
ctx.ui.notify(`Diagnostic exported to ${r.data}`, r.ok ? "info" : "error");
}
File mới: src/runtime/diagnostic-export.ts — collect manifest + tasks + recent events (limit 200) + heartbeat summary + agent status snapshot; redact secrets từ env/config (block list: *token*, *key*, *password*, *secret*); write JSON vào <crewRoot>/artifacts/{runId}/diagnostic-{ISO-timestamp}.json.
Skeleton:
// diagnostic-export.ts
export interface DiagnosticReport {
runId: string;
exportedAt: string;
manifest: TeamRunManifest;
tasks: TeamTaskState[];
recentEvents: TeamEvent[];
heartbeat: HeartbeatSummary;
agents: { taskId: string; status: AgentStatus }[];
envRedacted: Record<string, string>; // env vars with secrets masked as "***"
}
export async function exportDiagnostic(ctx: ExtensionContext, runId: string): Promise<{ path: string; report: DiagnosticReport }> { /* ... */ }
function redactSecrets(obj: unknown): unknown { /* recursive replace values where key matches block list */ }
Tests:
test/unit/diagnostic-export.test.ts— 5 cases (basic export, secret redaction, missing run errors, file path generation, JSON validity).- Smoke: export → open file → verify đầy đủ field + 0 secrets.
Phase 8.3 — Smart Notifications
8.3.A Notification router (1 dev-day)
File mới: src/extension/notification-router.ts
export type Severity = "info" | "warning" | "error" | "critical";
export interface NotificationDescriptor {
id?: string; // dedup key; nếu cùng id trong window → drop
severity: Severity;
source: string; // "run-completed" | "subagent-stuck" | "health" | ...
runId?: string;
title: string;
body?: string;
timestamp?: number;
}
export interface NotificationRouterOptions {
dedupWindowMs?: number; // default 30000
batchWindowMs?: number; // default 0 (no batching by default)
quietHours?: string; // "22:00-07:00" local
severityFilter?: Severity[]; // default: ["warning", "error", "critical"]
sink?: (n: NotificationDescriptor) => void; // optional file/stream sink
}
export class NotificationRouter {
constructor(private opts: NotificationRouterOptions = {}, private deliver: (n: NotificationDescriptor) => void) {}
enqueue(n: NotificationDescriptor): void { /* dedup check, severity filter, quiet-hours skip, batch buffer, sink */ }
flush(): void { /* deliver batched */ }
dispose(): void { /* clear timers */ }
}
Wrap sendFollowUp: trong register.ts, thay 2 call sites sendFollowUp(...) thành notificationRouter.enqueue({...}). Router decides có deliver qua sendFollowUp hay không.
Tests: test/unit/notification-router.test.ts — 8 cases (dedup, severity filter, quiet hours mock clock, batch, sink invocation, dispose cleanup).
8.3.B Quiet-hours + batching config (0.75 dev-day) — Q3=b cross-day wrap
Update src/schema/config-schema.ts:
notifications: Type.Optional(Type.Object({
enabled: Type.Optional(Type.Boolean()),
severityFilter: Type.Optional(Type.Array(Type.Union([Type.Literal("info"), Type.Literal("warning"), Type.Literal("error"), Type.Literal("critical")]))),
dedupWindowMs: Type.Optional(Type.Integer({ minimum: 1000 })),
batchWindowMs: Type.Optional(Type.Integer({ minimum: 0 })),
quietHours: Type.Optional(Type.String({ pattern: "^\\d{2}:\\d{2}-\\d{2}:\\d{2}$" })),
sinkRetentionDays: Type.Optional(Type.Integer({ minimum: 1, maximum: 90 })), // Q2=c, default 7
})),
Update src/config/defaults.ts: sane defaults (severityFilter: ["warning","error","critical"], dedupWindowMs: 30_000, batchWindowMs: 0, sinkRetentionDays: 7).
Update src/config/config.ts: parse + merge giống các section khác.
Cross-day parser (Q3=b): trong notification-router.ts, helper isolated cho easy testing:
// notification-router.ts (excerpt)
export function parseHHMMRange(range: string): { startMin: number; endMin: number } {
const [s, e] = range.split("-").map((part) => {
const [hh, mm] = part.split(":").map(Number);
return hh * 60 + mm;
});
return { startMin: s, endMin: e };
}
export function isInQuietHours(range: string, now: Date = new Date()): boolean {
const { startMin, endMin } = parseHHMMRange(range);
const cur = now.getHours() * 60 + now.getMinutes();
if (startMin === endMin) return false; // empty range
// Q3=b: cross-day wrap when start > end
return startMin <= endMin
? (cur >= startMin && cur < endMin)
: (cur >= startMin || cur < endMin);
}
Tests: test/unit/notification-router.test.ts thêm 4 cases parser:
"09:00-17:00"ở 12:00 → quiet (true)."09:00-17:00"ở 22:00 → not quiet (false)."22:00-07:00"ở 23:30 → quiet (cross-day true)."22:00-07:00"ở 03:00 → quiet (cross-day true)."22:00-07:00"ở 12:00 → not quiet (false).- Edge:
"00:00-23:59"ở 12:00 → quiet (always-quiet within day). - Edge:
"00:00-00:00"→ always not quiet (empty range).
8.3.C Toast badge integration (0.75 dev-day)
Logic: NotificationRouter.deliver → ngoài sendFollowUp, cộng unreadCount++ trong widgetState.notificationCount. Reset khi user mở mailbox detail hoặc nhấn H (Hush — dismiss-all notifications visible badge).
Update crew-widget.ts: model render thêm 🔔${count} nếu count > 0. Để tránh emoji compatibility issue → fallback [!${count}] khi terminal không support emoji (detect qua process.env.TERM).
Update powerbar-publisher.ts: segment pi-crew-active text append 🔔${count} (hoặc fallback) khi active.
Tests: test/unit/widget-notification-badge.test.ts — 5 cases (no count, count=1, count>9, dismiss reset, terminal fallback).
8.3.D Notification JSONL sink (0.5 dev-day) — Q2=c
File mới: src/extension/notification-sink.ts
Logic: khi config telemetry.enabled !== false, NotificationRouter delivery cũng gọi sink.write(descriptor). Sink writes vào <crewRoot>/state/notifications/{YYYY-MM-DD}.jsonl (1 file/day, append-only).
Rotation: start-of-day check (lazy, khi write đầu tiên) → delete files cũ hơn notifications.sinkRetentionDays (default 7).
Skeleton:
// notification-sink.ts
export interface NotificationSink {
write(n: NotificationDescriptor): void;
dispose(): void;
}
export function createJsonlSink(crewRoot: string, retentionDays: number): NotificationSink {
const dir = path.join(crewRoot, "state", "notifications");
let lastRotateDate = "";
return {
write(n) {
const today = new Date().toISOString().slice(0, 10);
if (today !== lastRotateDate) {
rotateOldFiles(dir, retentionDays);
lastRotateDate = today;
}
fs.mkdirSync(dir, { recursive: true });
fs.appendFileSync(path.join(dir, `${today}.jsonl`), JSON.stringify({ ...n, timestamp: n.timestamp ?? Date.now() }) + "\n");
},
dispose() { /* no-op */ },
};
}
function rotateOldFiles(dir: string, retentionDays: number): void {
if (!fs.existsSync(dir)) return;
const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1000;
for (const file of fs.readdirSync(dir)) {
if (!file.endsWith(".jsonl")) continue;
const stat = fs.statSync(path.join(dir, file));
if (stat.mtimeMs < cutoff) fs.unlinkSync(path.join(dir, file));
}
}
Wire trong register.ts: instantiate sink khi telemetry.enabled !== false, pass vào NotificationRouter options. Dispose trong cleanupRuntime.
Tests: test/unit/notification-sink.test.ts — 5 cases (write basic, daily rotation, retention prune, no rotation cùng ngày, telemetry disabled = no-op).
Phase 8.4 — Wiring (0.75 dev-day)
Update src/extension/register.ts:
- Instantiate
NotificationRoutercùng cấp vớirunSnapshotCache; checkloadConfig.telemetry?.enabled !== falseđể decide có passJsonlSinkkhông. - Pass router vào
subagentManagercallback (line 64-86) thay vì gọi trực tiếpsendFollowUp. - Pass router vào
RenderSchedulercallback cho 8.2.C auto-recovery alert. - Pass
getRunSnapshotCache+notificationRoutervàocommands.tsdeps. - Dispose router + sink trong
cleanupRuntime.
Update src/extension/registration/commands.ts:
- Handle
selection.action === "mailbox-detail"→ mởMailboxDetailOverlay, dispatch action result, toast. - Handle
selection.action === "health-recovery" | "health-kill-stale" | "health-diagnostic-export"(Q4=c) — flow chi tiết 8.2.D. - Pass
getRunSnapshotCachecho overlay (cần để re-render sau action). - Pass
confirmOverlayFactoryđể các handler reuseConfirmOverlay.
Phase 8.5 — Tests + Validation (2 dev-day)
Unit (mới ~52 cases):
run-action-dispatcher.test.ts(7)confirm-overlay.test.ts(4)mailbox-detail-overlay.test.ts(7 — bao gồm 3 cases ackAll Shift+X)mailbox-compose-overlay.test.ts(8)mailbox-compose-preview.test.ts(6) — Q1agent-picker-overlay.test.ts(4)heartbeat-aggregator.test.ts(6)health-pane.test.ts(6) — Q4 expandeddiagnostic-export.test.ts(5) — Q4notification-router.test.ts(8 + 4 quiet-hours parser cases = 12) — Q3notification-sink.test.ts(5) — Q2widget-notification-badge.test.ts(5)
Integration (mới ~6 cases):
test/integration/mailbox-action-roundtrip.test.ts— open dashboard → ack → snapshot invalidate → count giảm.test/integration/mailbox-ackall-confirm.test.ts— ackAll trigger ConfirmOverlay → confirm → loop ack 10 messages.test/integration/notification-dedup.test.ts— emit cùng event 5 lần trong 30s → 1 toast.test/integration/notification-quiet-hours.test.ts— set quietHours"22:00-07:00", mock now=23:30 → 0 toast; mock now=12:00 → 1 toast.test/integration/notification-sink-rotation.test.ts— write 8 days → oldest file deleted on day 8.test/integration/health-recovery-foreground.test.ts— foreground run dead → R action → ConfirmOverlay → confirm → foreground-interrupt fired.test/integration/health-diagnostic-export.test.ts— D action → diagnostic file written với secrets redacted; emit lần 2 trong 1min → ConfirmOverlay overwrite.
Acceptance trước commit:
npm test≥ 351 unit (current 299 + 52), 35 integration (current 29 + 6); 0 fail. Verified current suite: 351 unit + 44 integration.npm run typecheckclean.- Manual smoke coverage (8 scenarios — mục 6) captured as automated smoke in
test/integration/phase8-smoke.test.ts.
3. Wave Organization (parallel-friendly) — Updated với Q1-Q6
Wave 1 (parallel, 2.5 days)
├─ 8.0 Foundation (dispatcher + keybinding-map + ConfirmOverlay)
├─ 8.3.A NotificationRouter primitive
└─ 8.2.A Heartbeat aggregator
Wave 2 (sequential, 5 days) — depends on Wave 1
├─ 8.1.A Mailbox detail overlay
├─ 8.1.B Ack action
├─ 8.1.C Nudge action
├─ 8.1.D Compose form (Q6 ESC discard + confirm-if-long)
├─ 8.1.E Compose preview pane (Q1)
└─ 8.1.F ackAll Shift+X destructive (Q5)
Wave 3 (parallel, 4 days) — depends on Wave 1
├─ 8.2.B Health pane
├─ 8.2.C Auto-recovery toast (throttled 5min D13)
├─ 8.2.D Health action handlers R/K/D (Q4) + diagnostic-export module
├─ 8.3.B Quiet-hours cross-day parser (Q3) + batching config
├─ 8.3.C Toast badge widget/powerbar
└─ 8.3.D JSONL sink + retention (Q2)
Wave 4 (sequential, 2.75 days)
├─ 8.4 Wire register.ts + commands.ts (router, sink, action handlers)
└─ 8.5 Tests + smoke validation (52 unit + 6 integration mới)
Total estimate: 14-18 dev-days (vs Phase 7 baseline 18 days). Effort tăng 3.35 day so với plan gốc 11-14d do Q1-Q6 chosen options enrich scope. Phase 8 vẫn smaller hơn Phase 7 vì chủ yếu UI overlay + event router, không động state machine.
4. Files Affected — Updated với Q1-Q6
New (24 files)
| Path | Purpose | Est LOC |
|---|---|---|
src/ui/run-action-dispatcher.ts |
Wrapper team-tool calls (7 dispatchers) | ~140 |
src/ui/keybinding-map.ts |
Key registry (mailbox/health/notification scopes) | ~70 |
src/ui/overlays/confirm-overlay.ts |
(Q5) Reusable confirm primitive | ~80 |
src/ui/overlays/mailbox-detail-overlay.ts |
2-col mailbox view + ackAll | ~250 |
src/ui/overlays/mailbox-compose-overlay.ts |
Compose form + ESC guard | ~210 |
src/ui/overlays/mailbox-compose-preview.ts |
(Q1) Markdown preview pane | ~120 |
src/ui/overlays/agent-picker-overlay.ts |
Agent selector | ~110 |
src/ui/heartbeat-aggregator.ts |
Heartbeat summary fn | ~70 |
src/ui/dashboard-panes/health-pane.ts |
Health pane renderer with action hints | ~80 |
src/extension/notification-router.ts |
Router + dedup + quiet-hours parser (Q3) | ~220 |
src/extension/notification-sink.ts |
(Q2) JSONL sink + retention rotation | ~100 |
src/runtime/diagnostic-export.ts |
(Q4) Diagnostic JSON exporter + secret redaction | ~140 |
test/unit/run-action-dispatcher.test.ts |
~140 | |
test/unit/confirm-overlay.test.ts |
~80 | |
test/unit/mailbox-detail-overlay.test.ts |
~180 | |
test/unit/mailbox-compose-overlay.test.ts |
~180 | |
test/unit/mailbox-compose-preview.test.ts |
(Q1) | ~120 |
test/unit/agent-picker-overlay.test.ts |
~80 | |
test/unit/heartbeat-aggregator.test.ts |
~120 | |
test/unit/health-pane.test.ts |
Q4 expanded scenarios | ~140 |
test/unit/diagnostic-export.test.ts |
(Q4) | ~110 |
test/unit/notification-router.test.ts |
+ 4 cross-day parser cases | ~260 |
test/unit/notification-sink.test.ts |
(Q2) | ~100 |
test/unit/widget-notification-badge.test.ts |
~80 | |
test/integration/mailbox-action-roundtrip.test.ts |
~120 | |
test/integration/mailbox-ackall-confirm.test.ts |
(Q5) | ~100 |
test/integration/notification-dedup.test.ts |
~90 | |
test/integration/notification-quiet-hours.test.ts |
(Q3) mock clock | ~110 |
test/integration/notification-sink-rotation.test.ts |
(Q2) | ~110 |
test/integration/health-recovery-foreground.test.ts |
(Q4) | ~120 |
test/integration/health-diagnostic-export.test.ts |
(Q4) | ~120 |
Modified (10 files)
| Path | Change |
|---|---|
src/ui/run-dashboard.ts |
Refactor handleInput dùng keybinding-map; thêm pane "health" key 5; help line; emit health-recovery/health-kill-stale/health-diagnostic-export actions (Q4) |
src/ui/dashboard-panes/mailbox-pane.ts |
Update help text gợi ý A/N/C/Enter/Shift+X (ackAll) |
src/ui/crew-widget.ts |
Render notification badge 🔔N (fallback [!N] cho terminal không support emoji) |
src/ui/powerbar-publisher.ts |
Append badge cho pi-crew-active segment |
src/extension/register.ts |
Instantiate NotificationRouter + JsonlSink (gated bởi telemetry); wrap sendFollowUp; pass vào RenderScheduler + commands deps |
src/extension/registration/commands.ts |
Handle mailbox-detail + 3 health actions (Q4); mở overlay; reuse ConfirmOverlay (Q5) |
src/extension/team-tool/api.ts |
(no change) — dispatchers reuse existing operations |
src/schema/config-schema.ts |
Thêm notifications section + sinkRetentionDays (Q2) |
src/config/{config.ts,defaults.ts} |
Parse + default cho notifications (severityFilter, dedupWindowMs, batchWindowMs, quietHours, sinkRetentionDays) |
package.json |
Bump version 0.1.33 → 0.1.34 |
Docs (chỉ update khi user yêu cầu, theo project rule)
docs/architecture.md— bổ sung mục "Operator Actions", "Notification Router", "Diagnostic Export".
5. Risk Assessment — Updated với Q1-Q6
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Overlay hijack stdin của Pi UI | Med | High | Reuse pattern LiveRunSidebar (đã hoạt động); test với pi-ui-compat.ts shim |
| Keybinding conflict với Pi global hotkeys | Low | Med | Uppercase A/N/C/P/H/X (mailbox), R/K/D (health) — context-scoped; lowercase Pi defaults không đụng |
| Notification spam khi nhiều run concurrent | Med | Low-Med | Dedup window 30s default; severity filter excludes "info"; quiet-hours wrap (Q3) |
| Quiet-hours cross-day parser bug | Low | Med | Q3=b: 7 unit test cases bao gồm cross-midnight; mock clock pattern |
| MailboxDetailOverlay re-render slow | Low | Low | Reuse signature pattern từ RunDashboard; cache lines |
| Race khi ack trong khi snapshot đang refresh | Low | Med | Dispatch awaits then invalidate cache; render scheduler debounce 75ms |
sendFollowUp swap break existing flow |
Low | High | Wrap không thay; router default-on chỉ khi notifications.enabled !== false; fallback gọi sendFollowUp raw nếu router throws |
| Config schema breaking change | Low | High | New section notifications purely optional; missing → defaults |
| (Q1) Compose preview pane re-render bottleneck (debounce miss) | Low | Low | Debounce 100ms; cache last rendered tokens; tokenizer < 1ms cho 5KB body |
| (Q1) Markdown tokenizer edge case (nested code in list) | Med | Low | Reuse pattern parser nếu có; 6 unit test edge cases; preview "best-effort" |
| (Q2) Sink disk full / write fail | Low | Low | appendFileSync swallow errors qua logInternalError; sink failure không crash router |
| (Q2) Retention prune deletes file đang được tail | Low | Low | Chỉ prune .jsonl cũ hơn cutoff; daily rotation đảm bảo file hôm nay không bị touch |
| (Q2) PII trong notification body leak vào sink | Med | Med | Sink reuse secret redactor từ diagnostic-export.ts (Q4); router tag PII fields nếu cần |
(Q4) R recovery accidentally interrupt healthy run |
Low | High | ConfirmOverlay với dangerLevel: "high" + default cancel; foreground-only check; clear "tasks marked failed" warning |
(Q4) K kill stale workers race với worker self-recovery |
Low | Med | Mark dead heartbeats first → emit event → giải phóng claims; worker tự detect token mismatch sẽ exit |
| (Q4) Diagnostic export ghi đè artifact dir đang dùng | Low | Med | D14: check existing diag < 1min → ConfirmOverlay overwrite; timestamp suffix unique |
| (Q4) Diagnostic secret redaction miss key pattern mới | Med | High | Block list: *token*, *key*, *password*, *secret*, *credential*, *auth*; review qua test fixture với 20 key patterns |
(Q5) ConfirmOverlay default Y accidentally confirms destructive |
Low | High | Default action = "cancel"; first focus là [N]; ESC = cancel; UI hint underlined N |
| (Q6) ESC discard confirm fatigue (user complain phải confirm mỗi ESC) | Low | Low | Threshold 50 ký tự (configurable nếu user feedback); short body → discard ngay |
| (Q6) Body multi-line Ctrl+Enter not detected on Windows | Med | Low | Test với pi-ui-compat.ts; fallback Alt+Enter if Ctrl+Enter fails detection |
6. Testing Strategy — Updated với Q1-Q6
Unit-level (Wave 1-3):
- Mock
handleTeamTool→ assert dispatcher returns đúng{ok, message}cho 7 dispatchers. - Render overlay với fixture snapshot → assert lines layout.
- Heartbeat aggregator: parameterized test với fixture timestamps (6 cases).
- Health pane: 6 cases bao phủ foreground/async/healthy/dead/stale variations (Q4).
- Notification router: mock clock (
globalThis.Date.nowoverride theo pattern Phase 7); 8 base cases + 4 cross-day parser (Q3). - Sink: rotation, retention, telemetry-disabled no-op (Q2).
- Diagnostic export: secret redaction với 20-key fixture; JSON schema validate (Q4).
- Confirm overlay: 4 cases verify default-cancel safety (Q5).
- Compose preview: 6 cases markdown render (Q1).
Integration (Wave 4) — 7 scenarios:
mailbox-action-roundtrip.test.ts: open dashboard → ack → snapshot invalidate → count giảm.mailbox-ackall-confirm.test.ts(Q5): Shift+X → ConfirmOverlay → confirm → loop ack 10 messages → allacknowledged.notification-dedup.test.ts: emit 5x cùngcrew.run.failedtrong 30s →sendFollowUpmock called once.notification-quiet-hours.test.ts(Q3): quiet"22:00-07:00"mock now=23:30 → 0 toast; mock now=12:00 → 1 toast.notification-sink-rotation.test.ts(Q2): write 8 ngày fake mtime → oldest deleted on day 8.health-recovery-foreground.test.ts(Q4): foreground run với 2 dead workers → R action → ConfirmOverlay confirm →foreground-interruptAPI called → tasks marked failed.health-diagnostic-export.test.ts(Q4): D action → file written với 0 secrets in JSON; emit lần 2 trong 1min → ConfirmOverlay overwrite.
Smoke manual (8 scenarios):
- Chạy
team run1 task foreground → mở/team-dashboard→ key3mailbox → Enter → keyNnudge → verifyevents.jsonlcóagent.nudged. - Chạy 2 run, đợi xong → verify nhận 1-2 toast (dedup).
- Set
notifications.quietHours = "00:00-23:59"→ verify 0 toast. - (Q1) Compose form, gõ markdown body với bold/list/code → key
Ppreview → verify render đúng. - (Q5) ackAll trên run với 5 unread → ConfirmOverlay xuất hiện → N cancel → 0 message acked.
- (Q4) Foreground run với worker stuck > 1min → key
5health → keyR→ ConfirmOverlay → Y → tasks failed; keyD→ diagnostic file viết. - (Q2) Disable telemetry → run + emit notification → verify
<crewRoot>/state/notifications/không tồn tại. - (Q6) Compose body 100 chars → ESC → ConfirmOverlay xuất hiện → N → vẫn editing.
Performance budget:
- Mailbox overlay first render < 50ms với 100 messages.
- Compose preview render < 30ms với 5KB markdown body (Q1).
- Notification router enqueue overhead < 1ms.
- Sink write < 5ms (single append) (Q2).
- Health pane render < 5ms cho 50 tasks.
- Diagnostic export complete < 200ms cho run với 50 tasks + 200 events (Q4).
7. Open Questions — RESOLVED (Path X chosen)
| Q | Câu hỏi | Lựa chọn | Implementation reference |
|---|---|---|---|
| Q1 | Compose form có cần preview render trước khi submit? | (b) Có preview pane | 8.1.E mailbox-compose-preview.ts, key P toggle, render markdown read-only (bold/italic/code/list/heading), debounce 100ms. D15. +0.75d |
| Q2 | Notification sink default ghi <crewRoot>/state/notifications.jsonl? |
(c) Sink khi telemetry.enabled !== false |
8.3.D notification-sink.ts, JSONL <crewRoot>/state/notifications/{YYYY-MM-DD}.jsonl, rotate sinkRetentionDays default 7. D4. +0.5d |
| Q3 | Quiet-hours cross-day wrap? | (b) Wrap parser | 8.3.B parseHHMMRange + isInQuietHours cross-day logic; 7 unit cases bao gồm "22:00-07:00". D5. +0.25d |
| Q4 | Health pane recovery action button inline? | (c) Full action menu R/K/D | 8.2.D R recovery (foreground-only), K kill stale workers, D diagnostic export với secret redaction; 3 confirm flows. D9, D14. +1.5d |
| Q5 | Ack/nudge confirm cho destructive? | (c) Confirm chỉ destructive (ackAll/recovery/diag-overwrite) | 8.0 ConfirmOverlay reusable primitive; 8.1.F ackAll Shift+X with confirm. D12. +0.25d |
| Q6 | Compose form persist draft khi ESC? | (a) ESC discard + confirm-if-long | 8.1.D ESC behavior: body ≤ 50 chars → discard; > 50 → ConfirmOverlay. Defer draft persistence Phase 9. D11. +0.1d |
Tổng effort delta từ Q1-Q6: ~3.35 dev-day → bump từ 11-14d → 14-18d.
Mục tiêu Q1-Q6 đã đạt: mọi quyết định scope-shaping đã chốt; team có thể start Wave 1 mà không bị blocked clarification giữa chừng.
8. Dependencies & Sequencing
Phase 7 (DONE) ─────► Phase 8.0 Foundation
│
┌──────────┼──────────┐
▼ ▼ ▼
8.1 Mailbox 8.2 Health 8.3 Notif
│ │ │
└──────────┼──────────┘
▼
8.4 Wiring
│
▼
8.5 Tests
Hard prerequisites Phase 7: ✅ RunSnapshotCache, RenderScheduler, dashboard panes — đã có.
9. Effort Summary — Updated với Q1-Q6
| Wave | Items | Dev-days | Parallelizable |
|---|---|---|---|
| 1 | 8.0 (Foundation + ConfirmOverlay Q5) + 8.3.A (Router) + 8.2.A (Heartbeat) | 2.5 | Yes (3 streams) |
| 2 | 8.1.A → B → C → D (Q6) → E (Q1) → F (Q5) | 5 | No (sequential UX, share overlay state) |
| 3 | 8.2.B + 8.2.C + 8.2.D (Q4 R/K/D) + 8.3.B (Q3) + 8.3.C + 8.3.D (Q2) | 4 | Yes (5 streams) |
| 4 | 8.4 (Wire) + 8.5 (Tests) | 2.75 | No |
| Total | 17 sub-phases | 14-18 | — |
So với plan gốc: +3.35 dev-day, +5 sub-phases, +8 file mới, +27 unit case, +4 integration case.
10. Acceptance Checklist (Wave 4 exit criteria) — Updated
- Tất cả checkbox 8.0 → 8.5 ở mục 0 (Implementation Status) tick
[x]. npm test≥ 351 unit (current 299 + 52 mới), ≥ 35 integration (current 29 + 6 mới), 0 fail. Verified: 351 unit + 44 integration pass.npm run typecheckclean.- Manual smoke 8 scenarios pass (mục 6). Verified via automated smoke suite
test/integration/phase8-smoke.test.ts. - Performance budget thỏa: mailbox overlay <50ms, compose preview <30ms, sink write <5ms, diagnostic export <200ms. Verified microbench: mailbox 6.39ms, preview 1.61ms, health 0.29ms, sink 2.12ms, diagnostic 4.83ms.
- No regression: 299 unit + 29 integration cũ vẫn pass.
- Config breaking? No. Schema additive (
notificationssection optional). - Bump
package.jsonversion0.1.33→0.1.34. - Q1-Q6 implementations match decisions table mục 7.
- Secret redaction (Q4): test fixture with recursive key/value redaction pass; audit log avoids known token fixture.
11. Out of Scope (defer Phase 9+)
Phase 9 plan đã được tạo riêng tại
research-phase9-observability-reliability-plan.md.
- Telemetry/Metrics backbone (Counter/Gauge/Histogram + correlation ID + OTLP/Prometheus export) → Phase 9 (Theme B) per Path X plan.
- Run reliability — auto-retry executor + crash recovery + deadletter + heartbeat watcher → Phase 9 (Theme C).
- Cross-run mailbox routing (operator-broadcast) — Phase 10+.
- Mailbox threading / reply chains — Phase 10+.
- Compose draft persistence (Q6 b/c options) — defer Phase 9 nếu user feedback than.
- Multi-host run aggregation — Phase 10+.
- Slack/Discord webhook sink (router supports it via custom sink, but no built-in adapter) — Phase 10+.
- Markdown preview với images/links rendered (Q1 D15 skip) — Phase 10+.
Path X roadmap summary
| Phase | Theme | Effort | Plan file |
|---|---|---|---|
| 6 | .crew/ migration + autonomous policy |
~12d | refactor-tasks-phase6.md (DONE) |
| 7 | UI Optimization | ~18d | research-ui-optimization-plan.md (DONE) |
| 8 | Operator Experience (Theme A) | 14-18d | THIS FILE — ✅ DONE (verified 351 unit + 44 integration pass, version 0.1.34) |
| 9 | Observability + Reliability (B+C) | 19.5-22.5d | research-phase9-observability-reliability-plan.md (post-review updated 2026-04-29) |
| 10+ | TBD: Perf baseline, distributed | — | Future |
12. Implementation Kickoff Checklist (Pre-Wave 1)
Trước khi bắt đầu Wave 1, verify:
- Phase 7 đã commit (snapshot cache + render scheduler + 4 panes). Included in
phase-8-operator-experiencerelease commit. npm testbaseline pass (299 unit + 29 integration). Verified current suite: 351 unit + 44 integration pass.npm run typecheckclean.- Q1-Q6 đã chốt (đã làm — table mục 7).
- Branch mới
phase-8-operator-experiencetừ main. - Read once:
src/extension/team-tool/api.ts(đã có ack-message/send-message/nudge-agent operations — KHÔNG cần modify). - Read once:
src/ui/run-dashboard.ts:handleInputđể hiểu pattern key dispatch hiện tại. - Read once:
src/ui/live-run-sidebar.tsđể có template cho overlay implementation.
Sẵn sàng triển khai Phase 8 Path X.