@kkelly-offical/kkcode 0.1.7 → 0.2.3-preview.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/LICENSE +674 -674
  2. package/README.md +474 -387
  3. package/package.json +50 -46
  4. package/src/agent/agent.mjs +228 -220
  5. package/src/agent/custom-agent-loader.mjs +6 -3
  6. package/src/agent/generator.mjs +2 -2
  7. package/src/agent/prompt/assistant.txt +12 -0
  8. package/src/agent/prompt/bug-hunter.txt +89 -89
  9. package/src/agent/prompt/frontend-designer.txt +58 -58
  10. package/src/agent/prompt/guide.txt +1 -1
  11. package/src/agent/prompt/longagent-blueprint-agent.txt +83 -83
  12. package/src/agent/prompt/longagent-coding-agent.txt +37 -37
  13. package/src/agent/prompt/longagent-debugging-agent.txt +46 -46
  14. package/src/agent/prompt/longagent-preview-agent.txt +63 -63
  15. package/src/command/custom-commands.mjs +2 -2
  16. package/src/commands/agent.mjs +1 -1
  17. package/src/commands/background.mjs +145 -4
  18. package/src/commands/chat.mjs +117 -76
  19. package/src/commands/config.mjs +148 -1
  20. package/src/commands/doctor.mjs +30 -6
  21. package/src/commands/init.mjs +32 -6
  22. package/src/commands/longagent.mjs +117 -0
  23. package/src/commands/mcp.mjs +275 -43
  24. package/src/commands/permission.mjs +1 -1
  25. package/src/commands/session.mjs +195 -140
  26. package/src/commands/skill.mjs +63 -0
  27. package/src/commands/theme.mjs +1 -1
  28. package/src/commands/update.mjs +32 -0
  29. package/src/config/defaults.mjs +289 -260
  30. package/src/config/import-config.mjs +1 -1
  31. package/src/config/load-config.mjs +61 -4
  32. package/src/config/schema.mjs +604 -574
  33. package/src/context.mjs +4 -1
  34. package/src/core/constants.mjs +97 -91
  35. package/src/core/types.mjs +1 -1
  36. package/src/github/api.mjs +78 -78
  37. package/src/github/auth.mjs +294 -286
  38. package/src/github/flow.mjs +298 -298
  39. package/src/github/workspace.mjs +225 -212
  40. package/src/index.mjs +87 -82
  41. package/src/knowledge/frontend-aesthetics.txt +38 -38
  42. package/src/mcp/client-http.mjs +139 -141
  43. package/src/mcp/client-sse.mjs +297 -288
  44. package/src/mcp/client-stdio.mjs +534 -533
  45. package/src/mcp/constants.mjs +4 -2
  46. package/src/mcp/registry.mjs +498 -479
  47. package/src/mcp/stdio-framing.mjs +135 -133
  48. package/src/mcp/tool-result.mjs +24 -24
  49. package/src/observability/edit-diagnostics.mjs +449 -0
  50. package/src/observability/index.mjs +42 -42
  51. package/src/observability/metrics.mjs +165 -137
  52. package/src/observability/tracer.mjs +137 -137
  53. package/src/onboarding.mjs +209 -0
  54. package/src/orchestration/background-manager.mjs +567 -372
  55. package/src/orchestration/background-worker.mjs +419 -305
  56. package/src/orchestration/interruption-reason.mjs +21 -0
  57. package/src/orchestration/longagent-manager.mjs +197 -171
  58. package/src/orchestration/stage-scheduler.mjs +733 -728
  59. package/src/orchestration/subagent-router.mjs +7 -1
  60. package/src/orchestration/task-scheduler.mjs +219 -7
  61. package/src/permission/engine.mjs +1 -1
  62. package/src/permission/exec-policy.mjs +370 -370
  63. package/src/permission/file-edit-policy.mjs +108 -0
  64. package/src/permission/prompt.mjs +1 -1
  65. package/src/permission/rules.mjs +116 -7
  66. package/src/plugin/builtin-hooks/post-edit-format.mjs +2 -1
  67. package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +104 -40
  68. package/src/plugin/hook-bus.mjs +19 -5
  69. package/src/plugin/manifest-loader.mjs +222 -0
  70. package/src/provider/anthropic.mjs +396 -390
  71. package/src/provider/ollama.mjs +7 -1
  72. package/src/provider/openai.mjs +382 -340
  73. package/src/provider/retry-policy.mjs +74 -68
  74. package/src/provider/router.mjs +242 -241
  75. package/src/provider/sse.mjs +104 -104
  76. package/src/provider/wizard.mjs +556 -0
  77. package/src/repl/capability-facade.mjs +30 -0
  78. package/src/repl/command-surface.mjs +23 -0
  79. package/src/repl/controller-entry.mjs +40 -0
  80. package/src/repl/core-shell.mjs +208 -0
  81. package/src/repl/dialog-router.mjs +87 -0
  82. package/src/repl/input-engine.mjs +76 -0
  83. package/src/repl/keymap.mjs +7 -0
  84. package/src/repl/operator-surface.mjs +15 -0
  85. package/src/repl/permission-flow.mjs +49 -0
  86. package/src/repl/runtime-facade.mjs +36 -0
  87. package/src/repl/slash-router.mjs +62 -0
  88. package/src/repl/state-store.mjs +29 -0
  89. package/src/repl/turn-controller.mjs +58 -0
  90. package/src/repl/verification.mjs +23 -0
  91. package/src/repl.mjs +3371 -2981
  92. package/src/rules/load-rules.mjs +3 -3
  93. package/src/runtime.mjs +1 -1
  94. package/src/session/agent-transaction.mjs +86 -0
  95. package/src/session/checkpoint.mjs +302 -302
  96. package/src/session/compaction.mjs +298 -298
  97. package/src/session/engine.mjs +417 -232
  98. package/src/session/longagent-4stage.mjs +467 -460
  99. package/src/session/longagent-hybrid.mjs +1344 -1097
  100. package/src/session/longagent-plan.mjs +376 -365
  101. package/src/session/longagent-project-memory.mjs +53 -53
  102. package/src/session/longagent-scaffold.mjs +291 -291
  103. package/src/session/longagent-task-bus.mjs +138 -54
  104. package/src/session/longagent-utils.mjs +828 -472
  105. package/src/session/longagent.mjs +911 -900
  106. package/src/session/loop.mjs +1005 -930
  107. package/src/session/prompt/agent.txt +25 -25
  108. package/src/session/prompt/anthropic.txt +150 -150
  109. package/src/session/prompt/beast.txt +1 -1
  110. package/src/session/prompt/plan.txt +31 -31
  111. package/src/session/prompt/qwen.txt +46 -46
  112. package/src/session/recovery.mjs +21 -0
  113. package/src/session/rollback.mjs +196 -195
  114. package/src/session/routing-observability.mjs +72 -0
  115. package/src/session/runtime-state.mjs +47 -0
  116. package/src/session/store.mjs +523 -519
  117. package/src/session/system-prompt.mjs +308 -273
  118. package/src/session/task-validator.mjs +267 -267
  119. package/src/session/usability-gates.mjs +2 -2
  120. package/src/skill/builtin/commit.mjs +64 -64
  121. package/src/skill/builtin/design.mjs +76 -76
  122. package/src/skill/generator.mjs +18 -2
  123. package/src/skill/registry.mjs +642 -390
  124. package/src/storage/audit-store.mjs +18 -11
  125. package/src/storage/event-log.mjs +7 -1
  126. package/src/storage/ghost-commit-store.mjs +243 -245
  127. package/src/storage/paths.mjs +17 -0
  128. package/src/theme/default-theme.mjs +1 -1
  129. package/src/theme/markdown.mjs +4 -0
  130. package/src/theme/schema.mjs +1 -1
  131. package/src/theme/status-bar.mjs +162 -158
  132. package/src/tool/audit-wrapper.mjs +18 -2
  133. package/src/tool/edit-transaction.mjs +23 -0
  134. package/src/tool/executor.mjs +26 -1
  135. package/src/tool/file-read-state.mjs +65 -0
  136. package/src/tool/git-auto.mjs +526 -526
  137. package/src/tool/git-full-auto.mjs +487 -478
  138. package/src/tool/mutation-guard.mjs +54 -0
  139. package/src/tool/prompt/edit.txt +3 -3
  140. package/src/tool/prompt/multiedit.txt +1 -0
  141. package/src/tool/prompt/notebookedit.txt +2 -1
  142. package/src/tool/prompt/patch.txt +25 -24
  143. package/src/tool/prompt/read.txt +3 -3
  144. package/src/tool/prompt/sysinfo.txt +29 -0
  145. package/src/tool/prompt/task.txt +66 -4
  146. package/src/tool/prompt/write.txt +2 -2
  147. package/src/tool/question-prompt.mjs +99 -93
  148. package/src/tool/registry.mjs +1701 -1343
  149. package/src/tool/task-tool.mjs +14 -6
  150. package/src/ui/activity-renderer.mjs +667 -664
  151. package/src/ui/repl-background-panel.mjs +7 -0
  152. package/src/ui/repl-capability-panel.mjs +9 -0
  153. package/src/ui/repl-dashboard.mjs +54 -4
  154. package/src/ui/repl-help.mjs +110 -0
  155. package/src/ui/repl-operator-panel.mjs +12 -0
  156. package/src/ui/repl-route-feedback.mjs +35 -0
  157. package/src/ui/repl-status-view.mjs +76 -0
  158. package/src/ui/repl-task-panel.mjs +5 -0
  159. package/src/ui/repl-transcript-panel.mjs +56 -0
  160. package/src/ui/repl-turn-summary.mjs +135 -0
  161. package/src/update/checker.mjs +184 -0
  162. package/src/usage/pricing.mjs +122 -121
  163. package/src/usage/usage-meter.mjs +1 -0
  164. package/src/util/git.mjs +562 -519
  165. package/src/util/template.mjs +6 -1
  166. package/src/version.mjs +3 -0
@@ -1,664 +1,667 @@
1
- import { EventBus } from "../core/events.mjs"
2
- import { EVENT_TYPES } from "../core/constants.mjs"
3
- import { paint } from "../theme/color.mjs"
4
-
5
- let _theme = null
6
- function diffAdd() { return _theme?.components?.diff_add || "green" }
7
- function diffDel() { return _theme?.components?.diff_del || "red" }
8
-
9
- // ── Symbols ──────────────────────────────────────────────
10
- export const SYM = {
11
- dot: "●",
12
- dotHollow: "○",
13
- toolOk: "✓",
14
- toolErr: "",
15
- stage: "",
16
- iteration: "",
17
- phase: "",
18
- plan: "",
19
- planDone: "",
20
- recovery: "",
21
- alert: "!",
22
- thinking: "",
23
- thinkingOpen: "",
24
- search: "*",
25
- arrow: "",
26
- write: ""
27
- }
28
-
29
- // ── Helpers ──────────────────────────────────────────────
30
-
31
- function clipText(text, max) {
32
- const s = String(text || "").trim()
33
- if (s.length <= max) return s
34
- return s.slice(0, max - 3) + "..."
35
- }
36
-
37
- function shortPath(p) {
38
- const s = String(p || "").trim()
39
- // Show last 2-3 segments for readability
40
- const parts = s.replace(/\\/g, "/").split("/")
41
- if (parts.length <= 3) return s
42
- return ".../" + parts.slice(-3).join("/")
43
- }
44
-
45
- // ── Tool Display Formatters ──────────────────────────────
46
-
47
- export function formatToolStart(toolName, args) {
48
- // Compact single-line dim format (OpenCode style)
49
- const sym = toolName === "grep" || toolName === "glob" || toolName === "websearch"
50
- ? SYM.search
51
- : toolName === "write" || toolName === "edit" || toolName === "notebookedit"
52
- ? SYM.write
53
- : SYM.arrow
54
- const prefix = paint(sym, "#666666")
55
- const name = paint(toolName.charAt(0).toUpperCase() + toolName.slice(1), null, { dim: true })
56
-
57
- switch (toolName) {
58
- case "bash": {
59
- const desc = clipText(args?.description || args?.command, 80)
60
- return ` ${prefix} ${name} ${paint(desc, null, { dim: true })}`
61
- }
62
- case "write":
63
- case "edit":
64
- return ` ${prefix} ${name} ${paint(shortPath(args?.path), null, { dim: true })}`
65
- case "notebookedit":
66
- return ` ${prefix} ${name} ${paint(shortPath(args?.path), null, { dim: true })} ${paint(`cell ${args?.cell_number ?? 0}`, null, { dim: true })}`
67
- case "read":
68
- case "list":
69
- return ` ${prefix} ${name} ${paint(shortPath(args?.path || "."), null, { dim: true })}`
70
- case "grep":
71
- case "glob":
72
- return ` ${prefix} ${name} ${paint(clipText(args?.pattern, 60), null, { dim: true })}`
73
- case "task":
74
- return ` ${prefix} ${name} ${paint(clipText(args?.description || args?.prompt, 60), null, { dim: true })}`
75
- case "todowrite":
76
- return null // handled by result preview only
77
- case "webfetch":
78
- return ` ${prefix} ${name} ${paint(clipText(args?.url, 60), null, { dim: true })}`
79
- case "websearch":
80
- return ` ${prefix} ${name} ${paint(clipText(args?.query, 60), null, { dim: true })}`
81
- case "question":
82
- return ` ~ ${paint("Asking questions...", null, { dim: true })}`
83
- case "enter_plan":
84
- return ` ${paint(SYM.plan, "magenta")} ${paint("Enter Plan", "magenta")}`
85
- case "exit_plan":
86
- return ` ${paint(SYM.planDone, "green")} ${paint("Submit Plan", "green")}`
87
- default:
88
- return ` ${prefix} ${name} ${paint(clipText(args ? Object.keys(args).slice(0, 3).join(", ") : "", 40), null, { dim: true })}`
89
- }
90
- }
91
-
92
- export function formatToolFinish(toolName, status, durationMs, args) {
93
- if (status === "error") {
94
- return ` ${paint(SYM.toolErr, "red")} ${paint(toolName, null, { dim: true })} ${paint("error", "red")}${durationMs ? paint(` ${durationMs}ms`, null, { dim: true }) : ""}`
95
- }
96
- // For completed tools, return null — the start line + result preview is enough
97
- return null
98
- }
99
-
100
- export function formatToolResultPreview(toolName, output, status, args) {
101
- if (status !== "completed") return null
102
- const text = String(output || "").trim()
103
-
104
- switch (toolName) {
105
- case "bash": {
106
- const lines = text.split("\n").filter(Boolean)
107
- if (!lines.length) return null
108
- const first = clipText(lines[0], 90)
109
- const suffix = lines.length > 1 ? paint(` (+${lines.length - 1} lines)`, null, { dim: true }) : ""
110
- return ` ${paint(first, null, { dim: true })}${suffix}`
111
- }
112
- case "write": {
113
- const n = String(args?.content || "").split("\n").filter(Boolean).length
114
- return ` ${paint(`+${n} lines`, diffAdd(), { dim: true })}`
115
- }
116
- case "edit": {
117
- const added = String(args?.new_string || "").split("\n").filter(Boolean).length
118
- const removed = String(args?.old_string || "").split("\n").filter(Boolean).length
119
- const parts = []
120
- if (added > 0) parts.push(paint(`+${added}`, diffAdd()))
121
- if (removed > 0) parts.push(paint(`-${removed}`, diffDel()))
122
- return parts.length ? ` ${parts.join(" ")} ${paint("lines", null, { dim: true })}` : null
123
- }
124
- case "grep": {
125
- const lines = text.split("\n").filter(Boolean)
126
- if (text === "no matches" || !lines.length) return ` ${paint("no matches", null, { dim: true })}`
127
- return ` ${paint(`${lines.length} matches`, null, { dim: true })}`
128
- }
129
- case "read":
130
- return ` ${paint(`${text.split("\n").length} lines`, null, { dim: true })}`
131
- case "glob": {
132
- const lines = text.split("\n").filter(Boolean)
133
- if (!lines.length) return ` ${paint("no files", null, { dim: true })}`
134
- return ` ${paint(`${lines.length} files`, null, { dim: true })}`
135
- }
136
- case "todowrite": {
137
- const todos = Array.isArray(args?.todos) ? args.todos : []
138
- if (!todos.length) return null
139
- const result = []
140
- for (const t of todos.slice(0, 8)) {
141
- const s = t.status || "pending"
142
- const dot = s === "completed" ? paint(SYM.toolOk, "green")
143
- : s === "in_progress" ? paint(SYM.dot, "yellow")
144
- : paint(SYM.dotHollow, "#666666")
145
- const color = s === "completed" ? "green" : s === "in_progress" ? "yellow" : null
146
- const label = s === "in_progress" && t.activeForm ? t.activeForm : t.content
147
- result.push(` ${dot} ${paint(label || "", color, { dim: s === "completed" })}`)
148
- }
149
- if (todos.length > 8) result.push(paint(` ... +${todos.length - 8} more`, null, { dim: true }))
150
- return result
151
- }
152
- default:
153
- return null
154
- }
155
- }
156
-
157
- function formatToolError(error) {
158
- if (!error) return null
159
- return ` ${paint(clipText(error, 120), "red", { dim: true })}`
160
- }
161
-
162
- // ── Thinking Formatter ──────────────────────────────────
163
-
164
- export function formatThinkingHeader() {
165
- return `${paint(SYM.dot, "#666666")} ${paint("Thinking", null, { italic: true, dim: true })} ${paint("∨", null, { dim: true })}`
166
- }
167
-
168
- // ── LongAgent Display Formatters ─────────────────────────
169
-
170
- export function formatPhaseChange(prevPhase, nextPhase, reason) {
171
- const arrow = paint("→", null, { dim: true })
172
- const reasonText = reason ? paint(reason, null, { dim: true }) : ""
173
- return `${paint(SYM.phase, "magenta")} ${paint("phase", "magenta", { bold: true })} ${paint(prevPhase, null, { dim: true })} ${arrow} ${paint(nextPhase, "magenta", { bold: true })} ${reasonText}`
174
- }
175
-
176
- export function formatStageStarted(stageId, taskCount) {
177
- return `${paint(SYM.stage, "#fb923c", { bold: true })} ${paint("stage", "#fb923c", { bold: true })} ${paint(stageId, "white", { bold: true })} ${paint(`(${taskCount} tasks)`, null, { dim: true })}`
178
- }
179
-
180
- export function formatStageFinished(stageId, successCount, failCount) {
181
- const status = failCount === 0
182
- ? paint("PASS", "green", { bold: true })
183
- : paint(`FAIL (${failCount})`, "red", { bold: true })
184
- return `${paint(SYM.stage, "#fb923c")} ${paint("stage", "#fb923c")} ${paint(stageId, "white")} ${status} ${paint(`(${successCount} ok)`, null, { dim: true })}`
185
- }
186
-
187
- export function formatTaskDispatched(_stageId, taskId, attempt) {
188
- const attemptLabel = attempt > 1 ? paint(` retry#${attempt}`, "yellow") : ""
189
- return ` ${paint(SYM.dot, "#666666")} ${paint("task", "cyan")} ${paint(taskId, null, { dim: true })}${attemptLabel}`
190
- }
191
-
192
- export function formatTaskFinished(taskId, status) {
193
- const dot = status === "completed" ? paint(SYM.dot, "green") : paint(SYM.dot, "red")
194
- const color = status === "completed" ? "green" : "red"
195
- return ` ${dot} ${paint(taskId, null, { dim: true })} ${paint(status, color)}`
196
- }
197
-
198
- export function formatHeartbeat(iteration, maxIterations, phase, gate, progress, elapsed) {
199
- const iterLabel = maxIterations > 0 ? `${iteration}/${maxIterations}` : String(iteration)
200
- const progressLabel = progress?.percentage !== null && progress?.percentage !== undefined
201
- ? paint(`${progress.percentage}%`, "green")
202
- : paint("...", null, { dim: true })
203
- const elapsedLabel = elapsed !== undefined ? paint(`${elapsed}s`, null, { dim: true }) : ""
204
- return `${paint(SYM.iteration, "#fb923c")} ${paint("iter", "#fb923c")} ${paint(iterLabel, "white", { bold: true })} phase=${paint(phase || "-", "magenta")} gate=${paint(gate || "-", "cyan")} progress=${progressLabel} ${elapsedLabel}`
205
- }
206
-
207
- export function formatPlanFrozen(planId, stageCount) {
208
- return `${paint(SYM.planDone, "green", { bold: true })} ${paint("plan frozen", "green", { bold: true })} ${paint(planId || "", null, { dim: true })} ${paint(`${stageCount} stage(s)`, null, { dim: true })}`
209
- }
210
-
211
- export function formatRecovery(reason, recoveryCount) {
212
- return `${paint(SYM.recovery, "yellow", { bold: true })} ${paint("recovery", "yellow", { bold: true })} #${recoveryCount} ${paint(reason || "", null, { dim: true })}`
213
- }
214
-
215
- export function formatAlert(kind, message) {
216
- return `${paint(SYM.alert, "red", { bold: true })} ${paint("alert", "red", { bold: true })} [${kind}] ${paint(message || "", null, { dim: true })}`
217
- }
218
-
219
- export function formatIntakeStarted(objective) {
220
- const preview = clipText(objective, 80)
221
- return `${paint(SYM.phase, "magenta")} ${paint("intake", "magenta", { bold: true })} ${paint(preview, null, { dim: true })}`
222
- }
223
-
224
- export function formatGateChecked(gate, status) {
225
- const dot = status === "pass" ? paint(SYM.dot, "green") : paint(SYM.dot, "yellow")
226
- return ` ${dot} gate=${paint(gate || "-", "cyan")} ${paint(status || "-", status === "pass" ? "green" : "yellow")}`
227
- }
228
-
229
- // ── Hybrid Stage Formatters ──────────────────────────────
230
-
231
- function hybridBanner(label, color) {
232
- const line = paint("━".repeat(40), color, { dim: true })
233
- return `${line}\n${paint(label, color, { bold: true })}`
234
- }
235
-
236
- export function formatHybridPreviewStart(objective) {
237
- const preview = clipText(objective, 70)
238
- return `${hybridBanner("H1 Preview", "#3b82f6")}\n ${paint(preview, null, { dim: true })}`
239
- }
240
-
241
- export function formatHybridPreviewComplete(findingsLength) {
242
- return ` ${paint(SYM.toolOk, "green")} ${paint("preview complete", "green")} ${paint(`(${findingsLength} chars)`, null, { dim: true })}`
243
- }
244
-
245
- export function formatHybridBlueprintStart() {
246
- return hybridBanner("H2 Blueprint", "#a855f7")
247
- }
248
-
249
- export function formatHybridBlueprintComplete(planId, stageCount) {
250
- return ` ${paint(SYM.toolOk, "green")} ${paint("blueprint complete", "green")} ${paint(planId || "", null, { dim: true })} ${paint(`${stageCount} stage(s)`, null, { dim: true })}`
251
- }
252
-
253
- export function formatHybridBlueprintReview(planId) {
254
- return ` ${paint("⏳", "yellow")} ${paint("awaiting blueprint review", "yellow")} ${paint(planId || "", null, { dim: true })}`
255
- }
256
-
257
- export function formatHybridBlueprintValidated(totalTasks, totalFiles, valid) {
258
- const status = valid ? paint("PASS", "green") : paint("WARN", "yellow")
259
- return ` ${paint(SYM.dot, valid ? "green" : "yellow")} ${paint("blueprint validation", null, { dim: true })} ${status} ${paint(`${totalTasks} tasks, ${totalFiles} files`, null, { dim: true })}`
260
- }
261
-
262
- // ── Hybrid Debugging/Rollback Formatters ─────────────────
263
-
264
- export function formatHybridDebuggingStart(codingRollbackCount) {
265
- const suffix = codingRollbackCount > 0 ? ` ${paint(`(rollback #${codingRollbackCount})`, "yellow")}` : ""
266
- return `${hybridBanner("H5 Debugging", "#fb923c")}${suffix}`
267
- }
268
-
269
- export function formatHybridDebuggingComplete(debugIter, rollback) {
270
- const status = rollback
271
- ? paint("ROLLBACK", "yellow", { bold: true })
272
- : paint("PASS", "green", { bold: true })
273
- return ` ${paint(SYM.dot, rollback ? "yellow" : "green")} ${paint("debugging", null, { dim: true })} ${status} ${paint(`(${debugIter} iters)`, null, { dim: true })}`
274
- }
275
-
276
- export function formatHybridReturnToCoding(rollbackCount, failedTaskIds) {
277
- const tasks = failedTaskIds?.length ? paint(` [${failedTaskIds.join(", ")}]`, null, { dim: true }) : ""
278
- return ` ${paint(SYM.recovery, "yellow")} ${paint(`rollback to coding #${rollbackCount}`, "yellow")}${tasks}`
279
- }
280
-
281
- export function formatHybridCrossReview(fileCount) {
282
- return ` ${paint(SYM.dot, "cyan")} ${paint("cross-review", "cyan")} ${paint(`${fileCount} file(s)`, null, { dim: true })}`
283
- }
284
-
285
- // ── Hybrid Incremental/Budget/Context Formatters ─────────
286
-
287
- export function formatHybridIncrementalGate(stageId, passed) {
288
- const dot = passed ? paint(SYM.dot, "green") : paint(SYM.dot, "yellow")
289
- const status = passed ? paint("pass", "green") : paint("warn", "yellow")
290
- return ` ${dot} ${paint("gate", null, { dim: true })} ${paint(stageId, "cyan")} ${status}`
291
- }
292
-
293
- export function formatHybridContextCompressed(newLength) {
294
- return ` ${paint(SYM.dot, "#666666")} ${paint(`context compressed → ${newLength} chars`, null, { dim: true })}`
295
- }
296
-
297
- export function formatHybridBudgetWarning(totalTokens, budgetLimit, percentage) {
298
- const color = percentage >= 100 ? "red" : "yellow"
299
- return ` ${paint(SYM.alert, color)} ${paint("budget", color, { bold: true })} ${paint(`${percentage}%`, color)} ${paint(`(${totalTokens}/${budgetLimit})`, null, { dim: true })}`
300
- }
301
-
302
- export function formatHybridCheckpointResumed(stageIndex, iteration) {
303
- return ` ${paint(SYM.dot, "cyan")} ${paint("checkpoint resumed", "cyan")} ${paint(`stage ${stageIndex}, iter ${iteration}`, null, { dim: true })}`
304
- }
305
-
306
- export function formatHybridReplan(newStageCount) {
307
- return ` ${paint(SYM.dot, "#a855f7")} ${paint("replan", "#a855f7", { bold: true })} ${paint(`→ ${newStageCount} stage(s)`, null, { dim: true })}`
308
- }
309
-
310
- // ── Hybrid Memory Formatters ─────────────────────────────
311
-
312
- export function formatHybridMemoryLoaded(techStack) {
313
- const items = Array.isArray(techStack) ? techStack.slice(0, 5).join(", ") : ""
314
- return ` ${paint(SYM.dot, "#666666")} ${paint("memory loaded", null, { dim: true })} ${paint(items, null, { dim: true })}`
315
- }
316
-
317
- export function formatHybridMemorySaved(techStackCount) {
318
- return ` ${paint(SYM.dot, "#666666")} ${paint(`memory saved (${techStackCount} items)`, null, { dim: true })}`
319
- }
320
-
321
- // ── Git Formatters ───────────────────────────────────────
322
-
323
- export function formatGitBranchCreated(branch, baseBranch) {
324
- return ` ${paint(SYM.dot, "green")} ${paint("git branch", "green")} ${paint(branch, "white", { bold: true })} ${paint(`← ${baseBranch}`, null, { dim: true })}`
325
- }
326
-
327
- export function formatGitStageCommitted(stageId, message) {
328
- return ` ${paint(SYM.dot, "#666666")} ${paint("commit", null, { dim: true })} ${paint(clipText(message || stageId, 60), null, { dim: true })}`
329
- }
330
-
331
- export function formatGitMerged(branch, baseBranch) {
332
- return ` ${paint(SYM.toolOk, "green")} ${paint("git merged", "green", { bold: true })} ${paint(branch, null, { dim: true })} ${paint("→", null, { dim: true })} ${paint(baseBranch, "white")}`
333
- }
334
-
335
- // ── Plan Progress Formatter ──────────────────────────────
336
-
337
- export function formatPlanProgress(taskProgress) {
338
- if (!taskProgress || typeof taskProgress !== "object") return []
339
- const entries = Object.entries(taskProgress)
340
- if (!entries.length) return []
341
-
342
- const lines = [paint("Plan Progress:", "cyan", { bold: true })]
343
- for (const [taskId, tp] of entries) {
344
- const status = tp?.status || "pending"
345
- const dot = status === "completed"
346
- ? paint(SYM.dot, "green")
347
- : status === "error"
348
- ? paint(SYM.dot, "red")
349
- : paint(SYM.dotHollow, "#666666")
350
- const color = status === "completed" ? "green" : status === "error" ? "red" : "white"
351
- lines.push(` ${dot} ${taskId} ${paint(status, color)}`)
352
- }
353
- return lines
354
- }
355
-
356
- // ── Recovery Suggestions Formatter ───────────────────────
357
-
358
- export function formatRecoverySuggestions(recovery) {
359
- if (!recovery) return []
360
- const lines = []
361
- lines.push(paint("Recovery Suggestions:", "yellow", { bold: true }))
362
-
363
- if (recovery.summary) {
364
- lines.push(` ${paint(recovery.summary, null, { dim: true })}`)
365
- }
366
-
367
- if (recovery.suggestions?.length) {
368
- for (const s of recovery.suggestions) {
369
- lines.push(` ${paint(SYM.alert, "yellow")} ${paint(s, "yellow")}`)
370
- }
371
- }
372
-
373
- if (recovery.failedTasks?.length) {
374
- lines.push(paint(" Failed Tasks:", "red"))
375
- for (const t of recovery.failedTasks.slice(0, 5)) {
376
- lines.push(` ${paint(SYM.dot, "red")} ${t.taskId} [${t.category}]: ${paint(t.error || "", null, { dim: true })}`)
377
- }
378
- }
379
-
380
- if (recovery.manualSteps?.length) {
381
- lines.push(paint(" Manual Steps:", "cyan"))
382
- for (const step of recovery.manualSteps.slice(0, 5)) {
383
- lines.push(` ${paint(SYM.arrow, "cyan")} ${paint(step, null, { dim: true })}`)
384
- }
385
- }
386
-
387
- if (recovery.resumeHint) {
388
- lines.push(` ${paint(SYM.dot, "green")} ${paint(recovery.resumeHint, "green")}`)
389
- }
390
-
391
- return lines
392
- }
393
-
394
- // ── Renderer ─────────────────────────────────────────────
395
-
396
- export function createActivityRenderer({ output, theme = null }) {
397
- _theme = theme
398
- const log = typeof output?.appendLog === "function"
399
- ? output.appendLog
400
- : (text) => console.log(text)
401
-
402
- const toolTimers = new Map()
403
- let timerCounter = 0
404
- let unsubscribe = null
405
-
406
- function timerKey(sessionId, turnId, toolName) {
407
- return `${sessionId || ""}:${turnId || ""}:${toolName}:${timerCounter++}`
408
- }
409
-
410
- // Track the latest timer key per tool invocation
411
- const activeToolKeys = new Map()
412
- // Track tool args for finish formatting
413
- const activeToolArgs = new Map()
414
-
415
- function handleEvent(event) {
416
- const { type, payload, sessionId, turnId } = event
417
-
418
- switch (type) {
419
- case EVENT_TYPES.TOOL_START: {
420
- const key = timerKey(sessionId, turnId, payload.tool)
421
- const lookupKey = `${sessionId}:${turnId}:${payload.tool}`
422
- toolTimers.set(key, Date.now())
423
- activeToolKeys.set(lookupKey, key)
424
- activeToolArgs.set(lookupKey, payload.args)
425
- // Show tool call inline (compact dim line)
426
- const startLine = formatToolStart(payload.tool, payload.args)
427
- if (startLine) log(startLine)
428
- break
429
- }
430
-
431
- case EVENT_TYPES.TOOL_FINISH: {
432
- const lookupKey = `${sessionId}:${turnId}:${payload.tool}`
433
- const key = activeToolKeys.get(lookupKey)
434
- const savedArgs = activeToolArgs.get(lookupKey) || payload.args
435
- if (key) {
436
- toolTimers.delete(key)
437
- activeToolKeys.delete(lookupKey)
438
- activeToolArgs.delete(lookupKey)
439
- }
440
- const finishLine = formatToolFinish(payload.tool, payload.status, 0, savedArgs)
441
- if (finishLine) log(finishLine)
442
- const preview = formatToolResultPreview(payload.tool, payload.output, payload.status, savedArgs)
443
- if (preview) {
444
- if (Array.isArray(preview)) {
445
- for (const line of preview) log(line)
446
- } else {
447
- log(preview)
448
- }
449
- }
450
- // Blank line after tool for visual spacing
451
- if (payload.tool !== "todowrite") log("")
452
- break
453
- }
454
-
455
- case EVENT_TYPES.TOOL_ERROR: {
456
- const lookupKey = `${sessionId}:${turnId}:${payload.tool}`
457
- const key = activeToolKeys.get(lookupKey)
458
- const savedArgs = activeToolArgs.get(lookupKey) || payload.args
459
- if (key) {
460
- toolTimers.delete(key)
461
- activeToolKeys.delete(lookupKey)
462
- activeToolArgs.delete(lookupKey)
463
- }
464
- log(formatToolFinish(payload.tool, payload.status || "error", 0, savedArgs))
465
- const errLine = formatToolError(payload.error)
466
- if (errLine) log(errLine)
467
- break
468
- }
469
-
470
- case EVENT_TYPES.LONGAGENT_PHASE_CHANGED: {
471
- log(formatPhaseChange(payload.prevPhase, payload.nextPhase, payload.reason))
472
- break
473
- }
474
-
475
- case EVENT_TYPES.LONGAGENT_STAGE_STARTED: {
476
- log(formatStageStarted(payload.stageId, payload.taskCount))
477
- break
478
- }
479
-
480
- case EVENT_TYPES.LONGAGENT_STAGE_FINISHED: {
481
- log(formatStageFinished(payload.stageId, payload.successCount, payload.failCount))
482
- break
483
- }
484
-
485
- case EVENT_TYPES.LONGAGENT_STAGE_TASK_DISPATCHED: {
486
- log(formatTaskDispatched(payload.stageId, payload.taskId, payload.attempt))
487
- break
488
- }
489
-
490
- case EVENT_TYPES.LONGAGENT_STAGE_TASK_FINISHED: {
491
- log(formatTaskFinished(payload.taskId, payload.status))
492
- break
493
- }
494
-
495
- case EVENT_TYPES.LONGAGENT_HEARTBEAT: {
496
- log(formatHeartbeat(
497
- payload.iteration,
498
- payload.maxIterations,
499
- payload.phase,
500
- payload.gate,
501
- payload.progress,
502
- payload.elapsed
503
- ))
504
- break
505
- }
506
-
507
- case EVENT_TYPES.LONGAGENT_PLAN_FROZEN: {
508
- log(formatPlanFrozen(payload.planId, payload.stageCount))
509
- break
510
- }
511
-
512
- case EVENT_TYPES.LONGAGENT_RECOVERY_ENTERED: {
513
- log(formatRecovery(payload.reason, payload.recoveryCount))
514
- break
515
- }
516
-
517
- case EVENT_TYPES.LONGAGENT_ALERT: {
518
- log(formatAlert(payload.kind, payload.message))
519
- break
520
- }
521
-
522
- case EVENT_TYPES.LONGAGENT_INTAKE_STARTED: {
523
- log(formatIntakeStarted(payload.objective))
524
- break
525
- }
526
-
527
- case EVENT_TYPES.LONGAGENT_GATE_CHECKED: {
528
- log(formatGateChecked(payload.gate, payload.status))
529
- break
530
- }
531
-
532
- // ── Hybrid Events ──────────────────────────────
533
- case EVENT_TYPES.LONGAGENT_HYBRID_PREVIEW_START: {
534
- log(formatHybridPreviewStart(payload.objective))
535
- break
536
- }
537
- case EVENT_TYPES.LONGAGENT_HYBRID_PREVIEW_COMPLETE: {
538
- log(formatHybridPreviewComplete(payload.findingsLength))
539
- break
540
- }
541
- case EVENT_TYPES.LONGAGENT_HYBRID_BLUEPRINT_START: {
542
- log(formatHybridBlueprintStart())
543
- break
544
- }
545
- case EVENT_TYPES.LONGAGENT_HYBRID_BLUEPRINT_COMPLETE: {
546
- log(formatHybridBlueprintComplete(payload.planId, payload.stageCount))
547
- break
548
- }
549
- case EVENT_TYPES.LONGAGENT_HYBRID_BLUEPRINT_REVIEW: {
550
- log(formatHybridBlueprintReview(payload.planId))
551
- break
552
- }
553
- case EVENT_TYPES.LONGAGENT_HYBRID_BLUEPRINT_VALIDATED: {
554
- log(formatHybridBlueprintValidated(payload.totalTasks, payload.totalFiles, payload.valid))
555
- break
556
- }
557
- case EVENT_TYPES.LONGAGENT_HYBRID_DEBUGGING_START: {
558
- log(formatHybridDebuggingStart(payload.codingRollbackCount))
559
- break
560
- }
561
- case EVENT_TYPES.LONGAGENT_HYBRID_DEBUGGING_COMPLETE: {
562
- log(formatHybridDebuggingComplete(payload.debugIter, payload.rollback))
563
- break
564
- }
565
- case EVENT_TYPES.LONGAGENT_HYBRID_RETURN_TO_CODING: {
566
- log(formatHybridReturnToCoding(payload.rollbackCount, payload.failedTaskIds))
567
- break
568
- }
569
- case EVENT_TYPES.LONGAGENT_HYBRID_CROSS_REVIEW: {
570
- log(formatHybridCrossReview(payload.fileCount))
571
- break
572
- }
573
- case EVENT_TYPES.LONGAGENT_HYBRID_INCREMENTAL_GATE: {
574
- log(formatHybridIncrementalGate(payload.stageId, payload.passed))
575
- break
576
- }
577
- case EVENT_TYPES.LONGAGENT_HYBRID_CONTEXT_COMPRESSED: {
578
- log(formatHybridContextCompressed(payload.newLength))
579
- break
580
- }
581
- case EVENT_TYPES.LONGAGENT_HYBRID_BUDGET_WARNING: {
582
- log(formatHybridBudgetWarning(payload.totalTokens, payload.budgetLimit, payload.percentage))
583
- break
584
- }
585
- case EVENT_TYPES.LONGAGENT_HYBRID_CHECKPOINT_RESUMED: {
586
- log(formatHybridCheckpointResumed(payload.stageIndex, payload.iteration))
587
- break
588
- }
589
- case EVENT_TYPES.LONGAGENT_HYBRID_REPLAN: {
590
- log(formatHybridReplan(payload.newStageCount))
591
- break
592
- }
593
- case EVENT_TYPES.LONGAGENT_HYBRID_MEMORY_LOADED: {
594
- log(formatHybridMemoryLoaded(payload.techStack))
595
- break
596
- }
597
- case EVENT_TYPES.LONGAGENT_HYBRID_MEMORY_SAVED: {
598
- log(formatHybridMemorySaved(payload.techStackCount))
599
- break
600
- }
601
-
602
- // ── New Fault Recovery Events ──────────────────
603
- case EVENT_TYPES.LONGAGENT_DEGRADATION_APPLIED: {
604
- log(formatAlert("degradation", `${payload.strategy} applied in ${payload.phase}${payload.reason ? ` (${payload.reason})` : ""}`))
605
- break
606
- }
607
- case EVENT_TYPES.LONGAGENT_WRITE_LOOP_DETECTED: {
608
- log(formatAlert("write_loop", payload.message || "write loop detected"))
609
- break
610
- }
611
- case EVENT_TYPES.LONGAGENT_SEMANTIC_ERROR_REPEATED: {
612
- log(formatAlert("semantic_error", `repeated ${payload.count}x: ${(payload.error || "").slice(0, 80)}`))
613
- break
614
- }
615
- case EVENT_TYPES.LONGAGENT_PHASE_TIMEOUT: {
616
- log(formatAlert("phase_timeout", `${payload.phase} timed out after ${Math.round((payload.elapsed || 0) / 1000)}s`))
617
- break
618
- }
619
- case EVENT_TYPES.LONGAGENT_GIT_CONFLICT_RESOLUTION: {
620
- log(formatAlert("git_conflict", `resolving conflicts in ${(payload.files || []).length} file(s)`))
621
- break
622
- }
623
- case EVENT_TYPES.LONGAGENT_CHECKPOINT_CLEANED: {
624
- log(` ${paint(SYM.dot, "#666666")} ${paint(`checkpoints cleaned (${payload.removed} removed)`, null, { dim: true })}`)
625
- break
626
- }
627
-
628
- // ── Git Events ─────────────────────────────────
629
- case EVENT_TYPES.LONGAGENT_GIT_BRANCH_CREATED: {
630
- log(formatGitBranchCreated(payload.branch, payload.baseBranch))
631
- break
632
- }
633
- case EVENT_TYPES.LONGAGENT_GIT_STAGE_COMMITTED: {
634
- log(formatGitStageCommitted(payload.stageId, payload.message))
635
- break
636
- }
637
- case EVENT_TYPES.LONGAGENT_GIT_MERGED: {
638
- log(formatGitMerged(payload.branch, payload.baseBranch))
639
- break
640
- }
641
-
642
- case EVENT_TYPES.SESSION_COMPACTED: {
643
- log(`${paint(SYM.phase, "magenta")} ${paint("context compacted", "magenta", { dim: true })}`)
644
- break
645
- }
646
- }
647
- }
648
-
649
- return {
650
- start() {
651
- if (unsubscribe) return
652
- unsubscribe = EventBus.subscribe(handleEvent)
653
- },
654
- stop() {
655
- if (unsubscribe) {
656
- unsubscribe()
657
- unsubscribe = null
658
- }
659
- toolTimers.clear()
660
- activeToolKeys.clear()
661
- activeToolArgs.clear()
662
- }
663
- }
664
- }
1
+ import { EventBus } from "../core/events.mjs"
2
+ import { EVENT_TYPES } from "../core/constants.mjs"
3
+ import { paint } from "../theme/color.mjs"
4
+
5
+ const ANSI_RE = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\x07]*\x07)/g
6
+ function stripAnsi(text) { return String(text || "").replace(ANSI_RE, "") }
7
+
8
+ let _theme = null
9
+ function diffAdd(theme) { return (theme ?? _theme)?.components?.diff_add || "green" }
10
+ function diffDel(theme) { return (theme ?? _theme)?.components?.diff_del || "red" }
11
+
12
+ // ── Symbols ──────────────────────────────────────────────
13
+ export const SYM = {
14
+ dot: "",
15
+ dotHollow: "",
16
+ toolOk: "",
17
+ toolErr: "",
18
+ stage: "",
19
+ iteration: "",
20
+ phase: "",
21
+ plan: "",
22
+ planDone: "",
23
+ recovery: "",
24
+ alert: "!",
25
+ thinking: "",
26
+ thinkingOpen: "",
27
+ search: "*",
28
+ arrow: "→",
29
+ write: "◇"
30
+ }
31
+
32
+ // ── Helpers ──────────────────────────────────────────────
33
+
34
+ function clipText(text, max) {
35
+ const s = String(text || "").trim()
36
+ if (s.length <= max) return s
37
+ return s.slice(0, max - 3) + "..."
38
+ }
39
+
40
+ function shortPath(p) {
41
+ const s = String(p || "").trim()
42
+ // Show last 2-3 segments for readability
43
+ const parts = s.replace(/\\/g, "/").split("/")
44
+ if (parts.length <= 3) return s
45
+ return ".../" + parts.slice(-3).join("/")
46
+ }
47
+
48
+ // ── Tool Display Formatters ──────────────────────────────
49
+
50
+ export function formatToolStart(toolName, args) {
51
+ // Compact single-line dim format (OpenCode style)
52
+ const sym = toolName === "grep" || toolName === "glob" || toolName === "websearch"
53
+ ? SYM.search
54
+ : toolName === "write" || toolName === "edit" || toolName === "notebookedit"
55
+ ? SYM.write
56
+ : SYM.arrow
57
+ const prefix = paint(sym, "#666666")
58
+ const name = paint(toolName.charAt(0).toUpperCase() + toolName.slice(1), null, { dim: true })
59
+
60
+ switch (toolName) {
61
+ case "bash": {
62
+ const desc = clipText(args?.description || args?.command, 80)
63
+ return ` ${prefix} ${name} ${paint(desc, null, { dim: true })}`
64
+ }
65
+ case "write":
66
+ case "edit":
67
+ return ` ${prefix} ${name} ${paint(shortPath(args?.path), null, { dim: true })}`
68
+ case "notebookedit":
69
+ return ` ${prefix} ${name} ${paint(shortPath(args?.path), null, { dim: true })} ${paint(`cell ${args?.cell_number ?? 0}`, null, { dim: true })}`
70
+ case "read":
71
+ case "list":
72
+ return ` ${prefix} ${name} ${paint(shortPath(args?.path || "."), null, { dim: true })}`
73
+ case "grep":
74
+ case "glob":
75
+ return ` ${prefix} ${name} ${paint(clipText(args?.pattern, 60), null, { dim: true })}`
76
+ case "task":
77
+ return ` ${prefix} ${name} ${paint(clipText(args?.description || args?.prompt, 60), null, { dim: true })}`
78
+ case "todowrite":
79
+ return null // handled by result preview only
80
+ case "webfetch":
81
+ return ` ${prefix} ${name} ${paint(clipText(args?.url, 60), null, { dim: true })}`
82
+ case "websearch":
83
+ return ` ${prefix} ${name} ${paint(clipText(args?.query, 60), null, { dim: true })}`
84
+ case "question":
85
+ return ` ~ ${paint("Asking questions...", null, { dim: true })}`
86
+ case "enter_plan":
87
+ return ` ${paint(SYM.plan, "magenta")} ${paint("Enter Plan", "magenta")}`
88
+ case "exit_plan":
89
+ return ` ${paint(SYM.planDone, "green")} ${paint("Submit Plan", "green")}`
90
+ default:
91
+ return ` ${prefix} ${name} ${paint(clipText(args ? Object.keys(args).slice(0, 3).join(", ") : "", 40), null, { dim: true })}`
92
+ }
93
+ }
94
+
95
+ export function formatToolFinish(toolName, status, durationMs, args) {
96
+ if (status === "error") {
97
+ return ` ${paint(SYM.toolErr, "red")} ${paint(toolName, null, { dim: true })} ${paint("error", "red")}${durationMs ? paint(` ${durationMs}ms`, null, { dim: true }) : ""}`
98
+ }
99
+ // For completed tools, return null — the start line + result preview is enough
100
+ return null
101
+ }
102
+
103
+ export function formatToolResultPreview(toolName, output, status, args) {
104
+ if (status !== "completed") return null
105
+ const text = String(output || "").trim()
106
+
107
+ switch (toolName) {
108
+ case "bash": {
109
+ const lines = stripAnsi(text).split("\n").filter(Boolean)
110
+ if (!lines.length) return null
111
+ const first = clipText(lines[0], 90)
112
+ const suffix = lines.length > 1 ? paint(` (+${lines.length - 1} lines)`, null, { dim: true }) : ""
113
+ return ` ${paint(first, null, { dim: true })}${suffix}`
114
+ }
115
+ case "write": {
116
+ const n = String(args?.content || "").split("\n").filter(Boolean).length
117
+ return ` ${paint(`+${n} lines`, diffAdd(), { dim: true })}`
118
+ }
119
+ case "edit": {
120
+ const added = String(args?.new_string || "").split("\n").filter(Boolean).length
121
+ const removed = String(args?.old_string || "").split("\n").filter(Boolean).length
122
+ const parts = []
123
+ if (added > 0) parts.push(paint(`+${added}`, diffAdd()))
124
+ if (removed > 0) parts.push(paint(`-${removed}`, diffDel()))
125
+ return parts.length ? ` ${parts.join(" ")} ${paint("lines", null, { dim: true })}` : null
126
+ }
127
+ case "grep": {
128
+ const lines = text.split("\n").filter(Boolean)
129
+ if (text === "no matches" || !lines.length) return ` ${paint("no matches", null, { dim: true })}`
130
+ return ` ${paint(`${lines.length} matches`, null, { dim: true })}`
131
+ }
132
+ case "read":
133
+ return ` ${paint(`${text.split("\n").length} lines`, null, { dim: true })}`
134
+ case "glob": {
135
+ const lines = text.split("\n").filter(Boolean)
136
+ if (!lines.length) return ` ${paint("no files", null, { dim: true })}`
137
+ return ` ${paint(`${lines.length} files`, null, { dim: true })}`
138
+ }
139
+ case "todowrite": {
140
+ const todos = Array.isArray(args?.todos) ? args.todos : []
141
+ if (!todos.length) return null
142
+ const result = []
143
+ for (const t of todos.slice(0, 8)) {
144
+ const s = t.status || "pending"
145
+ const dot = s === "completed" ? paint(SYM.toolOk, "green")
146
+ : s === "in_progress" ? paint(SYM.dot, "yellow")
147
+ : paint(SYM.dotHollow, "#666666")
148
+ const color = s === "completed" ? "green" : s === "in_progress" ? "yellow" : null
149
+ const label = s === "in_progress" && t.activeForm ? t.activeForm : t.content
150
+ result.push(` ${dot} ${paint(label || "", color, { dim: s === "completed" })}`)
151
+ }
152
+ if (todos.length > 8) result.push(paint(` ... +${todos.length - 8} more`, null, { dim: true }))
153
+ return result
154
+ }
155
+ default:
156
+ return null
157
+ }
158
+ }
159
+
160
+ function formatToolError(error) {
161
+ if (!error) return null
162
+ return ` ${paint(clipText(stripAnsi(error), 120), "red", { dim: true })}`
163
+ }
164
+
165
+ // ── Thinking Formatter ──────────────────────────────────
166
+
167
+ export function formatThinkingHeader() {
168
+ return `${paint(SYM.dot, "#666666")} ${paint("Thinking", null, { italic: true, dim: true })} ${paint("∨", null, { dim: true })}`
169
+ }
170
+
171
+ // ── LongAgent Display Formatters ─────────────────────────
172
+
173
+ export function formatPhaseChange(prevPhase, nextPhase, reason) {
174
+ const arrow = paint("→", null, { dim: true })
175
+ const reasonText = reason ? paint(reason, null, { dim: true }) : ""
176
+ return `${paint(SYM.phase, "magenta")} ${paint("phase", "magenta", { bold: true })} ${paint(prevPhase, null, { dim: true })} ${arrow} ${paint(nextPhase, "magenta", { bold: true })} ${reasonText}`
177
+ }
178
+
179
+ export function formatStageStarted(stageId, taskCount) {
180
+ return `${paint(SYM.stage, "#fb923c", { bold: true })} ${paint("stage", "#fb923c", { bold: true })} ${paint(stageId, "white", { bold: true })} ${paint(`(${taskCount} tasks)`, null, { dim: true })}`
181
+ }
182
+
183
+ export function formatStageFinished(stageId, successCount, failCount) {
184
+ const status = failCount === 0
185
+ ? paint("PASS", "green", { bold: true })
186
+ : paint(`FAIL (${failCount})`, "red", { bold: true })
187
+ return `${paint(SYM.stage, "#fb923c")} ${paint("stage", "#fb923c")} ${paint(stageId, "white")} ${status} ${paint(`(${successCount} ok)`, null, { dim: true })}`
188
+ }
189
+
190
+ export function formatTaskDispatched(_stageId, taskId, attempt) {
191
+ const attemptLabel = attempt > 1 ? paint(` retry#${attempt}`, "yellow") : ""
192
+ return ` ${paint(SYM.dot, "#666666")} ${paint("task", "cyan")} ${paint(taskId, null, { dim: true })}${attemptLabel}`
193
+ }
194
+
195
+ export function formatTaskFinished(taskId, status) {
196
+ const dot = status === "completed" ? paint(SYM.dot, "green") : paint(SYM.dot, "red")
197
+ const color = status === "completed" ? "green" : "red"
198
+ return ` ${dot} ${paint(taskId, null, { dim: true })} ${paint(status, color)}`
199
+ }
200
+
201
+ export function formatHeartbeat(iteration, maxIterations, phase, gate, progress, elapsed) {
202
+ const iterLabel = maxIterations > 0 ? `${iteration}/${maxIterations}` : String(iteration)
203
+ const progressLabel = progress?.percentage !== null && progress?.percentage !== undefined
204
+ ? paint(`${progress.percentage}%`, "green")
205
+ : paint("...", null, { dim: true })
206
+ const elapsedLabel = elapsed !== undefined ? paint(`${elapsed}s`, null, { dim: true }) : ""
207
+ return `${paint(SYM.iteration, "#fb923c")} ${paint("iter", "#fb923c")} ${paint(iterLabel, "white", { bold: true })} phase=${paint(phase || "-", "magenta")} gate=${paint(gate || "-", "cyan")} progress=${progressLabel} ${elapsedLabel}`
208
+ }
209
+
210
+ export function formatPlanFrozen(planId, stageCount) {
211
+ return `${paint(SYM.planDone, "green", { bold: true })} ${paint("plan frozen", "green", { bold: true })} ${paint(planId || "", null, { dim: true })} ${paint(`${stageCount} stage(s)`, null, { dim: true })}`
212
+ }
213
+
214
+ export function formatRecovery(reason, recoveryCount) {
215
+ return `${paint(SYM.recovery, "yellow", { bold: true })} ${paint("recovery", "yellow", { bold: true })} #${recoveryCount} ${paint(reason || "", null, { dim: true })}`
216
+ }
217
+
218
+ export function formatAlert(kind, message) {
219
+ return `${paint(SYM.alert, "red", { bold: true })} ${paint("alert", "red", { bold: true })} [${kind}] ${paint(message || "", null, { dim: true })}`
220
+ }
221
+
222
+ export function formatIntakeStarted(objective) {
223
+ const preview = clipText(objective, 80)
224
+ return `${paint(SYM.phase, "magenta")} ${paint("intake", "magenta", { bold: true })} ${paint(preview, null, { dim: true })}`
225
+ }
226
+
227
+ export function formatGateChecked(gate, status) {
228
+ const dot = status === "pass" ? paint(SYM.dot, "green") : paint(SYM.dot, "yellow")
229
+ return ` ${dot} gate=${paint(gate || "-", "cyan")} ${paint(status || "-", status === "pass" ? "green" : "yellow")}`
230
+ }
231
+
232
+ // ── Hybrid Stage Formatters ──────────────────────────────
233
+
234
+ function hybridBanner(label, color) {
235
+ const line = paint("━".repeat(40), color, { dim: true })
236
+ return `${line}\n${paint(label, color, { bold: true })}`
237
+ }
238
+
239
+ export function formatHybridPreviewStart(objective) {
240
+ const preview = clipText(objective, 70)
241
+ return `${hybridBanner("H1 Preview", "#3b82f6")}\n ${paint(preview, null, { dim: true })}`
242
+ }
243
+
244
+ export function formatHybridPreviewComplete(findingsLength) {
245
+ return ` ${paint(SYM.toolOk, "green")} ${paint("preview complete", "green")} ${paint(`(${findingsLength} chars)`, null, { dim: true })}`
246
+ }
247
+
248
+ export function formatHybridBlueprintStart() {
249
+ return hybridBanner("H2 Blueprint", "#a855f7")
250
+ }
251
+
252
+ export function formatHybridBlueprintComplete(planId, stageCount) {
253
+ return ` ${paint(SYM.toolOk, "green")} ${paint("blueprint complete", "green")} ${paint(planId || "", null, { dim: true })} ${paint(`${stageCount} stage(s)`, null, { dim: true })}`
254
+ }
255
+
256
+ export function formatHybridBlueprintReview(planId) {
257
+ return ` ${paint("⏳", "yellow")} ${paint("awaiting blueprint review", "yellow")} ${paint(planId || "", null, { dim: true })}`
258
+ }
259
+
260
+ export function formatHybridBlueprintValidated(totalTasks, totalFiles, valid) {
261
+ const status = valid ? paint("PASS", "green") : paint("WARN", "yellow")
262
+ return ` ${paint(SYM.dot, valid ? "green" : "yellow")} ${paint("blueprint validation", null, { dim: true })} ${status} ${paint(`${totalTasks} tasks, ${totalFiles} files`, null, { dim: true })}`
263
+ }
264
+
265
+ // ── Hybrid Debugging/Rollback Formatters ─────────────────
266
+
267
+ export function formatHybridDebuggingStart(codingRollbackCount) {
268
+ const suffix = codingRollbackCount > 0 ? ` ${paint(`(rollback #${codingRollbackCount})`, "yellow")}` : ""
269
+ return `${hybridBanner("H5 Debugging", "#fb923c")}${suffix}`
270
+ }
271
+
272
+ export function formatHybridDebuggingComplete(debugIter, rollback) {
273
+ const status = rollback
274
+ ? paint("ROLLBACK", "yellow", { bold: true })
275
+ : paint("PASS", "green", { bold: true })
276
+ return ` ${paint(SYM.dot, rollback ? "yellow" : "green")} ${paint("debugging", null, { dim: true })} ${status} ${paint(`(${debugIter} iters)`, null, { dim: true })}`
277
+ }
278
+
279
+ export function formatHybridReturnToCoding(rollbackCount, failedTaskIds) {
280
+ const tasks = failedTaskIds?.length ? paint(` [${failedTaskIds.join(", ")}]`, null, { dim: true }) : ""
281
+ return ` ${paint(SYM.recovery, "yellow")} ${paint(`rollback to coding #${rollbackCount}`, "yellow")}${tasks}`
282
+ }
283
+
284
+ export function formatHybridCrossReview(fileCount) {
285
+ return ` ${paint(SYM.dot, "cyan")} ${paint("cross-review", "cyan")} ${paint(`${fileCount} file(s)`, null, { dim: true })}`
286
+ }
287
+
288
+ // ── Hybrid Incremental/Budget/Context Formatters ─────────
289
+
290
+ export function formatHybridIncrementalGate(stageId, passed) {
291
+ const dot = passed ? paint(SYM.dot, "green") : paint(SYM.dot, "yellow")
292
+ const status = passed ? paint("pass", "green") : paint("warn", "yellow")
293
+ return ` ${dot} ${paint("gate", null, { dim: true })} ${paint(stageId, "cyan")} ${status}`
294
+ }
295
+
296
+ export function formatHybridContextCompressed(newLength) {
297
+ return ` ${paint(SYM.dot, "#666666")} ${paint(`context compressed → ${newLength} chars`, null, { dim: true })}`
298
+ }
299
+
300
+ export function formatHybridBudgetWarning(totalTokens, budgetLimit, percentage) {
301
+ const color = percentage >= 100 ? "red" : "yellow"
302
+ return ` ${paint(SYM.alert, color)} ${paint("budget", color, { bold: true })} ${paint(`${percentage}%`, color)} ${paint(`(${totalTokens}/${budgetLimit})`, null, { dim: true })}`
303
+ }
304
+
305
+ export function formatHybridCheckpointResumed(stageIndex, iteration) {
306
+ return ` ${paint(SYM.dot, "cyan")} ${paint("checkpoint resumed", "cyan")} ${paint(`stage ${stageIndex}, iter ${iteration}`, null, { dim: true })}`
307
+ }
308
+
309
+ export function formatHybridReplan(newStageCount) {
310
+ return ` ${paint(SYM.dot, "#a855f7")} ${paint("replan", "#a855f7", { bold: true })} ${paint(`→ ${newStageCount} stage(s)`, null, { dim: true })}`
311
+ }
312
+
313
+ // ── Hybrid Memory Formatters ─────────────────────────────
314
+
315
+ export function formatHybridMemoryLoaded(techStack) {
316
+ const items = Array.isArray(techStack) ? techStack.slice(0, 5).join(", ") : ""
317
+ return ` ${paint(SYM.dot, "#666666")} ${paint("memory loaded", null, { dim: true })} ${paint(items, null, { dim: true })}`
318
+ }
319
+
320
+ export function formatHybridMemorySaved(techStackCount) {
321
+ return ` ${paint(SYM.dot, "#666666")} ${paint(`memory saved (${techStackCount} items)`, null, { dim: true })}`
322
+ }
323
+
324
+ // ── Git Formatters ───────────────────────────────────────
325
+
326
+ export function formatGitBranchCreated(branch, baseBranch) {
327
+ return ` ${paint(SYM.dot, "green")} ${paint("git branch", "green")} ${paint(branch, "white", { bold: true })} ${paint(`← ${baseBranch}`, null, { dim: true })}`
328
+ }
329
+
330
+ export function formatGitStageCommitted(stageId, message) {
331
+ return ` ${paint(SYM.dot, "#666666")} ${paint("commit", null, { dim: true })} ${paint(clipText(message || stageId, 60), null, { dim: true })}`
332
+ }
333
+
334
+ export function formatGitMerged(branch, baseBranch) {
335
+ return ` ${paint(SYM.toolOk, "green")} ${paint("git merged", "green", { bold: true })} ${paint(branch, null, { dim: true })} ${paint("→", null, { dim: true })} ${paint(baseBranch, "white")}`
336
+ }
337
+
338
+ // ── Plan Progress Formatter ──────────────────────────────
339
+
340
+ export function formatPlanProgress(taskProgress) {
341
+ if (!taskProgress || typeof taskProgress !== "object") return []
342
+ const entries = Object.entries(taskProgress)
343
+ if (!entries.length) return []
344
+
345
+ const lines = [paint("Plan Progress:", "cyan", { bold: true })]
346
+ for (const [taskId, tp] of entries) {
347
+ const status = tp?.status || "pending"
348
+ const dot = status === "completed"
349
+ ? paint(SYM.dot, "green")
350
+ : status === "error"
351
+ ? paint(SYM.dot, "red")
352
+ : paint(SYM.dotHollow, "#666666")
353
+ const color = status === "completed" ? "green" : status === "error" ? "red" : "white"
354
+ lines.push(` ${dot} ${taskId} ${paint(status, color)}`)
355
+ }
356
+ return lines
357
+ }
358
+
359
+ // ── Recovery Suggestions Formatter ───────────────────────
360
+
361
+ export function formatRecoverySuggestions(recovery) {
362
+ if (!recovery) return []
363
+ const lines = []
364
+ lines.push(paint("Recovery Suggestions:", "yellow", { bold: true }))
365
+
366
+ if (recovery.summary) {
367
+ lines.push(` ${paint(recovery.summary, null, { dim: true })}`)
368
+ }
369
+
370
+ if (recovery.suggestions?.length) {
371
+ for (const s of recovery.suggestions) {
372
+ lines.push(` ${paint(SYM.alert, "yellow")} ${paint(s, "yellow")}`)
373
+ }
374
+ }
375
+
376
+ if (recovery.failedTasks?.length) {
377
+ lines.push(paint(" Failed Tasks:", "red"))
378
+ for (const t of recovery.failedTasks.slice(0, 5)) {
379
+ lines.push(` ${paint(SYM.dot, "red")} ${t.taskId} [${t.category}]: ${paint(t.error || "", null, { dim: true })}`)
380
+ }
381
+ }
382
+
383
+ if (recovery.manualSteps?.length) {
384
+ lines.push(paint(" Manual Steps:", "cyan"))
385
+ for (const step of recovery.manualSteps.slice(0, 5)) {
386
+ lines.push(` ${paint(SYM.arrow, "cyan")} ${paint(step, null, { dim: true })}`)
387
+ }
388
+ }
389
+
390
+ if (recovery.resumeHint) {
391
+ lines.push(` ${paint(SYM.dot, "green")} ${paint(recovery.resumeHint, "green")}`)
392
+ }
393
+
394
+ return lines
395
+ }
396
+
397
+ // ── Renderer ─────────────────────────────────────────────
398
+
399
+ export function createActivityRenderer({ output, theme = null }) {
400
+ _theme = theme
401
+ const log = typeof output?.appendLog === "function"
402
+ ? output.appendLog
403
+ : (text) => console.log(text)
404
+
405
+ const toolTimers = new Map()
406
+ let timerCounter = 0
407
+ let unsubscribe = null
408
+
409
+ function timerKey(sessionId, turnId, toolName) {
410
+ return `${sessionId || ""}:${turnId || ""}:${toolName}:${timerCounter++}`
411
+ }
412
+
413
+ // Track the latest timer key per tool invocation
414
+ const activeToolKeys = new Map()
415
+ // Track tool args for finish formatting
416
+ const activeToolArgs = new Map()
417
+
418
+ function handleEvent(event) {
419
+ const { type, payload, sessionId, turnId } = event
420
+
421
+ switch (type) {
422
+ case EVENT_TYPES.TOOL_START: {
423
+ const key = timerKey(sessionId, turnId, payload.tool)
424
+ const lookupKey = `${sessionId}:${turnId}:${payload.tool}`
425
+ toolTimers.set(key, Date.now())
426
+ activeToolKeys.set(lookupKey, key)
427
+ activeToolArgs.set(lookupKey, payload.args)
428
+ // Show tool call inline (compact dim line)
429
+ const startLine = formatToolStart(payload.tool, payload.args)
430
+ if (startLine) log(startLine)
431
+ break
432
+ }
433
+
434
+ case EVENT_TYPES.TOOL_FINISH: {
435
+ const lookupKey = `${sessionId}:${turnId}:${payload.tool}`
436
+ const key = activeToolKeys.get(lookupKey)
437
+ const savedArgs = activeToolArgs.get(lookupKey) || payload.args
438
+ if (key) {
439
+ toolTimers.delete(key)
440
+ activeToolKeys.delete(lookupKey)
441
+ activeToolArgs.delete(lookupKey)
442
+ }
443
+ const finishLine = formatToolFinish(payload.tool, payload.status, 0, savedArgs)
444
+ if (finishLine) log(finishLine)
445
+ const preview = formatToolResultPreview(payload.tool, payload.output, payload.status, savedArgs)
446
+ if (preview) {
447
+ if (Array.isArray(preview)) {
448
+ for (const line of preview) log(line)
449
+ } else {
450
+ log(preview)
451
+ }
452
+ }
453
+ // Blank line after tool for visual spacing
454
+ if (payload.tool !== "todowrite") log("")
455
+ break
456
+ }
457
+
458
+ case EVENT_TYPES.TOOL_ERROR: {
459
+ const lookupKey = `${sessionId}:${turnId}:${payload.tool}`
460
+ const key = activeToolKeys.get(lookupKey)
461
+ const savedArgs = activeToolArgs.get(lookupKey) || payload.args
462
+ if (key) {
463
+ toolTimers.delete(key)
464
+ activeToolKeys.delete(lookupKey)
465
+ activeToolArgs.delete(lookupKey)
466
+ }
467
+ log(formatToolFinish(payload.tool, payload.status || "error", 0, savedArgs))
468
+ const errLine = formatToolError(payload.error)
469
+ if (errLine) log(errLine)
470
+ break
471
+ }
472
+
473
+ case EVENT_TYPES.LONGAGENT_PHASE_CHANGED: {
474
+ log(formatPhaseChange(payload.prevPhase, payload.nextPhase, payload.reason))
475
+ break
476
+ }
477
+
478
+ case EVENT_TYPES.LONGAGENT_STAGE_STARTED: {
479
+ log(formatStageStarted(payload.stageId, payload.taskCount))
480
+ break
481
+ }
482
+
483
+ case EVENT_TYPES.LONGAGENT_STAGE_FINISHED: {
484
+ log(formatStageFinished(payload.stageId, payload.successCount, payload.failCount))
485
+ break
486
+ }
487
+
488
+ case EVENT_TYPES.LONGAGENT_STAGE_TASK_DISPATCHED: {
489
+ log(formatTaskDispatched(payload.stageId, payload.taskId, payload.attempt))
490
+ break
491
+ }
492
+
493
+ case EVENT_TYPES.LONGAGENT_STAGE_TASK_FINISHED: {
494
+ log(formatTaskFinished(payload.taskId, payload.status))
495
+ break
496
+ }
497
+
498
+ case EVENT_TYPES.LONGAGENT_HEARTBEAT: {
499
+ log(formatHeartbeat(
500
+ payload.iteration,
501
+ payload.maxIterations,
502
+ payload.phase,
503
+ payload.gate,
504
+ payload.progress,
505
+ payload.elapsed
506
+ ))
507
+ break
508
+ }
509
+
510
+ case EVENT_TYPES.LONGAGENT_PLAN_FROZEN: {
511
+ log(formatPlanFrozen(payload.planId, payload.stageCount))
512
+ break
513
+ }
514
+
515
+ case EVENT_TYPES.LONGAGENT_RECOVERY_ENTERED: {
516
+ log(formatRecovery(payload.reason, payload.recoveryCount))
517
+ break
518
+ }
519
+
520
+ case EVENT_TYPES.LONGAGENT_ALERT: {
521
+ log(formatAlert(payload.kind, payload.message))
522
+ break
523
+ }
524
+
525
+ case EVENT_TYPES.LONGAGENT_INTAKE_STARTED: {
526
+ log(formatIntakeStarted(payload.objective))
527
+ break
528
+ }
529
+
530
+ case EVENT_TYPES.LONGAGENT_GATE_CHECKED: {
531
+ log(formatGateChecked(payload.gate, payload.status))
532
+ break
533
+ }
534
+
535
+ // ── Hybrid Events ──────────────────────────────
536
+ case EVENT_TYPES.LONGAGENT_HYBRID_PREVIEW_START: {
537
+ log(formatHybridPreviewStart(payload.objective))
538
+ break
539
+ }
540
+ case EVENT_TYPES.LONGAGENT_HYBRID_PREVIEW_COMPLETE: {
541
+ log(formatHybridPreviewComplete(payload.findingsLength))
542
+ break
543
+ }
544
+ case EVENT_TYPES.LONGAGENT_HYBRID_BLUEPRINT_START: {
545
+ log(formatHybridBlueprintStart())
546
+ break
547
+ }
548
+ case EVENT_TYPES.LONGAGENT_HYBRID_BLUEPRINT_COMPLETE: {
549
+ log(formatHybridBlueprintComplete(payload.planId, payload.stageCount))
550
+ break
551
+ }
552
+ case EVENT_TYPES.LONGAGENT_HYBRID_BLUEPRINT_REVIEW: {
553
+ log(formatHybridBlueprintReview(payload.planId))
554
+ break
555
+ }
556
+ case EVENT_TYPES.LONGAGENT_HYBRID_BLUEPRINT_VALIDATED: {
557
+ log(formatHybridBlueprintValidated(payload.totalTasks, payload.totalFiles, payload.valid))
558
+ break
559
+ }
560
+ case EVENT_TYPES.LONGAGENT_HYBRID_DEBUGGING_START: {
561
+ log(formatHybridDebuggingStart(payload.codingRollbackCount))
562
+ break
563
+ }
564
+ case EVENT_TYPES.LONGAGENT_HYBRID_DEBUGGING_COMPLETE: {
565
+ log(formatHybridDebuggingComplete(payload.debugIter, payload.rollback))
566
+ break
567
+ }
568
+ case EVENT_TYPES.LONGAGENT_HYBRID_RETURN_TO_CODING: {
569
+ log(formatHybridReturnToCoding(payload.rollbackCount, payload.failedTaskIds))
570
+ break
571
+ }
572
+ case EVENT_TYPES.LONGAGENT_HYBRID_CROSS_REVIEW: {
573
+ log(formatHybridCrossReview(payload.fileCount))
574
+ break
575
+ }
576
+ case EVENT_TYPES.LONGAGENT_HYBRID_INCREMENTAL_GATE: {
577
+ log(formatHybridIncrementalGate(payload.stageId, payload.passed))
578
+ break
579
+ }
580
+ case EVENT_TYPES.LONGAGENT_HYBRID_CONTEXT_COMPRESSED: {
581
+ log(formatHybridContextCompressed(payload.newLength))
582
+ break
583
+ }
584
+ case EVENT_TYPES.LONGAGENT_HYBRID_BUDGET_WARNING: {
585
+ log(formatHybridBudgetWarning(payload.totalTokens, payload.budgetLimit, payload.percentage))
586
+ break
587
+ }
588
+ case EVENT_TYPES.LONGAGENT_HYBRID_CHECKPOINT_RESUMED: {
589
+ log(formatHybridCheckpointResumed(payload.stageIndex, payload.iteration))
590
+ break
591
+ }
592
+ case EVENT_TYPES.LONGAGENT_HYBRID_REPLAN: {
593
+ log(formatHybridReplan(payload.newStageCount))
594
+ break
595
+ }
596
+ case EVENT_TYPES.LONGAGENT_HYBRID_MEMORY_LOADED: {
597
+ log(formatHybridMemoryLoaded(payload.techStack))
598
+ break
599
+ }
600
+ case EVENT_TYPES.LONGAGENT_HYBRID_MEMORY_SAVED: {
601
+ log(formatHybridMemorySaved(payload.techStackCount))
602
+ break
603
+ }
604
+
605
+ // ── New Fault Recovery Events ──────────────────
606
+ case EVENT_TYPES.LONGAGENT_DEGRADATION_APPLIED: {
607
+ log(formatAlert("degradation", `${payload.strategy} applied in ${payload.phase}${payload.reason ? ` (${payload.reason})` : ""}`))
608
+ break
609
+ }
610
+ case EVENT_TYPES.LONGAGENT_WRITE_LOOP_DETECTED: {
611
+ log(formatAlert("write_loop", payload.message || "write loop detected"))
612
+ break
613
+ }
614
+ case EVENT_TYPES.LONGAGENT_SEMANTIC_ERROR_REPEATED: {
615
+ log(formatAlert("semantic_error", `repeated ${payload.count}x: ${(payload.error || "").slice(0, 80)}`))
616
+ break
617
+ }
618
+ case EVENT_TYPES.LONGAGENT_PHASE_TIMEOUT: {
619
+ log(formatAlert("phase_timeout", `${payload.phase} timed out after ${Math.round((payload.elapsed || 0) / 1000)}s`))
620
+ break
621
+ }
622
+ case EVENT_TYPES.LONGAGENT_GIT_CONFLICT_RESOLUTION: {
623
+ log(formatAlert("git_conflict", `resolving conflicts in ${(payload.files || []).length} file(s)`))
624
+ break
625
+ }
626
+ case EVENT_TYPES.LONGAGENT_CHECKPOINT_CLEANED: {
627
+ log(` ${paint(SYM.dot, "#666666")} ${paint(`checkpoints cleaned (${payload.removed} removed)`, null, { dim: true })}`)
628
+ break
629
+ }
630
+
631
+ // ── Git Events ─────────────────────────────────
632
+ case EVENT_TYPES.LONGAGENT_GIT_BRANCH_CREATED: {
633
+ log(formatGitBranchCreated(payload.branch, payload.baseBranch))
634
+ break
635
+ }
636
+ case EVENT_TYPES.LONGAGENT_GIT_STAGE_COMMITTED: {
637
+ log(formatGitStageCommitted(payload.stageId, payload.message))
638
+ break
639
+ }
640
+ case EVENT_TYPES.LONGAGENT_GIT_MERGED: {
641
+ log(formatGitMerged(payload.branch, payload.baseBranch))
642
+ break
643
+ }
644
+
645
+ case EVENT_TYPES.SESSION_COMPACTED: {
646
+ log(`${paint(SYM.phase, "magenta")} ${paint("context compacted", "magenta", { dim: true })}`)
647
+ break
648
+ }
649
+ }
650
+ }
651
+
652
+ return {
653
+ start() {
654
+ if (unsubscribe) return
655
+ unsubscribe = EventBus.subscribe(handleEvent)
656
+ },
657
+ stop() {
658
+ if (unsubscribe) {
659
+ unsubscribe()
660
+ unsubscribe = null
661
+ }
662
+ toolTimers.clear()
663
+ activeToolKeys.clear()
664
+ activeToolArgs.clear()
665
+ }
666
+ }
667
+ }