@jonsoc/app 1.1.34

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 (139) hide show
  1. package/AGENTS.md +30 -0
  2. package/README.md +51 -0
  3. package/bunfig.toml +2 -0
  4. package/e2e/context.spec.ts +45 -0
  5. package/e2e/file-open.spec.ts +23 -0
  6. package/e2e/file-viewer.spec.ts +35 -0
  7. package/e2e/fixtures.ts +40 -0
  8. package/e2e/home.spec.ts +21 -0
  9. package/e2e/model-picker.spec.ts +43 -0
  10. package/e2e/navigation.spec.ts +9 -0
  11. package/e2e/palette.spec.ts +15 -0
  12. package/e2e/prompt-mention.spec.ts +26 -0
  13. package/e2e/prompt-slash-open.spec.ts +22 -0
  14. package/e2e/prompt.spec.ts +62 -0
  15. package/e2e/session.spec.ts +21 -0
  16. package/e2e/settings.spec.ts +44 -0
  17. package/e2e/sidebar.spec.ts +21 -0
  18. package/e2e/terminal-init.spec.ts +25 -0
  19. package/e2e/terminal.spec.ts +16 -0
  20. package/e2e/tsconfig.json +8 -0
  21. package/e2e/utils.ts +38 -0
  22. package/happydom.ts +75 -0
  23. package/index.html +23 -0
  24. package/package.json +72 -0
  25. package/playwright.config.ts +43 -0
  26. package/public/_headers +17 -0
  27. package/public/apple-touch-icon-v3.png +1 -0
  28. package/public/apple-touch-icon.png +1 -0
  29. package/public/favicon-96x96-v3.png +1 -0
  30. package/public/favicon-96x96.png +1 -0
  31. package/public/favicon-v3.ico +1 -0
  32. package/public/favicon-v3.svg +1 -0
  33. package/public/favicon.ico +1 -0
  34. package/public/favicon.svg +1 -0
  35. package/public/oc-theme-preload.js +28 -0
  36. package/public/site.webmanifest +1 -0
  37. package/public/social-share-zen.png +1 -0
  38. package/public/social-share.png +1 -0
  39. package/public/web-app-manifest-192x192.png +1 -0
  40. package/public/web-app-manifest-512x512.png +1 -0
  41. package/script/e2e-local.ts +143 -0
  42. package/src/addons/serialize.test.ts +319 -0
  43. package/src/addons/serialize.ts +591 -0
  44. package/src/app.tsx +150 -0
  45. package/src/components/dialog-connect-provider.tsx +428 -0
  46. package/src/components/dialog-edit-project.tsx +259 -0
  47. package/src/components/dialog-fork.tsx +104 -0
  48. package/src/components/dialog-manage-models.tsx +59 -0
  49. package/src/components/dialog-select-directory.tsx +208 -0
  50. package/src/components/dialog-select-file.tsx +196 -0
  51. package/src/components/dialog-select-mcp.tsx +96 -0
  52. package/src/components/dialog-select-model-unpaid.tsx +130 -0
  53. package/src/components/dialog-select-model.tsx +162 -0
  54. package/src/components/dialog-select-provider.tsx +70 -0
  55. package/src/components/dialog-select-server.tsx +249 -0
  56. package/src/components/dialog-settings.tsx +112 -0
  57. package/src/components/file-tree.tsx +112 -0
  58. package/src/components/link.tsx +17 -0
  59. package/src/components/model-tooltip.tsx +91 -0
  60. package/src/components/prompt-input.tsx +2076 -0
  61. package/src/components/session/index.ts +5 -0
  62. package/src/components/session/session-context-tab.tsx +428 -0
  63. package/src/components/session/session-header.tsx +343 -0
  64. package/src/components/session/session-new-view.tsx +93 -0
  65. package/src/components/session/session-sortable-tab.tsx +56 -0
  66. package/src/components/session/session-sortable-terminal-tab.tsx +187 -0
  67. package/src/components/session-context-usage.tsx +113 -0
  68. package/src/components/session-lsp-indicator.tsx +42 -0
  69. package/src/components/session-mcp-indicator.tsx +34 -0
  70. package/src/components/settings-agents.tsx +15 -0
  71. package/src/components/settings-commands.tsx +15 -0
  72. package/src/components/settings-general.tsx +306 -0
  73. package/src/components/settings-keybinds.tsx +437 -0
  74. package/src/components/settings-mcp.tsx +15 -0
  75. package/src/components/settings-models.tsx +15 -0
  76. package/src/components/settings-permissions.tsx +234 -0
  77. package/src/components/settings-providers.tsx +15 -0
  78. package/src/components/terminal.tsx +315 -0
  79. package/src/components/titlebar.tsx +156 -0
  80. package/src/context/command.tsx +308 -0
  81. package/src/context/comments.tsx +140 -0
  82. package/src/context/file.tsx +409 -0
  83. package/src/context/global-sdk.tsx +106 -0
  84. package/src/context/global-sync.tsx +898 -0
  85. package/src/context/language.tsx +161 -0
  86. package/src/context/layout-scroll.test.ts +73 -0
  87. package/src/context/layout-scroll.ts +118 -0
  88. package/src/context/layout.tsx +648 -0
  89. package/src/context/local.tsx +578 -0
  90. package/src/context/notification.tsx +173 -0
  91. package/src/context/permission.tsx +167 -0
  92. package/src/context/platform.tsx +59 -0
  93. package/src/context/prompt.tsx +245 -0
  94. package/src/context/sdk.tsx +48 -0
  95. package/src/context/server.tsx +214 -0
  96. package/src/context/settings.tsx +166 -0
  97. package/src/context/sync.tsx +320 -0
  98. package/src/context/terminal.tsx +267 -0
  99. package/src/custom-elements.d.ts +17 -0
  100. package/src/entry.tsx +76 -0
  101. package/src/env.d.ts +8 -0
  102. package/src/hooks/use-providers.ts +31 -0
  103. package/src/i18n/ar.ts +656 -0
  104. package/src/i18n/br.ts +667 -0
  105. package/src/i18n/da.ts +582 -0
  106. package/src/i18n/de.ts +591 -0
  107. package/src/i18n/en.ts +665 -0
  108. package/src/i18n/es.ts +585 -0
  109. package/src/i18n/fr.ts +592 -0
  110. package/src/i18n/ja.ts +579 -0
  111. package/src/i18n/ko.ts +580 -0
  112. package/src/i18n/no.ts +602 -0
  113. package/src/i18n/pl.ts +661 -0
  114. package/src/i18n/ru.ts +664 -0
  115. package/src/i18n/zh.ts +574 -0
  116. package/src/i18n/zht.ts +570 -0
  117. package/src/index.css +57 -0
  118. package/src/index.ts +2 -0
  119. package/src/pages/directory-layout.tsx +57 -0
  120. package/src/pages/error.tsx +290 -0
  121. package/src/pages/home.tsx +125 -0
  122. package/src/pages/layout.tsx +2599 -0
  123. package/src/pages/session.tsx +2505 -0
  124. package/src/sst-env.d.ts +10 -0
  125. package/src/utils/dom.ts +51 -0
  126. package/src/utils/id.ts +99 -0
  127. package/src/utils/index.ts +1 -0
  128. package/src/utils/perf.ts +135 -0
  129. package/src/utils/persist.ts +377 -0
  130. package/src/utils/prompt.ts +203 -0
  131. package/src/utils/same.ts +6 -0
  132. package/src/utils/solid-dnd.tsx +55 -0
  133. package/src/utils/sound.ts +110 -0
  134. package/src/utils/speech.ts +302 -0
  135. package/src/utils/worktree.ts +58 -0
  136. package/sst-env.d.ts +9 -0
  137. package/tsconfig.json +26 -0
  138. package/vite.config.ts +15 -0
  139. package/vite.js +26 -0
@@ -0,0 +1,5 @@
1
+ export { SessionHeader } from "./session-header"
2
+ export { SessionContextTab } from "./session-context-tab"
3
+ export { SortableTab, FileVisual } from "./session-sortable-tab"
4
+ export { SortableTerminalTab } from "./session-sortable-terminal-tab"
5
+ export { NewSessionView } from "./session-new-view"
@@ -0,0 +1,428 @@
1
+ import { createMemo, createEffect, on, onCleanup, For, Show } from "solid-js"
2
+ import type { JSX } from "solid-js"
3
+ import { useParams } from "@solidjs/router"
4
+ import { DateTime } from "luxon"
5
+ import { useSync } from "@/context/sync"
6
+ import { useLayout } from "@/context/layout"
7
+ import { checksum } from "@jonsoc/util/encode"
8
+ import { Icon } from "@jonsoc/ui/icon"
9
+ import { Accordion } from "@jonsoc/ui/accordion"
10
+ import { StickyAccordionHeader } from "@jonsoc/ui/sticky-accordion-header"
11
+ import { Code } from "@jonsoc/ui/code"
12
+ import { Markdown } from "@jonsoc/ui/markdown"
13
+ import type { AssistantMessage, Message, Part, UserMessage } from "@jonsoc/sdk/v2/client"
14
+ import { useLanguage } from "@/context/language"
15
+
16
+ interface SessionContextTabProps {
17
+ messages: () => Message[]
18
+ visibleUserMessages: () => UserMessage[]
19
+ view: () => ReturnType<ReturnType<typeof useLayout>["view"]>
20
+ info: () => ReturnType<ReturnType<typeof useSync>["session"]["get"]>
21
+ }
22
+
23
+ export function SessionContextTab(props: SessionContextTabProps) {
24
+ const params = useParams()
25
+ const sync = useSync()
26
+ const language = useLanguage()
27
+
28
+ const ctx = createMemo(() => {
29
+ const last = props.messages().findLast((x) => {
30
+ if (x.role !== "assistant") return false
31
+ const total = x.tokens.input + x.tokens.output + x.tokens.reasoning + x.tokens.cache.read + x.tokens.cache.write
32
+ return total > 0
33
+ }) as AssistantMessage
34
+ if (!last) return
35
+
36
+ const provider = sync.data.provider.all.find((x) => x.id === last.providerID)
37
+ const model = provider?.models[last.modelID]
38
+ const limit = model?.limit.context
39
+
40
+ const input = last.tokens.input
41
+ const output = last.tokens.output
42
+ const reasoning = last.tokens.reasoning
43
+ const cacheRead = last.tokens.cache.read
44
+ const cacheWrite = last.tokens.cache.write
45
+ const total = input + output + reasoning + cacheRead + cacheWrite
46
+ const usage = limit ? Math.round((total / limit) * 100) : null
47
+
48
+ return {
49
+ message: last,
50
+ provider,
51
+ model,
52
+ limit,
53
+ input,
54
+ output,
55
+ reasoning,
56
+ cacheRead,
57
+ cacheWrite,
58
+ total,
59
+ usage,
60
+ }
61
+ })
62
+
63
+ const cost = createMemo(() => {
64
+ const locale = language.locale()
65
+ const total = props.messages().reduce((sum, x) => sum + (x.role === "assistant" ? x.cost : 0), 0)
66
+ return new Intl.NumberFormat(locale, {
67
+ style: "currency",
68
+ currency: "USD",
69
+ }).format(total)
70
+ })
71
+
72
+ const counts = createMemo(() => {
73
+ const all = props.messages()
74
+ const user = all.reduce((count, x) => count + (x.role === "user" ? 1 : 0), 0)
75
+ const assistant = all.reduce((count, x) => count + (x.role === "assistant" ? 1 : 0), 0)
76
+ return {
77
+ all: all.length,
78
+ user,
79
+ assistant,
80
+ }
81
+ })
82
+
83
+ const systemPrompt = createMemo(() => {
84
+ const msg = props.visibleUserMessages().findLast((m) => !!m.system)
85
+ const system = msg?.system
86
+ if (!system) return
87
+ const trimmed = system.trim()
88
+ if (!trimmed) return
89
+ return trimmed
90
+ })
91
+
92
+ const number = (value: number | null | undefined) => {
93
+ if (value === undefined) return "—"
94
+ if (value === null) return "—"
95
+ return value.toLocaleString(language.locale())
96
+ }
97
+
98
+ const percent = (value: number | null | undefined) => {
99
+ if (value === undefined) return "—"
100
+ if (value === null) return "—"
101
+ return value.toLocaleString(language.locale()) + "%"
102
+ }
103
+
104
+ const time = (value: number | undefined) => {
105
+ if (!value) return "—"
106
+ return DateTime.fromMillis(value).setLocale(language.locale()).toLocaleString(DateTime.DATETIME_MED)
107
+ }
108
+
109
+ const providerLabel = createMemo(() => {
110
+ const c = ctx()
111
+ if (!c) return "—"
112
+ return c.provider?.name ?? c.message.providerID
113
+ })
114
+
115
+ const modelLabel = createMemo(() => {
116
+ const c = ctx()
117
+ if (!c) return "—"
118
+ if (c.model?.name) return c.model.name
119
+ return c.message.modelID
120
+ })
121
+
122
+ const breakdown = createMemo(
123
+ on(
124
+ () => [ctx()?.message.id, ctx()?.input, props.messages().length, systemPrompt()],
125
+ () => {
126
+ const c = ctx()
127
+ if (!c) return []
128
+ const input = c.input
129
+ if (!input) return []
130
+
131
+ const out = {
132
+ system: systemPrompt()?.length ?? 0,
133
+ user: 0,
134
+ assistant: 0,
135
+ tool: 0,
136
+ }
137
+
138
+ for (const msg of props.messages()) {
139
+ const parts = (sync.data.part[msg.id] ?? []) as Part[]
140
+
141
+ if (msg.role === "user") {
142
+ for (const part of parts) {
143
+ if (part.type === "text") out.user += part.text.length
144
+ if (part.type === "file") out.user += part.source?.text.value.length ?? 0
145
+ if (part.type === "agent") out.user += part.source?.value.length ?? 0
146
+ }
147
+ continue
148
+ }
149
+
150
+ if (msg.role === "assistant") {
151
+ for (const part of parts) {
152
+ if (part.type === "text") out.assistant += part.text.length
153
+ if (part.type === "reasoning") out.assistant += part.text.length
154
+ if (part.type === "tool") {
155
+ out.tool += Object.keys(part.state.input).length * 16
156
+ if (part.state.status === "pending") out.tool += part.state.raw.length
157
+ if (part.state.status === "completed") out.tool += part.state.output.length
158
+ if (part.state.status === "error") out.tool += part.state.error.length
159
+ }
160
+ }
161
+ }
162
+ }
163
+
164
+ const estimateTokens = (chars: number) => Math.ceil(chars / 4)
165
+ const system = estimateTokens(out.system)
166
+ const user = estimateTokens(out.user)
167
+ const assistant = estimateTokens(out.assistant)
168
+ const tool = estimateTokens(out.tool)
169
+ const estimated = system + user + assistant + tool
170
+
171
+ const pct = (tokens: number) => (tokens / input) * 100
172
+ const pctLabel = (tokens: number) => (Math.round(pct(tokens) * 10) / 10).toString() + "%"
173
+
174
+ const build = (tokens: { system: number; user: number; assistant: number; tool: number; other: number }) => {
175
+ return [
176
+ {
177
+ key: "system",
178
+ label: language.t("context.breakdown.system"),
179
+ tokens: tokens.system,
180
+ width: pct(tokens.system),
181
+ percent: pctLabel(tokens.system),
182
+ color: "var(--syntax-info)",
183
+ },
184
+ {
185
+ key: "user",
186
+ label: language.t("context.breakdown.user"),
187
+ tokens: tokens.user,
188
+ width: pct(tokens.user),
189
+ percent: pctLabel(tokens.user),
190
+ color: "var(--syntax-success)",
191
+ },
192
+ {
193
+ key: "assistant",
194
+ label: language.t("context.breakdown.assistant"),
195
+ tokens: tokens.assistant,
196
+ width: pct(tokens.assistant),
197
+ percent: pctLabel(tokens.assistant),
198
+ color: "var(--syntax-property)",
199
+ },
200
+ {
201
+ key: "tool",
202
+ label: language.t("context.breakdown.tool"),
203
+ tokens: tokens.tool,
204
+ width: pct(tokens.tool),
205
+ percent: pctLabel(tokens.tool),
206
+ color: "var(--syntax-warning)",
207
+ },
208
+ {
209
+ key: "other",
210
+ label: language.t("context.breakdown.other"),
211
+ tokens: tokens.other,
212
+ width: pct(tokens.other),
213
+ percent: pctLabel(tokens.other),
214
+ color: "var(--syntax-comment)",
215
+ },
216
+ ].filter((x) => x.tokens > 0)
217
+ }
218
+
219
+ if (estimated <= input) {
220
+ return build({ system, user, assistant, tool, other: input - estimated })
221
+ }
222
+
223
+ const scale = input / estimated
224
+ const scaled = {
225
+ system: Math.floor(system * scale),
226
+ user: Math.floor(user * scale),
227
+ assistant: Math.floor(assistant * scale),
228
+ tool: Math.floor(tool * scale),
229
+ }
230
+ const scaledTotal = scaled.system + scaled.user + scaled.assistant + scaled.tool
231
+ return build({ ...scaled, other: Math.max(0, input - scaledTotal) })
232
+ },
233
+ ),
234
+ )
235
+
236
+ function Stat(statProps: { label: string; value: JSX.Element }) {
237
+ return (
238
+ <div class="flex flex-col gap-1">
239
+ <div class="text-12-regular text-text-weak">{statProps.label}</div>
240
+ <div class="text-12-medium text-text-strong">{statProps.value}</div>
241
+ </div>
242
+ )
243
+ }
244
+
245
+ const stats = createMemo(() => {
246
+ const c = ctx()
247
+ const count = counts()
248
+ return [
249
+ { label: language.t("context.stats.session"), value: props.info()?.title ?? params.id ?? "—" },
250
+ { label: language.t("context.stats.messages"), value: count.all.toLocaleString(language.locale()) },
251
+ { label: language.t("context.stats.provider"), value: providerLabel() },
252
+ { label: language.t("context.stats.model"), value: modelLabel() },
253
+ { label: language.t("context.stats.limit"), value: number(c?.limit) },
254
+ { label: language.t("context.stats.totalTokens"), value: number(c?.total) },
255
+ { label: language.t("context.stats.usage"), value: percent(c?.usage) },
256
+ { label: language.t("context.stats.inputTokens"), value: number(c?.input) },
257
+ { label: language.t("context.stats.outputTokens"), value: number(c?.output) },
258
+ { label: language.t("context.stats.reasoningTokens"), value: number(c?.reasoning) },
259
+ {
260
+ label: language.t("context.stats.cacheTokens"),
261
+ value: `${number(c?.cacheRead)} / ${number(c?.cacheWrite)}`,
262
+ },
263
+ { label: language.t("context.stats.userMessages"), value: count.user.toLocaleString(language.locale()) },
264
+ {
265
+ label: language.t("context.stats.assistantMessages"),
266
+ value: count.assistant.toLocaleString(language.locale()),
267
+ },
268
+ { label: language.t("context.stats.totalCost"), value: cost() },
269
+ { label: language.t("context.stats.sessionCreated"), value: time(props.info()?.time.created) },
270
+ { label: language.t("context.stats.lastActivity"), value: time(c?.message.time.created) },
271
+ ] satisfies { label: string; value: JSX.Element }[]
272
+ })
273
+
274
+ function RawMessageContent(msgProps: { message: Message }) {
275
+ const file = createMemo(() => {
276
+ const parts = (sync.data.part[msgProps.message.id] ?? []) as Part[]
277
+ const contents = JSON.stringify({ message: msgProps.message, parts }, null, 2)
278
+ return {
279
+ name: `${msgProps.message.role}-${msgProps.message.id}.json`,
280
+ contents,
281
+ cacheKey: checksum(contents),
282
+ }
283
+ })
284
+
285
+ return (
286
+ <Code file={file()} overflow="wrap" class="select-text" onRendered={() => requestAnimationFrame(restoreScroll)} />
287
+ )
288
+ }
289
+
290
+ function RawMessage(msgProps: { message: Message }) {
291
+ return (
292
+ <Accordion.Item value={msgProps.message.id}>
293
+ <StickyAccordionHeader>
294
+ <Accordion.Trigger>
295
+ <div class="flex items-center justify-between gap-2 w-full">
296
+ <div class="min-w-0 truncate">
297
+ {msgProps.message.role} <span class="text-text-base">• {msgProps.message.id}</span>
298
+ </div>
299
+ <div class="flex items-center gap-3">
300
+ <div class="shrink-0 text-12-regular text-text-weak">{time(msgProps.message.time.created)}</div>
301
+ <Icon name="chevron-grabber-vertical" size="small" class="shrink-0 text-text-weak" />
302
+ </div>
303
+ </div>
304
+ </Accordion.Trigger>
305
+ </StickyAccordionHeader>
306
+ <Accordion.Content class="bg-background-base">
307
+ <div class="p-3">
308
+ <RawMessageContent message={msgProps.message} />
309
+ </div>
310
+ </Accordion.Content>
311
+ </Accordion.Item>
312
+ )
313
+ }
314
+
315
+ let scroll: HTMLDivElement | undefined
316
+ let frame: number | undefined
317
+ let pending: { x: number; y: number } | undefined
318
+
319
+ const restoreScroll = () => {
320
+ const el = scroll
321
+ if (!el) return
322
+
323
+ const s = props.view()?.scroll("context")
324
+ if (!s) return
325
+
326
+ if (el.scrollTop !== s.y) el.scrollTop = s.y
327
+ if (el.scrollLeft !== s.x) el.scrollLeft = s.x
328
+ }
329
+
330
+ const handleScroll = (event: Event & { currentTarget: HTMLDivElement }) => {
331
+ pending = {
332
+ x: event.currentTarget.scrollLeft,
333
+ y: event.currentTarget.scrollTop,
334
+ }
335
+ if (frame !== undefined) return
336
+
337
+ frame = requestAnimationFrame(() => {
338
+ frame = undefined
339
+
340
+ const next = pending
341
+ pending = undefined
342
+ if (!next) return
343
+
344
+ props.view().setScroll("context", next)
345
+ })
346
+ }
347
+
348
+ createEffect(
349
+ on(
350
+ () => props.messages().length,
351
+ () => {
352
+ requestAnimationFrame(restoreScroll)
353
+ },
354
+ { defer: true },
355
+ ),
356
+ )
357
+
358
+ onCleanup(() => {
359
+ if (frame === undefined) return
360
+ cancelAnimationFrame(frame)
361
+ })
362
+
363
+ return (
364
+ <div
365
+ class="@container h-full overflow-y-auto no-scrollbar pb-10"
366
+ ref={(el) => {
367
+ scroll = el
368
+ restoreScroll()
369
+ }}
370
+ onScroll={handleScroll}
371
+ >
372
+ <div class="px-6 pt-4 flex flex-col gap-10">
373
+ <div class="grid grid-cols-1 @[32rem]:grid-cols-2 gap-4">
374
+ <For each={stats()}>{(stat) => <Stat label={stat.label} value={stat.value} />}</For>
375
+ </div>
376
+
377
+ <Show when={breakdown().length > 0}>
378
+ <div class="flex flex-col gap-2">
379
+ <div class="text-12-regular text-text-weak">{language.t("context.breakdown.title")}</div>
380
+ <div class="h-2 w-full rounded-full bg-surface-base overflow-hidden flex">
381
+ <For each={breakdown()}>
382
+ {(segment) => (
383
+ <div
384
+ class="h-full"
385
+ style={{
386
+ width: `${segment.width}%`,
387
+ "background-color": segment.color,
388
+ }}
389
+ />
390
+ )}
391
+ </For>
392
+ </div>
393
+ <div class="flex flex-wrap gap-x-3 gap-y-1">
394
+ <For each={breakdown()}>
395
+ {(segment) => (
396
+ <div class="flex items-center gap-1 text-11-regular text-text-weak">
397
+ <div class="size-2 rounded-sm" style={{ "background-color": segment.color }} />
398
+ <div>{segment.label}</div>
399
+ <div class="text-text-weaker">{segment.percent}</div>
400
+ </div>
401
+ )}
402
+ </For>
403
+ </div>
404
+ <div class="hidden text-11-regular text-text-weaker">{language.t("context.breakdown.note")}</div>
405
+ </div>
406
+ </Show>
407
+
408
+ <Show when={systemPrompt()}>
409
+ {(prompt) => (
410
+ <div class="flex flex-col gap-2">
411
+ <div class="text-12-regular text-text-weak">{language.t("context.systemPrompt.title")}</div>
412
+ <div class="border border-border-base rounded-md bg-surface-base px-3 py-2">
413
+ <Markdown text={prompt()} class="text-12-regular" />
414
+ </div>
415
+ </div>
416
+ )}
417
+ </Show>
418
+
419
+ <div class="flex flex-col gap-2">
420
+ <div class="text-12-regular text-text-weak">{language.t("context.rawMessages.title")}</div>
421
+ <Accordion multiple>
422
+ <For each={props.messages()}>{(message) => <RawMessage message={message} />}</For>
423
+ </Accordion>
424
+ </div>
425
+ </div>
426
+ </div>
427
+ )
428
+ }