Files
pi-config/extensions/pi-crew/docs/research-phase8-operator-experience-plan.md

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 A trê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 P toggle, 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 — R recovery (foreground only), K kill stale workers, D diagnostic 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ọi handleTeamTool với runId + operation, normalize result thành { ok, message, data }.
  • src/ui/keybinding-map.ts — central registry mapping data (raw stdin) → action name; export KEY_RESERVED để overlay con check conflict.
  • src/ui/overlays/confirm-overlay.ts(Q5) reusable confirm primitive, anchor center, auto-focus N (safe default), Y/Enter=confirm, N/ESC=cancel. ~80 LOC.

Sửa:

  • src/ui/run-dashboard.ts — refactor handleInput dùng keybinding-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, mock handleTeamTool).
  • 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 — class MailboxDetailOverlay implement 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 — khi activePane === "mailbox" và user nhấn Enter, return {action: "mailbox-detail"} thay vì close.
  • src/extension/registration/commands.ts — handle selection.action === "mailbox-detail" → mở MailboxDetailOverlay qua ctx.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ở ConfirmOverlay vớ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 P toggle 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 YdispatchMailboxAckAll(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 type Pane.
  • Key 5activePane = "health".
  • Switch case render renderHealthPane với isForeground từ selectedRun.async ? false : true.
  • Trong handleInput: nếu activePane === "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 NotificationRouter cùng cấp với runSnapshotCache; check loadConfig.telemetry?.enabled !== false để decide có pass JsonlSink không.
  • Pass router vào subagentManager callback (line 64-86) thay vì gọi trực tiếp sendFollowUp.
  • Pass router vào RenderScheduler callback cho 8.2.C auto-recovery alert.
  • Pass getRunSnapshotCache + notificationRouter vào commands.ts deps.
  • 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 getRunSnapshotCache cho overlay (cần để re-render sau action).
  • Pass confirmOverlayFactory để các handler reuse ConfirmOverlay.

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) — Q1
  • agent-picker-overlay.test.ts (4)
  • heartbeat-aggregator.test.ts (6)
  • health-pane.test.ts (6) — Q4 expanded
  • diagnostic-export.test.ts (5) — Q4
  • notification-router.test.ts (8 + 4 quiet-hours parser cases = 12) — Q3
  • notification-sink.test.ts (5) — Q2
  • widget-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 typecheck clean.
  • 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.330.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.now override 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 → all acknowledged.
  • notification-dedup.test.ts: emit 5x cùng crew.run.failed trong 30s → sendFollowUp mock 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-interrupt API 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):

  1. Chạy team run 1 task foreground → mở /team-dashboard → key 3 mailbox → Enter → key N nudge → verify events.jsonlagent.nudged.
  2. Chạy 2 run, đợi xong → verify nhận 1-2 toast (dedup).
  3. Set notifications.quietHours = "00:00-23:59" → verify 0 toast.
  4. (Q1) Compose form, gõ markdown body với bold/list/code → key P preview → verify render đúng.
  5. (Q5) ackAll trên run với 5 unread → ConfirmOverlay xuất hiện → N cancel → 0 message acked.
  6. (Q4) Foreground run với worker stuck > 1min → key 5 health → key R → ConfirmOverlay → Y → tasks failed; key D → diagnostic file viết.
  7. (Q2) Disable telemetry → run + emit notification → verify <crewRoot>/state/notifications/ không tồn tại.
  8. (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 test351 unit (current 299 + 52 mới), ≥ 35 integration (current 29 + 6 mới), 0 fail. Verified: 351 unit + 44 integration pass.
  • npm run typecheck clean.
  • 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 (notifications section optional).
  • Bump package.json version 0.1.330.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-experience release commit.
  • npm test baseline pass (299 unit + 29 integration). Verified current suite: 351 unit + 44 integration pass.
  • npm run typecheck clean.
  • Q1-Q6 đã chốt (đã làm — table mục 7).
  • Branch mới phase-8-operator-experience từ 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.