pi-crew 0.1.44 → 0.1.45

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.
Files changed (142) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/agents/analyst.md +11 -11
  3. package/agents/critic.md +11 -11
  4. package/agents/executor.md +11 -11
  5. package/agents/explorer.md +11 -11
  6. package/agents/planner.md +11 -11
  7. package/agents/reviewer.md +11 -11
  8. package/agents/security-reviewer.md +11 -11
  9. package/agents/test-engineer.md +11 -11
  10. package/agents/verifier.md +11 -11
  11. package/agents/writer.md +11 -11
  12. package/docs/refactor-tasks-phase3.md +394 -394
  13. package/docs/refactor-tasks-phase4.md +564 -564
  14. package/docs/refactor-tasks-phase5.md +402 -402
  15. package/docs/refactor-tasks-phase6.md +662 -662
  16. package/docs/research-extension-examples.md +297 -297
  17. package/docs/research-extension-system.md +324 -324
  18. package/docs/research-optimization-plan.md +548 -548
  19. package/docs/research-phase10-distillation.md +198 -198
  20. package/docs/research-phase11-distillation.md +201 -201
  21. package/docs/research-pi-coding-agent.md +357 -357
  22. package/docs/research-source-pi-crew-reference.md +174 -174
  23. package/docs/runtime-flow.md +148 -148
  24. package/docs/source-runtime-refactor-map.md +83 -83
  25. package/index.ts +6 -6
  26. package/package.json +1 -1
  27. package/src/agents/agent-serializer.ts +34 -34
  28. package/src/extension/cross-extension-rpc.ts +82 -82
  29. package/src/extension/register.ts +8 -1
  30. package/src/extension/registration/commands.ts +18 -2
  31. package/src/extension/registration/compaction-guard.ts +125 -125
  32. package/src/extension/registration/subagent-tools.ts +148 -148
  33. package/src/extension/registration/team-tool.ts +26 -8
  34. package/src/extension/run-bundle-schema.ts +89 -89
  35. package/src/extension/run-maintenance.ts +43 -43
  36. package/src/extension/team-tool/cancel.ts +105 -102
  37. package/src/extension/team-tool/context.ts +1 -0
  38. package/src/extension/team-tool/handle-settings.ts +188 -188
  39. package/src/extension/team-tool/inspect.ts +41 -41
  40. package/src/extension/team-tool/lifecycle-actions.ts +79 -79
  41. package/src/extension/team-tool/plan.ts +19 -19
  42. package/src/extension/team-tool/respond.ts +83 -66
  43. package/src/extension/team-tool/run.ts +1 -0
  44. package/src/i18n.ts +184 -184
  45. package/src/observability/exporters/otlp-exporter.ts +77 -77
  46. package/src/prompt/prompt-runtime.ts +72 -72
  47. package/src/runtime/agent-control.ts +63 -63
  48. package/src/runtime/agent-memory.ts +72 -72
  49. package/src/runtime/agent-observability.ts +114 -114
  50. package/src/runtime/async-marker.ts +26 -26
  51. package/src/runtime/attention-events.ts +28 -28
  52. package/src/runtime/background-runner.ts +53 -53
  53. package/src/runtime/child-pi.ts +444 -444
  54. package/src/runtime/completion-guard.ts +190 -190
  55. package/src/runtime/crew-agent-records.ts +8 -0
  56. package/src/runtime/delivery-coordinator.ts +153 -142
  57. package/src/runtime/direct-run.ts +35 -35
  58. package/src/runtime/foreground-control.ts +82 -82
  59. package/src/runtime/green-contract.ts +46 -46
  60. package/src/runtime/group-join.ts +106 -106
  61. package/src/runtime/heartbeat-gradient.ts +28 -28
  62. package/src/runtime/heartbeat-watcher.ts +124 -124
  63. package/src/runtime/live-agent-control.ts +87 -87
  64. package/src/runtime/live-agent-manager.ts +85 -85
  65. package/src/runtime/live-control-realtime.ts +36 -36
  66. package/src/runtime/live-session-runtime.ts +305 -305
  67. package/src/runtime/overflow-recovery.ts +175 -156
  68. package/src/runtime/parallel-research.ts +44 -44
  69. package/src/runtime/pi-json-output.ts +111 -111
  70. package/src/runtime/policy-engine.ts +79 -79
  71. package/src/runtime/progress-event-coalescer.ts +43 -43
  72. package/src/runtime/recovery-recipes.ts +74 -74
  73. package/src/runtime/retry-executor.ts +64 -64
  74. package/src/runtime/role-permission.ts +39 -39
  75. package/src/runtime/session-resources.ts +25 -25
  76. package/src/runtime/session-snapshot.ts +59 -59
  77. package/src/runtime/session-usage.ts +79 -79
  78. package/src/runtime/sidechain-output.ts +29 -29
  79. package/src/runtime/stale-reconciler.ts +199 -179
  80. package/src/runtime/supervisor-contact.ts +59 -59
  81. package/src/runtime/task-display.ts +38 -38
  82. package/src/runtime/task-output-context.ts +127 -127
  83. package/src/runtime/task-runner/live-executor.ts +101 -101
  84. package/src/runtime/task-runner/progress.ts +119 -119
  85. package/src/runtime/task-runner/result-utils.ts +14 -14
  86. package/src/runtime/task-runner/state-helpers.ts +22 -22
  87. package/src/runtime/team-runner.ts +13 -4
  88. package/src/runtime/worker-heartbeat.ts +21 -21
  89. package/src/runtime/worker-startup.ts +57 -57
  90. package/src/state/state-store.ts +43 -0
  91. package/src/state/task-claims.ts +44 -44
  92. package/src/state/types.ts +2 -0
  93. package/src/state/usage.ts +29 -29
  94. package/src/subagents/async-entry.ts +1 -1
  95. package/src/subagents/index.ts +3 -3
  96. package/src/subagents/live/control.ts +1 -1
  97. package/src/subagents/live/manager.ts +1 -1
  98. package/src/subagents/live/realtime.ts +1 -1
  99. package/src/subagents/live/session-runtime.ts +1 -1
  100. package/src/subagents/manager.ts +1 -1
  101. package/src/subagents/spawn.ts +1 -1
  102. package/src/teams/team-serializer.ts +38 -38
  103. package/src/types/diff.d.ts +18 -18
  104. package/src/ui/crew-footer.ts +101 -101
  105. package/src/ui/crew-select-list.ts +111 -111
  106. package/src/ui/crew-widget.ts +5 -1
  107. package/src/ui/dashboard-panes/mailbox-pane.ts +2 -1
  108. package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
  109. package/src/ui/dynamic-border.ts +25 -25
  110. package/src/ui/layout-primitives.ts +106 -106
  111. package/src/ui/loaders.ts +158 -158
  112. package/src/ui/powerbar-publisher.ts +1 -1
  113. package/src/ui/render-diff.ts +119 -119
  114. package/src/ui/render-scheduler.ts +143 -143
  115. package/src/ui/run-snapshot-cache.ts +56 -37
  116. package/src/ui/snapshot-types.ts +5 -0
  117. package/src/ui/spinner.ts +17 -17
  118. package/src/ui/status-colors.ts +58 -58
  119. package/src/ui/syntax-highlight.ts +116 -116
  120. package/src/utils/atomic-write.ts +33 -33
  121. package/src/utils/completion-dedupe.ts +63 -63
  122. package/src/utils/frontmatter.ts +68 -68
  123. package/src/utils/git.ts +262 -262
  124. package/src/utils/ids.ts +12 -12
  125. package/src/utils/names.ts +27 -27
  126. package/src/utils/redaction.ts +44 -44
  127. package/src/utils/safe-paths.ts +47 -47
  128. package/src/utils/sleep.ts +32 -32
  129. package/src/workflows/validate-workflow.ts +40 -40
  130. package/src/worktree/branch-freshness.ts +45 -45
  131. package/teams/default.team.md +12 -12
  132. package/teams/fast-fix.team.md +11 -11
  133. package/teams/implementation.team.md +18 -18
  134. package/teams/parallel-research.team.md +14 -14
  135. package/teams/research.team.md +11 -11
  136. package/teams/review.team.md +12 -12
  137. package/workflows/default.workflow.md +29 -29
  138. package/workflows/fast-fix.workflow.md +22 -22
  139. package/workflows/implementation.workflow.md +38 -38
  140. package/workflows/parallel-research.workflow.md +46 -46
  141. package/workflows/research.workflow.md +22 -22
  142. package/workflows/review.workflow.md +30 -30
@@ -1,564 +1,564 @@
1
- # Phase 4 Refactor Plan — UI/Theme/Performance từ pi-mono coding-agent
2
-
3
- > Xuất xứ: review sâu `source/pi-mono/packages/coding-agent` + `source/pi-mono/packages/tui` (28/04/2026), so sánh với `pi-crew/src/ui/` hiện tại.
4
- > Mục tiêu: tăng hiệu năng render, dọn duplicate code, type-safe theme integration, port các UI component thiếu (diff/loader/visual-truncate/syntax highlight).
5
- > Phase 3 (#26–#37) đã hoàn tất, baseline: tsc 0 errors, 213 unit + 21 integration pass, commit `6f64c31`.
6
-
7
- ## Quy ước chung
8
- - Không phá vỡ public API (slash commands, tool actions, config schema). Mọi thay đổi nội bộ.
9
- - Sau mỗi task: `npx tsc --noEmit` + `npm run test:unit` (+ `test:integration` nếu liên quan render/layout).
10
- - Không thêm dependency runtime mới trừ khi task ghi rõ (chấp nhận `diff` cho Task #45 nếu chưa có).
11
- - Mỗi task = 1 commit độc lập có thể revert. Đặt tên test bám sát hành vi.
12
- - `theme` parameter đang là `unknown` — không được break `ctx.ui.custom((tui, theme, ...) => Component)` signature do pi-coding-agent dictate.
13
-
14
- ## Trạng thái cập nhật
15
- - [x] Task #38 — `utils/visual.ts` dedupe truncate/visibleWidth
16
- - [x] Task #39 — Render cache cho widget/sidebar
17
- - [x] Task #40 — File-coalescer apply vào readers UI
18
- - [x] Task #41 — Manifest cache với mtime invalidation
19
- - [x] Task #42 — Type-safe theme adapter
20
- - [x] Task #43 — Status palette helpers
21
- - [x] Task #44 — Refactor widgets sang pi-tui Container/Box/Text
22
- - [x] Task #45 — Port `renderDiff` (word-level intra-line)
23
- - [x] Task #46 — Port `BorderedLoader` + `CountdownTimer`
24
- - [x] Task #47 — Port `truncateToVisualLines` cho transcript
25
- - [x] Task #48 — Syntax highlight cho transcript JSONL
26
- - [x] Task #49 (optional) — Animated mascot easter egg
27
- ---
28
-
29
- ## Tier 1 — Performance (high ROI, low risk)
30
-
31
- Mục tiêu: 4 task, dedupe + cache + I/O coalescing. Risk thấp, không đổi API. Ước tính: 1–2 ngày.
32
-
33
- ### Task #38 — Dedupe truncate/visibleWidth → `src/utils/visual.ts`
34
- **Source**: `@mariozechner/pi-tui` (đã ship `visibleWidth`, `truncateToWidth`); pi-mono `components/visual-truncate.ts`
35
- **Đích**: `pi-crew/src/utils/visual.ts`
36
-
37
- **Lý do**: 4 file UI (`run-dashboard.ts`, `crew-widget.ts`, `live-run-sidebar.ts`, `transcript-viewer.ts`) mỗi file có bản copy của:
38
- - `ANSI_PATTERN = /\u001b\[[0-?]*[ -/]*[@-~]/g`
39
- - `visibleWidth(value)` / `visibleLength(value)`
40
- - `truncate(value, width)` (logic không hoàn toàn nhất quán giữa các bản)
41
- - `pad(value, width)` / `padVisible`
42
-
43
- → Lặp lại ~80 dòng × 4 file. Dễ xảy ra drift bug.
44
-
45
- **API export**:
46
- ```typescript
47
- export const ANSI_PATTERN: RegExp;
48
- export function visibleWidth(value: string): number;
49
- export function truncate(value: string, width: number, ellipsis?: string): string;
50
- export function pad(value: string, width: number): string;
51
- export function wrapHard(value: string, width: number): string[];
52
- export function boxLine(text: string, innerWidth: number): string; // "│ {pad/truncate} │"
53
- ```
54
-
55
- **Tích hợp**:
56
- - Re-export `visibleWidth` + `truncateToWidth` từ `@mariozechner/pi-tui` nếu có (kiểm tra `tui/utils.ts`).
57
- - 4 file UI thay `import { ... }` từ local helper → `from "../utils/visual.ts"`.
58
- - Xoá local helpers đã chuyển.
59
-
60
- **Acceptance**:
61
- - File mới + xoá ~80 LOC × 4 file (~320 LOC giảm).
62
- - Unit test `test/unit/visual.test.ts`: 6 case
63
- - `visibleWidth("\u001b[31mhello\u001b[0m")` = 5
64
- - `truncate("hello world", 5)` = "hell…"
65
- - `truncate(value, 0)` = ""
66
- - `truncate(value, 1)` = "…"
67
- - `pad("ab", 5)` = "ab "
68
- - `wrapHard("abcdefgh", 3)` = ["abc","def","gh"]
69
- - Snapshot test (optional): render `crew-widget` trước/sau giống bit-by-bit.
70
-
71
- **Risk**: Thấp. Behavior tương đương, chỉ tách module.
72
-
73
- **Verification**: `npx tsc --noEmit` + `npm run test:unit -- --grep visual` + `npm run test:unit -- --grep widget` (smoke).
74
-
75
- ---
76
-
77
- ### Task #39 — Render cache cho widget/sidebar (cachedWidth + version)
78
- **Source pattern**: `pi-mono/packages/coding-agent/src/modes/interactive/components/armin.ts` (cachedWidth + cachedVersion + invalidate)
79
- **Đích**: `crew-widget.ts`, `live-run-sidebar.ts`, `run-dashboard.ts`
80
-
81
- **Lý do**: Mỗi tick (`widgetDefaultFrameMs`, `dashboardLiveRefreshMs` = 100ms) toàn bộ box được rebuild dù dữ liệu chưa đổi và terminal width chưa đổi. Khi data nhiều agent (>10), render cost không trivial.
82
-
83
- **API pattern (per component)**:
84
- ```typescript
85
- class CrewWidgetComponent {
86
- private cachedWidth = 0;
87
- private cachedVersion = -1;
88
- private currentVersion = 0;
89
- private cachedLines: string[] = [];
90
-
91
- invalidate(): void {
92
- this.cachedWidth = 0; // forces rerender on next render() call
93
- }
94
-
95
- private dataSignature(): number {
96
- // Hash from runs.length + agents counts + max updatedAt + statuses
97
- // Bump currentVersion when signature differs from last computed
98
- }
99
-
100
- render(width: number): string[] {
101
- const sig = this.dataSignature();
102
- if (width === this.cachedWidth && this.cachedVersion === sig) return this.cachedLines;
103
- // ... build lines ...
104
- this.cachedWidth = width;
105
- this.cachedVersion = sig;
106
- return this.cachedLines;
107
- }
108
- }
109
- ```
110
-
111
- **Tích hợp**:
112
- - `CrewWidgetComponent.render()`: dataSignature từ `frame % spinnerLength` + run/agent hash.
113
- - Lưu ý spinner thay đổi mỗi tick → vẫn rerender header chứa spinner. Tách `staticBody` (cached) khỏi `spinnerLine` (live).
114
- - `LiveRunSidebar.render()`: dataSignature từ manifest.updatedAt + agents.length + tasks.length + active counts.
115
- - `RunDashboard.render()`: dataSignature từ runs.length + selected index + showFullProgress flag.
116
-
117
- **Acceptance**:
118
- - Unit test `test/unit/render-cache.test.ts`:
119
- - `render(80)` 2 lần liên tiếp với data không đổi → tham chiếu mảng giống nhau (re-use cached).
120
- - `render(80)` sau khi `invalidate()` → mảng mới.
121
- - `render(120)` sau `render(80)` → mảng mới (width đổi).
122
- - Manifest mtime đổi → signature đổi → mảng mới.
123
- - Microbenchmark (`scripts/bench-render.ts` mới):
124
- - Trước: `LiveRunSidebar.render(80) × 1000` ≥ 150ms
125
- - Sau: `≤ 50ms` (cache hit ratio > 90%)
126
-
127
- **Risk**: Trung bình. Nếu dataSignature không bắt được mọi mutation → stale UI. Mitigation: include `Date.now() / 1000 | 0` trong sig cho live components để rerender 1Hz tối thiểu.
128
-
129
- **Verification**: `npx tsc --noEmit` + `npm run test:unit` + bench.
130
-
131
- ---
132
-
133
- ### Task #40 — File coalescer apply vào readers UI
134
- **Source pattern**: `pi-crew/src/utils/file-coalescer.ts` (đã có từ Phase 2)
135
- **Đích**: `crew-widget.ts`, `live-run-sidebar.ts`, `run-dashboard.ts`, `powerbar-publisher.ts`
136
-
137
- **Lý do**: Mỗi tick render gọi:
138
- - `readCrewAgents(manifest)` → `fs.readFileSync(agents.json)` parse JSON
139
- - `readTasks(tasksPath)` → `fs.readFileSync(tasks.json)` parse JSON
140
-
141
- Khi 4 widget cùng tick (widget + sidebar + powerbar + dashboard nếu mở) → cùng file đọc 4 lần trong < 10ms.
142
-
143
- **Tích hợp**:
144
- - Bọc `readCrewAgents` + `readTasks` qua `coalesceReads(filePath, ttlMs=200)` cache.
145
- - Tránh stale: invalidate khi chính pi-crew write (set marker timestamp).
146
- - Pattern:
147
- ```typescript
148
- // crew-agent-records.ts
149
- import { coalesceReads } from "../utils/file-coalescer.ts";
150
- const COALESCE_TTL = 200;
151
- export function readCrewAgents(manifest: TeamRunManifest): CrewAgentRecord[] {
152
- return coalesceReads(manifest.agentsPath, COALESCE_TTL, () => parseAgentsFile(manifest.agentsPath));
153
- }
154
- ```
155
-
156
- **Acceptance**:
157
- - Unit test `test/unit/agents-coalesce.test.ts`:
158
- - Spy `fs.readFileSync` → 5 calls trong 100ms cho cùng path → chỉ đọc 1 lần.
159
- - Sau TTL → đọc lại.
160
- - Integration test: tick widget 10 lần trong 500ms → đọc agents.json tối đa 3 lần.
161
-
162
- **Risk**: Thấp. TTL ngắn (200ms) đảm bảo data fresh.
163
-
164
- **Verification**: `npm run test:unit -- --grep coalesce`.
165
-
166
- ---
167
-
168
- ### Task #41 — Manifest cache với mtime invalidation
169
- **Source pattern**: `pi-mono/packages/coding-agent/src/core/footer-data-provider.ts` (cached branch + watch + debounce 500ms)
170
- **Đích**: `pi-crew/src/runtime/manifest-cache.ts` (mới)
171
-
172
- **Lý do**: `loadRunManifestById` đọc `manifest.json` + parse. `LiveRunSidebar` gọi mỗi tick (10Hz). Tương tự `listRecentRuns` scan cả thư mục `runs/`.
173
-
174
- **API export**:
175
- ```typescript
176
- export interface ManifestCache {
177
- get(runId: string): TeamRunManifest | undefined;
178
- list(limit: number): TeamRunManifest[];
179
- invalidate(runId?: string): void;
180
- dispose(): void;
181
- }
182
- export function createManifestCache(cwd: string, options?: { debounceMs?: number; watch?: boolean }): ManifestCache;
183
- ```
184
-
185
- **Implementation**:
186
- - Cache Map<runId, { manifest, mtimeMs }>.
187
- - `get(runId)`: stat manifest path; nếu mtime khớp cache → return cached.
188
- - `list(limit)`: scan dir, return top N theo mtime; cache toàn bộ list 500ms.
189
- - Watcher (optional): `watchWithErrorHandler(runsDir)` + debounce 500ms → invalidate.
190
-
191
- **Tích hợp**:
192
- - `register.ts` tạo 1 instance ManifestCache khi `session_start`, dispose ở `session_shutdown`.
193
- - `LiveRunSidebar`, `RunDashboard`, `crew-widget`, `powerbar-publisher` nhận cache (qua context closure).
194
-
195
- **Acceptance**:
196
- - Unit test:
197
- - 5 calls `get(runId)` trong 100ms với mtime không đổi → 1 lần stat + 1 lần read.
198
- - Sau write manifest (mtime đổi) → cache invalidate, đọc lại.
199
- - `list(10)` cache 500ms.
200
- - `dispose()` close watchers.
201
- - Integration test: simulate 1Hz manifest update + 10Hz render → render dùng cached value, không đọc lại trừ khi manifest thực sự đổi.
202
-
203
- **Risk**: Trung bình. Watch on Windows có quirks (đã giảm bằng Phase 3 fs-watch wrapper).
204
-
205
- **Verification**: `npm run test:unit -- --grep manifest-cache` + `npm run test:integration`.
206
-
207
- ---
208
-
209
- ## Tier 2 — Theme Integration (clean API, type-safe)
210
-
211
- Mục tiêu: 3 task, type-safe theme + reuse pi-tui layout primitives. Risk trung bình. Ước tính: 1–2 ngày.
212
-
213
- ### Task #42 — Type-safe theme adapter `src/ui/theme-adapter.ts`
214
- **Source pattern**: `pi-mono/packages/coding-agent/src/modes/interactive/theme/theme.ts` (Theme class với fg/bg/bold/italic)
215
- **Đích**: `pi-crew/src/ui/theme-adapter.ts`
216
-
217
- **Lý do**: Hiện tại 5 file UI cast `theme as unknown as { fg?: ... }`. IDE không suggest color names, dễ typo (`accenT` không lỗi compile).
218
-
219
- **API export**:
220
- ```typescript
221
- export type CrewThemeColor =
222
- | "accent" | "border" | "borderAccent" | "borderMuted"
223
- | "success" | "error" | "warning"
224
- | "muted" | "dim" | "text"
225
- | "toolDiffAdded" | "toolDiffRemoved" | "toolDiffContext"
226
- | "syntaxKeyword" | "syntaxString" | "syntaxNumber" | "syntaxComment" | "syntaxFunction" | "syntaxVariable" | "syntaxType";
227
-
228
- export type CrewThemeBg = "selectedBg" | "userMessageBg" | "toolPendingBg" | "toolSuccessBg" | "toolErrorBg";
229
-
230
- export interface CrewTheme {
231
- fg(color: CrewThemeColor, text: string): string;
232
- bg?(color: CrewThemeBg, text: string): string;
233
- bold(text: string): string;
234
- italic?(text: string): string;
235
- underline?(text: string): string;
236
- inverse?(text: string): string;
237
- }
238
-
239
- export function asCrewTheme(raw: unknown): CrewTheme;
240
- ```
241
-
242
- **Implementation**:
243
- - `asCrewTheme`: validate raw có method `fg`/`bold`. Nếu thiếu → fallback no-op `(c, t) => t`.
244
- - Sub-set của pi-coding-agent Theme class — không trùng namespace `CrewThemeColor` nhưng align values.
245
-
246
- **Tích hợp**:
247
- - `crew-widget.ts`, `live-run-sidebar.ts`, `run-dashboard.ts`, `transcript-viewer.ts`:
248
- - Replace `theme.fg?.bind(theme) ?? ((_color, text) => text)` bằng `const t = asCrewTheme(rawTheme); t.fg("accent", x)`.
249
- - Param signature: `(theme: unknown)` đổi thành `(theme: CrewTheme | unknown)`.
250
-
251
- **Acceptance**:
252
- - Unit test `test/unit/theme-adapter.test.ts`:
253
- - `asCrewTheme(undefined)` → no-op fallback.
254
- - `asCrewTheme({})` → no-op.
255
- - `asCrewTheme({ fg: ..., bold: ... })` → uses provided methods.
256
- - Type test (compile-only): `t.fg("nonExistent", "x")` produces TS error.
257
- - Lint pass; tsc 0 errors sau khi thay 5 file.
258
-
259
- **Risk**: Thấp. Fallback an toàn cho host không cung cấp đủ method.
260
-
261
- **Verification**: `npx tsc --noEmit` + `npm run test:unit -- --grep theme-adapter`.
262
-
263
- ---
264
-
265
- ### Task #43 — Status palette helpers `src/ui/status-colors.ts`
266
- **Source pattern**: `pi-mono` highlight pattern + pi-crew current ad-hoc switch-case
267
- **Đích**: `pi-crew/src/ui/status-colors.ts`
268
-
269
- **Lý do**: 5 file (`run-dashboard:65-72`, `crew-widget:89-95`, `live-run-sidebar:35`, `transcript-viewer`, `powerbar-publisher`) mỗi nơi có `switch(status){...}` mapping → màu/icon. Hiện không nhất quán (vd `crew-widget` ưu tiên `runningGlyph`, `run-dashboard` không).
270
-
271
- **API export**:
272
- ```typescript
273
- export type RunStatus = "queued" | "running" | "completed" | "failed" | "cancelled" | "blocked" | "stale" | "stopped" | (string & {});
274
-
275
- export function colorForStatus(status: RunStatus): CrewThemeColor;
276
- export function iconForStatus(status: RunStatus, options?: { runningGlyph?: string }): string;
277
- export function colorForActivity(activityState: string | undefined): CrewThemeColor;
278
- export function applyStatusColor(theme: CrewTheme, status: RunStatus, text: string): string;
279
- ```
280
-
281
- **Implementation**:
282
- - `colorForStatus`: `completed→success`, `failed|stale|error→error`, `cancelled|blocked|stopped→warning`, `running→accent`, `queued→muted`, default→dim.
283
- - `iconForStatus`: `completed→✓`, `failed/stale→✗`, `cancelled/stopped→■`, `running→runningGlyph || ▶`, `queued→◦`, `blocked→⏸`, default→·.
284
-
285
- **Tích hợp**:
286
- - 5 file UI thay switch-case bằng 1 dòng `colorForStatus(status)`.
287
- - `crew-widget.colorWidgetLine` regex map icon → dùng `iconForStatus` direct.
288
-
289
- **Acceptance**:
290
- - Unit test `test/unit/status-colors.test.ts`: 8 case theo từng status + edge case unknown status.
291
- - Snapshot widget/dashboard render không thay đổi (test regression).
292
-
293
- **Risk**: Thấp. Pure mapping function.
294
-
295
- **Verification**: `npm run test:unit -- --grep status-colors`.
296
-
297
- ---
298
-
299
- ### Task #44 — Refactor widgets dùng pi-tui Container/Box/Text
300
- **Source pattern**: `pi-mono/packages/tui/src/components/box.ts`, `text.ts`, plus `pi-mono/components/footer.ts` để tham chiếu cách compose.
301
- **Đích**: `live-run-sidebar.ts`, `run-dashboard.ts` (giảm độ phức tạp)
302
-
303
- **Lý do**: 2 file đang vẽ box bằng string concatenation `╭─╮│├┤╰╯` thủ công, mỗi line gọi `pad(truncate(...))`. Dễ vỡ khi terminal resize. pi-tui đã có `Container` + `Box` (rounded border tự động) + `DynamicBorder` từ pi-coding-agent.
304
-
305
- **Tích hợp**:
306
- - `LiveRunSidebar` → extend `Container`:
307
- ```typescript
308
- class LiveRunSidebar extends Container {
309
- constructor(input) {
310
- super();
311
- this.addChild(new DynamicBorder(c => theme.fg("border", c)));
312
- this.addChild(new Text(theme.bold("pi-crew live sidebar"), 1, 0));
313
- // ...
314
- }
315
- render(width: number): string[] { /* parent handles layout */ }
316
- }
317
- ```
318
- - `RunDashboard` tương tự — sections dùng `Spacer(1)` + `Text`.
319
- - Lưu ý: `ctx.ui.custom((tui, theme, keys, done) => Component)` — trả về `Container` instance vẫn OK vì `Container` implements `Component`.
320
-
321
- **Acceptance**:
322
- - LOC giảm ≥ 30% cho 2 file.
323
- - Visual snapshot test: render 80 + 120 width, content đồng nhất với baseline (allow whitespace diff).
324
- - handleInput logic giữ nguyên semantics (q/esc/j/k/p/r/s/u/a/i/d/e/o/v).
325
-
326
- **Risk**: Trung bình. Nếu Container layout không match cách hiện tại render padding thì box edge dịch chuyển. Mitigation: viết test snapshot trước khi refactor.
327
-
328
- **Verification**: `npx tsc --noEmit` + `npm run test:unit` + manual `team-dashboard` smoke.
329
-
330
- ---
331
-
332
- ## Tier 3 — UI Components mới
333
-
334
- Mục tiêu: 4 task, port các utility UI thiếu. Risk trung-cao. Ước tính: 2–3 ngày.
335
-
336
- ### Task #45 — Port `renderDiff` (word-level intra-line)
337
- **Source**: `pi-mono/packages/coding-agent/src/modes/interactive/components/diff.ts`
338
- **Đích**: `pi-crew/src/ui/render-diff.ts`
339
-
340
- **Lý do**: pi-crew có agents `code-modify`, `reviewer`, `verifier` thường tạo diff artifacts. Hiện tại transcript viewer + result viewer chỉ in raw text. `renderDiff` cho phép:
341
- - Removed line: red với inverse trên token thay đổi.
342
- - Added line: green với inverse trên token thay đổi.
343
- - Context: dim/gray.
344
-
345
- **Dependency check**: package `diff` (npm). Verify `pi-crew/package.json` chưa có → nếu thêm: `npm i diff @types/diff`.
346
-
347
- **API export**:
348
- ```typescript
349
- export interface RenderDiffOptions { filePath?: string }
350
- export function renderDiff(diffText: string, theme: CrewTheme, options?: RenderDiffOptions): string;
351
- ```
352
-
353
- **Implementation**: Copy `pi-mono/diff.ts` + thay `theme.inverse` import từ adapter; replace `theme.fg("toolDiff*", ...)` (đã thêm vào `CrewThemeColor` Task #42).
354
-
355
- **Tích hợp**:
356
- - `transcript-viewer.ts`: detect `[Tool: edit]` blocks chứa unified diff format → call `renderDiff`.
357
- - Slash command `/team-diff <runId> <taskId>` (optional Task #45.b): render artifact diff trực tiếp.
358
-
359
- **Acceptance**:
360
- - Unit test `test/unit/render-diff.test.ts`:
361
- - Single line modification → intra-line word diff with inverse.
362
- - Multi line block → no intra-line, just full-line color.
363
- - Context line preserved.
364
- - Empty diff → empty string.
365
- - Manual: render fixture `before.ts` vs `after.ts` diff trong overlay.
366
-
367
- **Risk**: Trung bình. Add deps `diff` (~30KB). Acceptable.
368
-
369
- **Verification**: `npx tsc --noEmit` + `npm run test:unit -- --grep render-diff`.
370
-
371
- ---
372
-
373
- ### Task #46 — Port `BorderedLoader` + `CountdownTimer`
374
- **Source**: `pi-mono/packages/coding-agent/src/modes/interactive/components/bordered-loader.ts` + `countdown-timer.ts`
375
- **Đích**: `pi-crew/src/ui/loaders.ts`
376
-
377
- **Lý do**:
378
- - `team run` async start có thể mất 2–5s spawn child. Hiện không feedback UI.
379
- - `team cancel runId=...` force-kill nhưng không hiển thị countdown trước SIGKILL.
380
- - `team-doctor` chạy 1–3s I/O không có loader.
381
-
382
- **API export**:
383
- ```typescript
384
- export interface CrewBorderedLoaderOptions {
385
- cancellable?: boolean;
386
- message: string;
387
- }
388
- export class CrewBorderedLoader extends Container {
389
- constructor(tui: TUI, theme: CrewTheme, options: CrewBorderedLoaderOptions);
390
- get signal(): AbortSignal;
391
- set onAbort(fn: (() => void) | undefined);
392
- dispose(): void;
393
- }
394
-
395
- export interface CountdownTimerOptions {
396
- timeoutMs: number;
397
- onTick: (seconds: number) => void;
398
- onExpire: () => void;
399
- tui?: TUI;
400
- }
401
- export class CountdownTimer {
402
- constructor(options: CountdownTimerOptions);
403
- dispose(): void;
404
- }
405
- ```
406
-
407
- **Implementation**: Copy code from pi-mono, thay theme reference qua adapter. Lưu ý `CancellableLoader`/`Loader` được pi-tui export — verify trước khi import.
408
-
409
- **Tích hợp** (per use case, có thể commit riêng):
410
- - `team-tool/run.ts`: trước khi spawn, hiển thị `CrewBorderedLoader` với message "spawning crew agents...". Khi run started, dispose loader + open sidebar.
411
- - `team-tool/cancel.ts`: tạo `CountdownTimer({ timeoutMs: 5000, onTick: s => loader.setMessage(`cancelling in ${s}s, press y to skip`) })`.
412
-
413
- **Acceptance**:
414
- - Unit test `test/unit/loaders.test.ts`:
415
- - `CrewBorderedLoader.signal.aborted` = false ban đầu, true sau khi user trigger Esc.
416
- - `dispose()` clear interval + remove listeners.
417
- - `CountdownTimer` tick → onTick gọi với seconds giảm dần.
418
- - `CountdownTimer` expire sau timeoutMs → onExpire gọi 1 lần.
419
- - Manual smoke trong `team-run` overlay.
420
-
421
- **Risk**: Trung bình. Phụ thuộc pi-tui exports `CancellableLoader`/`Loader` (tham khảo tui/index.ts).
422
-
423
- **Verification**: `npm run test:unit -- --grep loaders`.
424
-
425
- ---
426
-
427
- ### Task #47 — Port `truncateToVisualLines` cho transcript
428
- **Source**: `pi-mono/packages/coding-agent/src/modes/interactive/components/visual-truncate.ts`
429
- **Đích**: `pi-crew/src/utils/visual.ts` (mở rộng từ Task #38)
430
-
431
- **Lý do**: `transcript-viewer.ts` hiện dùng `wrap()` thủ công không tính ANSI codes → wrap sai khi line có color → tràn box hoặc hiển thị loang lổ. `truncateToVisualLines` của pi-mono dùng `Text.render(width)` từ pi-tui để tính chính xác visual lines.
432
-
433
- **API export** (bổ sung vào visual.ts):
434
- ```typescript
435
- export interface VisualTruncateResult { visualLines: string[]; skippedCount: number }
436
- export function truncateToVisualLines(text: string, maxVisualLines: number, width: number, paddingX?: number): VisualTruncateResult;
437
- ```
438
-
439
- **Tích hợp**:
440
- - `DurableTextViewer.render` + `DurableTranscriptViewer.render`: thay `body.flatMap(wrap)` bằng `truncateToVisualLines`.
441
- - Hiển thị `... (X lines truncated above)` khi `skippedCount > 0`.
442
-
443
- **Acceptance**:
444
- - Unit test:
445
- - Line không vượt width → trả nguyên + skippedCount=0.
446
- - Line vượt → wrap đúng số dòng + giữ ANSI codes nguyên vẹn.
447
- - `maxVisualLines = 5` với 10 dòng → trả 5 dòng cuối + skippedCount = 5.
448
- - Visual smoke: open transcript có code block ANSI dài → no overflow.
449
-
450
- **Risk**: Thấp. Pure utility.
451
-
452
- **Verification**: `npm run test:unit -- --grep visual-truncate`.
453
-
454
- ---
455
-
456
- ### Task #48 — Syntax highlight cho transcript JSONL events
457
- **Source**: `pi-mono/packages/coding-agent/src/modes/interactive/theme/theme.ts` (`highlightCode`, `getLanguageFromPath`)
458
- **Đích**: `pi-crew/src/ui/syntax-highlight.ts` (mới)
459
-
460
- **Lý do**: `transcript-viewer.ts` in JSON tool args + assistant code blocks plain text. Highlight tăng readability:
461
- - JSON keys → blue, strings → orange, numbers → green
462
- - Code in messages: detect language → highlight.
463
-
464
- **Dependency check**: `cli-highlight` đã có trong pi-mono. Verify pi-crew `package.json` — nếu chưa: `npm i cli-highlight`.
465
-
466
- **API export**:
467
- ```typescript
468
- export function highlightCode(code: string, lang: string | undefined, theme: CrewTheme): string[];
469
- export function highlightJson(json: string, theme: CrewTheme): string;
470
- export function detectLanguageFromPath(filePath: string): string | undefined;
471
- ```
472
-
473
- **Implementation**:
474
- - Copy `highlightCode` + `getLanguageFromPath` từ pi-mono.
475
- - Thay `theme` reference qua adapter (Task #42).
476
- - `highlightJson` shorthand cho `lang="json"`.
477
-
478
- **Tích hợp**:
479
- - `formatTranscriptEvent`: khi event là `[Tool: edit]` với JSON args → `highlightJson(stringify(args), theme)`.
480
- - `[Assistant]` content có ```code``` block → extract lang + highlight.
481
-
482
- **Acceptance**:
483
- - Unit test:
484
- - `highlightJson('{"a":1,"b":"x"}')` → lines có ANSI color codes.
485
- - `highlightCode("function f(){}", "typescript")` → keyword màu.
486
- - Invalid lang → fallback plain.
487
- - Manual: `team-transcript` xem JSON tool args có màu.
488
-
489
- **Risk**: Trung bình. `cli-highlight` ~100KB dep.
490
-
491
- **Verification**: `npx tsc --noEmit` + `npm run test:unit -- --grep syntax-highlight`.
492
-
493
- ---
494
-
495
- ## Tier 4 — Polish (optional)
496
-
497
- ### Task #49 (optional) — Animated mascot easter egg `/team-mascot`
498
- **Source**: `pi-mono/packages/coding-agent/src/modes/interactive/components/armin.ts`
499
- **Đích**: `pi-crew/src/ui/mascot.ts` + slash command `/team-mascot`
500
-
501
- **Lý do**: Branding/morale. Pi có Armin, pi-crew có thể có mascot riêng (vd: 1 nhóm 3 robots).
502
-
503
- **Implementation**:
504
- - XBM bitmap riêng (nhỏ ~30×30) hoặc reuse art logic từ armin.
505
- - 7 effects: typewriter, scanline, rain, fade, crt, glitch, dissolve.
506
-
507
- **Acceptance**:
508
- - Slash command `/team-mascot` mở overlay 5s rồi auto-close.
509
- - Không impact startup time (lazy load asset khi gọi).
510
-
511
- **Risk**: Thấp. Optional/cosmetic.
512
-
513
- **Verification**: Manual smoke.
514
-
515
- ---
516
-
517
- ## Tracking template (sao chép vào commit message)
518
-
519
- ```
520
- Phase 4 #NN — <short title>
521
-
522
- Source: source/pi-mono/packages/coding-agent/src/<file>.ts (or pi-tui/...)
523
- Target: pi-crew/src/<dir>/<file>.ts
524
- Risk: low | medium | high
525
- Tests added: test/unit/<file>.test.ts
526
- Verification: tsc --noEmit OK; test:unit OK; test:integration <OK|N/A>; bench <numbers>
527
-
528
- Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
529
- ```
530
-
531
- ---
532
-
533
- ## Thứ tự gợi ý thực hiện
534
-
535
- 1. **Tuần 1 — Tier 1 (Performance)**: #38 → #40 → #39 → #41
536
- - #38 dedupe trước (pre-req cho mọi refactor sau).
537
- - #40 file-coalescer (low risk, immediate I/O save).
538
- - #39 render cache (cần #38 để có visual.ts).
539
- - #41 manifest cache (cần #31 fs-watch từ Phase 3).
540
- - Bench trước/sau để chứng minh ≥ 4× improvement render hot path.
541
-
542
- 2. **Tuần 2 — Tier 2 (Theme)**: #42 → #43 → #44
543
- - #42 type-safe adapter (pre-req cho mọi UI refactor).
544
- - #43 status palette (low risk, mapping pure).
545
- - #44 layout primitives (cần snapshot test trước refactor).
546
-
547
- 3. **Tuần 3 — Tier 3 (UI components)**: #45 → #46 → #47 → #48
548
- - Có thể song song nếu nhiều dev. Ngược lại theo thứ tự diff → loader → visual-truncate → syntax-highlight.
549
- - #45 + #48 cần thêm runtime dep (`diff`, `cli-highlight`) — review trước khi merge.
550
-
551
- 4. **Tier 4 (#49)**: nếu còn thời gian. Branding/morale, không ảnh hưởng functionality.
552
-
553
- Toàn bộ Phase 4 ước tính 4–7 ngày focus work, thêm 2 runtime deps (`diff`, `cli-highlight`) khi triển khai #45 + #48 (verify chưa có trong package.json trước khi cài).
554
-
555
- ---
556
-
557
- ## Metrics mục tiêu (verification cuối Phase 4)
558
-
559
- - **Render cost**: `LiveRunSidebar.render(80) × 1000` từ ~150ms → ≤ 50ms.
560
- - **Disk I/O**: Tick 10Hz × 10s, đọc `agents.json` từ ~100 lần → ≤ 25 lần.
561
- - **LOC**: 5 file UI giảm ≥ 25% (~400 dòng).
562
- - **Test count**: 213 unit → ~245 unit (thêm ~32 test cho 12 task).
563
- - **Type safety**: 0 `as unknown as { fg?: ... }` cast trong `src/ui/`.
564
- - **Deps mới**: tối đa +2 (`diff`, `cli-highlight`), tổng size +130KB.
1
+ # Phase 4 Refactor Plan — UI/Theme/Performance từ pi-mono coding-agent
2
+
3
+ > Xuất xứ: review sâu `source/pi-mono/packages/coding-agent` + `source/pi-mono/packages/tui` (28/04/2026), so sánh với `pi-crew/src/ui/` hiện tại.
4
+ > Mục tiêu: tăng hiệu năng render, dọn duplicate code, type-safe theme integration, port các UI component thiếu (diff/loader/visual-truncate/syntax highlight).
5
+ > Phase 3 (#26–#37) đã hoàn tất, baseline: tsc 0 errors, 213 unit + 21 integration pass, commit `6f64c31`.
6
+
7
+ ## Quy ước chung
8
+ - Không phá vỡ public API (slash commands, tool actions, config schema). Mọi thay đổi nội bộ.
9
+ - Sau mỗi task: `npx tsc --noEmit` + `npm run test:unit` (+ `test:integration` nếu liên quan render/layout).
10
+ - Không thêm dependency runtime mới trừ khi task ghi rõ (chấp nhận `diff` cho Task #45 nếu chưa có).
11
+ - Mỗi task = 1 commit độc lập có thể revert. Đặt tên test bám sát hành vi.
12
+ - `theme` parameter đang là `unknown` — không được break `ctx.ui.custom((tui, theme, ...) => Component)` signature do pi-coding-agent dictate.
13
+
14
+ ## Trạng thái cập nhật
15
+ - [x] Task #38 — `utils/visual.ts` dedupe truncate/visibleWidth
16
+ - [x] Task #39 — Render cache cho widget/sidebar
17
+ - [x] Task #40 — File-coalescer apply vào readers UI
18
+ - [x] Task #41 — Manifest cache với mtime invalidation
19
+ - [x] Task #42 — Type-safe theme adapter
20
+ - [x] Task #43 — Status palette helpers
21
+ - [x] Task #44 — Refactor widgets sang pi-tui Container/Box/Text
22
+ - [x] Task #45 — Port `renderDiff` (word-level intra-line)
23
+ - [x] Task #46 — Port `BorderedLoader` + `CountdownTimer`
24
+ - [x] Task #47 — Port `truncateToVisualLines` cho transcript
25
+ - [x] Task #48 — Syntax highlight cho transcript JSONL
26
+ - [x] Task #49 (optional) — Animated mascot easter egg
27
+ ---
28
+
29
+ ## Tier 1 — Performance (high ROI, low risk)
30
+
31
+ Mục tiêu: 4 task, dedupe + cache + I/O coalescing. Risk thấp, không đổi API. Ước tính: 1–2 ngày.
32
+
33
+ ### Task #38 — Dedupe truncate/visibleWidth → `src/utils/visual.ts`
34
+ **Source**: `@mariozechner/pi-tui` (đã ship `visibleWidth`, `truncateToWidth`); pi-mono `components/visual-truncate.ts`
35
+ **Đích**: `pi-crew/src/utils/visual.ts`
36
+
37
+ **Lý do**: 4 file UI (`run-dashboard.ts`, `crew-widget.ts`, `live-run-sidebar.ts`, `transcript-viewer.ts`) mỗi file có bản copy của:
38
+ - `ANSI_PATTERN = /\u001b\[[0-?]*[ -/]*[@-~]/g`
39
+ - `visibleWidth(value)` / `visibleLength(value)`
40
+ - `truncate(value, width)` (logic không hoàn toàn nhất quán giữa các bản)
41
+ - `pad(value, width)` / `padVisible`
42
+
43
+ → Lặp lại ~80 dòng × 4 file. Dễ xảy ra drift bug.
44
+
45
+ **API export**:
46
+ ```typescript
47
+ export const ANSI_PATTERN: RegExp;
48
+ export function visibleWidth(value: string): number;
49
+ export function truncate(value: string, width: number, ellipsis?: string): string;
50
+ export function pad(value: string, width: number): string;
51
+ export function wrapHard(value: string, width: number): string[];
52
+ export function boxLine(text: string, innerWidth: number): string; // "│ {pad/truncate} │"
53
+ ```
54
+
55
+ **Tích hợp**:
56
+ - Re-export `visibleWidth` + `truncateToWidth` từ `@mariozechner/pi-tui` nếu có (kiểm tra `tui/utils.ts`).
57
+ - 4 file UI thay `import { ... }` từ local helper → `from "../utils/visual.ts"`.
58
+ - Xoá local helpers đã chuyển.
59
+
60
+ **Acceptance**:
61
+ - File mới + xoá ~80 LOC × 4 file (~320 LOC giảm).
62
+ - Unit test `test/unit/visual.test.ts`: 6 case
63
+ - `visibleWidth("\u001b[31mhello\u001b[0m")` = 5
64
+ - `truncate("hello world", 5)` = "hell…"
65
+ - `truncate(value, 0)` = ""
66
+ - `truncate(value, 1)` = "…"
67
+ - `pad("ab", 5)` = "ab "
68
+ - `wrapHard("abcdefgh", 3)` = ["abc","def","gh"]
69
+ - Snapshot test (optional): render `crew-widget` trước/sau giống bit-by-bit.
70
+
71
+ **Risk**: Thấp. Behavior tương đương, chỉ tách module.
72
+
73
+ **Verification**: `npx tsc --noEmit` + `npm run test:unit -- --grep visual` + `npm run test:unit -- --grep widget` (smoke).
74
+
75
+ ---
76
+
77
+ ### Task #39 — Render cache cho widget/sidebar (cachedWidth + version)
78
+ **Source pattern**: `pi-mono/packages/coding-agent/src/modes/interactive/components/armin.ts` (cachedWidth + cachedVersion + invalidate)
79
+ **Đích**: `crew-widget.ts`, `live-run-sidebar.ts`, `run-dashboard.ts`
80
+
81
+ **Lý do**: Mỗi tick (`widgetDefaultFrameMs`, `dashboardLiveRefreshMs` = 100ms) toàn bộ box được rebuild dù dữ liệu chưa đổi và terminal width chưa đổi. Khi data nhiều agent (>10), render cost không trivial.
82
+
83
+ **API pattern (per component)**:
84
+ ```typescript
85
+ class CrewWidgetComponent {
86
+ private cachedWidth = 0;
87
+ private cachedVersion = -1;
88
+ private currentVersion = 0;
89
+ private cachedLines: string[] = [];
90
+
91
+ invalidate(): void {
92
+ this.cachedWidth = 0; // forces rerender on next render() call
93
+ }
94
+
95
+ private dataSignature(): number {
96
+ // Hash from runs.length + agents counts + max updatedAt + statuses
97
+ // Bump currentVersion when signature differs from last computed
98
+ }
99
+
100
+ render(width: number): string[] {
101
+ const sig = this.dataSignature();
102
+ if (width === this.cachedWidth && this.cachedVersion === sig) return this.cachedLines;
103
+ // ... build lines ...
104
+ this.cachedWidth = width;
105
+ this.cachedVersion = sig;
106
+ return this.cachedLines;
107
+ }
108
+ }
109
+ ```
110
+
111
+ **Tích hợp**:
112
+ - `CrewWidgetComponent.render()`: dataSignature từ `frame % spinnerLength` + run/agent hash.
113
+ - Lưu ý spinner thay đổi mỗi tick → vẫn rerender header chứa spinner. Tách `staticBody` (cached) khỏi `spinnerLine` (live).
114
+ - `LiveRunSidebar.render()`: dataSignature từ manifest.updatedAt + agents.length + tasks.length + active counts.
115
+ - `RunDashboard.render()`: dataSignature từ runs.length + selected index + showFullProgress flag.
116
+
117
+ **Acceptance**:
118
+ - Unit test `test/unit/render-cache.test.ts`:
119
+ - `render(80)` 2 lần liên tiếp với data không đổi → tham chiếu mảng giống nhau (re-use cached).
120
+ - `render(80)` sau khi `invalidate()` → mảng mới.
121
+ - `render(120)` sau `render(80)` → mảng mới (width đổi).
122
+ - Manifest mtime đổi → signature đổi → mảng mới.
123
+ - Microbenchmark (`scripts/bench-render.ts` mới):
124
+ - Trước: `LiveRunSidebar.render(80) × 1000` ≥ 150ms
125
+ - Sau: `≤ 50ms` (cache hit ratio > 90%)
126
+
127
+ **Risk**: Trung bình. Nếu dataSignature không bắt được mọi mutation → stale UI. Mitigation: include `Date.now() / 1000 | 0` trong sig cho live components để rerender 1Hz tối thiểu.
128
+
129
+ **Verification**: `npx tsc --noEmit` + `npm run test:unit` + bench.
130
+
131
+ ---
132
+
133
+ ### Task #40 — File coalescer apply vào readers UI
134
+ **Source pattern**: `pi-crew/src/utils/file-coalescer.ts` (đã có từ Phase 2)
135
+ **Đích**: `crew-widget.ts`, `live-run-sidebar.ts`, `run-dashboard.ts`, `powerbar-publisher.ts`
136
+
137
+ **Lý do**: Mỗi tick render gọi:
138
+ - `readCrewAgents(manifest)` → `fs.readFileSync(agents.json)` parse JSON
139
+ - `readTasks(tasksPath)` → `fs.readFileSync(tasks.json)` parse JSON
140
+
141
+ Khi 4 widget cùng tick (widget + sidebar + powerbar + dashboard nếu mở) → cùng file đọc 4 lần trong < 10ms.
142
+
143
+ **Tích hợp**:
144
+ - Bọc `readCrewAgents` + `readTasks` qua `coalesceReads(filePath, ttlMs=200)` cache.
145
+ - Tránh stale: invalidate khi chính pi-crew write (set marker timestamp).
146
+ - Pattern:
147
+ ```typescript
148
+ // crew-agent-records.ts
149
+ import { coalesceReads } from "../utils/file-coalescer.ts";
150
+ const COALESCE_TTL = 200;
151
+ export function readCrewAgents(manifest: TeamRunManifest): CrewAgentRecord[] {
152
+ return coalesceReads(manifest.agentsPath, COALESCE_TTL, () => parseAgentsFile(manifest.agentsPath));
153
+ }
154
+ ```
155
+
156
+ **Acceptance**:
157
+ - Unit test `test/unit/agents-coalesce.test.ts`:
158
+ - Spy `fs.readFileSync` → 5 calls trong 100ms cho cùng path → chỉ đọc 1 lần.
159
+ - Sau TTL → đọc lại.
160
+ - Integration test: tick widget 10 lần trong 500ms → đọc agents.json tối đa 3 lần.
161
+
162
+ **Risk**: Thấp. TTL ngắn (200ms) đảm bảo data fresh.
163
+
164
+ **Verification**: `npm run test:unit -- --grep coalesce`.
165
+
166
+ ---
167
+
168
+ ### Task #41 — Manifest cache với mtime invalidation
169
+ **Source pattern**: `pi-mono/packages/coding-agent/src/core/footer-data-provider.ts` (cached branch + watch + debounce 500ms)
170
+ **Đích**: `pi-crew/src/runtime/manifest-cache.ts` (mới)
171
+
172
+ **Lý do**: `loadRunManifestById` đọc `manifest.json` + parse. `LiveRunSidebar` gọi mỗi tick (10Hz). Tương tự `listRecentRuns` scan cả thư mục `runs/`.
173
+
174
+ **API export**:
175
+ ```typescript
176
+ export interface ManifestCache {
177
+ get(runId: string): TeamRunManifest | undefined;
178
+ list(limit: number): TeamRunManifest[];
179
+ invalidate(runId?: string): void;
180
+ dispose(): void;
181
+ }
182
+ export function createManifestCache(cwd: string, options?: { debounceMs?: number; watch?: boolean }): ManifestCache;
183
+ ```
184
+
185
+ **Implementation**:
186
+ - Cache Map<runId, { manifest, mtimeMs }>.
187
+ - `get(runId)`: stat manifest path; nếu mtime khớp cache → return cached.
188
+ - `list(limit)`: scan dir, return top N theo mtime; cache toàn bộ list 500ms.
189
+ - Watcher (optional): `watchWithErrorHandler(runsDir)` + debounce 500ms → invalidate.
190
+
191
+ **Tích hợp**:
192
+ - `register.ts` tạo 1 instance ManifestCache khi `session_start`, dispose ở `session_shutdown`.
193
+ - `LiveRunSidebar`, `RunDashboard`, `crew-widget`, `powerbar-publisher` nhận cache (qua context closure).
194
+
195
+ **Acceptance**:
196
+ - Unit test:
197
+ - 5 calls `get(runId)` trong 100ms với mtime không đổi → 1 lần stat + 1 lần read.
198
+ - Sau write manifest (mtime đổi) → cache invalidate, đọc lại.
199
+ - `list(10)` cache 500ms.
200
+ - `dispose()` close watchers.
201
+ - Integration test: simulate 1Hz manifest update + 10Hz render → render dùng cached value, không đọc lại trừ khi manifest thực sự đổi.
202
+
203
+ **Risk**: Trung bình. Watch on Windows có quirks (đã giảm bằng Phase 3 fs-watch wrapper).
204
+
205
+ **Verification**: `npm run test:unit -- --grep manifest-cache` + `npm run test:integration`.
206
+
207
+ ---
208
+
209
+ ## Tier 2 — Theme Integration (clean API, type-safe)
210
+
211
+ Mục tiêu: 3 task, type-safe theme + reuse pi-tui layout primitives. Risk trung bình. Ước tính: 1–2 ngày.
212
+
213
+ ### Task #42 — Type-safe theme adapter `src/ui/theme-adapter.ts`
214
+ **Source pattern**: `pi-mono/packages/coding-agent/src/modes/interactive/theme/theme.ts` (Theme class với fg/bg/bold/italic)
215
+ **Đích**: `pi-crew/src/ui/theme-adapter.ts`
216
+
217
+ **Lý do**: Hiện tại 5 file UI cast `theme as unknown as { fg?: ... }`. IDE không suggest color names, dễ typo (`accenT` không lỗi compile).
218
+
219
+ **API export**:
220
+ ```typescript
221
+ export type CrewThemeColor =
222
+ | "accent" | "border" | "borderAccent" | "borderMuted"
223
+ | "success" | "error" | "warning"
224
+ | "muted" | "dim" | "text"
225
+ | "toolDiffAdded" | "toolDiffRemoved" | "toolDiffContext"
226
+ | "syntaxKeyword" | "syntaxString" | "syntaxNumber" | "syntaxComment" | "syntaxFunction" | "syntaxVariable" | "syntaxType";
227
+
228
+ export type CrewThemeBg = "selectedBg" | "userMessageBg" | "toolPendingBg" | "toolSuccessBg" | "toolErrorBg";
229
+
230
+ export interface CrewTheme {
231
+ fg(color: CrewThemeColor, text: string): string;
232
+ bg?(color: CrewThemeBg, text: string): string;
233
+ bold(text: string): string;
234
+ italic?(text: string): string;
235
+ underline?(text: string): string;
236
+ inverse?(text: string): string;
237
+ }
238
+
239
+ export function asCrewTheme(raw: unknown): CrewTheme;
240
+ ```
241
+
242
+ **Implementation**:
243
+ - `asCrewTheme`: validate raw có method `fg`/`bold`. Nếu thiếu → fallback no-op `(c, t) => t`.
244
+ - Sub-set của pi-coding-agent Theme class — không trùng namespace `CrewThemeColor` nhưng align values.
245
+
246
+ **Tích hợp**:
247
+ - `crew-widget.ts`, `live-run-sidebar.ts`, `run-dashboard.ts`, `transcript-viewer.ts`:
248
+ - Replace `theme.fg?.bind(theme) ?? ((_color, text) => text)` bằng `const t = asCrewTheme(rawTheme); t.fg("accent", x)`.
249
+ - Param signature: `(theme: unknown)` đổi thành `(theme: CrewTheme | unknown)`.
250
+
251
+ **Acceptance**:
252
+ - Unit test `test/unit/theme-adapter.test.ts`:
253
+ - `asCrewTheme(undefined)` → no-op fallback.
254
+ - `asCrewTheme({})` → no-op.
255
+ - `asCrewTheme({ fg: ..., bold: ... })` → uses provided methods.
256
+ - Type test (compile-only): `t.fg("nonExistent", "x")` produces TS error.
257
+ - Lint pass; tsc 0 errors sau khi thay 5 file.
258
+
259
+ **Risk**: Thấp. Fallback an toàn cho host không cung cấp đủ method.
260
+
261
+ **Verification**: `npx tsc --noEmit` + `npm run test:unit -- --grep theme-adapter`.
262
+
263
+ ---
264
+
265
+ ### Task #43 — Status palette helpers `src/ui/status-colors.ts`
266
+ **Source pattern**: `pi-mono` highlight pattern + pi-crew current ad-hoc switch-case
267
+ **Đích**: `pi-crew/src/ui/status-colors.ts`
268
+
269
+ **Lý do**: 5 file (`run-dashboard:65-72`, `crew-widget:89-95`, `live-run-sidebar:35`, `transcript-viewer`, `powerbar-publisher`) mỗi nơi có `switch(status){...}` mapping → màu/icon. Hiện không nhất quán (vd `crew-widget` ưu tiên `runningGlyph`, `run-dashboard` không).
270
+
271
+ **API export**:
272
+ ```typescript
273
+ export type RunStatus = "queued" | "running" | "completed" | "failed" | "cancelled" | "blocked" | "stale" | "stopped" | (string & {});
274
+
275
+ export function colorForStatus(status: RunStatus): CrewThemeColor;
276
+ export function iconForStatus(status: RunStatus, options?: { runningGlyph?: string }): string;
277
+ export function colorForActivity(activityState: string | undefined): CrewThemeColor;
278
+ export function applyStatusColor(theme: CrewTheme, status: RunStatus, text: string): string;
279
+ ```
280
+
281
+ **Implementation**:
282
+ - `colorForStatus`: `completed→success`, `failed|stale|error→error`, `cancelled|blocked|stopped→warning`, `running→accent`, `queued→muted`, default→dim.
283
+ - `iconForStatus`: `completed→✓`, `failed/stale→✗`, `cancelled/stopped→■`, `running→runningGlyph || ▶`, `queued→◦`, `blocked→⏸`, default→·.
284
+
285
+ **Tích hợp**:
286
+ - 5 file UI thay switch-case bằng 1 dòng `colorForStatus(status)`.
287
+ - `crew-widget.colorWidgetLine` regex map icon → dùng `iconForStatus` direct.
288
+
289
+ **Acceptance**:
290
+ - Unit test `test/unit/status-colors.test.ts`: 8 case theo từng status + edge case unknown status.
291
+ - Snapshot widget/dashboard render không thay đổi (test regression).
292
+
293
+ **Risk**: Thấp. Pure mapping function.
294
+
295
+ **Verification**: `npm run test:unit -- --grep status-colors`.
296
+
297
+ ---
298
+
299
+ ### Task #44 — Refactor widgets dùng pi-tui Container/Box/Text
300
+ **Source pattern**: `pi-mono/packages/tui/src/components/box.ts`, `text.ts`, plus `pi-mono/components/footer.ts` để tham chiếu cách compose.
301
+ **Đích**: `live-run-sidebar.ts`, `run-dashboard.ts` (giảm độ phức tạp)
302
+
303
+ **Lý do**: 2 file đang vẽ box bằng string concatenation `╭─╮│├┤╰╯` thủ công, mỗi line gọi `pad(truncate(...))`. Dễ vỡ khi terminal resize. pi-tui đã có `Container` + `Box` (rounded border tự động) + `DynamicBorder` từ pi-coding-agent.
304
+
305
+ **Tích hợp**:
306
+ - `LiveRunSidebar` → extend `Container`:
307
+ ```typescript
308
+ class LiveRunSidebar extends Container {
309
+ constructor(input) {
310
+ super();
311
+ this.addChild(new DynamicBorder(c => theme.fg("border", c)));
312
+ this.addChild(new Text(theme.bold("pi-crew live sidebar"), 1, 0));
313
+ // ...
314
+ }
315
+ render(width: number): string[] { /* parent handles layout */ }
316
+ }
317
+ ```
318
+ - `RunDashboard` tương tự — sections dùng `Spacer(1)` + `Text`.
319
+ - Lưu ý: `ctx.ui.custom((tui, theme, keys, done) => Component)` — trả về `Container` instance vẫn OK vì `Container` implements `Component`.
320
+
321
+ **Acceptance**:
322
+ - LOC giảm ≥ 30% cho 2 file.
323
+ - Visual snapshot test: render 80 + 120 width, content đồng nhất với baseline (allow whitespace diff).
324
+ - handleInput logic giữ nguyên semantics (q/esc/j/k/p/r/s/u/a/i/d/e/o/v).
325
+
326
+ **Risk**: Trung bình. Nếu Container layout không match cách hiện tại render padding thì box edge dịch chuyển. Mitigation: viết test snapshot trước khi refactor.
327
+
328
+ **Verification**: `npx tsc --noEmit` + `npm run test:unit` + manual `team-dashboard` smoke.
329
+
330
+ ---
331
+
332
+ ## Tier 3 — UI Components mới
333
+
334
+ Mục tiêu: 4 task, port các utility UI thiếu. Risk trung-cao. Ước tính: 2–3 ngày.
335
+
336
+ ### Task #45 — Port `renderDiff` (word-level intra-line)
337
+ **Source**: `pi-mono/packages/coding-agent/src/modes/interactive/components/diff.ts`
338
+ **Đích**: `pi-crew/src/ui/render-diff.ts`
339
+
340
+ **Lý do**: pi-crew có agents `code-modify`, `reviewer`, `verifier` thường tạo diff artifacts. Hiện tại transcript viewer + result viewer chỉ in raw text. `renderDiff` cho phép:
341
+ - Removed line: red với inverse trên token thay đổi.
342
+ - Added line: green với inverse trên token thay đổi.
343
+ - Context: dim/gray.
344
+
345
+ **Dependency check**: package `diff` (npm). Verify `pi-crew/package.json` chưa có → nếu thêm: `npm i diff @types/diff`.
346
+
347
+ **API export**:
348
+ ```typescript
349
+ export interface RenderDiffOptions { filePath?: string }
350
+ export function renderDiff(diffText: string, theme: CrewTheme, options?: RenderDiffOptions): string;
351
+ ```
352
+
353
+ **Implementation**: Copy `pi-mono/diff.ts` + thay `theme.inverse` import từ adapter; replace `theme.fg("toolDiff*", ...)` (đã thêm vào `CrewThemeColor` Task #42).
354
+
355
+ **Tích hợp**:
356
+ - `transcript-viewer.ts`: detect `[Tool: edit]` blocks chứa unified diff format → call `renderDiff`.
357
+ - Slash command `/team-diff <runId> <taskId>` (optional Task #45.b): render artifact diff trực tiếp.
358
+
359
+ **Acceptance**:
360
+ - Unit test `test/unit/render-diff.test.ts`:
361
+ - Single line modification → intra-line word diff with inverse.
362
+ - Multi line block → no intra-line, just full-line color.
363
+ - Context line preserved.
364
+ - Empty diff → empty string.
365
+ - Manual: render fixture `before.ts` vs `after.ts` diff trong overlay.
366
+
367
+ **Risk**: Trung bình. Add deps `diff` (~30KB). Acceptable.
368
+
369
+ **Verification**: `npx tsc --noEmit` + `npm run test:unit -- --grep render-diff`.
370
+
371
+ ---
372
+
373
+ ### Task #46 — Port `BorderedLoader` + `CountdownTimer`
374
+ **Source**: `pi-mono/packages/coding-agent/src/modes/interactive/components/bordered-loader.ts` + `countdown-timer.ts`
375
+ **Đích**: `pi-crew/src/ui/loaders.ts`
376
+
377
+ **Lý do**:
378
+ - `team run` async start có thể mất 2–5s spawn child. Hiện không feedback UI.
379
+ - `team cancel runId=...` force-kill nhưng không hiển thị countdown trước SIGKILL.
380
+ - `team-doctor` chạy 1–3s I/O không có loader.
381
+
382
+ **API export**:
383
+ ```typescript
384
+ export interface CrewBorderedLoaderOptions {
385
+ cancellable?: boolean;
386
+ message: string;
387
+ }
388
+ export class CrewBorderedLoader extends Container {
389
+ constructor(tui: TUI, theme: CrewTheme, options: CrewBorderedLoaderOptions);
390
+ get signal(): AbortSignal;
391
+ set onAbort(fn: (() => void) | undefined);
392
+ dispose(): void;
393
+ }
394
+
395
+ export interface CountdownTimerOptions {
396
+ timeoutMs: number;
397
+ onTick: (seconds: number) => void;
398
+ onExpire: () => void;
399
+ tui?: TUI;
400
+ }
401
+ export class CountdownTimer {
402
+ constructor(options: CountdownTimerOptions);
403
+ dispose(): void;
404
+ }
405
+ ```
406
+
407
+ **Implementation**: Copy code from pi-mono, thay theme reference qua adapter. Lưu ý `CancellableLoader`/`Loader` được pi-tui export — verify trước khi import.
408
+
409
+ **Tích hợp** (per use case, có thể commit riêng):
410
+ - `team-tool/run.ts`: trước khi spawn, hiển thị `CrewBorderedLoader` với message "spawning crew agents...". Khi run started, dispose loader + open sidebar.
411
+ - `team-tool/cancel.ts`: tạo `CountdownTimer({ timeoutMs: 5000, onTick: s => loader.setMessage(`cancelling in ${s}s, press y to skip`) })`.
412
+
413
+ **Acceptance**:
414
+ - Unit test `test/unit/loaders.test.ts`:
415
+ - `CrewBorderedLoader.signal.aborted` = false ban đầu, true sau khi user trigger Esc.
416
+ - `dispose()` clear interval + remove listeners.
417
+ - `CountdownTimer` tick → onTick gọi với seconds giảm dần.
418
+ - `CountdownTimer` expire sau timeoutMs → onExpire gọi 1 lần.
419
+ - Manual smoke trong `team-run` overlay.
420
+
421
+ **Risk**: Trung bình. Phụ thuộc pi-tui exports `CancellableLoader`/`Loader` (tham khảo tui/index.ts).
422
+
423
+ **Verification**: `npm run test:unit -- --grep loaders`.
424
+
425
+ ---
426
+
427
+ ### Task #47 — Port `truncateToVisualLines` cho transcript
428
+ **Source**: `pi-mono/packages/coding-agent/src/modes/interactive/components/visual-truncate.ts`
429
+ **Đích**: `pi-crew/src/utils/visual.ts` (mở rộng từ Task #38)
430
+
431
+ **Lý do**: `transcript-viewer.ts` hiện dùng `wrap()` thủ công không tính ANSI codes → wrap sai khi line có color → tràn box hoặc hiển thị loang lổ. `truncateToVisualLines` của pi-mono dùng `Text.render(width)` từ pi-tui để tính chính xác visual lines.
432
+
433
+ **API export** (bổ sung vào visual.ts):
434
+ ```typescript
435
+ export interface VisualTruncateResult { visualLines: string[]; skippedCount: number }
436
+ export function truncateToVisualLines(text: string, maxVisualLines: number, width: number, paddingX?: number): VisualTruncateResult;
437
+ ```
438
+
439
+ **Tích hợp**:
440
+ - `DurableTextViewer.render` + `DurableTranscriptViewer.render`: thay `body.flatMap(wrap)` bằng `truncateToVisualLines`.
441
+ - Hiển thị `... (X lines truncated above)` khi `skippedCount > 0`.
442
+
443
+ **Acceptance**:
444
+ - Unit test:
445
+ - Line không vượt width → trả nguyên + skippedCount=0.
446
+ - Line vượt → wrap đúng số dòng + giữ ANSI codes nguyên vẹn.
447
+ - `maxVisualLines = 5` với 10 dòng → trả 5 dòng cuối + skippedCount = 5.
448
+ - Visual smoke: open transcript có code block ANSI dài → no overflow.
449
+
450
+ **Risk**: Thấp. Pure utility.
451
+
452
+ **Verification**: `npm run test:unit -- --grep visual-truncate`.
453
+
454
+ ---
455
+
456
+ ### Task #48 — Syntax highlight cho transcript JSONL events
457
+ **Source**: `pi-mono/packages/coding-agent/src/modes/interactive/theme/theme.ts` (`highlightCode`, `getLanguageFromPath`)
458
+ **Đích**: `pi-crew/src/ui/syntax-highlight.ts` (mới)
459
+
460
+ **Lý do**: `transcript-viewer.ts` in JSON tool args + assistant code blocks plain text. Highlight tăng readability:
461
+ - JSON keys → blue, strings → orange, numbers → green
462
+ - Code in messages: detect language → highlight.
463
+
464
+ **Dependency check**: `cli-highlight` đã có trong pi-mono. Verify pi-crew `package.json` — nếu chưa: `npm i cli-highlight`.
465
+
466
+ **API export**:
467
+ ```typescript
468
+ export function highlightCode(code: string, lang: string | undefined, theme: CrewTheme): string[];
469
+ export function highlightJson(json: string, theme: CrewTheme): string;
470
+ export function detectLanguageFromPath(filePath: string): string | undefined;
471
+ ```
472
+
473
+ **Implementation**:
474
+ - Copy `highlightCode` + `getLanguageFromPath` từ pi-mono.
475
+ - Thay `theme` reference qua adapter (Task #42).
476
+ - `highlightJson` shorthand cho `lang="json"`.
477
+
478
+ **Tích hợp**:
479
+ - `formatTranscriptEvent`: khi event là `[Tool: edit]` với JSON args → `highlightJson(stringify(args), theme)`.
480
+ - `[Assistant]` content có ```code``` block → extract lang + highlight.
481
+
482
+ **Acceptance**:
483
+ - Unit test:
484
+ - `highlightJson('{"a":1,"b":"x"}')` → lines có ANSI color codes.
485
+ - `highlightCode("function f(){}", "typescript")` → keyword màu.
486
+ - Invalid lang → fallback plain.
487
+ - Manual: `team-transcript` xem JSON tool args có màu.
488
+
489
+ **Risk**: Trung bình. `cli-highlight` ~100KB dep.
490
+
491
+ **Verification**: `npx tsc --noEmit` + `npm run test:unit -- --grep syntax-highlight`.
492
+
493
+ ---
494
+
495
+ ## Tier 4 — Polish (optional)
496
+
497
+ ### Task #49 (optional) — Animated mascot easter egg `/team-mascot`
498
+ **Source**: `pi-mono/packages/coding-agent/src/modes/interactive/components/armin.ts`
499
+ **Đích**: `pi-crew/src/ui/mascot.ts` + slash command `/team-mascot`
500
+
501
+ **Lý do**: Branding/morale. Pi có Armin, pi-crew có thể có mascot riêng (vd: 1 nhóm 3 robots).
502
+
503
+ **Implementation**:
504
+ - XBM bitmap riêng (nhỏ ~30×30) hoặc reuse art logic từ armin.
505
+ - 7 effects: typewriter, scanline, rain, fade, crt, glitch, dissolve.
506
+
507
+ **Acceptance**:
508
+ - Slash command `/team-mascot` mở overlay 5s rồi auto-close.
509
+ - Không impact startup time (lazy load asset khi gọi).
510
+
511
+ **Risk**: Thấp. Optional/cosmetic.
512
+
513
+ **Verification**: Manual smoke.
514
+
515
+ ---
516
+
517
+ ## Tracking template (sao chép vào commit message)
518
+
519
+ ```
520
+ Phase 4 #NN — <short title>
521
+
522
+ Source: source/pi-mono/packages/coding-agent/src/<file>.ts (or pi-tui/...)
523
+ Target: pi-crew/src/<dir>/<file>.ts
524
+ Risk: low | medium | high
525
+ Tests added: test/unit/<file>.test.ts
526
+ Verification: tsc --noEmit OK; test:unit OK; test:integration <OK|N/A>; bench <numbers>
527
+
528
+ Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
529
+ ```
530
+
531
+ ---
532
+
533
+ ## Thứ tự gợi ý thực hiện
534
+
535
+ 1. **Tuần 1 — Tier 1 (Performance)**: #38 → #40 → #39 → #41
536
+ - #38 dedupe trước (pre-req cho mọi refactor sau).
537
+ - #40 file-coalescer (low risk, immediate I/O save).
538
+ - #39 render cache (cần #38 để có visual.ts).
539
+ - #41 manifest cache (cần #31 fs-watch từ Phase 3).
540
+ - Bench trước/sau để chứng minh ≥ 4× improvement render hot path.
541
+
542
+ 2. **Tuần 2 — Tier 2 (Theme)**: #42 → #43 → #44
543
+ - #42 type-safe adapter (pre-req cho mọi UI refactor).
544
+ - #43 status palette (low risk, mapping pure).
545
+ - #44 layout primitives (cần snapshot test trước refactor).
546
+
547
+ 3. **Tuần 3 — Tier 3 (UI components)**: #45 → #46 → #47 → #48
548
+ - Có thể song song nếu nhiều dev. Ngược lại theo thứ tự diff → loader → visual-truncate → syntax-highlight.
549
+ - #45 + #48 cần thêm runtime dep (`diff`, `cli-highlight`) — review trước khi merge.
550
+
551
+ 4. **Tier 4 (#49)**: nếu còn thời gian. Branding/morale, không ảnh hưởng functionality.
552
+
553
+ Toàn bộ Phase 4 ước tính 4–7 ngày focus work, thêm 2 runtime deps (`diff`, `cli-highlight`) khi triển khai #45 + #48 (verify chưa có trong package.json trước khi cài).
554
+
555
+ ---
556
+
557
+ ## Metrics mục tiêu (verification cuối Phase 4)
558
+
559
+ - **Render cost**: `LiveRunSidebar.render(80) × 1000` từ ~150ms → ≤ 50ms.
560
+ - **Disk I/O**: Tick 10Hz × 10s, đọc `agents.json` từ ~100 lần → ≤ 25 lần.
561
+ - **LOC**: 5 file UI giảm ≥ 25% (~400 dòng).
562
+ - **Test count**: 213 unit → ~245 unit (thêm ~32 test cho 12 task).
563
+ - **Type safety**: 0 `as unknown as { fg?: ... }` cast trong `src/ui/`.
564
+ - **Deps mới**: tối đa +2 (`diff`, `cli-highlight`), tổng size +130KB.