opencode-sidechat 1.0.0 → 1.1.0

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.
@@ -1,348 +1,348 @@
1
- /** @jsxImportSource @opentui/solid */
2
- import { createMemo, For } from "solid-js";
3
- import type { InputRenderable } from "@opentui/core";
4
- import { THINKING_TEXT } from "../constants";
5
- import type { OverlayState } from "../types";
6
-
7
- const MAX_VISIBLE_MESSAGES = 20;
8
-
9
- function renderInlineMarkdown(text: string) {
10
- const parts: Array<{ type: string; text: string }> = [];
11
- let remaining = text;
12
-
13
- while (remaining.length > 0) {
14
- if (remaining.startsWith("`")) {
15
- const end = remaining.indexOf("`", 1);
16
- if (end === -1) {
17
- parts.push({ type: "text", text: remaining });
18
- break;
19
- }
20
- parts.push({ type: "code", text: remaining.slice(1, end) });
21
- remaining = remaining.slice(end + 1);
22
- } else if (remaining.startsWith("**")) {
23
- const end = remaining.indexOf("**", 2);
24
- if (end === -1) {
25
- parts.push({ type: "text", text: remaining });
26
- break;
27
- }
28
- parts.push({ type: "bold", text: remaining.slice(2, end) });
29
- remaining = remaining.slice(end + 2);
30
- } else if (remaining.startsWith("*") && !remaining.startsWith("**")) {
31
- const end = remaining.indexOf("*", 1);
32
- if (end === -1) {
33
- parts.push({ type: "text", text: remaining });
34
- break;
35
- }
36
- parts.push({ type: "italic", text: remaining.slice(1, end) });
37
- remaining = remaining.slice(end + 1);
38
- } else if (remaining.startsWith("[") && remaining.includes("](")) {
39
- const closeBracket = remaining.indexOf("](");
40
- const closeParen = remaining.indexOf(")", closeBracket);
41
- if (closeParen === -1) {
42
- parts.push({ type: "text", text: remaining });
43
- break;
44
- }
45
- parts.push({ type: "text", text: remaining.slice(1, closeBracket) });
46
- remaining = remaining.slice(closeParen + 1);
47
- } else {
48
- const nextSpecial = searchSpecialChar(remaining);
49
- if (nextSpecial === -1) {
50
- parts.push({ type: "text", text: remaining });
51
- break;
52
- }
53
- if (nextSpecial > 0) {
54
- parts.push({ type: "text", text: remaining.slice(0, nextSpecial) });
55
- }
56
- remaining = remaining.slice(nextSpecial);
57
- }
58
- }
59
-
60
- return parts;
61
- }
62
-
63
- function searchSpecialChar(text: string): number {
64
- for (let i = 0; i < text.length; i++) {
65
- const c = text[i];
66
- if (c === "`" || c === "*" || (c === "[" && text.includes("](", i))) {
67
- return i;
68
- }
69
- }
70
- return -1;
71
- }
72
-
73
- export function SideChat(props: OverlayState & { width: number; transcriptHeight: number; tokenLimit: number }) {
74
- const theme = props.api.theme.current;
75
- let input: InputRenderable | undefined;
76
- let inputValue = "";
77
-
78
- const panelWidth = props.width;
79
- const contentWidth = props.width - 4;
80
-
81
- const msgs = createMemo(() => {
82
- const messages = props.state.entries
83
- .map((entry) => {
84
- const textParts: string[] = [];
85
- const reasoning: Array<{ id: string; text: string }> = [];
86
-
87
- for (const p of entry.parts) {
88
- if (p.type === "text") {
89
- if (p.text.trim()) textParts.push(p.text.trim());
90
- } else if (p.type === "reasoning") {
91
- if (p.text) reasoning.push({ id: p.id, text: p.text });
92
- }
93
- }
94
-
95
- if (textParts.length === 0 && reasoning.length === 0) return null;
96
-
97
- return {
98
- id: entry.info.id,
99
- role: entry.info.role as "user" | "assistant",
100
- text: textParts.join("\n"),
101
- reasoning,
102
- };
103
- })
104
- .filter((m): m is NonNullable<typeof m> => m !== null)
105
- .slice(-MAX_VISIBLE_MESSAGES);
106
-
107
- if (props.state.loading && props.state.streamingAnswer) {
108
- const streaming = props.state.streamingAnswer.trim();
109
- const last = messages[messages.length - 1];
110
- const lastText = last?.role === "assistant" ? last.text : "";
111
- if (streaming && streaming !== lastText) {
112
- messages.push({
113
- id: "__streaming__",
114
- role: "assistant",
115
- text: streaming,
116
- reasoning: [],
117
- });
118
- }
119
- }
120
-
121
- return messages;
122
- });
123
-
124
- const ctxLabel = createMemo(() => {
125
- const n = props.state.tokenCount ?? 0;
126
- if (n <= 0) return "";
127
- const current = n >= 1000 ? (n / 1000).toFixed(1) + "k" : String(n);
128
- const limit = props.tokenLimit >= 1000 ? (props.tokenLimit / 1000).toFixed(0) + "k" : String(props.tokenLimit);
129
- return current + "/" + limit + " ctx";
130
- });
131
-
132
- const shortModelName = createMemo(() => {
133
- const name = props.modelName;
134
- const parts = name.split("/");
135
- return parts.length >= 2 ? parts[parts.length - 1] : name;
136
- });
137
-
138
- const renderThinking = (r: { id: string; text: string }) => {
139
- if (!props.thinkCollapsed) {
140
- return (
141
- <box flexDirection="column">
142
- <text fg={theme.textMuted}>
143
- {"▼ thinking:"}
144
- </text>
145
- <text fg={theme.textMuted}>{r.text}</text>
146
- </box>
147
- );
148
- }
149
-
150
- const label = props.thinkConfig.showSummary
151
- ? "▶ thinking: " + r.text.slice(0, 60).replace(/\n/g, " ") + (r.text.length > 60 ? "..." : "")
152
- : "▶ thinking (" + r.text.length + " chars)";
153
-
154
- return <text fg={theme.textMuted}>{label}</text>;
155
- };
156
-
157
- return (
158
- <box
159
- position="absolute"
160
- bottom={0}
161
- right={0}
162
- >
163
- <box
164
- width={panelWidth}
165
- flexDirection="column"
166
- border={true}
167
- borderColor={theme.borderActive}
168
- backgroundColor={theme.backgroundPanel}
169
- >
170
- <box
171
- flexDirection="row"
172
- justifyContent="space-between"
173
- alignItems="center"
174
- paddingTop={1}
175
- paddingLeft={1}
176
- paddingRight={1}
177
- >
178
- <box flexDirection="row" gap={1} alignItems="center">
179
- <box paddingLeft={1} paddingRight={1} backgroundColor={theme.accent}>
180
- <text fg={theme.background}>
181
- <b>{"SideChat"}</b>
182
- </text>
183
- </box>
184
- <text fg={theme.success}>
185
- <b>{"[f]"}</b>
186
- </text>
187
- </box>
188
- <box flexDirection="row" gap={1} alignItems="center">
189
- <text fg={theme.textMuted}>{shortModelName()}</text>
190
- {ctxLabel() ? (
191
- <text fg={theme.textMuted}>{ctxLabel()}</text>
192
- ) : (
193
- <text>{""}</text>
194
- )}
195
- </box>
196
- </box>
197
-
198
- <box paddingLeft={1} paddingRight={1}>
199
- <scrollbox
200
- scrollY={true}
201
- stickyScroll={true}
202
- stickyStart="bottom"
203
- height={props.transcriptHeight}
204
- width={contentWidth}
205
- >
206
- <box flexDirection="column" gap={1} paddingTop={1} paddingBottom={1} width={contentWidth - 2}>
207
- {msgs().length > 0 ? (
208
- msgs().map((msg) => (
209
- <box flexDirection="column" gap={0}>
210
- <text fg={msg.role === "assistant" ? theme.secondary : theme.text}>
211
- <b>{msg.role === "assistant" ? "A:" : "You:"}</b>
212
- </text>
213
- {msg.reasoning.map((r) => renderThinking(r))}
214
- {msg.text ? (
215
- <box flexDirection="column">
216
- <RenderMarkdown text={msg.text} theme={theme} />
217
- </box>
218
- ) : (
219
- <text>{""}</text>
220
- )}
221
- </box>
222
- ))
223
- ) : props.state.loading ? (
224
- <text fg={theme.textMuted}>{THINKING_TEXT}</text>
225
- ) : (
226
- <text>{""}</text>
227
- )}
228
- {props.state.error ? (
229
- <text fg={theme.error}>{"Error: " + String(props.state.error)}</text>
230
- ) : (
231
- <text>{""}</text>
232
- )}
233
- </box>
234
- </scrollbox>
235
- </box>
236
-
237
- <box paddingLeft={1} paddingRight={1}>
238
- <input
239
- ref={(node) => { input = node; props.onInput?.(node); }}
240
- width={contentWidth}
241
- placeholder={props.state.loading ? "..." : ">"}
242
- textColor={theme.text}
243
- placeholderColor={theme.textMuted}
244
- backgroundColor={theme.backgroundElement}
245
- focusedTextColor={theme.text}
246
- cursorColor={theme.primary}
247
- focusedBackgroundColor={theme.backgroundElement}
248
- onInput={(value) => {
249
- inputValue = value;
250
- }}
251
- onSubmit={() => {
252
- const submitted = (input?.value ?? inputValue).trim();
253
- if (!submitted || props.state.loading) return;
254
- if (!props.onSubmit(submitted)) return;
255
- inputValue = "";
256
- if (input) input.value = "";
257
- }}
258
- />
259
- </box>
260
-
261
- <box
262
- flexDirection="row"
263
- gap={1}
264
- paddingTop={0}
265
- paddingBottom={1}
266
- paddingLeft={1}
267
- paddingRight={1}
268
- alignItems="center"
269
- >
270
- <text fg={theme.secondary}><b>{"Alt+C"}</b></text>
271
- <text fg={theme.primary}>{"Clear"}</text>
272
- <text fg={theme.textMuted}>{"·"}</text>
273
- <text fg={theme.secondary}><b>{"Alt+T"}</b></text>
274
- <text fg={theme.primary}>{"Thinking"}</text>
275
- <text fg={theme.textMuted}>{props.thinkCollapsed ? "" : "(on)"}</text>
276
- <text fg={theme.textMuted}>{"·"}</text>
277
- <text fg={theme.secondary}><b>{"Tab"}</b></text>
278
- <text fg={theme.primary}>{"Model"}</text>
279
- </box>
280
- </box>
281
- </box>
282
- );
283
- }
284
-
285
- function RenderMarkdown(props: { text: string; theme: import("@opencode-ai/plugin/tui").TuiThemeCurrent }) {
286
- const lines = createMemo(() => {
287
- const text = props.text;
288
- const result: Array<{ type: "line" | "codeblock"; parts: Array<{ type: string; text: string }>; codeText?: string }> = [];
289
- let inCodeBlock = false;
290
- let codeBuffer = "";
291
-
292
- for (const line of text.split("\n")) {
293
- if (line.startsWith("```")) {
294
- if (inCodeBlock) {
295
- result.push({ type: "codeblock", parts: [], codeText: codeBuffer });
296
- codeBuffer = "";
297
- }
298
- inCodeBlock = !inCodeBlock;
299
- continue;
300
- }
301
- if (inCodeBlock) {
302
- codeBuffer += (codeBuffer ? "\n" : "") + line;
303
- continue;
304
- }
305
-
306
- const parts = renderInlineMarkdown(line);
307
- result.push({ type: "line", parts });
308
- }
309
-
310
- if (inCodeBlock && codeBuffer) {
311
- result.push({ type: "codeblock", parts: [], codeText: codeBuffer });
312
- }
313
-
314
- return result;
315
- });
316
-
317
- const t = props.theme;
318
-
319
- return (
320
- <For each={lines()}>
321
- {(block) => {
322
- if (block.type === "codeblock") {
323
- return (
324
- <box
325
- backgroundColor={t.backgroundElement}
326
- paddingLeft={1}
327
- paddingRight={1}
328
- >
329
- <text fg={t.markdownCode}>{block.codeText}</text>
330
- </box>
331
- );
332
- }
333
- return (
334
- <box flexDirection="row" flexWrap="wrap" gap={0}>
335
- <For each={block.parts}>
336
- {(part) => {
337
- if (part.type === "bold") return <text><b>{part.text}</b></text>;
338
- if (part.type === "italic") return <text><i>{part.text}</i></text>;
339
- if (part.type === "code") return <text fg={t.markdownCode}>{part.text}</text>;
340
- return <text>{part.text}</text>;
341
- }}
342
- </For>
343
- </box>
344
- );
345
- }}
346
- </For>
347
- );
348
- }
1
+ /** @jsxImportSource @opentui/solid */
2
+ import { createMemo, For } from "solid-js";
3
+ import type { InputRenderable } from "@opentui/core";
4
+ import { THINKING_TEXT } from "../constants";
5
+ import type { OverlayState } from "../types";
6
+
7
+ const MAX_VISIBLE_MESSAGES = 20;
8
+
9
+ function renderInlineMarkdown(text: string) {
10
+ const parts: Array<{ type: string; text: string }> = [];
11
+ let remaining = text;
12
+
13
+ while (remaining.length > 0) {
14
+ if (remaining.startsWith("`")) {
15
+ const end = remaining.indexOf("`", 1);
16
+ if (end === -1) {
17
+ parts.push({ type: "text", text: remaining });
18
+ break;
19
+ }
20
+ parts.push({ type: "code", text: remaining.slice(1, end) });
21
+ remaining = remaining.slice(end + 1);
22
+ } else if (remaining.startsWith("**")) {
23
+ const end = remaining.indexOf("**", 2);
24
+ if (end === -1) {
25
+ parts.push({ type: "text", text: remaining });
26
+ break;
27
+ }
28
+ parts.push({ type: "bold", text: remaining.slice(2, end) });
29
+ remaining = remaining.slice(end + 2);
30
+ } else if (remaining.startsWith("*") && !remaining.startsWith("**")) {
31
+ const end = remaining.indexOf("*", 1);
32
+ if (end === -1) {
33
+ parts.push({ type: "text", text: remaining });
34
+ break;
35
+ }
36
+ parts.push({ type: "italic", text: remaining.slice(1, end) });
37
+ remaining = remaining.slice(end + 1);
38
+ } else if (remaining.startsWith("[") && remaining.includes("](")) {
39
+ const closeBracket = remaining.indexOf("](");
40
+ const closeParen = remaining.indexOf(")", closeBracket);
41
+ if (closeParen === -1) {
42
+ parts.push({ type: "text", text: remaining });
43
+ break;
44
+ }
45
+ parts.push({ type: "text", text: remaining.slice(1, closeBracket) });
46
+ remaining = remaining.slice(closeParen + 1);
47
+ } else {
48
+ const nextSpecial = searchSpecialChar(remaining);
49
+ if (nextSpecial === -1) {
50
+ parts.push({ type: "text", text: remaining });
51
+ break;
52
+ }
53
+ if (nextSpecial > 0) {
54
+ parts.push({ type: "text", text: remaining.slice(0, nextSpecial) });
55
+ }
56
+ remaining = remaining.slice(nextSpecial);
57
+ }
58
+ }
59
+
60
+ return parts;
61
+ }
62
+
63
+ function searchSpecialChar(text: string): number {
64
+ for (let i = 0; i < text.length; i++) {
65
+ const c = text[i];
66
+ if (c === "`" || c === "*" || (c === "[" && text.includes("](", i))) {
67
+ return i;
68
+ }
69
+ }
70
+ return -1;
71
+ }
72
+
73
+ export function SideChat(props: OverlayState & { width: number; transcriptHeight: number; tokenLimit: number }) {
74
+ const theme = props.api.theme.current;
75
+ let input: InputRenderable | undefined;
76
+ let inputValue = "";
77
+
78
+ const panelWidth = props.width;
79
+ const contentWidth = props.width - 4;
80
+
81
+ const msgs = createMemo(() => {
82
+ const messages = props.state.entries
83
+ .map((entry) => {
84
+ const textParts: string[] = [];
85
+ const reasoning: Array<{ id: string; text: string }> = [];
86
+
87
+ for (const p of entry.parts) {
88
+ if (p.type === "text") {
89
+ if (p.text.trim()) textParts.push(p.text.trim());
90
+ } else if (p.type === "reasoning") {
91
+ if (p.text) reasoning.push({ id: p.id, text: p.text });
92
+ }
93
+ }
94
+
95
+ if (textParts.length === 0 && reasoning.length === 0) return null;
96
+
97
+ return {
98
+ id: entry.info.id,
99
+ role: entry.info.role as "user" | "assistant",
100
+ text: textParts.join("\n"),
101
+ reasoning,
102
+ };
103
+ })
104
+ .filter((m): m is NonNullable<typeof m> => m !== null)
105
+ .slice(-MAX_VISIBLE_MESSAGES);
106
+
107
+ if (props.state.loading && props.streamingAnswer) {
108
+ const streaming = props.streamingAnswer.trim();
109
+ const last = messages[messages.length - 1];
110
+ const lastText = last?.role === "assistant" ? last.text : "";
111
+ if (streaming && streaming !== lastText) {
112
+ messages.push({
113
+ id: "__streaming__",
114
+ role: "assistant",
115
+ text: streaming,
116
+ reasoning: [],
117
+ });
118
+ }
119
+ }
120
+
121
+ return messages;
122
+ });
123
+
124
+ const ctxLabel = createMemo(() => {
125
+ const n = props.state.tokenCount ?? 0;
126
+ if (n <= 0) return "";
127
+ const current = n >= 1000 ? (n / 1000).toFixed(1) + "k" : String(n);
128
+ const limit = props.tokenLimit >= 1000 ? (props.tokenLimit / 1000).toFixed(0) + "k" : String(props.tokenLimit);
129
+ return current + "/" + limit + " ctx";
130
+ });
131
+
132
+ const shortModelName = createMemo(() => {
133
+ const name = props.modelName;
134
+ const parts = name.split("/");
135
+ return parts.length >= 2 ? parts[parts.length - 1] : name;
136
+ });
137
+
138
+ const renderThinking = (r: { id: string; text: string }) => {
139
+ if (!props.thinkCollapsed) {
140
+ return (
141
+ <box flexDirection="column">
142
+ <text fg={theme.textMuted}>
143
+ {"▼ thinking:"}
144
+ </text>
145
+ <text fg={theme.textMuted}>{r.text}</text>
146
+ </box>
147
+ );
148
+ }
149
+
150
+ const label = props.thinkConfig.showSummary
151
+ ? "▶ thinking: " + r.text.slice(0, 60).replace(/\n/g, " ") + (r.text.length > 60 ? "..." : "")
152
+ : "▶ thinking (" + r.text.length + " chars)";
153
+
154
+ return <text fg={theme.textMuted}>{label}</text>;
155
+ };
156
+
157
+ return (
158
+ <box
159
+ position="absolute"
160
+ bottom={0}
161
+ right={0}
162
+ >
163
+ <box
164
+ width={panelWidth}
165
+ flexDirection="column"
166
+ border={true}
167
+ borderColor={theme.borderActive}
168
+ backgroundColor={theme.backgroundPanel}
169
+ >
170
+ <box
171
+ flexDirection="row"
172
+ justifyContent="space-between"
173
+ alignItems="center"
174
+ paddingTop={1}
175
+ paddingLeft={1}
176
+ paddingRight={1}
177
+ >
178
+ <box flexDirection="row" gap={1} alignItems="center">
179
+ <box paddingLeft={1} paddingRight={1} backgroundColor={theme.accent}>
180
+ <text fg={theme.background}>
181
+ <b>{"OpenCode-SideChat"}</b>
182
+ </text>
183
+ </box>
184
+ <text fg={theme.success}>
185
+ <b>{"[f]"}</b>
186
+ </text>
187
+ </box>
188
+ <box flexDirection="row" gap={1} alignItems="center">
189
+ <text fg={theme.textMuted}>{shortModelName()}</text>
190
+ {ctxLabel() ? (
191
+ <text fg={theme.textMuted}>{ctxLabel()}</text>
192
+ ) : (
193
+ <text>{""}</text>
194
+ )}
195
+ </box>
196
+ </box>
197
+
198
+ <box paddingLeft={1} paddingRight={1}>
199
+ <scrollbox
200
+ scrollY={true}
201
+ stickyScroll={true}
202
+ stickyStart="bottom"
203
+ height={props.transcriptHeight}
204
+ width={contentWidth}
205
+ >
206
+ <box flexDirection="column" gap={1} paddingTop={1} paddingBottom={1} width={contentWidth - 2}>
207
+ {msgs().length > 0 ? (
208
+ msgs().map((msg) => (
209
+ <box flexDirection="column" gap={0}>
210
+ <text fg={msg.role === "assistant" ? theme.secondary : theme.text}>
211
+ <b>{msg.role === "assistant" ? "A:" : "You:"}</b>
212
+ </text>
213
+ {msg.reasoning.map((r) => renderThinking(r))}
214
+ {msg.text ? (
215
+ <box flexDirection="column">
216
+ <RenderMarkdown text={msg.text} theme={theme} />
217
+ </box>
218
+ ) : (
219
+ <text>{""}</text>
220
+ )}
221
+ </box>
222
+ ))
223
+ ) : props.state.loading ? (
224
+ <text fg={theme.textMuted}>{THINKING_TEXT}</text>
225
+ ) : (
226
+ <text>{""}</text>
227
+ )}
228
+ {props.state.error ? (
229
+ <text fg={theme.error}>{"Error: " + String(props.state.error)}</text>
230
+ ) : (
231
+ <text>{""}</text>
232
+ )}
233
+ </box>
234
+ </scrollbox>
235
+ </box>
236
+
237
+ <box paddingLeft={1} paddingRight={1}>
238
+ <input
239
+ ref={(node) => { input = node; props.onInput?.(node); }}
240
+ width={contentWidth}
241
+ placeholder={props.state.loading ? "..." : ">"}
242
+ textColor={theme.text}
243
+ placeholderColor={theme.textMuted}
244
+ backgroundColor={theme.backgroundElement}
245
+ focusedTextColor={theme.text}
246
+ cursorColor={theme.primary}
247
+ focusedBackgroundColor={theme.backgroundElement}
248
+ onInput={(value) => {
249
+ inputValue = value;
250
+ }}
251
+ onSubmit={() => {
252
+ const submitted = (input?.value ?? inputValue).trim();
253
+ if (!submitted || props.state.loading) return;
254
+ if (!props.onSubmit(submitted)) return;
255
+ inputValue = "";
256
+ if (input) input.value = "";
257
+ }}
258
+ />
259
+ </box>
260
+
261
+ <box
262
+ flexDirection="row"
263
+ gap={1}
264
+ paddingTop={0}
265
+ paddingBottom={1}
266
+ paddingLeft={1}
267
+ paddingRight={1}
268
+ alignItems="center"
269
+ >
270
+ <text fg={theme.secondary}><b>{"Alt+C"}</b></text>
271
+ <text fg={theme.primary}>{"Clear"}</text>
272
+ <text fg={theme.textMuted}>{"·"}</text>
273
+ <text fg={theme.secondary}><b>{"Alt+T"}</b></text>
274
+ <text fg={theme.primary}>{"Thinking"}</text>
275
+ <text fg={theme.textMuted}>{props.thinkCollapsed ? "" : "(on)"}</text>
276
+ <text fg={theme.textMuted}>{"·"}</text>
277
+ <text fg={theme.secondary}><b>{"Tab"}</b></text>
278
+ <text fg={theme.primary}>{"Model"}</text>
279
+ </box>
280
+ </box>
281
+ </box>
282
+ );
283
+ }
284
+
285
+ function RenderMarkdown(props: { text: string; theme: import("@opencode-ai/plugin/tui").TuiThemeCurrent }) {
286
+ const lines = createMemo(() => {
287
+ const text = props.text;
288
+ const result: Array<{ type: "line" | "codeblock"; parts: Array<{ type: string; text: string }>; codeText?: string }> = [];
289
+ let inCodeBlock = false;
290
+ let codeBuffer = "";
291
+
292
+ for (const line of text.split("\n")) {
293
+ if (line.startsWith("```")) {
294
+ if (inCodeBlock) {
295
+ result.push({ type: "codeblock", parts: [], codeText: codeBuffer });
296
+ codeBuffer = "";
297
+ }
298
+ inCodeBlock = !inCodeBlock;
299
+ continue;
300
+ }
301
+ if (inCodeBlock) {
302
+ codeBuffer += (codeBuffer ? "\n" : "") + line;
303
+ continue;
304
+ }
305
+
306
+ const parts = renderInlineMarkdown(line);
307
+ result.push({ type: "line", parts });
308
+ }
309
+
310
+ if (inCodeBlock && codeBuffer) {
311
+ result.push({ type: "codeblock", parts: [], codeText: codeBuffer });
312
+ }
313
+
314
+ return result;
315
+ });
316
+
317
+ const t = props.theme;
318
+
319
+ return (
320
+ <For each={lines()}>
321
+ {(block) => {
322
+ if (block.type === "codeblock") {
323
+ return (
324
+ <box
325
+ backgroundColor={t.backgroundElement}
326
+ paddingLeft={1}
327
+ paddingRight={1}
328
+ >
329
+ <text fg={t.markdownCode}>{block.codeText}</text>
330
+ </box>
331
+ );
332
+ }
333
+ return (
334
+ <box flexDirection="row" flexWrap="wrap" gap={0}>
335
+ <For each={block.parts}>
336
+ {(part) => {
337
+ if (part.type === "bold") return <text><b>{part.text}</b></text>;
338
+ if (part.type === "italic") return <text><i>{part.text}</i></text>;
339
+ if (part.type === "code") return <text fg={t.markdownCode}>{part.text}</text>;
340
+ return <text>{part.text}</text>;
341
+ }}
342
+ </For>
343
+ </box>
344
+ );
345
+ }}
346
+ </For>
347
+ );
348
+ }