@neurocode-ai/tui 1.18.8

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 (241) hide show
  1. package/bunfig.toml +4 -0
  2. package/package.json +72 -0
  3. package/src/app.tsx +1134 -0
  4. package/src/attention.ts +260 -0
  5. package/src/audio.d.ts +9 -0
  6. package/src/audio.ts +53 -0
  7. package/src/clipboard.ts +124 -0
  8. package/src/component/bg-pulse-render.ts +436 -0
  9. package/src/component/bg-pulse.tsx +99 -0
  10. package/src/component/command-palette.tsx +79 -0
  11. package/src/component/dialog-agent.tsx +31 -0
  12. package/src/component/dialog-console-org.tsx +135 -0
  13. package/src/component/dialog-debug.tsx +90 -0
  14. package/src/component/dialog-mcp.tsx +85 -0
  15. package/src/component/dialog-model.tsx +197 -0
  16. package/src/component/dialog-move-session.tsx +353 -0
  17. package/src/component/dialog-provider.tsx +469 -0
  18. package/src/component/dialog-retry-action.tsx +160 -0
  19. package/src/component/dialog-session-delete-failed.tsx +99 -0
  20. package/src/component/dialog-session-list.tsx +364 -0
  21. package/src/component/dialog-session-rename.tsx +31 -0
  22. package/src/component/dialog-skill.tsx +70 -0
  23. package/src/component/dialog-stash.tsx +87 -0
  24. package/src/component/dialog-status.tsx +168 -0
  25. package/src/component/dialog-tag.tsx +47 -0
  26. package/src/component/dialog-theme-list.tsx +50 -0
  27. package/src/component/dialog-variant.tsx +39 -0
  28. package/src/component/dialog-workspace-create.tsx +308 -0
  29. package/src/component/dialog-workspace-file-changes.tsx +144 -0
  30. package/src/component/dialog-workspace-list.tsx +112 -0
  31. package/src/component/dialog-workspace-unavailable.tsx +69 -0
  32. package/src/component/error-component.tsx +240 -0
  33. package/src/component/logo.tsx +61 -0
  34. package/src/component/plugin-route-missing.tsx +14 -0
  35. package/src/component/prompt/autocomplete.tsx +781 -0
  36. package/src/component/prompt/cwd.ts +0 -0
  37. package/src/component/prompt/frecency.tsx +1 -0
  38. package/src/component/prompt/history.tsx +1 -0
  39. package/src/component/prompt/index.tsx +1713 -0
  40. package/src/component/prompt/local-attachment.ts +48 -0
  41. package/src/component/prompt/move.tsx +205 -0
  42. package/src/component/prompt/stash.tsx +1 -0
  43. package/src/component/prompt/workspace.tsx +137 -0
  44. package/src/component/register-spinner.ts +6 -0
  45. package/src/component/spinner.tsx +26 -0
  46. package/src/component/startup-loading.tsx +63 -0
  47. package/src/component/todo-item.tsx +32 -0
  48. package/src/component/use-connected.tsx +12 -0
  49. package/src/component/workspace-label.tsx +19 -0
  50. package/src/config/index.tsx +129 -0
  51. package/src/config/keybind.ts +471 -0
  52. package/src/context/args.tsx +16 -0
  53. package/src/context/clipboard.tsx +18 -0
  54. package/src/context/data.tsx +569 -0
  55. package/src/context/directory.ts +17 -0
  56. package/src/context/editor.ts +408 -0
  57. package/src/context/epilogue.tsx +6 -0
  58. package/src/context/event.ts +36 -0
  59. package/src/context/exit.tsx +8 -0
  60. package/src/context/helper.tsx +26 -0
  61. package/src/context/kv.tsx +66 -0
  62. package/src/context/local.tsx +542 -0
  63. package/src/context/location.tsx +14 -0
  64. package/src/context/path-format.tsx +24 -0
  65. package/src/context/permission.tsx +26 -0
  66. package/src/context/project.tsx +115 -0
  67. package/src/context/prompt.tsx +18 -0
  68. package/src/context/route.tsx +60 -0
  69. package/src/context/runtime.tsx +62 -0
  70. package/src/context/sdk.tsx +151 -0
  71. package/src/context/sync.tsx +666 -0
  72. package/src/context/theme.tsx +332 -0
  73. package/src/context/thinking.ts +67 -0
  74. package/src/editor-zed.ts +286 -0
  75. package/src/editor.ts +101 -0
  76. package/src/feature-plugins/builtins.ts +36 -0
  77. package/src/feature-plugins/home/footer.tsx +100 -0
  78. package/src/feature-plugins/home/tips-view.tsx +287 -0
  79. package/src/feature-plugins/home/tips.tsx +59 -0
  80. package/src/feature-plugins/sidebar/context.tsx +65 -0
  81. package/src/feature-plugins/sidebar/files.tsx +70 -0
  82. package/src/feature-plugins/sidebar/footer.tsx +98 -0
  83. package/src/feature-plugins/sidebar/lsp.tsx +65 -0
  84. package/src/feature-plugins/sidebar/mcp.tsx +97 -0
  85. package/src/feature-plugins/sidebar/todo.tsx +49 -0
  86. package/src/feature-plugins/system/diff-viewer-file-tree-utils.ts +232 -0
  87. package/src/feature-plugins/system/diff-viewer-file-tree.tsx +162 -0
  88. package/src/feature-plugins/system/diff-viewer-ui.tsx +103 -0
  89. package/src/feature-plugins/system/diff-viewer.tsx +1077 -0
  90. package/src/feature-plugins/system/notifications.ts +94 -0
  91. package/src/feature-plugins/system/plugins.tsx +269 -0
  92. package/src/feature-plugins/system/which-key.tsx +608 -0
  93. package/src/index.tsx +1 -0
  94. package/src/keymap.tsx +290 -0
  95. package/src/logo.ts +11 -0
  96. package/src/parsers-config.ts +386 -0
  97. package/src/plugin/adapters.tsx +355 -0
  98. package/src/plugin/api.ts +52 -0
  99. package/src/plugin/command-shim.ts +109 -0
  100. package/src/plugin/runtime.tsx +81 -0
  101. package/src/plugin/slots.tsx +65 -0
  102. package/src/prompt/display.ts +48 -0
  103. package/src/prompt/frecency.tsx +80 -0
  104. package/src/prompt/history.tsx +111 -0
  105. package/src/prompt/part.ts +29 -0
  106. package/src/prompt/stash.tsx +89 -0
  107. package/src/prompt/traits.ts +29 -0
  108. package/src/routes/home/session-destination.tsx +41 -0
  109. package/src/routes/home.tsx +95 -0
  110. package/src/routes/session/dialog-fork-from-timeline.tsx +76 -0
  111. package/src/routes/session/dialog-message.tsx +109 -0
  112. package/src/routes/session/dialog-subagent.tsx +26 -0
  113. package/src/routes/session/dialog-timeline.tsx +47 -0
  114. package/src/routes/session/footer.tsx +91 -0
  115. package/src/routes/session/index.tsx +2710 -0
  116. package/src/routes/session/permission.tsx +718 -0
  117. package/src/routes/session/question.tsx +514 -0
  118. package/src/routes/session/sidebar.tsx +103 -0
  119. package/src/routes/session/subagent-footer.tsx +132 -0
  120. package/src/runtime.tsx +9 -0
  121. package/src/terminal-win32.ts +130 -0
  122. package/src/theme/assets/aura.json +69 -0
  123. package/src/theme/assets/ayu.json +80 -0
  124. package/src/theme/assets/carbonfox.json +248 -0
  125. package/src/theme/assets/catppuccin-frappe.json +230 -0
  126. package/src/theme/assets/catppuccin-macchiato.json +230 -0
  127. package/src/theme/assets/catppuccin.json +112 -0
  128. package/src/theme/assets/cobalt2.json +225 -0
  129. package/src/theme/assets/cursor.json +249 -0
  130. package/src/theme/assets/dracula.json +219 -0
  131. package/src/theme/assets/everforest.json +241 -0
  132. package/src/theme/assets/flexoki.json +237 -0
  133. package/src/theme/assets/github.json +233 -0
  134. package/src/theme/assets/gruvbox.json +242 -0
  135. package/src/theme/assets/kanagawa.json +77 -0
  136. package/src/theme/assets/lucent-orng.json +234 -0
  137. package/src/theme/assets/material.json +235 -0
  138. package/src/theme/assets/matrix.json +77 -0
  139. package/src/theme/assets/mercury.json +252 -0
  140. package/src/theme/assets/monokai.json +221 -0
  141. package/src/theme/assets/nightowl.json +221 -0
  142. package/src/theme/assets/nord.json +223 -0
  143. package/src/theme/assets/one-dark.json +84 -0
  144. package/src/theme/assets/opencode.json +245 -0
  145. package/src/theme/assets/orng.json +249 -0
  146. package/src/theme/assets/osaka-jade.json +93 -0
  147. package/src/theme/assets/palenight.json +222 -0
  148. package/src/theme/assets/rosepine.json +234 -0
  149. package/src/theme/assets/solarized.json +223 -0
  150. package/src/theme/assets/synthwave84.json +226 -0
  151. package/src/theme/assets/tokyonight.json +243 -0
  152. package/src/theme/assets/vercel.json +245 -0
  153. package/src/theme/assets/vesper.json +218 -0
  154. package/src/theme/assets/zenburn.json +223 -0
  155. package/src/theme/index.ts +1089 -0
  156. package/src/ui/border.ts +21 -0
  157. package/src/ui/dialog-alert.tsx +66 -0
  158. package/src/ui/dialog-confirm.tsx +108 -0
  159. package/src/ui/dialog-export-options.tsx +217 -0
  160. package/src/ui/dialog-help.tsx +40 -0
  161. package/src/ui/dialog-prompt.tsx +126 -0
  162. package/src/ui/dialog-select.tsx +790 -0
  163. package/src/ui/dialog.tsx +231 -0
  164. package/src/ui/link.tsx +34 -0
  165. package/src/ui/spinner.ts +368 -0
  166. package/src/ui/toast.tsx +102 -0
  167. package/src/util/collapse-tool-output.ts +19 -0
  168. package/src/util/error.ts +182 -0
  169. package/src/util/filetype.ts +130 -0
  170. package/src/util/format.ts +20 -0
  171. package/src/util/layout.ts +25 -0
  172. package/src/util/locale.ts +86 -0
  173. package/src/util/model.ts +28 -0
  174. package/src/util/path.ts +12 -0
  175. package/src/util/persistence.ts +33 -0
  176. package/src/util/presentation.ts +38 -0
  177. package/src/util/provider-origin.ts +7 -0
  178. package/src/util/record.ts +3 -0
  179. package/src/util/renderer.ts +7 -0
  180. package/src/util/revert-diff.ts +18 -0
  181. package/src/util/scroll.ts +27 -0
  182. package/src/util/selection.ts +79 -0
  183. package/src/util/session.ts +3 -0
  184. package/src/util/signal.ts +51 -0
  185. package/src/util/system.ts +20 -0
  186. package/src/util/tool-display.ts +13 -0
  187. package/src/util/transcript.ts +112 -0
  188. package/sst-env.d.ts +10 -0
  189. package/test/app-lifecycle.test.tsx +128 -0
  190. package/test/cli/cmd/tui/dialog-workspace-create.test.ts +28 -0
  191. package/test/cli/cmd/tui/model-options.test.ts +32 -0
  192. package/test/cli/cmd/tui/notifications.test.ts +267 -0
  193. package/test/cli/cmd/tui/provider-options.test.ts +41 -0
  194. package/test/cli/cmd/tui/sync-fixture.tsx +70 -0
  195. package/test/cli/cmd/tui/sync-live-hydration.test.tsx +262 -0
  196. package/test/cli/cmd/tui/sync-undefined-messages.test.tsx +43 -0
  197. package/test/cli/cmd/tui/sync.test.tsx +65 -0
  198. package/test/cli/tui/__snapshots__/inline-tool-wrap-snapshot.test.tsx.snap +92 -0
  199. package/test/cli/tui/data.test.tsx +486 -0
  200. package/test/cli/tui/dialog-prompt.test.tsx +147 -0
  201. package/test/cli/tui/diff-viewer-file-tree.test.tsx +200 -0
  202. package/test/cli/tui/diff-viewer.test.tsx +268 -0
  203. package/test/cli/tui/inline-tool-wrap-snapshot.test.tsx +351 -0
  204. package/test/cli/tui/prompt-submit-race.test.ts +98 -0
  205. package/test/cli/tui/thinking.test.ts +36 -0
  206. package/test/cli/tui/use-event.test.tsx +148 -0
  207. package/test/clipboard.test.ts +19 -0
  208. package/test/component/dialog-session-list.test.ts +46 -0
  209. package/test/config.test.tsx +127 -0
  210. package/test/context/local.test.ts +22 -0
  211. package/test/editor.test.ts +32 -0
  212. package/test/feature-plugins/diff-viewer-file-tree-utils.test.ts +323 -0
  213. package/test/fixture/fixture.ts +13 -0
  214. package/test/fixture/tui-environment.tsx +32 -0
  215. package/test/fixture/tui-plugin.ts +36 -0
  216. package/test/fixture/tui-runtime.ts +12 -0
  217. package/test/fixture/tui-sdk.ts +109 -0
  218. package/test/index.test.tsx +6 -0
  219. package/test/keymap.test.tsx +141 -0
  220. package/test/plugin/runtime.test.ts +50 -0
  221. package/test/plugin/slots.test.tsx +38 -0
  222. package/test/prompt/display.test.ts +33 -0
  223. package/test/prompt/history.test.ts +39 -0
  224. package/test/prompt/jsonl.test.ts +24 -0
  225. package/test/prompt/local-attachment.test.ts +43 -0
  226. package/test/prompt/part.test.ts +53 -0
  227. package/test/prompt/persistence.test.ts +23 -0
  228. package/test/prompt/traits.test.ts +25 -0
  229. package/test/runtime.test.tsx +37 -0
  230. package/test/theme.test.ts +81 -0
  231. package/test/util/error.test.ts +49 -0
  232. package/test/util/filetype.test.ts +16 -0
  233. package/test/util/format.test.ts +59 -0
  234. package/test/util/model.test.ts +9 -0
  235. package/test/util/presentation.test.ts +8 -0
  236. package/test/util/renderer.test.ts +30 -0
  237. package/test/util/revert-diff.test.ts +35 -0
  238. package/test/util/session.test.ts +10 -0
  239. package/test/util/tool-display.test.ts +40 -0
  240. package/test/util/transcript.test.ts +421 -0
  241. package/tsconfig.json +10 -0
@@ -0,0 +1,421 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { formatAssistantHeader, formatMessage, formatPart, formatTranscript } from "../../src/util/transcript"
3
+ import type { AssistantMessage, Part, Provider, UserMessage } from "@neurocode-ai/sdk/v2"
4
+
5
+ const providers: Provider[] = [
6
+ {
7
+ id: "anthropic",
8
+ name: "Anthropic",
9
+ source: "api",
10
+ env: [],
11
+ options: {},
12
+ models: {
13
+ "claude-sonnet-4-20250514": {
14
+ id: "claude-sonnet-4-20250514",
15
+ providerID: "anthropic",
16
+ api: {
17
+ id: "claude-sonnet-4-20250514",
18
+ url: "https://example.com/claude-sonnet-4-20250514",
19
+ npm: "@ai-sdk/anthropic",
20
+ },
21
+ name: "Claude Sonnet 4",
22
+ capabilities: {
23
+ temperature: true,
24
+ reasoning: true,
25
+ attachment: true,
26
+ toolcall: true,
27
+ input: {
28
+ text: true,
29
+ audio: false,
30
+ image: true,
31
+ video: false,
32
+ pdf: true,
33
+ },
34
+ output: {
35
+ text: true,
36
+ audio: false,
37
+ image: false,
38
+ video: false,
39
+ pdf: false,
40
+ },
41
+ interleaved: false,
42
+ },
43
+ cost: {
44
+ input: 0,
45
+ output: 0,
46
+ cache: {
47
+ read: 0,
48
+ write: 0,
49
+ },
50
+ },
51
+ limit: {
52
+ context: 200_000,
53
+ output: 8_192,
54
+ },
55
+ status: "active",
56
+ options: {},
57
+ headers: {},
58
+ release_date: "2025-05-14",
59
+ },
60
+ },
61
+ },
62
+ ]
63
+
64
+ describe("transcript", () => {
65
+ describe("formatAssistantHeader", () => {
66
+ const baseMsg: AssistantMessage = {
67
+ id: "msg_123",
68
+ sessionID: "ses_123",
69
+ role: "assistant",
70
+ agent: "build",
71
+ modelID: "claude-sonnet-4-20250514",
72
+ providerID: "anthropic",
73
+ mode: "",
74
+ parentID: "msg_parent",
75
+ path: { cwd: "/test", root: "/test" },
76
+ cost: 0.001,
77
+ tokens: { input: 100, output: 50, reasoning: 0, cache: { read: 0, write: 0 } },
78
+ time: { created: 1000000, completed: 1005400 },
79
+ }
80
+
81
+ test("includes metadata when enabled", () => {
82
+ const result = formatAssistantHeader(baseMsg, true)
83
+ expect(result).toBe("## Assistant (Build · claude-sonnet-4-20250514 · 5.4s)\n\n")
84
+ })
85
+
86
+ test("uses model display name when available", () => {
87
+ const result = formatAssistantHeader(baseMsg, true, providers)
88
+ expect(result).toBe("## Assistant (Build · Claude Sonnet 4 · 5.4s)\n\n")
89
+ })
90
+
91
+ test("excludes metadata when disabled", () => {
92
+ const result = formatAssistantHeader(baseMsg, false)
93
+ expect(result).toBe("## Assistant\n\n")
94
+ })
95
+
96
+ test("handles missing completed time", () => {
97
+ const msg = { ...baseMsg, time: { created: 1000000 } }
98
+ const result = formatAssistantHeader(msg as AssistantMessage, true)
99
+ expect(result).toBe("## Assistant (Build · claude-sonnet-4-20250514)\n\n")
100
+ })
101
+
102
+ test("titlecases agent name", () => {
103
+ const msg = { ...baseMsg, agent: "plan" }
104
+ const result = formatAssistantHeader(msg, true)
105
+ expect(result).toContain("Plan")
106
+ })
107
+ })
108
+
109
+ describe("formatPart", () => {
110
+ const options = { thinking: true, toolDetails: true, assistantMetadata: true }
111
+
112
+ test("formats text part", () => {
113
+ const part: Part = {
114
+ id: "part_1",
115
+ sessionID: "ses_123",
116
+ messageID: "msg_123",
117
+ type: "text",
118
+ text: "Hello world",
119
+ }
120
+ const result = formatPart(part, options)
121
+ expect(result).toBe("Hello world\n\n")
122
+ })
123
+
124
+ test("skips synthetic text parts", () => {
125
+ const part: Part = {
126
+ id: "part_1",
127
+ sessionID: "ses_123",
128
+ messageID: "msg_123",
129
+ type: "text",
130
+ text: "Synthetic content",
131
+ synthetic: true,
132
+ }
133
+ const result = formatPart(part, options)
134
+ expect(result).toBe("")
135
+ })
136
+
137
+ test("formats reasoning when thinking enabled", () => {
138
+ const part: Part = {
139
+ id: "part_1",
140
+ sessionID: "ses_123",
141
+ messageID: "msg_123",
142
+ type: "reasoning",
143
+ text: "Let me think...",
144
+ time: { start: 1000 },
145
+ }
146
+ const result = formatPart(part, options)
147
+ expect(result).toBe("_Thinking:_\n\nLet me think...\n\n")
148
+ })
149
+
150
+ test("skips reasoning when thinking disabled", () => {
151
+ const part: Part = {
152
+ id: "part_1",
153
+ sessionID: "ses_123",
154
+ messageID: "msg_123",
155
+ type: "reasoning",
156
+ text: "Let me think...",
157
+ time: { start: 1000 },
158
+ }
159
+ const result = formatPart(part, { ...options, thinking: false })
160
+ expect(result).toBe("")
161
+ })
162
+
163
+ test("formats tool part with details", () => {
164
+ const part: Part = {
165
+ id: "part_1",
166
+ sessionID: "ses_123",
167
+ messageID: "msg_123",
168
+ type: "tool",
169
+ callID: "call_1",
170
+ tool: "bash",
171
+ state: {
172
+ status: "completed",
173
+ input: { command: "ls" },
174
+ output: "file1.txt\nfile2.txt",
175
+ title: "List files",
176
+ metadata: {},
177
+ time: { start: 1000, end: 1100 },
178
+ },
179
+ }
180
+ const result = formatPart(part, options)
181
+ expect(result).toContain("**Tool: bash**")
182
+ expect(result).toContain("**Input:**")
183
+ expect(result).toContain('"command": "ls"')
184
+ expect(result).toContain("**Output:**")
185
+ expect(result).toContain("file1.txt")
186
+ })
187
+
188
+ test("formats tool output containing triple backticks without breaking markdown", () => {
189
+ const part: Part = {
190
+ id: "part_1",
191
+ sessionID: "ses_123",
192
+ messageID: "msg_123",
193
+ type: "tool",
194
+ callID: "call_1",
195
+ tool: "bash",
196
+ state: {
197
+ status: "completed",
198
+ input: { command: "echo '```hello```'" },
199
+ output: "```hello```",
200
+ title: "Echo backticks",
201
+ metadata: {},
202
+ time: { start: 1000, end: 1100 },
203
+ },
204
+ }
205
+ const result = formatPart(part, options)
206
+ // The tool header should not be inside a code block
207
+ expect(result).toStartWith("**Tool: bash**\n")
208
+ // Input and output should each be in their own code blocks
209
+ expect(result).toContain("**Input:**\n```json")
210
+ expect(result).toContain("**Output:**\n```\n```hello```\n```")
211
+ })
212
+
213
+ test("formats tool part without details when disabled", () => {
214
+ const part: Part = {
215
+ id: "part_1",
216
+ sessionID: "ses_123",
217
+ messageID: "msg_123",
218
+ type: "tool",
219
+ callID: "call_1",
220
+ tool: "bash",
221
+ state: {
222
+ status: "completed",
223
+ input: { command: "ls" },
224
+ output: "file1.txt",
225
+ title: "List files",
226
+ metadata: {},
227
+ time: { start: 1000, end: 1100 },
228
+ },
229
+ }
230
+ const result = formatPart(part, { ...options, toolDetails: false })
231
+ expect(result).toContain("**Tool: bash**")
232
+ expect(result).not.toContain("**Input:**")
233
+ expect(result).not.toContain("**Output:**")
234
+ })
235
+
236
+ test("formats tool error", () => {
237
+ const part: Part = {
238
+ id: "part_1",
239
+ sessionID: "ses_123",
240
+ messageID: "msg_123",
241
+ type: "tool",
242
+ callID: "call_1",
243
+ tool: "bash",
244
+ state: {
245
+ status: "error",
246
+ input: { command: "invalid" },
247
+ error: "Command failed",
248
+ time: { start: 1000, end: 1100 },
249
+ },
250
+ }
251
+ const result = formatPart(part, options)
252
+ expect(result).toContain("**Error:**")
253
+ expect(result).toContain("Command failed")
254
+ })
255
+ })
256
+
257
+ describe("formatMessage", () => {
258
+ const options = { thinking: true, toolDetails: true, assistantMetadata: true, providers }
259
+
260
+ test("formats user message", () => {
261
+ const msg: UserMessage = {
262
+ id: "msg_123",
263
+ sessionID: "ses_123",
264
+ role: "user",
265
+ agent: "build",
266
+ model: { providerID: "anthropic", modelID: "claude-sonnet-4-20250514" },
267
+ time: { created: 1000000 },
268
+ }
269
+ const parts: Part[] = [{ id: "p1", sessionID: "ses_123", messageID: "msg_123", type: "text", text: "Hello" }]
270
+ const result = formatMessage(msg, parts, options)
271
+ expect(result).toContain("## User")
272
+ expect(result).toContain("Hello")
273
+ })
274
+
275
+ test("formats assistant message with metadata", () => {
276
+ const msg: AssistantMessage = {
277
+ id: "msg_123",
278
+ sessionID: "ses_123",
279
+ role: "assistant",
280
+ agent: "build",
281
+ modelID: "claude-sonnet-4-20250514",
282
+ providerID: "anthropic",
283
+ mode: "",
284
+ parentID: "msg_parent",
285
+ path: { cwd: "/test", root: "/test" },
286
+ cost: 0.001,
287
+ tokens: { input: 100, output: 50, reasoning: 0, cache: { read: 0, write: 0 } },
288
+ time: { created: 1000000, completed: 1005400 },
289
+ }
290
+ const parts: Part[] = [{ id: "p1", sessionID: "ses_123", messageID: "msg_123", type: "text", text: "Hi there" }]
291
+ const result = formatMessage(msg, parts, options)
292
+ expect(result).toContain("## Assistant (Build · Claude Sonnet 4 · 5.4s)")
293
+ expect(result).toContain("Hi there")
294
+ })
295
+ })
296
+
297
+ describe("formatTranscript", () => {
298
+ test("formats complete transcript", () => {
299
+ const session = {
300
+ id: "ses_abc123",
301
+ title: "Test Session",
302
+ time: { created: 1000000000000, updated: 1000000001000 },
303
+ }
304
+ const messages = [
305
+ {
306
+ info: {
307
+ id: "msg_1",
308
+ sessionID: "ses_abc123",
309
+ role: "user" as const,
310
+ agent: "build",
311
+ model: { providerID: "anthropic", modelID: "claude-sonnet-4-20250514" },
312
+ time: { created: 1000000000000 },
313
+ },
314
+ parts: [{ id: "p1", sessionID: "ses_abc123", messageID: "msg_1", type: "text" as const, text: "Hello" }],
315
+ },
316
+ {
317
+ info: {
318
+ id: "msg_2",
319
+ sessionID: "ses_abc123",
320
+ role: "assistant" as const,
321
+ agent: "build",
322
+ modelID: "claude-sonnet-4-20250514",
323
+ providerID: "anthropic",
324
+ mode: "",
325
+ parentID: "msg_1",
326
+ path: { cwd: "/test", root: "/test" },
327
+ cost: 0.001,
328
+ tokens: { input: 100, output: 50, reasoning: 0, cache: { read: 0, write: 0 } },
329
+ time: { created: 1000000000100, completed: 1000000000600 },
330
+ },
331
+ parts: [{ id: "p2", sessionID: "ses_abc123", messageID: "msg_2", type: "text" as const, text: "Hi!" }],
332
+ },
333
+ ]
334
+ const options = {
335
+ thinking: false,
336
+ toolDetails: false,
337
+ assistantMetadata: true,
338
+ providers,
339
+ }
340
+
341
+ const result = formatTranscript(session, messages, options)
342
+
343
+ expect(result).toContain("# Test Session")
344
+ expect(result).toContain("**Session ID:** ses_abc123")
345
+ expect(result).toContain("## User")
346
+ expect(result).toContain("Hello")
347
+ expect(result).toContain("## Assistant (Build · Claude Sonnet 4 · 0.5s)")
348
+ expect(result).toContain("Hi!")
349
+ expect(result).toContain("---")
350
+ })
351
+
352
+ test("falls back to raw model id when provider data is missing", () => {
353
+ const session = {
354
+ id: "ses_abc123",
355
+ title: "Test Session",
356
+ time: { created: 1000000000000, updated: 1000000001000 },
357
+ }
358
+ const messages = [
359
+ {
360
+ info: {
361
+ id: "msg_1",
362
+ sessionID: "ses_abc123",
363
+ role: "assistant" as const,
364
+ agent: "build",
365
+ modelID: "claude-sonnet-4-20250514",
366
+ providerID: "anthropic",
367
+ mode: "",
368
+ parentID: "msg_0",
369
+ path: { cwd: "/test", root: "/test" },
370
+ cost: 0.001,
371
+ tokens: { input: 100, output: 50, reasoning: 0, cache: { read: 0, write: 0 } },
372
+ time: { created: 1000000000100, completed: 1000000000600 },
373
+ },
374
+ parts: [{ id: "p1", sessionID: "ses_abc123", messageID: "msg_1", type: "text" as const, text: "Response" }],
375
+ },
376
+ ]
377
+
378
+ const result = formatTranscript(session, messages, {
379
+ thinking: false,
380
+ toolDetails: false,
381
+ assistantMetadata: true,
382
+ })
383
+
384
+ expect(result).toContain("## Assistant (Build · claude-sonnet-4-20250514 · 0.5s)")
385
+ })
386
+
387
+ test("formats transcript without assistant metadata", () => {
388
+ const session = {
389
+ id: "ses_abc123",
390
+ title: "Test Session",
391
+ time: { created: 1000000000000, updated: 1000000001000 },
392
+ }
393
+ const messages = [
394
+ {
395
+ info: {
396
+ id: "msg_1",
397
+ sessionID: "ses_abc123",
398
+ role: "assistant" as const,
399
+ agent: "build",
400
+ modelID: "claude-sonnet-4-20250514",
401
+ providerID: "anthropic",
402
+ mode: "",
403
+ parentID: "msg_0",
404
+ path: { cwd: "/test", root: "/test" },
405
+ cost: 0.001,
406
+ tokens: { input: 100, output: 50, reasoning: 0, cache: { read: 0, write: 0 } },
407
+ time: { created: 1000000000100, completed: 1000000000600 },
408
+ },
409
+ parts: [{ id: "p1", sessionID: "ses_abc123", messageID: "msg_1", type: "text" as const, text: "Response" }],
410
+ },
411
+ ]
412
+ const options = { thinking: false, toolDetails: false, assistantMetadata: false }
413
+
414
+ const result = formatTranscript(session, messages, options)
415
+
416
+ expect(result).toContain("## Assistant\n\n")
417
+ expect(result).not.toContain("Build")
418
+ expect(result).not.toContain("claude-sonnet-4-20250514")
419
+ })
420
+ })
421
+ })
package/tsconfig.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/tsconfig",
3
+ "extends": "@tsconfig/bun/tsconfig.json",
4
+ "compilerOptions": {
5
+ "jsx": "preserve",
6
+ "jsxImportSource": "@opentui/solid",
7
+ "lib": ["ESNext", "DOM", "DOM.Iterable"],
8
+ "noUncheckedIndexedAccess": false
9
+ }
10
+ }