@kkelly-offical/kkcode 0.1.2

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 (196) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +445 -0
  3. package/package.json +46 -0
  4. package/src/agent/agent.mjs +170 -0
  5. package/src/agent/custom-agent-loader.mjs +158 -0
  6. package/src/agent/generator.mjs +115 -0
  7. package/src/agent/prompt/architect.txt +36 -0
  8. package/src/agent/prompt/build-fixer.txt +71 -0
  9. package/src/agent/prompt/build.txt +101 -0
  10. package/src/agent/prompt/compaction.txt +12 -0
  11. package/src/agent/prompt/explore.txt +29 -0
  12. package/src/agent/prompt/guide.txt +40 -0
  13. package/src/agent/prompt/longagent.txt +178 -0
  14. package/src/agent/prompt/plan.txt +50 -0
  15. package/src/agent/prompt/researcher.txt +23 -0
  16. package/src/agent/prompt/reviewer.txt +44 -0
  17. package/src/agent/prompt/security-reviewer.txt +62 -0
  18. package/src/agent/prompt/tdd-guide.txt +84 -0
  19. package/src/agent/prompt/title.txt +8 -0
  20. package/src/command/custom-commands.mjs +57 -0
  21. package/src/commands/agent.mjs +71 -0
  22. package/src/commands/audit.mjs +77 -0
  23. package/src/commands/background.mjs +86 -0
  24. package/src/commands/chat.mjs +114 -0
  25. package/src/commands/command.mjs +41 -0
  26. package/src/commands/config.mjs +44 -0
  27. package/src/commands/doctor.mjs +148 -0
  28. package/src/commands/hook.mjs +29 -0
  29. package/src/commands/init.mjs +141 -0
  30. package/src/commands/longagent.mjs +100 -0
  31. package/src/commands/mcp.mjs +89 -0
  32. package/src/commands/permission.mjs +36 -0
  33. package/src/commands/prompt.mjs +42 -0
  34. package/src/commands/review.mjs +266 -0
  35. package/src/commands/rule.mjs +34 -0
  36. package/src/commands/session.mjs +235 -0
  37. package/src/commands/theme.mjs +98 -0
  38. package/src/commands/usage.mjs +91 -0
  39. package/src/config/defaults.mjs +195 -0
  40. package/src/config/import-config.mjs +76 -0
  41. package/src/config/load-config.mjs +76 -0
  42. package/src/config/schema.mjs +509 -0
  43. package/src/context.mjs +40 -0
  44. package/src/core/constants.mjs +46 -0
  45. package/src/core/errors.mjs +57 -0
  46. package/src/core/events.mjs +29 -0
  47. package/src/core/types.mjs +57 -0
  48. package/src/github/api.mjs +78 -0
  49. package/src/github/auth.mjs +286 -0
  50. package/src/github/flow.mjs +298 -0
  51. package/src/github/workspace.mjs +212 -0
  52. package/src/index.mjs +82 -0
  53. package/src/knowledge/api-design.txt +9 -0
  54. package/src/knowledge/cpp.txt +10 -0
  55. package/src/knowledge/docker.txt +10 -0
  56. package/src/knowledge/dotnet.txt +9 -0
  57. package/src/knowledge/electron.txt +10 -0
  58. package/src/knowledge/flutter.txt +10 -0
  59. package/src/knowledge/go.txt +9 -0
  60. package/src/knowledge/graphql.txt +10 -0
  61. package/src/knowledge/java.txt +9 -0
  62. package/src/knowledge/kotlin.txt +10 -0
  63. package/src/knowledge/loader.mjs +125 -0
  64. package/src/knowledge/next.txt +8 -0
  65. package/src/knowledge/node.txt +8 -0
  66. package/src/knowledge/nuxt.txt +9 -0
  67. package/src/knowledge/php.txt +10 -0
  68. package/src/knowledge/python.txt +10 -0
  69. package/src/knowledge/react-native.txt +10 -0
  70. package/src/knowledge/react.txt +9 -0
  71. package/src/knowledge/ruby.txt +11 -0
  72. package/src/knowledge/rust.txt +9 -0
  73. package/src/knowledge/svelte.txt +9 -0
  74. package/src/knowledge/swift.txt +10 -0
  75. package/src/knowledge/tailwind.txt +10 -0
  76. package/src/knowledge/testing.txt +8 -0
  77. package/src/knowledge/typescript.txt +8 -0
  78. package/src/knowledge/vue.txt +9 -0
  79. package/src/mcp/client-http.mjs +157 -0
  80. package/src/mcp/client-sse.mjs +286 -0
  81. package/src/mcp/client-stdio.mjs +451 -0
  82. package/src/mcp/registry.mjs +394 -0
  83. package/src/mcp/stdio-framing.mjs +127 -0
  84. package/src/orchestration/background-manager.mjs +358 -0
  85. package/src/orchestration/background-worker.mjs +245 -0
  86. package/src/orchestration/longagent-manager.mjs +116 -0
  87. package/src/orchestration/stage-scheduler.mjs +489 -0
  88. package/src/orchestration/subagent-router.mjs +62 -0
  89. package/src/orchestration/task-scheduler.mjs +74 -0
  90. package/src/permission/engine.mjs +92 -0
  91. package/src/permission/exec-policy.mjs +372 -0
  92. package/src/permission/prompt.mjs +39 -0
  93. package/src/permission/rules.mjs +120 -0
  94. package/src/permission/workspace-trust.mjs +44 -0
  95. package/src/plugin/builtin-hooks/console-warn.mjs +41 -0
  96. package/src/plugin/builtin-hooks/extract-patterns.mjs +75 -0
  97. package/src/plugin/builtin-hooks/post-edit-format.mjs +57 -0
  98. package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +61 -0
  99. package/src/plugin/builtin-hooks/strategic-compaction.mjs +38 -0
  100. package/src/plugin/hook-bus.mjs +154 -0
  101. package/src/provider/anthropic.mjs +389 -0
  102. package/src/provider/ollama.mjs +236 -0
  103. package/src/provider/openai-compatible.mjs +1 -0
  104. package/src/provider/openai.mjs +339 -0
  105. package/src/provider/retry-policy.mjs +68 -0
  106. package/src/provider/router.mjs +228 -0
  107. package/src/provider/sse.mjs +91 -0
  108. package/src/repl.mjs +2929 -0
  109. package/src/review/diff-parser.mjs +36 -0
  110. package/src/review/rejection-queue.mjs +62 -0
  111. package/src/review/review-store.mjs +21 -0
  112. package/src/review/risk-score.mjs +61 -0
  113. package/src/rules/load-rules.mjs +64 -0
  114. package/src/runtime.mjs +1 -0
  115. package/src/session/checkpoint.mjs +239 -0
  116. package/src/session/compaction.mjs +276 -0
  117. package/src/session/engine.mjs +225 -0
  118. package/src/session/instinct-manager.mjs +172 -0
  119. package/src/session/instruction-loader.mjs +25 -0
  120. package/src/session/longagent-plan.mjs +329 -0
  121. package/src/session/longagent-scaffold.mjs +100 -0
  122. package/src/session/longagent.mjs +1462 -0
  123. package/src/session/loop.mjs +905 -0
  124. package/src/session/memory-loader.mjs +75 -0
  125. package/src/session/project-context.mjs +367 -0
  126. package/src/session/prompt/anthropic.txt +151 -0
  127. package/src/session/prompt/beast.txt +37 -0
  128. package/src/session/prompt/max-steps.txt +6 -0
  129. package/src/session/prompt/plan.txt +9 -0
  130. package/src/session/prompt/qwen.txt +46 -0
  131. package/src/session/prompt-loader.mjs +18 -0
  132. package/src/session/recovery.mjs +52 -0
  133. package/src/session/store.mjs +503 -0
  134. package/src/session/system-prompt.mjs +260 -0
  135. package/src/session/task-validator.mjs +266 -0
  136. package/src/session/usability-gates.mjs +379 -0
  137. package/src/skill/builtin/backend-patterns.mjs +123 -0
  138. package/src/skill/builtin/commit.mjs +64 -0
  139. package/src/skill/builtin/debug.mjs +45 -0
  140. package/src/skill/builtin/frontend-patterns.mjs +120 -0
  141. package/src/skill/builtin/frontend.mjs +188 -0
  142. package/src/skill/builtin/init.mjs +220 -0
  143. package/src/skill/builtin/review.mjs +49 -0
  144. package/src/skill/builtin/security-checklist.mjs +80 -0
  145. package/src/skill/builtin/tdd.mjs +54 -0
  146. package/src/skill/generator.mjs +113 -0
  147. package/src/skill/registry.mjs +336 -0
  148. package/src/storage/audit-store.mjs +83 -0
  149. package/src/storage/event-log.mjs +82 -0
  150. package/src/storage/ghost-commit-store.mjs +235 -0
  151. package/src/storage/json-store.mjs +53 -0
  152. package/src/storage/paths.mjs +148 -0
  153. package/src/theme/color.mjs +64 -0
  154. package/src/theme/default-theme.mjs +29 -0
  155. package/src/theme/load-theme.mjs +71 -0
  156. package/src/theme/markdown.mjs +135 -0
  157. package/src/theme/schema.mjs +45 -0
  158. package/src/theme/status-bar.mjs +158 -0
  159. package/src/tool/audit-wrapper.mjs +38 -0
  160. package/src/tool/edit-transaction.mjs +126 -0
  161. package/src/tool/executor.mjs +109 -0
  162. package/src/tool/file-lock-manager.mjs +85 -0
  163. package/src/tool/git-auto.mjs +545 -0
  164. package/src/tool/git-full-auto.mjs +478 -0
  165. package/src/tool/image-util.mjs +276 -0
  166. package/src/tool/prompt/background_cancel.txt +1 -0
  167. package/src/tool/prompt/background_output.txt +1 -0
  168. package/src/tool/prompt/bash.txt +71 -0
  169. package/src/tool/prompt/codesearch.txt +18 -0
  170. package/src/tool/prompt/edit.txt +27 -0
  171. package/src/tool/prompt/enter_plan.txt +74 -0
  172. package/src/tool/prompt/exit_plan.txt +62 -0
  173. package/src/tool/prompt/glob.txt +33 -0
  174. package/src/tool/prompt/grep.txt +43 -0
  175. package/src/tool/prompt/list.txt +8 -0
  176. package/src/tool/prompt/multiedit.txt +20 -0
  177. package/src/tool/prompt/notebookedit.txt +21 -0
  178. package/src/tool/prompt/patch.txt +24 -0
  179. package/src/tool/prompt/question.txt +44 -0
  180. package/src/tool/prompt/read.txt +40 -0
  181. package/src/tool/prompt/task.txt +83 -0
  182. package/src/tool/prompt/todowrite.txt +117 -0
  183. package/src/tool/prompt/webfetch.txt +38 -0
  184. package/src/tool/prompt/websearch.txt +43 -0
  185. package/src/tool/prompt/write.txt +38 -0
  186. package/src/tool/prompt-loader.mjs +18 -0
  187. package/src/tool/question-prompt.mjs +86 -0
  188. package/src/tool/registry.mjs +1309 -0
  189. package/src/tool/task-tool.mjs +28 -0
  190. package/src/ui/activity-renderer.mjs +410 -0
  191. package/src/ui/repl-dashboard.mjs +357 -0
  192. package/src/usage/pricing.mjs +121 -0
  193. package/src/usage/usage-meter.mjs +113 -0
  194. package/src/util/git.mjs +496 -0
  195. package/src/util/template.mjs +10 -0
  196. package/src/util/yaml.mjs +100 -0
@@ -0,0 +1,28 @@
1
+ export function createTaskTool() {
2
+ return {
3
+ name: "task",
4
+ description: "Delegate complex multi-step work to a subagent that makes its own LLM calls. IMPORTANT: Do NOT use this for simple file operations — use 'write' to create files and 'edit' to modify files directly. Only use 'task' when the work requires multiple tool calls, reasoning, or autonomous decision-making (e.g. 'refactor module X', 'write tests for Y'). Background tasks (run_in_background) spawn a separate worker process.",
5
+ inputSchema: {
6
+ type: "object",
7
+ properties: {
8
+ prompt: { type: "string", description: "task prompt" },
9
+ description: { type: "string", description: "short task description" },
10
+ subagent_type: { type: "string", description: "explicit subagent type" },
11
+ category: { type: "string", description: "routing category" },
12
+ run_in_background: { type: "boolean", description: "run async in background" },
13
+ session_id: { type: "string", description: "continue from existing sub session" },
14
+ stage_id: { type: "string", description: "optional stage id for orchestration" },
15
+ task_id: { type: "string", description: "optional logical task id" },
16
+ planned_files: { type: "array", items: { type: "string" }, description: "planned files for this task" },
17
+ allow_question: { type: "boolean", description: "allow question tool during delegated run" }
18
+ },
19
+ required: []
20
+ },
21
+ async execute(args, ctx) {
22
+ if (typeof ctx.delegateTask !== "function") {
23
+ return { error: "task delegate unavailable" }
24
+ }
25
+ return ctx.delegateTask(args || {})
26
+ }
27
+ }
28
+ }
@@ -0,0 +1,410 @@
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
+ // ── Plan Progress Formatter ──────────────────────────────
230
+
231
+ export function formatPlanProgress(taskProgress) {
232
+ if (!taskProgress || typeof taskProgress !== "object") return []
233
+ const entries = Object.entries(taskProgress)
234
+ if (!entries.length) return []
235
+
236
+ const lines = [paint("Plan Progress:", "cyan", { bold: true })]
237
+ for (const [taskId, tp] of entries) {
238
+ const status = tp?.status || "pending"
239
+ const dot = status === "completed"
240
+ ? paint(SYM.dot, "green")
241
+ : status === "error"
242
+ ? paint(SYM.dot, "red")
243
+ : paint(SYM.dotHollow, "#666666")
244
+ const color = status === "completed" ? "green" : status === "error" ? "red" : "white"
245
+ lines.push(` ${dot} ${taskId} ${paint(status, color)}`)
246
+ }
247
+ return lines
248
+ }
249
+
250
+ // ── Renderer ─────────────────────────────────────────────
251
+
252
+ export function createActivityRenderer({ output, theme = null }) {
253
+ _theme = theme
254
+ const log = typeof output?.appendLog === "function"
255
+ ? output.appendLog
256
+ : (text) => console.log(text)
257
+
258
+ const toolTimers = new Map()
259
+ let timerCounter = 0
260
+ let unsubscribe = null
261
+
262
+ function timerKey(sessionId, turnId, toolName) {
263
+ return `${sessionId || ""}:${turnId || ""}:${toolName}:${timerCounter++}`
264
+ }
265
+
266
+ // Track the latest timer key per tool invocation
267
+ const activeToolKeys = new Map()
268
+ // Track tool args for finish formatting
269
+ const activeToolArgs = new Map()
270
+
271
+ function handleEvent(event) {
272
+ const { type, payload, sessionId, turnId } = event
273
+
274
+ switch (type) {
275
+ case EVENT_TYPES.TOOL_START: {
276
+ const key = timerKey(sessionId, turnId, payload.tool)
277
+ const lookupKey = `${sessionId}:${turnId}:${payload.tool}`
278
+ toolTimers.set(key, Date.now())
279
+ activeToolKeys.set(lookupKey, key)
280
+ activeToolArgs.set(lookupKey, payload.args)
281
+ // Show tool call inline (compact dim line)
282
+ const startLine = formatToolStart(payload.tool, payload.args)
283
+ if (startLine) log(startLine)
284
+ break
285
+ }
286
+
287
+ case EVENT_TYPES.TOOL_FINISH: {
288
+ const lookupKey = `${sessionId}:${turnId}:${payload.tool}`
289
+ const key = activeToolKeys.get(lookupKey)
290
+ const savedArgs = activeToolArgs.get(lookupKey) || payload.args
291
+ if (key) {
292
+ toolTimers.delete(key)
293
+ activeToolKeys.delete(lookupKey)
294
+ activeToolArgs.delete(lookupKey)
295
+ }
296
+ const finishLine = formatToolFinish(payload.tool, payload.status, 0, savedArgs)
297
+ if (finishLine) log(finishLine)
298
+ const preview = formatToolResultPreview(payload.tool, payload.output, payload.status, savedArgs)
299
+ if (preview) {
300
+ if (Array.isArray(preview)) {
301
+ for (const line of preview) log(line)
302
+ } else {
303
+ log(preview)
304
+ }
305
+ }
306
+ // Blank line after tool for visual spacing
307
+ if (payload.tool !== "todowrite") log("")
308
+ break
309
+ }
310
+
311
+ case EVENT_TYPES.TOOL_ERROR: {
312
+ const lookupKey = `${sessionId}:${turnId}:${payload.tool}`
313
+ const key = activeToolKeys.get(lookupKey)
314
+ const savedArgs = activeToolArgs.get(lookupKey) || payload.args
315
+ if (key) {
316
+ toolTimers.delete(key)
317
+ activeToolKeys.delete(lookupKey)
318
+ activeToolArgs.delete(lookupKey)
319
+ }
320
+ log(formatToolFinish(payload.tool, payload.status || "error", 0, savedArgs))
321
+ const errLine = formatToolError(payload.error)
322
+ if (errLine) log(errLine)
323
+ break
324
+ }
325
+
326
+ case EVENT_TYPES.LONGAGENT_PHASE_CHANGED: {
327
+ log(formatPhaseChange(payload.prevPhase, payload.nextPhase, payload.reason))
328
+ break
329
+ }
330
+
331
+ case EVENT_TYPES.LONGAGENT_STAGE_STARTED: {
332
+ log(formatStageStarted(payload.stageId, payload.taskCount))
333
+ break
334
+ }
335
+
336
+ case EVENT_TYPES.LONGAGENT_STAGE_FINISHED: {
337
+ log(formatStageFinished(payload.stageId, payload.successCount, payload.failCount))
338
+ break
339
+ }
340
+
341
+ case EVENT_TYPES.LONGAGENT_STAGE_TASK_DISPATCHED: {
342
+ log(formatTaskDispatched(payload.stageId, payload.taskId, payload.attempt))
343
+ break
344
+ }
345
+
346
+ case EVENT_TYPES.LONGAGENT_STAGE_TASK_FINISHED: {
347
+ log(formatTaskFinished(payload.taskId, payload.status))
348
+ break
349
+ }
350
+
351
+ case EVENT_TYPES.LONGAGENT_HEARTBEAT: {
352
+ log(formatHeartbeat(
353
+ payload.iteration,
354
+ payload.maxIterations,
355
+ payload.phase,
356
+ payload.gate,
357
+ payload.progress,
358
+ payload.elapsed
359
+ ))
360
+ break
361
+ }
362
+
363
+ case EVENT_TYPES.LONGAGENT_PLAN_FROZEN: {
364
+ log(formatPlanFrozen(payload.planId, payload.stageCount))
365
+ break
366
+ }
367
+
368
+ case EVENT_TYPES.LONGAGENT_RECOVERY_ENTERED: {
369
+ log(formatRecovery(payload.reason, payload.recoveryCount))
370
+ break
371
+ }
372
+
373
+ case EVENT_TYPES.LONGAGENT_ALERT: {
374
+ log(formatAlert(payload.kind, payload.message))
375
+ break
376
+ }
377
+
378
+ case EVENT_TYPES.LONGAGENT_INTAKE_STARTED: {
379
+ log(formatIntakeStarted(payload.objective))
380
+ break
381
+ }
382
+
383
+ case EVENT_TYPES.LONGAGENT_GATE_CHECKED: {
384
+ log(formatGateChecked(payload.gate, payload.status))
385
+ break
386
+ }
387
+
388
+ case EVENT_TYPES.SESSION_COMPACTED: {
389
+ log(`${paint(SYM.phase, "magenta")} ${paint("context compacted", "magenta", { dim: true })}`)
390
+ break
391
+ }
392
+ }
393
+ }
394
+
395
+ return {
396
+ start() {
397
+ if (unsubscribe) return
398
+ unsubscribe = EventBus.subscribe(handleEvent)
399
+ },
400
+ stop() {
401
+ if (unsubscribe) {
402
+ unsubscribe()
403
+ unsubscribe = null
404
+ }
405
+ toolTimers.clear()
406
+ activeToolKeys.clear()
407
+ activeToolArgs.clear()
408
+ }
409
+ }
410
+ }