pi-crew 0.1.41 → 0.1.44

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 (191) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +51 -0
  3. package/agents/analyst.md +11 -11
  4. package/agents/critic.md +11 -11
  5. package/agents/executor.md +11 -11
  6. package/agents/explorer.md +11 -11
  7. package/agents/planner.md +11 -11
  8. package/agents/reviewer.md +11 -11
  9. package/agents/security-reviewer.md +11 -11
  10. package/agents/test-engineer.md +11 -11
  11. package/agents/verifier.md +11 -11
  12. package/agents/writer.md +11 -11
  13. package/docs/refactor-tasks-phase3.md +394 -394
  14. package/docs/refactor-tasks-phase4.md +564 -564
  15. package/docs/refactor-tasks-phase5.md +402 -402
  16. package/docs/refactor-tasks-phase6.md +662 -662
  17. package/docs/research-extension-examples.md +297 -297
  18. package/docs/research-extension-system.md +324 -324
  19. package/docs/research-optimization-plan.md +548 -548
  20. package/docs/research-phase10-distillation.md +199 -0
  21. package/docs/research-phase11-distillation.md +201 -0
  22. package/docs/research-pi-coding-agent.md +357 -357
  23. package/docs/research-source-pi-crew-reference.md +174 -174
  24. package/docs/runtime-flow.md +148 -148
  25. package/docs/source-runtime-refactor-map.md +83 -83
  26. package/index.ts +6 -6
  27. package/package.json +1 -1
  28. package/src/agents/agent-serializer.ts +34 -34
  29. package/src/agents/discover-agents.ts +5 -4
  30. package/src/config/config.ts +28 -4
  31. package/src/extension/cross-extension-rpc.ts +82 -82
  32. package/src/extension/management.ts +37 -8
  33. package/src/extension/notification-router.ts +2 -2
  34. package/src/extension/register.ts +130 -8
  35. package/src/extension/registration/commands.ts +11 -9
  36. package/src/extension/registration/compaction-guard.ts +125 -125
  37. package/src/extension/registration/subagent-tools.ts +28 -19
  38. package/src/extension/registration/team-tool.ts +2 -1
  39. package/src/extension/result-watcher.ts +4 -4
  40. package/src/extension/run-bundle-schema.ts +8 -4
  41. package/src/extension/run-import.ts +4 -0
  42. package/src/extension/run-index.ts +23 -1
  43. package/src/extension/run-maintenance.ts +43 -24
  44. package/src/extension/team-tool/api.ts +2 -2
  45. package/src/extension/team-tool/cancel.ts +76 -4
  46. package/src/extension/team-tool/context.ts +1 -0
  47. package/src/extension/team-tool/doctor.ts +8 -1
  48. package/src/extension/team-tool/handle-settings.ts +188 -0
  49. package/src/extension/team-tool/inspect.ts +41 -41
  50. package/src/extension/team-tool/lifecycle-actions.ts +79 -79
  51. package/src/extension/team-tool/plan.ts +19 -19
  52. package/src/extension/team-tool/respond.ts +67 -0
  53. package/src/extension/team-tool/run.ts +6 -4
  54. package/src/extension/team-tool/status.ts +99 -93
  55. package/src/extension/team-tool-types.ts +4 -0
  56. package/src/extension/team-tool.ts +5 -1
  57. package/src/i18n.ts +184 -0
  58. package/src/observability/correlation.ts +2 -2
  59. package/src/observability/event-to-metric.ts +10 -3
  60. package/src/observability/exporters/adapter.ts +7 -1
  61. package/src/observability/exporters/otlp-exporter.ts +14 -2
  62. package/src/observability/exporters/prometheus-exporter.ts +9 -2
  63. package/src/observability/metric-registry.ts +18 -3
  64. package/src/observability/metric-retention.ts +11 -3
  65. package/src/observability/metric-sink.ts +9 -4
  66. package/src/observability/metrics-primitives.ts +4 -3
  67. package/src/prompt/prompt-runtime.ts +72 -68
  68. package/src/runtime/agent-control.ts +63 -63
  69. package/src/runtime/agent-memory.ts +72 -72
  70. package/src/runtime/agent-observability.ts +114 -114
  71. package/src/runtime/async-marker.ts +26 -26
  72. package/src/runtime/attention-events.ts +28 -23
  73. package/src/runtime/background-runner.ts +53 -53
  74. package/src/runtime/child-pi.ts +4 -4
  75. package/src/runtime/completion-guard.ts +95 -4
  76. package/src/runtime/concurrency.ts +1 -1
  77. package/src/runtime/crash-recovery.ts +32 -1
  78. package/src/runtime/crew-agent-runtime.ts +59 -58
  79. package/src/runtime/deadletter.ts +14 -4
  80. package/src/runtime/delivery-coordinator.ts +143 -0
  81. package/src/runtime/direct-run.ts +35 -35
  82. package/src/runtime/foreground-control.ts +82 -82
  83. package/src/runtime/green-contract.ts +46 -46
  84. package/src/runtime/group-join.ts +106 -106
  85. package/src/runtime/heartbeat-gradient.ts +28 -28
  86. package/src/runtime/heartbeat-watcher.ts +48 -4
  87. package/src/runtime/live-agent-control.ts +87 -87
  88. package/src/runtime/live-agent-manager.ts +85 -85
  89. package/src/runtime/live-control-realtime.ts +36 -36
  90. package/src/runtime/live-session-runtime.ts +305 -305
  91. package/src/runtime/manifest-cache.ts +2 -2
  92. package/src/runtime/model-fallback.ts +272 -261
  93. package/src/runtime/overflow-recovery.ts +157 -0
  94. package/src/runtime/parallel-research.ts +44 -44
  95. package/src/runtime/parallel-utils.ts +1 -1
  96. package/src/runtime/pi-json-output.ts +111 -111
  97. package/src/runtime/policy-engine.ts +79 -78
  98. package/src/runtime/post-exit-stdio-guard.ts +2 -2
  99. package/src/runtime/process-status.ts +56 -56
  100. package/src/runtime/progress-event-coalescer.ts +43 -43
  101. package/src/runtime/recovery-recipes.ts +74 -74
  102. package/src/runtime/retry-executor.ts +5 -0
  103. package/src/runtime/role-permission.ts +39 -39
  104. package/src/runtime/runtime-resolver.ts +1 -1
  105. package/src/runtime/session-resources.ts +25 -0
  106. package/src/runtime/session-snapshot.ts +59 -0
  107. package/src/runtime/session-usage.ts +79 -79
  108. package/src/runtime/sidechain-output.ts +29 -29
  109. package/src/runtime/stale-reconciler.ts +179 -0
  110. package/src/runtime/subagent-manager.ts +3 -3
  111. package/src/runtime/supervisor-contact.ts +59 -0
  112. package/src/runtime/task-display.ts +38 -38
  113. package/src/runtime/task-output-context.ts +127 -127
  114. package/src/runtime/task-runner/live-executor.ts +101 -101
  115. package/src/runtime/task-runner/progress.ts +119 -111
  116. package/src/runtime/task-runner/result-utils.ts +14 -14
  117. package/src/runtime/task-runner/state-helpers.ts +22 -22
  118. package/src/runtime/task-runner.ts +14 -0
  119. package/src/runtime/team-runner.ts +9 -10
  120. package/src/runtime/worker-heartbeat.ts +21 -21
  121. package/src/runtime/worker-startup.ts +57 -57
  122. package/src/schema/config-schema.ts +2 -1
  123. package/src/schema/team-tool-schema.ts +115 -109
  124. package/src/state/artifact-store.ts +4 -2
  125. package/src/state/atomic-write.ts +12 -4
  126. package/src/state/contracts.ts +109 -105
  127. package/src/state/event-log.ts +3 -4
  128. package/src/state/jsonl-writer.ts +4 -1
  129. package/src/state/locks.ts +9 -1
  130. package/src/state/task-claims.ts +44 -42
  131. package/src/state/usage.ts +29 -29
  132. package/src/subagents/async-entry.ts +1 -1
  133. package/src/subagents/index.ts +3 -3
  134. package/src/subagents/live/control.ts +1 -1
  135. package/src/subagents/live/manager.ts +1 -1
  136. package/src/subagents/live/realtime.ts +1 -1
  137. package/src/subagents/live/session-runtime.ts +1 -1
  138. package/src/subagents/manager.ts +1 -1
  139. package/src/subagents/spawn.ts +1 -1
  140. package/src/teams/discover-teams.ts +2 -2
  141. package/src/teams/team-serializer.ts +38 -38
  142. package/src/types/diff.d.ts +18 -18
  143. package/src/ui/crew-footer.ts +101 -101
  144. package/src/ui/crew-select-list.ts +111 -111
  145. package/src/ui/crew-widget.ts +5 -4
  146. package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
  147. package/src/ui/dynamic-border.ts +25 -25
  148. package/src/ui/layout-primitives.ts +106 -106
  149. package/src/ui/live-run-sidebar.ts +1 -1
  150. package/src/ui/loaders.ts +158 -158
  151. package/src/ui/mascot.ts +3 -2
  152. package/src/ui/powerbar-publisher.ts +7 -6
  153. package/src/ui/render-diff.ts +119 -119
  154. package/src/ui/render-scheduler.ts +54 -14
  155. package/src/ui/run-dashboard.ts +39 -11
  156. package/src/ui/run-snapshot-cache.ts +336 -36
  157. package/src/ui/spinner.ts +17 -17
  158. package/src/ui/status-colors.ts +58 -54
  159. package/src/ui/syntax-highlight.ts +116 -116
  160. package/src/ui/theme-adapter.ts +1 -1
  161. package/src/ui/transcript-viewer.ts +7 -2
  162. package/src/utils/atomic-write.ts +33 -0
  163. package/src/utils/completion-dedupe.ts +63 -63
  164. package/src/utils/file-coalescer.ts +5 -3
  165. package/src/utils/frontmatter.ts +68 -36
  166. package/src/utils/git.ts +262 -262
  167. package/src/utils/ids.ts +12 -12
  168. package/src/utils/internal-error.ts +1 -1
  169. package/src/utils/names.ts +27 -26
  170. package/src/utils/paths.ts +1 -1
  171. package/src/utils/redaction.ts +44 -41
  172. package/src/utils/safe-paths.ts +47 -34
  173. package/src/utils/sleep.ts +2 -2
  174. package/src/utils/timings.ts +2 -0
  175. package/src/utils/visual.ts +9 -1
  176. package/src/workflows/discover-workflows.ts +4 -1
  177. package/src/workflows/validate-workflow.ts +40 -40
  178. package/src/worktree/branch-freshness.ts +45 -45
  179. package/src/worktree/worktree-manager.ts +6 -1
  180. package/teams/default.team.md +12 -12
  181. package/teams/fast-fix.team.md +11 -11
  182. package/teams/implementation.team.md +18 -18
  183. package/teams/parallel-research.team.md +14 -14
  184. package/teams/research.team.md +11 -11
  185. package/teams/review.team.md +12 -12
  186. package/workflows/default.workflow.md +29 -29
  187. package/workflows/fast-fix.workflow.md +22 -22
  188. package/workflows/implementation.workflow.md +38 -38
  189. package/workflows/parallel-research.workflow.md +46 -46
  190. package/workflows/research.workflow.md +22 -22
  191. package/workflows/review.workflow.md +30 -30
@@ -1,402 +1,402 @@
1
- # Phase 5 Refactor Plan — Footer/Selectlist/Hot-reload từ pi-mono coding-agent
2
-
3
- > Xuất xứ: re-read `source/pi-mono/packages/coding-agent/src/modes/interactive/components/{footer,bordered-loader,dynamic-border,visual-truncate,diff,countdown-timer,extension-selector,theme-selector,custom-message,tool-execution,bash-execution}.ts` + `theme/theme.ts` (28/04/2026).
4
- > Mục tiêu: vá lỗi subtle còn lại từ Phase 4, hot-reload theme, port footer/select-list pattern, chuẩn hóa border + tool state styling.
5
- > Phase 4 đã hoàn tất, baseline: tsc 0 errors, 222 unit + 21 integration pass, commit `44fdd02`.
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/runtime).
10
- - Không thêm dependency runtime mới. Tất cả implement self-contained hoặc qua peer dep `@mariozechner/pi-tui` đã 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
- - Ưu tiên backward compatibility: default behavior không đổi, opt-in qua config khi có hành vi mới.
13
-
14
- ## Trạng thái cập nhật
15
- - [x] Task #50 — Fix `truncateToVisualLines` slice-after-merge bug
16
- - [x] Task #51 — Memoize `visibleWidth` LRU cache
17
- - [x] Task #52 — Theme hot-reload subscription
18
- - [x] Task #53 — Theme adapter `inverse` ANSI fallback
19
- - [x] Task #54 — `CrewFooter` component port
20
- - [x] Task #55 — `CrewSelectList` adapter
21
- - [x] Task #56 — `DynamicCrewBorder` reusable + CountdownTimer 1s tick
22
- - [x] Task #57 — Tool state styling cho transcript-viewer
23
- ---
24
-
25
- ## Tier 1 — Bug fixes & correctness (low risk, immediate value)
26
-
27
- Mục tiêu: 2 task, vá bug từ Phase 4 + tăng hiệu năng nhỏ. Ước tính: 0.5 ngày.
28
-
29
- ### Task #50 — Fix `truncateToVisualLines` slice-after-merge bug
30
- **Source**: `pi-mono/coding-agent/components/visual-truncate.ts`
31
- **Đích**: `pi-crew/src/utils/visual.ts`
32
-
33
- **Lý do**: Phase 4 #47 implement `truncateToVisualLines` với logic:
34
- ```ts
35
- const visualLines = text.split("\n").flatMap((line) =>
36
- wrapHard(pad(line, ...).trimEnd(), effectiveWidth).slice(0, Math.max(1, maxVisualLines))
37
- );
38
- ```
39
- Bug: `slice(0, maxVisualLines)` áp dụng **per source line** thay vì **toàn bộ visual lines sau merge**. Nếu 1 source line wrap thành N visual lines (N > maxVisualLines), kết quả lấy đầu line đó, không phải tail của toàn bộ output. Khi nhiều source line, tổng visual có thể vượt maxVisualLines.
40
-
41
- pi-mono dùng pattern đúng: render rồi `slice(-maxVisualLines)`.
42
-
43
- **Logic chuẩn**:
44
- ```ts
45
- export function truncateToVisualLines(text, maxVisualLines, width, paddingX = 0) {
46
- if (!text) return { visualLines: [], skippedCount: 0 };
47
- const effectiveWidth = Math.max(1, width - paddingX * 2);
48
- const allVisual = text.split("\n").flatMap((line) =>
49
- wrapHard(pad(line, effectiveWidth).trimEnd(), effectiveWidth)
50
- );
51
- if (allVisual.length <= maxVisualLines) return { visualLines: allVisual, skippedCount: 0 };
52
- return { visualLines: allVisual.slice(-maxVisualLines), skippedCount: allVisual.length - maxVisualLines };
53
- }
54
- ```
55
-
56
- **Acceptance**:
57
- - 1 source line wrap thành 5 visual lines, maxVisualLines=2 → trả về 2 visual lines cuối + skippedCount=3
58
- - 3 source lines × 2 visual mỗi line = 6 visual, maxVisualLines=4 → trả về 4 cuối + skippedCount=2
59
- - empty input → `{ visualLines: [], skippedCount: 0 }` (đổi từ `[""]` về `[]` để khớp pi-mono)
60
-
61
- **Verification**: 2 unit test mới trong `test/unit/visual.test.ts`. Verify transcript-viewer integration vẫn pass test cũ.
62
-
63
- **Risk**: thay đổi semantic empty input — kiểm tra all callers (transcript-viewer, run-dashboard) handle `[]` thay vì `[""]`.
64
-
65
- ---
66
-
67
- ### Task #51 — Memoize `visibleWidth` qua LRU cache
68
- **Source**: pattern caching từ pi-tui `utils.ts`
69
- **Đích**: `pi-crew/src/utils/visual.ts`
70
-
71
- **Lý do**: `visibleWidth(value)` được gọi trong:
72
- - `pad`, `truncateToWidth`, `wrapHard` (mỗi character iter)
73
- - `crew-widget.ts colorWidgetLine` (mỗi line, mỗi tick 250ms)
74
- - `RunDashboard.render` (5-10 lần per render)
75
- - Total ước tính: 50+ calls/render × 4 render/sec = 200+ regex ops/sec.
76
-
77
- Cache key = string identity, value = width. Reset khi cache > 256 entries (FIFO eviction).
78
-
79
- **API**:
80
- ```ts
81
- const widthCache = new Map<string, number>();
82
- const CACHE_LIMIT = 256;
83
-
84
- export function visibleWidth(value: string): number {
85
- const cached = widthCache.get(value);
86
- if (cached !== undefined) return cached;
87
- let length = 0;
88
- for (const char of value.replace(ANSI_PATTERN, "")) {
89
- if (char !== "\n") length += 1;
90
- }
91
- if (widthCache.size >= CACHE_LIMIT) {
92
- const firstKey = widthCache.keys().next().value;
93
- if (firstKey !== undefined) widthCache.delete(firstKey);
94
- }
95
- widthCache.set(value, length);
96
- return length;
97
- }
98
- ```
99
-
100
- **Acceptance**:
101
- - `visibleWidth("foo")` gọi 1000 lần → chỉ tính 1 lần (kiểm qua spy với regex.exec count nếu có Diff bench).
102
- - Cache không leak: limit 256, sau 1000 unique strings thì size = 256.
103
- - Output identical với version không cache (regression test).
104
-
105
- **Verification**:
106
- - 1 unit test cache hit
107
- - 1 unit test eviction (insert 257 strings, kiểm size === 256)
108
- - Bench: `visibleWidth(longString) × 10000` → time giảm ≥ 5× (ms log).
109
-
110
- **Risk**: cache miss khi string concat/template (mỗi lần object identity khác). Nhận diện qua bench thực tế.
111
-
112
- ---
113
-
114
- ## Tier 2 — Theme & style consistency
115
-
116
- Mục tiêu: 2 task, hot-reload + inverse fallback. Ước tính: 0.5 ngày.
117
-
118
- ### Task #52 — Theme hot-reload subscription
119
- **Source**: `pi-mono/coding-agent/theme/theme.ts` `onThemeChange()` + `startThemeWatcher()`
120
- **Đích**: `pi-crew/src/ui/theme-adapter.ts`, `src/extension/register.ts`
121
-
122
- **Lý do**: pi-mono có cơ chế watch custom theme JSON, debounce 100ms reload, emit callback. pi-crew adapter chỉ snapshot theme 1 lần ở `ctx.ui.custom((tui, theme, ...) => Component)`. Khi user gõ `/theme dark` từ pi-coding-agent, các pi-crew widget hold theme cũ cho tới khi recreate component.
123
-
124
- **Approach**:
125
- 1. Add `subscribeThemeChange(theme: unknown, callback: () => void): () => void` trong theme-adapter.ts. Internally:
126
- - Test if `theme` object có `addEventListener?.("change", ...)` hoặc `onThemeChange?.(...)` API.
127
- - Fallback: poll `theme.getColorMode?.()` + key signature mỗi 1s, callback nếu thay đổi.
128
- 2. CrewWidgetComponent / LiveRunSidebar / RunDashboard / DurableTextViewer: gọi `subscribeThemeChange` trong constructor, store unsubscribe, gọi `this.invalidate()` khi callback fires.
129
- 3. dispose: unsubscribe.
130
-
131
- **Acceptance**:
132
- - Mock theme với `onThemeChange` API → callback fires trong 200ms.
133
- - Mock theme polling → kiểm callback fires sau 1.1s khi sig thay đổi.
134
- - Dispose component → no further callback.
135
-
136
- **Verification**: 2 unit test mock theme objects. Manual test: chạy pi với `/theme light` rồi `/theme dark`, kiểm RunDashboard re-render.
137
-
138
- **Risk**: polling 1s × N components → overhead. Mitigate: shared global subscription, fan-out tới components qua singleton subscriber list. Implement singleton trong theme-adapter.
139
-
140
- ---
141
-
142
- ### Task #53 — Theme adapter `inverse` ANSI fallback
143
- **Source**: `pi-mono` dùng `chalk.inverse(text)` = `\x1b[7m{text}\x1b[27m`
144
- **Đích**: `pi-crew/src/ui/theme-adapter.ts`
145
-
146
- **Lý do**: `asCrewTheme` hiện chỉ pass-through nếu source theme có `inverse`, fallback identity (return text nguyên). render-diff dùng `theme.inverse?.(value) ?? value` → khi theme nguồn không có inverse, intra-line diff highlight bị mất hoàn toàn. Bug visual subtle, không có test catch.
147
-
148
- **Logic chuẩn**:
149
- ```ts
150
- function asInverse(value: unknown): (text: string) => string {
151
- const fn = asUnaryFn(value);
152
- if (fn) return fn;
153
- return (text) => `\u001b[7m${text}\u001b[27m`;
154
- }
155
- ```
156
-
157
- **Acceptance**:
158
- - `asCrewTheme(undefined).inverse?.("x")` → `"\u001b[7mx\u001b[27m"`.
159
- - `asCrewTheme(realTheme).inverse?.("x")` → output từ chalk (test bằng `includes("\u001b[7m")`).
160
- - renderDiff với theme tối giản vẫn highlight inverse lookup.
161
-
162
- **Verification**: cập nhật `loaders.test.ts`/thêm `theme-adapter.test.ts` 2 test (default fallback + provided theme passthrough).
163
-
164
- **Risk**: thấp — additive change.
165
-
166
- ---
167
-
168
- ## Tier 3 — UX components (port pattern từ pi-mono)
169
-
170
- Mục tiêu: 3 task, footer + selectlist + dynamic border. Ước tính: 1 ngày.
171
-
172
- ### Task #54 — `CrewFooter` component port
173
- **Source**: `pi-mono/coding-agent/components/footer.ts`
174
- **Đích**: `pi-crew/src/ui/crew-footer.ts` (mới), tích hợp vào `RunDashboard`.
175
-
176
- **Lý do**: pi-mono Footer là pattern multi-line trang trí (pwd+branch, tokens, context %, model). pi-crew RunDashboard có summary 1 line trộn rời rạc. Port để đồng bộ visual với coding-agent.
177
-
178
- **Layout (3 lines)**:
179
- ```
180
- ~/proj (main) • runId • running (dim)
181
- ↑in ↓out R cache W cache $cost • 45.3%/200k (dim, % colored)
182
- [badge1] [badge2] ... (extension statuses)
183
- ```
184
-
185
- **API**:
186
- ```ts
187
- export interface CrewFooterData {
188
- pwd: string;
189
- branch?: string;
190
- runId?: string;
191
- status?: RunStatus;
192
- usage?: UsageState;
193
- contextWindow?: number;
194
- contextPercent?: number;
195
- badges?: string[]; // raw text per extension status
196
- }
197
-
198
- export class CrewFooter {
199
- constructor(private data: CrewFooterData, private theme: CrewTheme) {}
200
- setData(data: CrewFooterData): void;
201
- render(width: number): string[];
202
- invalidate(): void;
203
- }
204
- ```
205
-
206
- **Color logic**:
207
- - contextPercent > 90 → `theme.fg("error", ...)`
208
- - > 70 → `theme.fg("warning", ...)`
209
- - ≤ 70 → no color
210
-
211
- **Acceptance**:
212
- - Render cho run với usage tokens → output chứa `↑`, `↓`, `$cost`.
213
- - Truncate khi width nhỏ → ellipsis `...`.
214
- - contextPercent NaN/undefined → display `?/window`.
215
-
216
- **Verification**:
217
- - `test/unit/crew-footer.test.ts` 4 test (basic render, color thresholds, truncation, missing data).
218
- - Integrate vào `RunDashboard.renderFooter` (thay phần legacy footer).
219
-
220
- **Risk**: RunDashboard layout shift — kiểm snapshot lines count với existing tests.
221
-
222
- ---
223
-
224
- ### Task #55 — `CrewSelectList` adapter
225
- **Source**: `@mariozechner/pi-tui` `SelectList` (peer dep) + pi-mono `extension-selector.ts`/`theme-selector.ts` patterns
226
- **Đích**: `pi-crew/src/ui/crew-select-list.ts`
227
-
228
- **Lý do**: RunDashboard handle keyboard navigation thủ công (j/k/enter), không có visual highlight selected, không support `onPreview`. pi-tui SelectList có sẵn nhưng pi-crew chưa wrap. Cần adapter để xài SelectList từ peer dep pi-tui (optional dep — kiểm `import { SelectList } from "@mariozechner/pi-tui"` available).
229
-
230
- **Approach**:
231
- 1. Detect runtime: `try { require.resolve("@mariozechner/pi-tui"); }` → dùng pi-tui SelectList.
232
- 2. Fallback: simple list component port từ extension-selector.ts (j/k/↑/↓/enter/esc handlers, highlight ` → ` cho selected).
233
- 3. API:
234
- ```ts
235
- export interface CrewSelectItem<T = string> {
236
- value: T;
237
- label: string;
238
- description?: string;
239
- }
240
-
241
- export class CrewSelectList<T = string> {
242
- constructor(
243
- items: CrewSelectItem<T>[],
244
- theme: CrewTheme,
245
- options: {
246
- onSelect: (item: CrewSelectItem<T>) => void;
247
- onCancel: () => void;
248
- onPreview?: (item: CrewSelectItem<T>) => void;
249
- maxHeight?: number;
250
- }
251
- ) {}
252
- render(width: number): string[];
253
- handleInput(data: string): void;
254
- invalidate(): void;
255
- setSelectedIndex(i: number): void;
256
- getSelected(): CrewSelectItem<T> | undefined;
257
- }
258
- ```
259
-
260
- **Acceptance**:
261
- - Render với 5 items → 5 lines, selected có ` → `.
262
- - handleInput("j") → selected index +1, callback onPreview fired.
263
- - handleInput("\n") → callback onSelect with current item.
264
- - maxHeight=3 với 10 items → scroll, indicator `↑ N more`/`↓ N more`.
265
-
266
- **Verification**: `test/unit/crew-select-list.test.ts` 5 test.
267
-
268
- **Risk**: API mismatch nếu pi-tui SelectList API đổi version. Pin behavior qua adapter, fallback always available.
269
-
270
- ---
271
-
272
- ### Task #56 — `DynamicCrewBorder` reusable + CountdownTimer 1s tick
273
- **Source**: `pi-mono/coding-agent/components/dynamic-border.ts` + `countdown-timer.ts`
274
- **Đích**: `pi-crew/src/ui/dynamic-border.ts` (mới), refactor `loaders.ts`
275
-
276
- **Lý do**:
277
- 1. **DynamicBorder**: 10 LOC, render single line `─×width`. pi-crew có 3 nơi tự vẽ border:
278
- - `loaders.ts CrewBorderedLoader`: `┌─┐│└─┘` static template
279
- - `mascot.ts`: tự build `╭─╮│╰─╯`
280
- - `run-dashboard.ts/transcript-viewer.ts`: tự pad border lines
281
- → Refactor dùng chung `DynamicCrewBorder` cho horizontal lines, giữ corner chars riêng.
282
- 2. **CountdownTimer 1s tick**: hiện tại tick 250ms (4×/s). pi-mono tick chính xác 1000ms + `tui.requestRender()`. 4× tick là wasteful, gây re-render trùng lặp.
283
-
284
- **API**:
285
- ```ts
286
- // dynamic-border.ts
287
- export interface DynamicCrewBorderOptions {
288
- color?: (s: string) => string;
289
- char?: string; // default "─"
290
- }
291
- export class DynamicCrewBorder {
292
- constructor(theme: CrewTheme, options?: DynamicCrewBorderOptions) {}
293
- render(width: number): string[];
294
- invalidate(): void;
295
- }
296
- ```
297
-
298
- CountdownTimer change:
299
- ```ts
300
- // trong loaders.ts CountdownTimer
301
- - this.timer = setInterval(() => { ... }, 250);
302
- + this.timer = setInterval(() => {
303
- + const seconds = this.secondsLeft();
304
- + this.onTick(seconds);
305
- + if (seconds <= 0) this.emitExpire();
306
- + }, 1000);
307
- ```
308
-
309
- **Acceptance**:
310
- - DynamicCrewBorder.render(20) → `["─".repeat(20)]` (with color).
311
- - DynamicCrewBorder dùng trong CrewBorderedLoader, mascot box, run-dashboard separators.
312
- - CountdownTimer onTick called ~3 lần trong 3.5s (giây 3, 2, 1, 0 không nhiều hơn).
313
-
314
- **Verification**:
315
- - 2 unit test cho DynamicCrewBorder (basic render, custom char).
316
- - Update `loaders.test.ts` CountdownTimer test: kiểm onTick count = ceil(timeoutMs/1000) + 1.
317
-
318
- **Risk**: mascot CountdownTimer (nếu có) cần điều chỉnh cùng. Visual flicker giảm bằng tick 1s thay 250ms.
319
-
320
- ---
321
-
322
- ## Tier 4 — Power features
323
-
324
- Mục tiêu: 1 task, tool state styling. Ước tính: 0.25 ngày.
325
-
326
- ### Task #57 — Tool state styling cho transcript-viewer
327
- **Source**: `pi-mono/coding-agent/components/tool-execution.ts` (toolPendingBg/toolSuccessBg/toolErrorBg state)
328
- **Đích**: `pi-crew/src/ui/transcript-viewer.ts`
329
-
330
- **Lý do**: transcript-viewer hiện render `[Tool: name] type` plain text. Không phân biệt:
331
- - partial vs final result
332
- - success vs error (`result.isError`)
333
- - queued vs running
334
-
335
- User scan transcript khó tìm ra error tool nhanh.
336
-
337
- **Logic update `formatTranscriptEvent`**:
338
- ```ts
339
- const isError = obj.isError === true || asRecord(obj.result)?.isError === true;
340
- const isPartial = obj.isPartial === true;
341
- const status: RunStatus = isError ? "failed" : isPartial ? "running" : "completed";
342
- const icon = iconForStatus(status, { runningGlyph: "⋯" });
343
- const headerColor = colorForStatus(status);
344
- const header = theme.fg(headerColor, `${icon} [Tool${toolName ? `: ${toolName}` : ""}] ${type}`);
345
- ```
346
-
347
- **Acceptance**:
348
- - Event với `isError: true` → header có icon `✗`, color `error`.
349
- - Event với `isPartial: true` → header có icon `⋯`/`▶`, color `accent`.
350
- - Event normal → icon `✓`, color `success`.
351
- - Existing tests `formatTranscriptText formats message and tool JSONL into conversation lines` vẫn pass.
352
-
353
- **Verification**: thêm 2 test cho transcript-viewer (error tool, partial tool).
354
-
355
- **Risk**: thấp — schema event đã có `isError`, chỉ unwrap đúng.
356
-
357
- ---
358
-
359
- ## Thứ tự gợi ý thực hiện
360
-
361
- 1. **Day 1 — Tier 1 (bug fix + perf)**: #50 → #51
362
- - #50 fix bug subtle có thể impact nhiều screen.
363
- - #51 cache độc lập, không phụ thuộc #50.
364
-
365
- 2. **Day 1.5 — Tier 2 (theme)**: #52 → #53
366
- - #53 nhanh (additive). #52 cần test với mock theme objects.
367
-
368
- 3. **Day 2 — Tier 3 (UX)**: #54 → #55 → #56
369
- - #54 footer độc lập, không break.
370
- - #55 select-list pre-req cho future RunDashboard refactor.
371
- - #56 dynamic-border refactor 3 file (loaders, mascot, dashboard).
372
-
373
- 4. **Day 2 close — Tier 4 (#57)**: tool state styling, kết hợp với existing iconForStatus.
374
-
375
- Toàn bộ Phase 5 ước tính 1.5–2 ngày focus work, **0 dependency mới**.
376
-
377
- ---
378
-
379
- ## Metrics mục tiêu (verification cuối Phase 5)
380
-
381
- - **truncateToVisualLines correctness**: 0 known bug. New tests catch slice-after-merge.
382
- - **visibleWidth perf**: cache hit rate ≥ 80% trong tick loop, regex calls giảm ≥ 5× theo bench.
383
- - **Theme reload latency**: < 200ms từ `onThemeChange` callback tới UI re-render.
384
- - **Footer info density**: RunDashboard footer 2-3 line giống pi-coding-agent.
385
- - **Border consistency**: 1 DynamicCrewBorder thay 3 self-rolled patterns.
386
- - **Test count**: 222 unit → ~234 unit (thêm ~12 test cho 8 task).
387
- - **Type safety**: 0 unsafe theme cast (giữ nguyên Phase 4).
388
- - **Deps mới**: 0.
389
-
390
- ---
391
-
392
- ## Tracking template (per commit message)
393
-
394
- ```
395
- Phase 5 task #<num>: <title>
396
-
397
- <body — what changed, why, refs to source pi-mono>
398
-
399
- Verification: tsc --noEmit OK; test:unit OK; test:integration <OK|N/A>
400
-
401
- Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
402
- ```
1
+ # Phase 5 Refactor Plan — Footer/Selectlist/Hot-reload từ pi-mono coding-agent
2
+
3
+ > Xuất xứ: re-read `source/pi-mono/packages/coding-agent/src/modes/interactive/components/{footer,bordered-loader,dynamic-border,visual-truncate,diff,countdown-timer,extension-selector,theme-selector,custom-message,tool-execution,bash-execution}.ts` + `theme/theme.ts` (28/04/2026).
4
+ > Mục tiêu: vá lỗi subtle còn lại từ Phase 4, hot-reload theme, port footer/select-list pattern, chuẩn hóa border + tool state styling.
5
+ > Phase 4 đã hoàn tất, baseline: tsc 0 errors, 222 unit + 21 integration pass, commit `44fdd02`.
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/runtime).
10
+ - Không thêm dependency runtime mới. Tất cả implement self-contained hoặc qua peer dep `@mariozechner/pi-tui` đã 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
+ - Ưu tiên backward compatibility: default behavior không đổi, opt-in qua config khi có hành vi mới.
13
+
14
+ ## Trạng thái cập nhật
15
+ - [x] Task #50 — Fix `truncateToVisualLines` slice-after-merge bug
16
+ - [x] Task #51 — Memoize `visibleWidth` LRU cache
17
+ - [x] Task #52 — Theme hot-reload subscription
18
+ - [x] Task #53 — Theme adapter `inverse` ANSI fallback
19
+ - [x] Task #54 — `CrewFooter` component port
20
+ - [x] Task #55 — `CrewSelectList` adapter
21
+ - [x] Task #56 — `DynamicCrewBorder` reusable + CountdownTimer 1s tick
22
+ - [x] Task #57 — Tool state styling cho transcript-viewer
23
+ ---
24
+
25
+ ## Tier 1 — Bug fixes & correctness (low risk, immediate value)
26
+
27
+ Mục tiêu: 2 task, vá bug từ Phase 4 + tăng hiệu năng nhỏ. Ước tính: 0.5 ngày.
28
+
29
+ ### Task #50 — Fix `truncateToVisualLines` slice-after-merge bug
30
+ **Source**: `pi-mono/coding-agent/components/visual-truncate.ts`
31
+ **Đích**: `pi-crew/src/utils/visual.ts`
32
+
33
+ **Lý do**: Phase 4 #47 implement `truncateToVisualLines` với logic:
34
+ ```ts
35
+ const visualLines = text.split("\n").flatMap((line) =>
36
+ wrapHard(pad(line, ...).trimEnd(), effectiveWidth).slice(0, Math.max(1, maxVisualLines))
37
+ );
38
+ ```
39
+ Bug: `slice(0, maxVisualLines)` áp dụng **per source line** thay vì **toàn bộ visual lines sau merge**. Nếu 1 source line wrap thành N visual lines (N > maxVisualLines), kết quả lấy đầu line đó, không phải tail của toàn bộ output. Khi nhiều source line, tổng visual có thể vượt maxVisualLines.
40
+
41
+ pi-mono dùng pattern đúng: render rồi `slice(-maxVisualLines)`.
42
+
43
+ **Logic chuẩn**:
44
+ ```ts
45
+ export function truncateToVisualLines(text, maxVisualLines, width, paddingX = 0) {
46
+ if (!text) return { visualLines: [], skippedCount: 0 };
47
+ const effectiveWidth = Math.max(1, width - paddingX * 2);
48
+ const allVisual = text.split("\n").flatMap((line) =>
49
+ wrapHard(pad(line, effectiveWidth).trimEnd(), effectiveWidth)
50
+ );
51
+ if (allVisual.length <= maxVisualLines) return { visualLines: allVisual, skippedCount: 0 };
52
+ return { visualLines: allVisual.slice(-maxVisualLines), skippedCount: allVisual.length - maxVisualLines };
53
+ }
54
+ ```
55
+
56
+ **Acceptance**:
57
+ - 1 source line wrap thành 5 visual lines, maxVisualLines=2 → trả về 2 visual lines cuối + skippedCount=3
58
+ - 3 source lines × 2 visual mỗi line = 6 visual, maxVisualLines=4 → trả về 4 cuối + skippedCount=2
59
+ - empty input → `{ visualLines: [], skippedCount: 0 }` (đổi từ `[""]` về `[]` để khớp pi-mono)
60
+
61
+ **Verification**: 2 unit test mới trong `test/unit/visual.test.ts`. Verify transcript-viewer integration vẫn pass test cũ.
62
+
63
+ **Risk**: thay đổi semantic empty input — kiểm tra all callers (transcript-viewer, run-dashboard) handle `[]` thay vì `[""]`.
64
+
65
+ ---
66
+
67
+ ### Task #51 — Memoize `visibleWidth` qua LRU cache
68
+ **Source**: pattern caching từ pi-tui `utils.ts`
69
+ **Đích**: `pi-crew/src/utils/visual.ts`
70
+
71
+ **Lý do**: `visibleWidth(value)` được gọi trong:
72
+ - `pad`, `truncateToWidth`, `wrapHard` (mỗi character iter)
73
+ - `crew-widget.ts colorWidgetLine` (mỗi line, mỗi tick 250ms)
74
+ - `RunDashboard.render` (5-10 lần per render)
75
+ - Total ước tính: 50+ calls/render × 4 render/sec = 200+ regex ops/sec.
76
+
77
+ Cache key = string identity, value = width. Reset khi cache > 256 entries (FIFO eviction).
78
+
79
+ **API**:
80
+ ```ts
81
+ const widthCache = new Map<string, number>();
82
+ const CACHE_LIMIT = 256;
83
+
84
+ export function visibleWidth(value: string): number {
85
+ const cached = widthCache.get(value);
86
+ if (cached !== undefined) return cached;
87
+ let length = 0;
88
+ for (const char of value.replace(ANSI_PATTERN, "")) {
89
+ if (char !== "\n") length += 1;
90
+ }
91
+ if (widthCache.size >= CACHE_LIMIT) {
92
+ const firstKey = widthCache.keys().next().value;
93
+ if (firstKey !== undefined) widthCache.delete(firstKey);
94
+ }
95
+ widthCache.set(value, length);
96
+ return length;
97
+ }
98
+ ```
99
+
100
+ **Acceptance**:
101
+ - `visibleWidth("foo")` gọi 1000 lần → chỉ tính 1 lần (kiểm qua spy với regex.exec count nếu có Diff bench).
102
+ - Cache không leak: limit 256, sau 1000 unique strings thì size = 256.
103
+ - Output identical với version không cache (regression test).
104
+
105
+ **Verification**:
106
+ - 1 unit test cache hit
107
+ - 1 unit test eviction (insert 257 strings, kiểm size === 256)
108
+ - Bench: `visibleWidth(longString) × 10000` → time giảm ≥ 5× (ms log).
109
+
110
+ **Risk**: cache miss khi string concat/template (mỗi lần object identity khác). Nhận diện qua bench thực tế.
111
+
112
+ ---
113
+
114
+ ## Tier 2 — Theme & style consistency
115
+
116
+ Mục tiêu: 2 task, hot-reload + inverse fallback. Ước tính: 0.5 ngày.
117
+
118
+ ### Task #52 — Theme hot-reload subscription
119
+ **Source**: `pi-mono/coding-agent/theme/theme.ts` `onThemeChange()` + `startThemeWatcher()`
120
+ **Đích**: `pi-crew/src/ui/theme-adapter.ts`, `src/extension/register.ts`
121
+
122
+ **Lý do**: pi-mono có cơ chế watch custom theme JSON, debounce 100ms reload, emit callback. pi-crew adapter chỉ snapshot theme 1 lần ở `ctx.ui.custom((tui, theme, ...) => Component)`. Khi user gõ `/theme dark` từ pi-coding-agent, các pi-crew widget hold theme cũ cho tới khi recreate component.
123
+
124
+ **Approach**:
125
+ 1. Add `subscribeThemeChange(theme: unknown, callback: () => void): () => void` trong theme-adapter.ts. Internally:
126
+ - Test if `theme` object có `addEventListener?.("change", ...)` hoặc `onThemeChange?.(...)` API.
127
+ - Fallback: poll `theme.getColorMode?.()` + key signature mỗi 1s, callback nếu thay đổi.
128
+ 2. CrewWidgetComponent / LiveRunSidebar / RunDashboard / DurableTextViewer: gọi `subscribeThemeChange` trong constructor, store unsubscribe, gọi `this.invalidate()` khi callback fires.
129
+ 3. dispose: unsubscribe.
130
+
131
+ **Acceptance**:
132
+ - Mock theme với `onThemeChange` API → callback fires trong 200ms.
133
+ - Mock theme polling → kiểm callback fires sau 1.1s khi sig thay đổi.
134
+ - Dispose component → no further callback.
135
+
136
+ **Verification**: 2 unit test mock theme objects. Manual test: chạy pi với `/theme light` rồi `/theme dark`, kiểm RunDashboard re-render.
137
+
138
+ **Risk**: polling 1s × N components → overhead. Mitigate: shared global subscription, fan-out tới components qua singleton subscriber list. Implement singleton trong theme-adapter.
139
+
140
+ ---
141
+
142
+ ### Task #53 — Theme adapter `inverse` ANSI fallback
143
+ **Source**: `pi-mono` dùng `chalk.inverse(text)` = `\x1b[7m{text}\x1b[27m`
144
+ **Đích**: `pi-crew/src/ui/theme-adapter.ts`
145
+
146
+ **Lý do**: `asCrewTheme` hiện chỉ pass-through nếu source theme có `inverse`, fallback identity (return text nguyên). render-diff dùng `theme.inverse?.(value) ?? value` → khi theme nguồn không có inverse, intra-line diff highlight bị mất hoàn toàn. Bug visual subtle, không có test catch.
147
+
148
+ **Logic chuẩn**:
149
+ ```ts
150
+ function asInverse(value: unknown): (text: string) => string {
151
+ const fn = asUnaryFn(value);
152
+ if (fn) return fn;
153
+ return (text) => `\u001b[7m${text}\u001b[27m`;
154
+ }
155
+ ```
156
+
157
+ **Acceptance**:
158
+ - `asCrewTheme(undefined).inverse?.("x")` → `"\u001b[7mx\u001b[27m"`.
159
+ - `asCrewTheme(realTheme).inverse?.("x")` → output từ chalk (test bằng `includes("\u001b[7m")`).
160
+ - renderDiff với theme tối giản vẫn highlight inverse lookup.
161
+
162
+ **Verification**: cập nhật `loaders.test.ts`/thêm `theme-adapter.test.ts` 2 test (default fallback + provided theme passthrough).
163
+
164
+ **Risk**: thấp — additive change.
165
+
166
+ ---
167
+
168
+ ## Tier 3 — UX components (port pattern từ pi-mono)
169
+
170
+ Mục tiêu: 3 task, footer + selectlist + dynamic border. Ước tính: 1 ngày.
171
+
172
+ ### Task #54 — `CrewFooter` component port
173
+ **Source**: `pi-mono/coding-agent/components/footer.ts`
174
+ **Đích**: `pi-crew/src/ui/crew-footer.ts` (mới), tích hợp vào `RunDashboard`.
175
+
176
+ **Lý do**: pi-mono Footer là pattern multi-line trang trí (pwd+branch, tokens, context %, model). pi-crew RunDashboard có summary 1 line trộn rời rạc. Port để đồng bộ visual với coding-agent.
177
+
178
+ **Layout (3 lines)**:
179
+ ```
180
+ ~/proj (main) • runId • running (dim)
181
+ ↑in ↓out R cache W cache $cost • 45.3%/200k (dim, % colored)
182
+ [badge1] [badge2] ... (extension statuses)
183
+ ```
184
+
185
+ **API**:
186
+ ```ts
187
+ export interface CrewFooterData {
188
+ pwd: string;
189
+ branch?: string;
190
+ runId?: string;
191
+ status?: RunStatus;
192
+ usage?: UsageState;
193
+ contextWindow?: number;
194
+ contextPercent?: number;
195
+ badges?: string[]; // raw text per extension status
196
+ }
197
+
198
+ export class CrewFooter {
199
+ constructor(private data: CrewFooterData, private theme: CrewTheme) {}
200
+ setData(data: CrewFooterData): void;
201
+ render(width: number): string[];
202
+ invalidate(): void;
203
+ }
204
+ ```
205
+
206
+ **Color logic**:
207
+ - contextPercent > 90 → `theme.fg("error", ...)`
208
+ - > 70 → `theme.fg("warning", ...)`
209
+ - ≤ 70 → no color
210
+
211
+ **Acceptance**:
212
+ - Render cho run với usage tokens → output chứa `↑`, `↓`, `$cost`.
213
+ - Truncate khi width nhỏ → ellipsis `...`.
214
+ - contextPercent NaN/undefined → display `?/window`.
215
+
216
+ **Verification**:
217
+ - `test/unit/crew-footer.test.ts` 4 test (basic render, color thresholds, truncation, missing data).
218
+ - Integrate vào `RunDashboard.renderFooter` (thay phần legacy footer).
219
+
220
+ **Risk**: RunDashboard layout shift — kiểm snapshot lines count với existing tests.
221
+
222
+ ---
223
+
224
+ ### Task #55 — `CrewSelectList` adapter
225
+ **Source**: `@mariozechner/pi-tui` `SelectList` (peer dep) + pi-mono `extension-selector.ts`/`theme-selector.ts` patterns
226
+ **Đích**: `pi-crew/src/ui/crew-select-list.ts`
227
+
228
+ **Lý do**: RunDashboard handle keyboard navigation thủ công (j/k/enter), không có visual highlight selected, không support `onPreview`. pi-tui SelectList có sẵn nhưng pi-crew chưa wrap. Cần adapter để xài SelectList từ peer dep pi-tui (optional dep — kiểm `import { SelectList } from "@mariozechner/pi-tui"` available).
229
+
230
+ **Approach**:
231
+ 1. Detect runtime: `try { require.resolve("@mariozechner/pi-tui"); }` → dùng pi-tui SelectList.
232
+ 2. Fallback: simple list component port từ extension-selector.ts (j/k/↑/↓/enter/esc handlers, highlight ` → ` cho selected).
233
+ 3. API:
234
+ ```ts
235
+ export interface CrewSelectItem<T = string> {
236
+ value: T;
237
+ label: string;
238
+ description?: string;
239
+ }
240
+
241
+ export class CrewSelectList<T = string> {
242
+ constructor(
243
+ items: CrewSelectItem<T>[],
244
+ theme: CrewTheme,
245
+ options: {
246
+ onSelect: (item: CrewSelectItem<T>) => void;
247
+ onCancel: () => void;
248
+ onPreview?: (item: CrewSelectItem<T>) => void;
249
+ maxHeight?: number;
250
+ }
251
+ ) {}
252
+ render(width: number): string[];
253
+ handleInput(data: string): void;
254
+ invalidate(): void;
255
+ setSelectedIndex(i: number): void;
256
+ getSelected(): CrewSelectItem<T> | undefined;
257
+ }
258
+ ```
259
+
260
+ **Acceptance**:
261
+ - Render với 5 items → 5 lines, selected có ` → `.
262
+ - handleInput("j") → selected index +1, callback onPreview fired.
263
+ - handleInput("\n") → callback onSelect with current item.
264
+ - maxHeight=3 với 10 items → scroll, indicator `↑ N more`/`↓ N more`.
265
+
266
+ **Verification**: `test/unit/crew-select-list.test.ts` 5 test.
267
+
268
+ **Risk**: API mismatch nếu pi-tui SelectList API đổi version. Pin behavior qua adapter, fallback always available.
269
+
270
+ ---
271
+
272
+ ### Task #56 — `DynamicCrewBorder` reusable + CountdownTimer 1s tick
273
+ **Source**: `pi-mono/coding-agent/components/dynamic-border.ts` + `countdown-timer.ts`
274
+ **Đích**: `pi-crew/src/ui/dynamic-border.ts` (mới), refactor `loaders.ts`
275
+
276
+ **Lý do**:
277
+ 1. **DynamicBorder**: 10 LOC, render single line `─×width`. pi-crew có 3 nơi tự vẽ border:
278
+ - `loaders.ts CrewBorderedLoader`: `┌─┐│└─┘` static template
279
+ - `mascot.ts`: tự build `╭─╮│╰─╯`
280
+ - `run-dashboard.ts/transcript-viewer.ts`: tự pad border lines
281
+ → Refactor dùng chung `DynamicCrewBorder` cho horizontal lines, giữ corner chars riêng.
282
+ 2. **CountdownTimer 1s tick**: hiện tại tick 250ms (4×/s). pi-mono tick chính xác 1000ms + `tui.requestRender()`. 4× tick là wasteful, gây re-render trùng lặp.
283
+
284
+ **API**:
285
+ ```ts
286
+ // dynamic-border.ts
287
+ export interface DynamicCrewBorderOptions {
288
+ color?: (s: string) => string;
289
+ char?: string; // default "─"
290
+ }
291
+ export class DynamicCrewBorder {
292
+ constructor(theme: CrewTheme, options?: DynamicCrewBorderOptions) {}
293
+ render(width: number): string[];
294
+ invalidate(): void;
295
+ }
296
+ ```
297
+
298
+ CountdownTimer change:
299
+ ```ts
300
+ // trong loaders.ts CountdownTimer
301
+ - this.timer = setInterval(() => { ... }, 250);
302
+ + this.timer = setInterval(() => {
303
+ + const seconds = this.secondsLeft();
304
+ + this.onTick(seconds);
305
+ + if (seconds <= 0) this.emitExpire();
306
+ + }, 1000);
307
+ ```
308
+
309
+ **Acceptance**:
310
+ - DynamicCrewBorder.render(20) → `["─".repeat(20)]` (with color).
311
+ - DynamicCrewBorder dùng trong CrewBorderedLoader, mascot box, run-dashboard separators.
312
+ - CountdownTimer onTick called ~3 lần trong 3.5s (giây 3, 2, 1, 0 không nhiều hơn).
313
+
314
+ **Verification**:
315
+ - 2 unit test cho DynamicCrewBorder (basic render, custom char).
316
+ - Update `loaders.test.ts` CountdownTimer test: kiểm onTick count = ceil(timeoutMs/1000) + 1.
317
+
318
+ **Risk**: mascot CountdownTimer (nếu có) cần điều chỉnh cùng. Visual flicker giảm bằng tick 1s thay 250ms.
319
+
320
+ ---
321
+
322
+ ## Tier 4 — Power features
323
+
324
+ Mục tiêu: 1 task, tool state styling. Ước tính: 0.25 ngày.
325
+
326
+ ### Task #57 — Tool state styling cho transcript-viewer
327
+ **Source**: `pi-mono/coding-agent/components/tool-execution.ts` (toolPendingBg/toolSuccessBg/toolErrorBg state)
328
+ **Đích**: `pi-crew/src/ui/transcript-viewer.ts`
329
+
330
+ **Lý do**: transcript-viewer hiện render `[Tool: name] type` plain text. Không phân biệt:
331
+ - partial vs final result
332
+ - success vs error (`result.isError`)
333
+ - queued vs running
334
+
335
+ User scan transcript khó tìm ra error tool nhanh.
336
+
337
+ **Logic update `formatTranscriptEvent`**:
338
+ ```ts
339
+ const isError = obj.isError === true || asRecord(obj.result)?.isError === true;
340
+ const isPartial = obj.isPartial === true;
341
+ const status: RunStatus = isError ? "failed" : isPartial ? "running" : "completed";
342
+ const icon = iconForStatus(status, { runningGlyph: "⋯" });
343
+ const headerColor = colorForStatus(status);
344
+ const header = theme.fg(headerColor, `${icon} [Tool${toolName ? `: ${toolName}` : ""}] ${type}`);
345
+ ```
346
+
347
+ **Acceptance**:
348
+ - Event với `isError: true` → header có icon `✗`, color `error`.
349
+ - Event với `isPartial: true` → header có icon `⋯`/`▶`, color `accent`.
350
+ - Event normal → icon `✓`, color `success`.
351
+ - Existing tests `formatTranscriptText formats message and tool JSONL into conversation lines` vẫn pass.
352
+
353
+ **Verification**: thêm 2 test cho transcript-viewer (error tool, partial tool).
354
+
355
+ **Risk**: thấp — schema event đã có `isError`, chỉ unwrap đúng.
356
+
357
+ ---
358
+
359
+ ## Thứ tự gợi ý thực hiện
360
+
361
+ 1. **Day 1 — Tier 1 (bug fix + perf)**: #50 → #51
362
+ - #50 fix bug subtle có thể impact nhiều screen.
363
+ - #51 cache độc lập, không phụ thuộc #50.
364
+
365
+ 2. **Day 1.5 — Tier 2 (theme)**: #52 → #53
366
+ - #53 nhanh (additive). #52 cần test với mock theme objects.
367
+
368
+ 3. **Day 2 — Tier 3 (UX)**: #54 → #55 → #56
369
+ - #54 footer độc lập, không break.
370
+ - #55 select-list pre-req cho future RunDashboard refactor.
371
+ - #56 dynamic-border refactor 3 file (loaders, mascot, dashboard).
372
+
373
+ 4. **Day 2 close — Tier 4 (#57)**: tool state styling, kết hợp với existing iconForStatus.
374
+
375
+ Toàn bộ Phase 5 ước tính 1.5–2 ngày focus work, **0 dependency mới**.
376
+
377
+ ---
378
+
379
+ ## Metrics mục tiêu (verification cuối Phase 5)
380
+
381
+ - **truncateToVisualLines correctness**: 0 known bug. New tests catch slice-after-merge.
382
+ - **visibleWidth perf**: cache hit rate ≥ 80% trong tick loop, regex calls giảm ≥ 5× theo bench.
383
+ - **Theme reload latency**: < 200ms từ `onThemeChange` callback tới UI re-render.
384
+ - **Footer info density**: RunDashboard footer 2-3 line giống pi-coding-agent.
385
+ - **Border consistency**: 1 DynamicCrewBorder thay 3 self-rolled patterns.
386
+ - **Test count**: 222 unit → ~234 unit (thêm ~12 test cho 8 task).
387
+ - **Type safety**: 0 unsafe theme cast (giữ nguyên Phase 4).
388
+ - **Deps mới**: 0.
389
+
390
+ ---
391
+
392
+ ## Tracking template (per commit message)
393
+
394
+ ```
395
+ Phase 5 task #<num>: <title>
396
+
397
+ <body — what changed, why, refs to source pi-mono>
398
+
399
+ Verification: tsc --noEmit OK; test:unit OK; test:integration <OK|N/A>
400
+
401
+ Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
402
+ ```