@axplusb/kepler 0.0.1 → 1.0.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 (218) hide show
  1. package/README.md +82 -0
  2. package/package.json +36 -4
  3. package/pulse/app/activity/page.tsx +190 -0
  4. package/pulse/app/api/activity/route.ts +138 -0
  5. package/pulse/app/api/costs/route.ts +88 -0
  6. package/pulse/app/api/export/route.ts +77 -0
  7. package/pulse/app/api/history/route.ts +11 -0
  8. package/pulse/app/api/import/route.ts +31 -0
  9. package/pulse/app/api/memory/route.ts +52 -0
  10. package/pulse/app/api/plans/route.ts +9 -0
  11. package/pulse/app/api/projects/[slug]/route.ts +96 -0
  12. package/pulse/app/api/projects/route.ts +121 -0
  13. package/pulse/app/api/sessions/[id]/replay/route.ts +20 -0
  14. package/pulse/app/api/sessions/[id]/route.ts +31 -0
  15. package/pulse/app/api/sessions/route.ts +112 -0
  16. package/pulse/app/api/settings/route.ts +14 -0
  17. package/pulse/app/api/stats/route.ts +143 -0
  18. package/pulse/app/api/todos/route.ts +9 -0
  19. package/pulse/app/api/tools/route.ts +160 -0
  20. package/pulse/app/costs/page.tsx +179 -0
  21. package/pulse/app/export/page.tsx +465 -0
  22. package/pulse/app/favicon.ico +0 -0
  23. package/pulse/app/globals.css +263 -0
  24. package/pulse/app/help/page.tsx +142 -0
  25. package/pulse/app/history/page.tsx +157 -0
  26. package/pulse/app/layout.tsx +46 -0
  27. package/pulse/app/memory/page.tsx +365 -0
  28. package/pulse/app/overview-client.tsx +393 -0
  29. package/pulse/app/page.tsx +14 -0
  30. package/pulse/app/plans/page.tsx +308 -0
  31. package/pulse/app/projects/[slug]/page.tsx +390 -0
  32. package/pulse/app/projects/page.tsx +110 -0
  33. package/pulse/app/sessions/[id]/page.tsx +243 -0
  34. package/pulse/app/sessions/page.tsx +39 -0
  35. package/pulse/app/settings/page.tsx +188 -0
  36. package/pulse/app/todos/page.tsx +211 -0
  37. package/pulse/app/tools/page.tsx +249 -0
  38. package/pulse/cli.js +159 -0
  39. package/pulse/components/activity/day-of-week-chart.tsx +35 -0
  40. package/pulse/components/activity/streak-card.tsx +36 -0
  41. package/pulse/components/costs/cache-efficiency-panel.tsx +76 -0
  42. package/pulse/components/costs/cost-by-project-chart.tsx +48 -0
  43. package/pulse/components/costs/cost-over-time-chart.tsx +95 -0
  44. package/pulse/components/costs/model-token-table.tsx +60 -0
  45. package/pulse/components/global-search.tsx +193 -0
  46. package/pulse/components/keyboard-nav-provider.tsx +23 -0
  47. package/pulse/components/layout/bottom-nav.tsx +52 -0
  48. package/pulse/components/layout/client-layout.tsx +31 -0
  49. package/pulse/components/layout/sidebar-context.tsx +50 -0
  50. package/pulse/components/layout/sidebar.tsx +182 -0
  51. package/pulse/components/layout/top-bar.tsx +121 -0
  52. package/pulse/components/overview/activity-heatmap.tsx +107 -0
  53. package/pulse/components/overview/conversation-table.tsx +148 -0
  54. package/pulse/components/overview/model-breakdown-donut.tsx +95 -0
  55. package/pulse/components/overview/peak-hours-chart.tsx +87 -0
  56. package/pulse/components/overview/project-activity-donut.tsx +96 -0
  57. package/pulse/components/overview/stat-card.tsx +102 -0
  58. package/pulse/components/overview/usage-over-time-chart.tsx +166 -0
  59. package/pulse/components/projects/project-card.tsx +175 -0
  60. package/pulse/components/sessions/replay/assistant-markdown.tsx +94 -0
  61. package/pulse/components/sessions/replay/compaction-card.tsx +25 -0
  62. package/pulse/components/sessions/replay/session-sidebar.tsx +231 -0
  63. package/pulse/components/sessions/replay/token-accumulation-chart.tsx +98 -0
  64. package/pulse/components/sessions/replay/tool-call-badge.tsx +127 -0
  65. package/pulse/components/sessions/replay/turn-cards.tsx +220 -0
  66. package/pulse/components/sessions/replay/user-tool-result.tsx +158 -0
  67. package/pulse/components/sessions/session-badges.tsx +49 -0
  68. package/pulse/components/sessions/session-table.tsx +299 -0
  69. package/pulse/components/theme-provider.tsx +44 -0
  70. package/pulse/components/tools/feature-adoption-table.tsx +58 -0
  71. package/pulse/components/tools/mcp-server-panel.tsx +45 -0
  72. package/pulse/components/tools/tool-ranking-chart.tsx +57 -0
  73. package/pulse/components/tools/version-history-table.tsx +32 -0
  74. package/pulse/components/ui/alert.tsx +66 -0
  75. package/pulse/components/ui/badge.tsx +48 -0
  76. package/pulse/components/ui/breadcrumb.tsx +109 -0
  77. package/pulse/components/ui/button.tsx +64 -0
  78. package/pulse/components/ui/calendar.tsx +220 -0
  79. package/pulse/components/ui/card.tsx +92 -0
  80. package/pulse/components/ui/command.tsx +158 -0
  81. package/pulse/components/ui/dialog.tsx +158 -0
  82. package/pulse/components/ui/input.tsx +21 -0
  83. package/pulse/components/ui/popover.tsx +89 -0
  84. package/pulse/components/ui/progress.tsx +31 -0
  85. package/pulse/components/ui/select.tsx +190 -0
  86. package/pulse/components/ui/separator.tsx +28 -0
  87. package/pulse/components/ui/sheet.tsx +143 -0
  88. package/pulse/components/ui/skeleton.tsx +13 -0
  89. package/pulse/components/ui/table.tsx +116 -0
  90. package/pulse/components/ui/tabs.tsx +91 -0
  91. package/pulse/components/ui/tooltip.tsx +57 -0
  92. package/pulse/components/use-global-keyboard-nav.ts +79 -0
  93. package/pulse/components.json +23 -0
  94. package/pulse/eslint.config.mjs +18 -0
  95. package/pulse/lib/claude-reader.ts +594 -0
  96. package/pulse/lib/decode.ts +129 -0
  97. package/pulse/lib/pricing.ts +102 -0
  98. package/pulse/lib/replay-parser.ts +165 -0
  99. package/pulse/lib/tool-categories.ts +127 -0
  100. package/pulse/lib/utils.ts +6 -0
  101. package/pulse/next-env.d.ts +6 -0
  102. package/pulse/next.config.ts +16 -0
  103. package/pulse/package.json +45 -0
  104. package/pulse/postcss.config.mjs +7 -0
  105. package/pulse/public/activity.png +0 -0
  106. package/pulse/public/cc-lens.png +0 -0
  107. package/pulse/public/command-k.png +0 -0
  108. package/pulse/public/costs.png +0 -0
  109. package/pulse/public/dashboard-dark.png +0 -0
  110. package/pulse/public/dashboard-white.png +0 -0
  111. package/pulse/public/export.png +0 -0
  112. package/pulse/public/file.svg +1 -0
  113. package/pulse/public/globe.svg +1 -0
  114. package/pulse/public/next.svg +1 -0
  115. package/pulse/public/projects.png +0 -0
  116. package/pulse/public/session-chat.png +0 -0
  117. package/pulse/public/todos.png +0 -0
  118. package/pulse/public/tools.png +0 -0
  119. package/pulse/public/vercel.svg +1 -0
  120. package/pulse/public/window.svg +1 -0
  121. package/pulse/tsconfig.json +34 -0
  122. package/pulse/types/claude.ts +294 -0
  123. package/src/agents/loader.mjs +89 -0
  124. package/src/agents/parser.mjs +98 -0
  125. package/src/agents/teams.mjs +123 -0
  126. package/src/auth/oauth.mjs +220 -0
  127. package/src/auth/tarang-auth.mjs +277 -0
  128. package/src/config/cli-args.mjs +173 -0
  129. package/src/config/env.mjs +263 -0
  130. package/src/config/settings.mjs +132 -0
  131. package/src/context/ast-parser.mjs +298 -0
  132. package/src/context/bm25.mjs +85 -0
  133. package/src/context/retriever.mjs +270 -0
  134. package/src/context/skeleton.mjs +134 -0
  135. package/src/core/agent-loop.mjs +480 -0
  136. package/src/core/approval.mjs +273 -0
  137. package/src/core/backend-url.mjs +57 -0
  138. package/src/core/cache.mjs +105 -0
  139. package/src/core/callback-client.mjs +149 -0
  140. package/src/core/checkpoints.mjs +142 -0
  141. package/src/core/context-manager.mjs +198 -0
  142. package/src/core/headless.mjs +168 -0
  143. package/src/core/hooks-manager.mjs +87 -0
  144. package/src/core/jsonl-writer.mjs +351 -0
  145. package/src/core/local-agent.mjs +429 -0
  146. package/src/core/local-store.mjs +325 -0
  147. package/src/core/mode-selector.mjs +51 -0
  148. package/src/core/output-filter.mjs +177 -0
  149. package/src/core/paths.mjs +101 -0
  150. package/src/core/pricing.mjs +314 -0
  151. package/src/core/providers.mjs +219 -0
  152. package/src/core/rate-limiter.mjs +119 -0
  153. package/src/core/safety.mjs +200 -0
  154. package/src/core/scheduler.mjs +173 -0
  155. package/src/core/session-manager.mjs +317 -0
  156. package/src/core/session.mjs +143 -0
  157. package/src/core/settings-sync.mjs +85 -0
  158. package/src/core/stagnation.mjs +57 -0
  159. package/src/core/stream-client.mjs +367 -0
  160. package/src/core/streaming.mjs +182 -0
  161. package/src/core/system-prompt.mjs +135 -0
  162. package/src/core/tool-executor.mjs +725 -0
  163. package/src/hooks/engine.mjs +162 -0
  164. package/src/index.mjs +370 -0
  165. package/src/mcp/client.mjs +253 -0
  166. package/src/mcp/transport-shttp.mjs +130 -0
  167. package/src/mcp/transport-sse.mjs +131 -0
  168. package/src/mcp/transport-ws.mjs +134 -0
  169. package/src/permissions/checker.mjs +57 -0
  170. package/src/permissions/command-classifier.mjs +573 -0
  171. package/src/permissions/injection-check.mjs +60 -0
  172. package/src/permissions/path-check.mjs +102 -0
  173. package/src/permissions/prompt.mjs +73 -0
  174. package/src/permissions/sandbox.mjs +112 -0
  175. package/src/plugins/loader.mjs +138 -0
  176. package/src/skills/loader.mjs +147 -0
  177. package/src/skills/runner.mjs +55 -0
  178. package/src/telemetry/index.mjs +96 -0
  179. package/src/terminal/agents.mjs +177 -0
  180. package/src/terminal/analytics.mjs +292 -0
  181. package/src/terminal/ansi.mjs +421 -0
  182. package/src/terminal/main.mjs +150 -0
  183. package/src/terminal/repl.mjs +1484 -0
  184. package/src/terminal/tool-display.mjs +58 -0
  185. package/src/tools/agent.mjs +137 -0
  186. package/src/tools/ask-user.mjs +61 -0
  187. package/src/tools/bash.mjs +148 -0
  188. package/src/tools/cron-create.mjs +120 -0
  189. package/src/tools/cron-delete.mjs +49 -0
  190. package/src/tools/cron-list.mjs +37 -0
  191. package/src/tools/edit.mjs +82 -0
  192. package/src/tools/enter-worktree.mjs +69 -0
  193. package/src/tools/exit-worktree.mjs +57 -0
  194. package/src/tools/glob.mjs +117 -0
  195. package/src/tools/grep.mjs +129 -0
  196. package/src/tools/lint.mjs +71 -0
  197. package/src/tools/ls.mjs +58 -0
  198. package/src/tools/lsp.mjs +115 -0
  199. package/src/tools/multi-edit.mjs +94 -0
  200. package/src/tools/notebook-edit.mjs +96 -0
  201. package/src/tools/read-mcp-resource.mjs +57 -0
  202. package/src/tools/read.mjs +138 -0
  203. package/src/tools/registry.mjs +132 -0
  204. package/src/tools/remote-trigger.mjs +84 -0
  205. package/src/tools/send-message.mjs +64 -0
  206. package/src/tools/skill.mjs +52 -0
  207. package/src/tools/test-runner.mjs +49 -0
  208. package/src/tools/todo-write.mjs +68 -0
  209. package/src/tools/tool-search.mjs +77 -0
  210. package/src/tools/web-fetch.mjs +65 -0
  211. package/src/tools/web-search.mjs +89 -0
  212. package/src/tools/write.mjs +55 -0
  213. package/src/ui/banner.mjs +237 -0
  214. package/src/ui/commands.mjs +499 -0
  215. package/src/ui/formatter.mjs +379 -0
  216. package/src/ui/markdown.mjs +278 -0
  217. package/src/ui/slash-commands.mjs +258 -0
  218. package/index.js +0 -1
@@ -0,0 +1,220 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import { ToolCallBadge } from './tool-call-badge'
5
+ import { CompactionCard } from './compaction-card'
6
+ import { AssistantMarkdown } from './assistant-markdown'
7
+ import { UserToolResult } from './user-tool-result'
8
+ import { formatCost, formatTokens, formatDurationMs } from '@/lib/decode'
9
+ import type { ReplayTurn, CompactionEvent } from '@/types/claude'
10
+ import { Badge } from '@/components/ui/badge'
11
+ import { Button } from '@/components/ui/button'
12
+ import { cn } from '@/lib/utils'
13
+ import { ChevronDown, ChevronUp, Brain, Clock, Coins } from 'lucide-react'
14
+
15
+ /** Show “Show more” when assistant text exceeds this length (markdown; avoid slicing mid-block). */
16
+ const ASSISTANT_COLLAPSE_THRESHOLD = 900
17
+
18
+ interface TurnCardProps {
19
+ turn: ReplayTurn
20
+ turnNumber: number
21
+ compactionBefore?: CompactionEvent
22
+ toolResults: Map<string, { content: string; is_error: boolean }>
23
+ }
24
+
25
+ function TokenBreakdown({ turn }: { turn: ReplayTurn }) {
26
+ if (!turn.usage) return null
27
+ const u = turn.usage
28
+ const items = [
29
+ u.input_tokens ? { label: 'In', value: u.input_tokens, color: 'var(--viz-sky)' } : null,
30
+ u.output_tokens ? { label: 'Out', value: u.output_tokens, color: '#d97706' } : null,
31
+ u.cache_creation_input_tokens ? { label: 'cW', value: u.cache_creation_input_tokens, color: '#a78bfa' } : null,
32
+ u.cache_read_input_tokens ? { label: 'cR', value: u.cache_read_input_tokens, color: '#34d399' } : null,
33
+ ].filter(Boolean) as { label: string; value: number; color: string }[]
34
+
35
+ if (items.length === 0) return null
36
+
37
+ return (
38
+ <div className="flex items-center gap-1 flex-wrap">
39
+ <Coins className="w-3 h-3 text-muted-foreground/50 shrink-0" />
40
+ {items.map(({ label, value, color }) => (
41
+ <span
42
+ key={label}
43
+ className="text-[11px] font-mono px-1.5 py-0.5 rounded border border-border/50 bg-muted/50"
44
+ style={{ color }}
45
+ >
46
+ {label}:{formatTokens(value)}
47
+ </span>
48
+ ))}
49
+ {turn.estimated_cost ? (
50
+ <span className="text-[11px] font-mono text-[#d97706] px-1 py-0.5">
51
+ {formatCost(turn.estimated_cost)}
52
+ </span>
53
+ ) : null}
54
+ </div>
55
+ )
56
+ }
57
+
58
+ export function UserTurnCard({ turn, compactionBefore, toolResults }: TurnCardProps) {
59
+ return (
60
+ <div>
61
+ {compactionBefore && <CompactionCard event={compactionBefore} />}
62
+
63
+ <div className="mb-5 flex flex-col items-end gap-1.5">
64
+ {/* Timestamp label */}
65
+ <span className="text-[11px] text-muted-foreground/40 pr-1">
66
+ {new Date(turn.timestamp).toLocaleTimeString()}
67
+ </span>
68
+
69
+ {/* User bubble (right-aligned) */}
70
+ {turn.text && (
71
+ <div className="max-w-[85%] bg-primary/10 border border-primary/20 rounded-2xl rounded-tr-sm px-4 py-3">
72
+ <p className="text-sm text-foreground/90 whitespace-pre-wrap leading-relaxed">
73
+ {turn.text}
74
+ </p>
75
+ </div>
76
+ )}
77
+
78
+ {/* Tool results (user feedback) */}
79
+ {turn.tool_results && turn.tool_results.length > 0 && (
80
+ <div className="flex w-full max-w-[90%] flex-col gap-2">
81
+ {turn.tool_results.map(r => (
82
+ <UserToolResult key={r.tool_use_id} content={r.content} isError={r.is_error} />
83
+ ))}
84
+ </div>
85
+ )}
86
+ </div>
87
+ </div>
88
+ )
89
+ }
90
+
91
+ export function AssistantTurnCard({ turn, turnNumber, toolResults }: TurnCardProps) {
92
+ const [thinkingOpen, setThinkingOpen] = useState(false)
93
+ const [expanded, setExpanded] = useState(false)
94
+
95
+ const modelShort = turn.model?.includes('opus-4-6') ? 'Opus 4.6'
96
+ : turn.model?.includes('opus-4-5') ? 'Opus 4.5'
97
+ : turn.model?.includes('opus-4') ? 'Opus 4'
98
+ : turn.model?.includes('sonnet-4-6') ? 'Sonnet 4.6'
99
+ : turn.model?.includes('sonnet-4-5') ? 'Sonnet 4.5'
100
+ : turn.model?.includes('sonnet') ? 'Sonnet'
101
+ : turn.model?.includes('haiku') ? 'Haiku'
102
+ : turn.model ?? 'Claude'
103
+
104
+ const textToShow = turn.text ?? ''
105
+ const needsExpandToggle = textToShow.length > ASSISTANT_COLLAPSE_THRESHOLD
106
+
107
+ return (
108
+ <div className="mb-6 flex flex-col gap-1.5">
109
+ {/* Header row */}
110
+ <div className="flex items-center gap-2 flex-wrap">
111
+ <div className="flex items-center gap-1.5">
112
+ <div className="w-6 h-6 rounded-full bg-primary/20 border border-primary/30 flex items-center justify-center shrink-0">
113
+ <span className="text-[10px] font-bold text-primary">C</span>
114
+ </div>
115
+ <span className="text-xs font-semibold text-primary/80">Claude</span>
116
+ </div>
117
+ <Badge variant="outline" className="text-[11px] px-1.5 py-0 h-5 font-mono">
118
+ {modelShort}
119
+ </Badge>
120
+ <span className="text-[11px] text-muted-foreground/30">#{turnNumber}</span>
121
+ {turn.turn_duration_ms && (
122
+ <span className="text-[11px] text-muted-foreground/40 flex items-center gap-1">
123
+ <Clock className="w-3 h-3" />
124
+ {formatDurationMs(turn.turn_duration_ms)}
125
+ </span>
126
+ )}
127
+ </div>
128
+
129
+ {/* Thinking block */}
130
+ {turn.has_thinking && (
131
+ <div className="ml-8">
132
+ <Button
133
+ type="button"
134
+ variant="ghost"
135
+ size="sm"
136
+ className="h-auto gap-1.5 px-2 py-1.5 text-xs font-medium text-indigo-700 hover:bg-indigo-500/10 hover:text-indigo-900 dark:text-indigo-400/90 dark:hover:text-indigo-300"
137
+ onClick={() => setThinkingOpen(o => !o)}
138
+ >
139
+ <Brain className="h-3.5 w-3.5 shrink-0" />
140
+ Extended thinking
141
+ <ChevronDown
142
+ className={cn('h-3.5 w-3.5 shrink-0 transition-transform duration-200', thinkingOpen && 'rotate-180')}
143
+ />
144
+ </Button>
145
+ {thinkingOpen && turn.thinking_text && (
146
+ <div className="mt-1 bg-indigo-50 border border-indigo-200/80 rounded-xl px-4 py-3 dark:bg-indigo-950/20 dark:border-indigo-800/25">
147
+ <pre className="text-xs text-indigo-950/90 whitespace-pre-wrap max-h-56 overflow-auto leading-relaxed dark:text-indigo-200/50">
148
+ {turn.thinking_text.slice(0, 3000)}
149
+ {turn.thinking_text.length > 3000 && (
150
+ <span className="text-indigo-400/40"> …[{(turn.thinking_text.length - 3000).toLocaleString()} more chars]</span>
151
+ )}
152
+ </pre>
153
+ </div>
154
+ )}
155
+ </div>
156
+ )}
157
+
158
+ {/* Tool calls */}
159
+ {turn.tool_calls && turn.tool_calls.length > 0 && (
160
+ <div className="ml-8 space-y-1">
161
+ {turn.tool_calls.map(tc => (
162
+ <ToolCallBadge
163
+ key={tc.id}
164
+ tool={tc}
165
+ result={toolResults.get(tc.id)}
166
+ />
167
+ ))}
168
+ </div>
169
+ )}
170
+
171
+ {/* Response text bubble */}
172
+ {textToShow && (
173
+ <div className="ml-8">
174
+ <div className="rounded-2xl rounded-tl-sm border border-border/60 bg-card px-4 py-3">
175
+ <div
176
+ className={cn(
177
+ 'relative',
178
+ needsExpandToggle && !expanded && 'max-h-112 overflow-hidden'
179
+ )}
180
+ >
181
+ <AssistantMarkdown content={textToShow} />
182
+ {needsExpandToggle && !expanded && (
183
+ <div
184
+ className="pointer-events-none absolute inset-x-0 bottom-0 h-14 bg-linear-to-t from-card to-transparent"
185
+ aria-hidden
186
+ />
187
+ )}
188
+ </div>
189
+ {needsExpandToggle && (
190
+ <Button
191
+ type="button"
192
+ variant="ghost"
193
+ size="sm"
194
+ className="mt-2 h-7 gap-1 px-2 text-xs text-muted-foreground hover:text-foreground"
195
+ onClick={() => setExpanded(e => !e)}
196
+ >
197
+ {expanded ? (
198
+ <>
199
+ <ChevronUp className="h-3 w-3" /> Show less
200
+ </>
201
+ ) : (
202
+ <>
203
+ <ChevronDown className="h-3 w-3" /> Show full response
204
+ </>
205
+ )}
206
+ </Button>
207
+ )}
208
+ </div>
209
+ </div>
210
+ )}
211
+
212
+ {/* Token breakdown */}
213
+ {turn.usage && (
214
+ <div className="ml-8 mt-0.5">
215
+ <TokenBreakdown turn={turn} />
216
+ </div>
217
+ )}
218
+ </div>
219
+ )
220
+ }
@@ -0,0 +1,158 @@
1
+ 'use client'
2
+
3
+ import { cn } from '@/lib/utils'
4
+ import { AlertCircle, CheckCircle2, FileEdit, FileText, Info } from 'lucide-react'
5
+
6
+ export type ParsedToolResult =
7
+ | { kind: 'file_updated'; path: string; note?: string }
8
+ | { kind: 'file_written'; path: string; note?: string }
9
+ | { kind: 'file_read'; path: string; note?: string }
10
+ | { kind: 'plain'; text: string }
11
+
12
+ function restAfterAction(s: string, idx: number, needleLen: number): string | undefined {
13
+ let rest = s.slice(idx + needleLen).trim()
14
+ rest = rest.replace(/^\.\s*/, '').trim()
15
+ if (rest.startsWith('(') && rest.endsWith(')')) {
16
+ rest = rest.slice(1, -1).trim()
17
+ }
18
+ return rest || undefined
19
+ }
20
+
21
+ /**
22
+ * Best-effort parse of common Orca / tool sandbox result strings.
23
+ */
24
+ export function parseToolResultMessage(raw: string): ParsedToolResult {
25
+ const s = raw.trim()
26
+ if (!s) return { kind: 'plain', text: raw }
27
+
28
+ const prefix = 'The file '
29
+
30
+ const needles: Array<{ needle: string; kind: 'file_updated' | 'file_written' | 'file_read' }> = [
31
+ { needle: ' has been updated successfully', kind: 'file_updated' },
32
+ { needle: ' has been written successfully', kind: 'file_written' },
33
+ { needle: ' has been written.', kind: 'file_written' },
34
+ { needle: ' was read successfully', kind: 'file_read' },
35
+ { needle: ' has been read.', kind: 'file_read' },
36
+ ]
37
+
38
+ if (s.startsWith(prefix)) {
39
+ for (const { needle, kind } of needles) {
40
+ const i = s.indexOf(needle)
41
+ if (i <= prefix.length) continue
42
+ const path = s.slice(prefix.length, i).trim()
43
+ if (!path) continue
44
+ const note = restAfterAction(s, i, needle.length)
45
+ if (kind === 'file_updated') return { kind: 'file_updated', path, note }
46
+ if (kind === 'file_written') return { kind: 'file_written', path, note }
47
+ return { kind: 'file_read', path, note }
48
+ }
49
+ }
50
+
51
+ return { kind: 'plain', text: s }
52
+ }
53
+
54
+ function formatPathForUi(path: string, max = 100): string {
55
+ if (path.length <= max) return path
56
+ const parts = path.split('/')
57
+ if (parts.length <= 2) return path.slice(0, max - 1) + '…'
58
+ const file = parts[parts.length - 1] ?? path
59
+ const start = parts.slice(0, 2).join('/')
60
+ return `${start}/…/${file}`
61
+ }
62
+
63
+ interface Props {
64
+ content: string
65
+ isError: boolean
66
+ }
67
+
68
+ export function UserToolResult({ content, isError }: Props) {
69
+ const parsed = parseToolResultMessage(content)
70
+
71
+ if (isError) {
72
+ return (
73
+ <div className="flex gap-2.5 rounded-xl border border-red-500/25 bg-red-950/25 px-3 py-2.5 text-left">
74
+ <AlertCircle className="mt-0.5 h-4 w-4 shrink-0 text-red-400" />
75
+ <div className="min-w-0 flex-1">
76
+ <p className="text-[11px] font-semibold uppercase tracking-wide text-red-400/90">Tool error</p>
77
+ <pre className="mt-1 whitespace-pre-wrap wrap-break-word font-mono text-[12px] leading-relaxed text-red-200/90">
78
+ {content}
79
+ </pre>
80
+ </div>
81
+ </div>
82
+ )
83
+ }
84
+
85
+ if (parsed.kind === 'file_updated') {
86
+ return (
87
+ <div className="flex gap-2.5 rounded-xl border border-emerald-500/20 bg-emerald-500/6 px-3 py-2.5 text-left">
88
+ <FileEdit className="mt-0.5 h-4 w-4 shrink-0 text-emerald-500" />
89
+ <div className="min-w-0 flex-1 space-y-1">
90
+ <div className="flex flex-wrap items-center gap-1.5">
91
+ <span className="text-[13px] font-medium text-foreground">File updated</span>
92
+ <CheckCircle2 className="h-3.5 w-3.5 text-emerald-500/90" aria-hidden />
93
+ </div>
94
+ <p
95
+ className="break-all font-mono text-[12px] leading-snug text-foreground/85"
96
+ title={parsed.path}
97
+ >
98
+ {formatPathForUi(parsed.path)}
99
+ </p>
100
+ {parsed.note ? (
101
+ <p className="flex gap-1.5 text-[11px] leading-relaxed text-muted-foreground">
102
+ <Info className="mt-0.5 h-3 w-3 shrink-0 text-muted-foreground/50" />
103
+ <span>{parsed.note}</span>
104
+ </p>
105
+ ) : null}
106
+ </div>
107
+ </div>
108
+ )
109
+ }
110
+
111
+ if (parsed.kind === 'file_written') {
112
+ return (
113
+ <div className="flex gap-2.5 rounded-xl border border-blue-700/20 bg-blue-700/[0.07] dark:border-sky-500/20 dark:bg-sky-500/6 px-3 py-2.5 text-left">
114
+ <FileText className="mt-0.5 h-4 w-4 shrink-0 text-blue-700 dark:text-sky-500" />
115
+ <div className="min-w-0 flex-1 space-y-1">
116
+ <span className="text-[13px] font-medium text-foreground">File written</span>
117
+ <p className="break-all font-mono text-[12px] leading-snug text-foreground/85" title={parsed.path}>
118
+ {formatPathForUi(parsed.path)}
119
+ </p>
120
+ {parsed.note ? <p className="text-[11px] text-muted-foreground">{parsed.note}</p> : null}
121
+ </div>
122
+ </div>
123
+ )
124
+ }
125
+
126
+ if (parsed.kind === 'file_read') {
127
+ return (
128
+ <div className="flex gap-2.5 rounded-xl border border-border/60 bg-muted/30 px-3 py-2.5 text-left">
129
+ <FileText className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
130
+ <div className="min-w-0 flex-1 space-y-1">
131
+ <span className="text-[13px] font-medium text-foreground/90">File read</span>
132
+ <p className="break-all font-mono text-[12px] leading-snug text-muted-foreground" title={parsed.path}>
133
+ {formatPathForUi(parsed.path)}
134
+ </p>
135
+ {parsed.note ? <p className="text-[11px] text-muted-foreground/80">{parsed.note}</p> : null}
136
+ </div>
137
+ </div>
138
+ )
139
+ }
140
+
141
+ const text = parsed.text
142
+ const long = text.length > 320
143
+
144
+ return (
145
+ <div
146
+ className={cn(
147
+ 'rounded-xl border border-border/50 bg-muted/35 px-3 py-2.5 text-left',
148
+ 'text-[13px] leading-relaxed text-foreground/85'
149
+ )}
150
+ >
151
+ <p className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground/70">Result</p>
152
+ <p className={cn('mt-1.5 whitespace-pre-wrap wrap-break-word', long && 'line-clamp-6')}>{text}</p>
153
+ {long && (
154
+ <p className="mt-2 text-[11px] text-muted-foreground/50">… {text.length.toLocaleString()} characters total</p>
155
+ )}
156
+ </div>
157
+ )
158
+ }
@@ -0,0 +1,49 @@
1
+ 'use client'
2
+
3
+ interface BadgeProps {
4
+ has_compaction?: boolean
5
+ uses_task_agent?: boolean
6
+ uses_mcp?: boolean
7
+ uses_web_search?: boolean
8
+ uses_web_fetch?: boolean
9
+ has_thinking?: boolean
10
+ }
11
+
12
+ export function SessionBadges({
13
+ has_compaction,
14
+ uses_task_agent,
15
+ uses_mcp,
16
+ uses_web_search,
17
+ uses_web_fetch,
18
+ has_thinking,
19
+ }: BadgeProps) {
20
+ return (
21
+ <div className="flex flex-wrap gap-1">
22
+ {has_compaction && (
23
+ <span className="inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded text-[12px] font-medium bg-amber-500/20 text-amber-400 border border-amber-500/30">
24
+ ⚡ compacted
25
+ </span>
26
+ )}
27
+ {uses_task_agent && (
28
+ <span className="inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded text-[12px] font-medium bg-purple-500/20 text-purple-400 border border-purple-500/30">
29
+ 🤖 agent
30
+ </span>
31
+ )}
32
+ {uses_mcp && (
33
+ <span className="inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded text-[12px] font-medium bg-blue-500/20 text-blue-700 dark:text-blue-400 border border-blue-500/30">
34
+ 🔌 mcp
35
+ </span>
36
+ )}
37
+ {(uses_web_search || uses_web_fetch) && (
38
+ <span className="inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded text-[12px] font-medium bg-green-500/20 text-green-400 border border-green-500/30">
39
+ 🔍 web
40
+ </span>
41
+ )}
42
+ {has_thinking && (
43
+ <span className="inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded text-[12px] font-medium bg-indigo-500/20 text-indigo-400 border border-indigo-500/30">
44
+ 🧠 thinking
45
+ </span>
46
+ )}
47
+ </div>
48
+ )
49
+ }