@raingor/pi-web-switch 0.4.1 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +32 -8
- package/README.md +58 -10
- package/README.zh-CN.md +31 -7
- package/dist-electron/main/main.cjs +2453 -0
- package/package.json +18 -5
- package/pi-package/index.ts +228 -4
- package/public/apple-touch-icon.png +0 -0
- package/public/icon-192.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/pi.svg +6 -41
- package/public/trayIconTemplate.png +0 -0
- package/server/pi-reader.ts +470 -31
- package/src/App.tsx +0 -2
- package/src/components/dashboard/DashboardPage.tsx +45 -13
- package/src/components/layout/Sidebar.tsx +15 -6
- package/src/components/providers/ProvidersModelsPage.tsx +56 -4
- package/src/components/sessions/SessionsPage.tsx +36 -2
- package/src/components/settings/SettingsPage.tsx +72 -0
- package/src/lib/translations/en.ts +20 -58
- package/src/lib/translations/ja.ts +20 -58
- package/src/lib/translations/zh-CN.ts +20 -58
- package/src/lib/translations/zh-TW.ts +20 -58
- package/src/main.tsx +29 -5
- package/src/types/index.ts +2 -0
- package/vite.config.ts +106 -8
- package/server/agent-session-manager.ts +0 -827
- package/server/chat-api-plugin.ts +0 -488
- package/src/components/chat/ChatInput.tsx +0 -863
- package/src/components/chat/ChatPage.tsx +0 -617
- package/src/components/chat/ChatWindow.tsx +0 -338
- package/src/components/chat/MessageView.tsx +0 -595
- package/src/hooks/useAgentSession.ts +0 -1104
|
@@ -1,595 +0,0 @@
|
|
|
1
|
-
// MessageView — renders a single chat message (user, assistant, tool result, bash).
|
|
2
|
-
// Ported and simplified from pi-web's components/MessageView.tsx.
|
|
3
|
-
|
|
4
|
-
import { memo, useState, type ReactNode } from "react";
|
|
5
|
-
import type {
|
|
6
|
-
AgentMessage,
|
|
7
|
-
AssistantMessage,
|
|
8
|
-
ToolResultMessage,
|
|
9
|
-
ToolCallContent,
|
|
10
|
-
TextContent,
|
|
11
|
-
ThinkingContent,
|
|
12
|
-
ImageContent,
|
|
13
|
-
BashExecutionMessage,
|
|
14
|
-
CustomMessage,
|
|
15
|
-
} from "@/types/chat";
|
|
16
|
-
|
|
17
|
-
interface Props {
|
|
18
|
-
message: AgentMessage;
|
|
19
|
-
isStreaming?: boolean;
|
|
20
|
-
modelNames?: Record<string, string>;
|
|
21
|
-
cwd?: string;
|
|
22
|
-
onOpenFile?: (filePath: string) => void;
|
|
23
|
-
entryId?: string;
|
|
24
|
-
onFork?: (entryId: string) => void;
|
|
25
|
-
forking?: boolean;
|
|
26
|
-
onEditContent?: (content: string) => void;
|
|
27
|
-
showTimestamp?: boolean;
|
|
28
|
-
sessionId?: string;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function formatTimestamp(ts?: number): string {
|
|
32
|
-
if (!ts) return "";
|
|
33
|
-
const d = new Date(ts);
|
|
34
|
-
return d.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" });
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function extractText(content: string | (TextContent | ImageContent)[]): string {
|
|
38
|
-
if (typeof content === "string") return content;
|
|
39
|
-
return content
|
|
40
|
-
.filter((b): b is TextContent => b.type === "text")
|
|
41
|
-
.map((b) => b.text)
|
|
42
|
-
.join("\n");
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// ─── Tool Call Block ─────────────────────────────────────
|
|
46
|
-
|
|
47
|
-
function ToolCallBlock({ block }: { block: ToolCallContent }) {
|
|
48
|
-
const [expanded, setExpanded] = useState(false);
|
|
49
|
-
const inputStr = JSON.stringify(block.input, null, 2);
|
|
50
|
-
|
|
51
|
-
return (
|
|
52
|
-
<div style={{
|
|
53
|
-
border: "1px solid var(--border, #e5e7eb)",
|
|
54
|
-
borderRadius: 8,
|
|
55
|
-
background: "var(--bg-panel, #f9fafb)",
|
|
56
|
-
overflow: "hidden",
|
|
57
|
-
margin: "8px 0",
|
|
58
|
-
}}>
|
|
59
|
-
<button
|
|
60
|
-
onClick={() => setExpanded(!expanded)}
|
|
61
|
-
style={{
|
|
62
|
-
display: "flex",
|
|
63
|
-
alignItems: "center",
|
|
64
|
-
gap: 8,
|
|
65
|
-
width: "100%",
|
|
66
|
-
padding: "8px 12px",
|
|
67
|
-
background: "none",
|
|
68
|
-
border: "none",
|
|
69
|
-
cursor: "pointer",
|
|
70
|
-
textAlign: "left",
|
|
71
|
-
color: "var(--text-muted, #6b7280)",
|
|
72
|
-
fontSize: 13,
|
|
73
|
-
}}
|
|
74
|
-
>
|
|
75
|
-
<svg
|
|
76
|
-
width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.6"
|
|
77
|
-
strokeLinecap="round" strokeLinejoin="round"
|
|
78
|
-
style={{ transform: expanded ? "rotate(90deg)" : "none", transition: "transform 0.15s", flexShrink: 0 }}
|
|
79
|
-
>
|
|
80
|
-
<polyline points="4 2.5 7.5 6 4 9.5" />
|
|
81
|
-
</svg>
|
|
82
|
-
<span style={{ fontWeight: 600, color: "var(--accent, #2563eb)" }}>{block.toolName}</span>
|
|
83
|
-
<span style={{ fontSize: 11, opacity: 0.6 }}>tool call</span>
|
|
84
|
-
</button>
|
|
85
|
-
{expanded && (
|
|
86
|
-
<pre style={{
|
|
87
|
-
margin: 0,
|
|
88
|
-
padding: "8px 12px",
|
|
89
|
-
borderTop: "1px solid var(--border, #e5e7eb)",
|
|
90
|
-
fontSize: 12,
|
|
91
|
-
fontFamily: "var(--font-mono, monospace)",
|
|
92
|
-
color: "var(--text-muted, #6b7280)",
|
|
93
|
-
whiteSpace: "pre-wrap",
|
|
94
|
-
wordBreak: "break-word",
|
|
95
|
-
overflow: "auto",
|
|
96
|
-
maxHeight: 300,
|
|
97
|
-
}}>
|
|
98
|
-
{inputStr}
|
|
99
|
-
</pre>
|
|
100
|
-
)}
|
|
101
|
-
</div>
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// ─── Thinking Block ──────────────────────────────────────
|
|
106
|
-
|
|
107
|
-
function ThinkingBlock({ block }: { block: ThinkingContent }) {
|
|
108
|
-
const [expanded, setExpanded] = useState(false);
|
|
109
|
-
if (!block.thinking && block.deferred) {
|
|
110
|
-
return (
|
|
111
|
-
<div style={{ margin: "6px 0", fontSize: 12, color: "var(--text-dim)", fontStyle: "italic" }}>
|
|
112
|
-
Thinking (collapsed)
|
|
113
|
-
</div>
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return (
|
|
118
|
-
<div style={{ margin: "8px 0" }}>
|
|
119
|
-
<button
|
|
120
|
-
onClick={() => setExpanded(!expanded)}
|
|
121
|
-
style={{
|
|
122
|
-
display: "flex",
|
|
123
|
-
alignItems: "center",
|
|
124
|
-
gap: 6,
|
|
125
|
-
background: "none",
|
|
126
|
-
border: "none",
|
|
127
|
-
cursor: "pointer",
|
|
128
|
-
fontSize: 12,
|
|
129
|
-
color: "var(--text-muted, #6b7280)",
|
|
130
|
-
}}
|
|
131
|
-
>
|
|
132
|
-
<svg
|
|
133
|
-
width="10" height="10" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.6"
|
|
134
|
-
strokeLinecap="round" strokeLinejoin="round"
|
|
135
|
-
style={{ transform: expanded ? "rotate(90deg)" : "none", transition: "transform 0.15s" }}
|
|
136
|
-
>
|
|
137
|
-
<polyline points="4 2.5 7.5 6 4 9.5" />
|
|
138
|
-
</svg>
|
|
139
|
-
Thinking
|
|
140
|
-
</button>
|
|
141
|
-
{expanded && (
|
|
142
|
-
<div style={{
|
|
143
|
-
marginTop: 6,
|
|
144
|
-
padding: "8px 12px",
|
|
145
|
-
borderLeft: "2px solid var(--border, #e5e7eb)",
|
|
146
|
-
fontSize: 13,
|
|
147
|
-
color: "var(--text-muted, #6b7280)",
|
|
148
|
-
whiteSpace: "pre-wrap",
|
|
149
|
-
fontFamily: "var(--font-mono, monospace)",
|
|
150
|
-
lineHeight: 1.6,
|
|
151
|
-
}}>
|
|
152
|
-
{block.thinking}
|
|
153
|
-
</div>
|
|
154
|
-
)}
|
|
155
|
-
</div>
|
|
156
|
-
);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// ─── Image Block ─────────────────────────────────────────
|
|
160
|
-
|
|
161
|
-
function ImageBlock({ block }: { block: ImageContent }) {
|
|
162
|
-
const src = block.source.type === "url"
|
|
163
|
-
? block.source.url
|
|
164
|
-
: `data:${block.source.media_type ?? "image/png"};base64,${block.source.data}`;
|
|
165
|
-
return (
|
|
166
|
-
<img
|
|
167
|
-
src={src}
|
|
168
|
-
alt="attachment"
|
|
169
|
-
style={{
|
|
170
|
-
maxWidth: "100%",
|
|
171
|
-
maxHeight: 400,
|
|
172
|
-
borderRadius: 8,
|
|
173
|
-
margin: "8px 0",
|
|
174
|
-
}}
|
|
175
|
-
/>
|
|
176
|
-
);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// ─── Tool Result Block ───────────────────────────────────
|
|
180
|
-
|
|
181
|
-
function ToolResultView({ message }: { message: ToolResultMessage }) {
|
|
182
|
-
const [expanded, setExpanded] = useState(false);
|
|
183
|
-
const textContent = message.content
|
|
184
|
-
.filter((b): b is TextContent => b.type === "text")
|
|
185
|
-
.map((b) => b.text)
|
|
186
|
-
.join("\n");
|
|
187
|
-
|
|
188
|
-
const truncated = textContent.length > 500;
|
|
189
|
-
const displayText = expanded || !truncated ? textContent : textContent.slice(0, 500) + "...";
|
|
190
|
-
|
|
191
|
-
return (
|
|
192
|
-
<div style={{
|
|
193
|
-
margin: "4px 0 8px 0",
|
|
194
|
-
padding: "8px 12px",
|
|
195
|
-
borderRadius: 8,
|
|
196
|
-
background: message.isError ? "rgba(239,68,68,0.06)" : "var(--bg-panel, #f9fafb)",
|
|
197
|
-
border: `1px solid ${message.isError ? "rgba(239,68,68,0.2)" : "var(--border, #e5e7eb)"}`,
|
|
198
|
-
fontSize: 13,
|
|
199
|
-
}}>
|
|
200
|
-
{message.toolName && (
|
|
201
|
-
<div style={{
|
|
202
|
-
fontSize: 11,
|
|
203
|
-
fontWeight: 600,
|
|
204
|
-
color: message.isError ? "#dc2626" : "var(--text-muted, #6b7280)",
|
|
205
|
-
marginBottom: 4,
|
|
206
|
-
}}>
|
|
207
|
-
{message.toolName} result {message.isError ? "(error)" : ""}
|
|
208
|
-
</div>
|
|
209
|
-
)}
|
|
210
|
-
<pre style={{
|
|
211
|
-
margin: 0,
|
|
212
|
-
fontFamily: "var(--font-mono, monospace)",
|
|
213
|
-
color: message.isError ? "#dc2626" : "var(--text-muted, #6b7280)",
|
|
214
|
-
whiteSpace: "pre-wrap",
|
|
215
|
-
wordBreak: "break-word",
|
|
216
|
-
lineHeight: 1.5,
|
|
217
|
-
}}>
|
|
218
|
-
{displayText}
|
|
219
|
-
</pre>
|
|
220
|
-
{truncated && (
|
|
221
|
-
<button
|
|
222
|
-
onClick={() => setExpanded(!expanded)}
|
|
223
|
-
style={{
|
|
224
|
-
marginTop: 4,
|
|
225
|
-
background: "none",
|
|
226
|
-
border: "none",
|
|
227
|
-
color: "var(--accent, #2563eb)",
|
|
228
|
-
cursor: "pointer",
|
|
229
|
-
fontSize: 12,
|
|
230
|
-
}}
|
|
231
|
-
>
|
|
232
|
-
{expanded ? "Show less" : `Show ${textContent.length - 500} more characters`}
|
|
233
|
-
</button>
|
|
234
|
-
)}
|
|
235
|
-
</div>
|
|
236
|
-
);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// ─── Bash Execution View ─────────────────────────────────
|
|
240
|
-
|
|
241
|
-
function BashExecutionView({ message }: { message: BashExecutionMessage }) {
|
|
242
|
-
return (
|
|
243
|
-
<div style={{
|
|
244
|
-
margin: "8px 0",
|
|
245
|
-
borderRadius: 8,
|
|
246
|
-
overflow: "hidden",
|
|
247
|
-
border: "1px solid var(--border, #e5e7eb)",
|
|
248
|
-
}}>
|
|
249
|
-
<div style={{
|
|
250
|
-
padding: "6px 12px",
|
|
251
|
-
background: "var(--bg-panel, #f9fafb)",
|
|
252
|
-
fontSize: 12,
|
|
253
|
-
fontFamily: "var(--font-mono, monospace)",
|
|
254
|
-
color: "var(--text-muted, #6b7280)",
|
|
255
|
-
display: "flex",
|
|
256
|
-
alignItems: "center",
|
|
257
|
-
gap: 6,
|
|
258
|
-
}}>
|
|
259
|
-
<span style={{ color: "var(--accent, #2563eb)" }}>$</span>
|
|
260
|
-
<span>{message.command}</span>
|
|
261
|
-
{message.exitCode !== undefined && (
|
|
262
|
-
<span style={{ marginLeft: "auto", color: message.exitCode === 0 ? "#10b981" : "#dc2626" }}>
|
|
263
|
-
exit {message.exitCode}
|
|
264
|
-
</span>
|
|
265
|
-
)}
|
|
266
|
-
</div>
|
|
267
|
-
{message.output && (
|
|
268
|
-
<pre style={{
|
|
269
|
-
margin: 0,
|
|
270
|
-
padding: "8px 12px",
|
|
271
|
-
fontSize: 12,
|
|
272
|
-
fontFamily: "var(--font-mono, monospace)",
|
|
273
|
-
color: "var(--text-muted, #6b7280)",
|
|
274
|
-
whiteSpace: "pre-wrap",
|
|
275
|
-
wordBreak: "break-word",
|
|
276
|
-
maxHeight: 300,
|
|
277
|
-
overflow: "auto",
|
|
278
|
-
}}>
|
|
279
|
-
{message.output}
|
|
280
|
-
</pre>
|
|
281
|
-
)}
|
|
282
|
-
</div>
|
|
283
|
-
);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// ─── Markdown Renderer (simplified) ──────────────────────
|
|
287
|
-
|
|
288
|
-
function renderMarkdown(text: string): ReactNode {
|
|
289
|
-
// Simple markdown rendering: code blocks, inline code, bold, links, line breaks
|
|
290
|
-
const parts: ReactNode[] = [];
|
|
291
|
-
const codeBlockRegex = /```(\w+)?\n?([\s\S]*?)```/g;
|
|
292
|
-
let lastIndex = 0;
|
|
293
|
-
let match;
|
|
294
|
-
let key = 0;
|
|
295
|
-
|
|
296
|
-
while ((match = codeBlockRegex.exec(text)) !== null) {
|
|
297
|
-
if (match.index > lastIndex) {
|
|
298
|
-
parts.push(renderInlineMarkdown(text.slice(lastIndex, match.index), `md-${key++}`));
|
|
299
|
-
}
|
|
300
|
-
const lang = match[1] || "text";
|
|
301
|
-
const code = match[2];
|
|
302
|
-
parts.push(
|
|
303
|
-
<div key={`code-${key++}`} style={{
|
|
304
|
-
margin: "8px 0",
|
|
305
|
-
borderRadius: 8,
|
|
306
|
-
overflow: "hidden",
|
|
307
|
-
border: "1px solid var(--border, #e5e7eb)",
|
|
308
|
-
}}>
|
|
309
|
-
<div style={{
|
|
310
|
-
padding: "4px 12px",
|
|
311
|
-
background: "var(--bg-panel, #f9fafb)",
|
|
312
|
-
fontSize: 11,
|
|
313
|
-
color: "var(--text-dim)",
|
|
314
|
-
fontFamily: "var(--font-mono, monospace)",
|
|
315
|
-
borderBottom: "1px solid var(--border, #e5e7eb)",
|
|
316
|
-
}}>
|
|
317
|
-
{lang}
|
|
318
|
-
</div>
|
|
319
|
-
<pre style={{
|
|
320
|
-
margin: 0,
|
|
321
|
-
padding: "10px 12px",
|
|
322
|
-
fontSize: 13,
|
|
323
|
-
fontFamily: "var(--font-mono, monospace)",
|
|
324
|
-
color: "var(--text, #1f2937)",
|
|
325
|
-
whiteSpace: "pre-wrap",
|
|
326
|
-
wordBreak: "break-word",
|
|
327
|
-
overflow: "auto",
|
|
328
|
-
maxHeight: 500,
|
|
329
|
-
}}>
|
|
330
|
-
<code>{code}</code>
|
|
331
|
-
</pre>
|
|
332
|
-
</div>
|
|
333
|
-
);
|
|
334
|
-
lastIndex = match.index + match[0].length;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
if (lastIndex < text.length) {
|
|
338
|
-
parts.push(renderInlineMarkdown(text.slice(lastIndex), `md-${key++}`));
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
return <>{parts}</>;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
function renderInlineMarkdown(text: string, keyPrefix: string): ReactNode {
|
|
345
|
-
// Split by lines for paragraph rendering
|
|
346
|
-
const lines = text.split("\n");
|
|
347
|
-
return (
|
|
348
|
-
<div key={keyPrefix}>
|
|
349
|
-
{lines.map((line, i) => (
|
|
350
|
-
<div key={`${keyPrefix}-${i}`} style={{ minHeight: "1.5em" }}>
|
|
351
|
-
{renderLine(line, `${keyPrefix}-${i}`)}
|
|
352
|
-
</div>
|
|
353
|
-
))}
|
|
354
|
-
</div>
|
|
355
|
-
);
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
function renderLine(line: string, keyPrefix: string): ReactNode {
|
|
359
|
-
// Handle inline code, bold, links
|
|
360
|
-
const parts: ReactNode[] = [];
|
|
361
|
-
const regex = /(`[^`]+`)|(\*\*[^*]+\*\*)|(\[[^\]]+\]\([^)]+\))/g;
|
|
362
|
-
let lastIndex = 0;
|
|
363
|
-
let match;
|
|
364
|
-
let key = 0;
|
|
365
|
-
|
|
366
|
-
while ((match = regex.exec(line)) !== null) {
|
|
367
|
-
if (match.index > lastIndex) {
|
|
368
|
-
parts.push(line.slice(lastIndex, match.index));
|
|
369
|
-
}
|
|
370
|
-
const token = match[0];
|
|
371
|
-
if (token.startsWith("`")) {
|
|
372
|
-
parts.push(
|
|
373
|
-
<code key={`${keyPrefix}-${key++}`} style={{
|
|
374
|
-
padding: "1px 5px",
|
|
375
|
-
borderRadius: 4,
|
|
376
|
-
background: "var(--bg-panel, #f3f4f6)",
|
|
377
|
-
fontSize: "0.9em",
|
|
378
|
-
fontFamily: "var(--font-mono, monospace)",
|
|
379
|
-
}}>
|
|
380
|
-
{token.slice(1, -1)}
|
|
381
|
-
</code>
|
|
382
|
-
);
|
|
383
|
-
} else if (token.startsWith("**")) {
|
|
384
|
-
parts.push(<strong key={`${keyPrefix}-${key++}`}>{token.slice(2, -2)}</strong>);
|
|
385
|
-
} else if (token.startsWith("[")) {
|
|
386
|
-
const linkMatch = token.match(/\[([^\]]+)\]\(([^)]+)\)/);
|
|
387
|
-
if (linkMatch) {
|
|
388
|
-
parts.push(
|
|
389
|
-
<a key={`${keyPrefix}-${key++}`} href={linkMatch[2]} target="_blank" rel="noopener noreferrer"
|
|
390
|
-
style={{ color: "var(--accent, #2563eb)", textDecoration: "underline" }}>
|
|
391
|
-
{linkMatch[1]}
|
|
392
|
-
</a>
|
|
393
|
-
);
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
lastIndex = match.index + token.length;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
if (lastIndex < line.length) {
|
|
400
|
-
parts.push(line.slice(lastIndex));
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
return parts.length > 0 ? parts : (line === "" ? "\u200B" : line);
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
// ─── Main MessageView ────────────────────────────────────
|
|
407
|
-
|
|
408
|
-
function MessageViewImpl({
|
|
409
|
-
message,
|
|
410
|
-
isStreaming,
|
|
411
|
-
modelNames,
|
|
412
|
-
cwd,
|
|
413
|
-
onOpenFile,
|
|
414
|
-
entryId,
|
|
415
|
-
onFork,
|
|
416
|
-
forking,
|
|
417
|
-
onEditContent,
|
|
418
|
-
showTimestamp,
|
|
419
|
-
sessionId,
|
|
420
|
-
}: Props) {
|
|
421
|
-
if (message.role === "user") {
|
|
422
|
-
const text = typeof message.content === "string"
|
|
423
|
-
? message.content
|
|
424
|
-
: extractText(message.content);
|
|
425
|
-
const images = Array.isArray(message.content)
|
|
426
|
-
? message.content.filter((b): b is ImageContent => b.type === "image")
|
|
427
|
-
: [];
|
|
428
|
-
|
|
429
|
-
return (
|
|
430
|
-
<div style={{
|
|
431
|
-
display: "flex",
|
|
432
|
-
justifyContent: "flex-end",
|
|
433
|
-
marginBottom: 16,
|
|
434
|
-
}}>
|
|
435
|
-
<div style={{
|
|
436
|
-
maxWidth: "80%",
|
|
437
|
-
padding: "10px 14px",
|
|
438
|
-
borderRadius: "16px 16px 4px 16px",
|
|
439
|
-
background: "var(--accent, #2563eb)",
|
|
440
|
-
color: "#fff",
|
|
441
|
-
fontSize: 14,
|
|
442
|
-
lineHeight: 1.6,
|
|
443
|
-
wordBreak: "break-word",
|
|
444
|
-
}}>
|
|
445
|
-
{text && <div>{renderMarkdown(text)}</div>}
|
|
446
|
-
{images.map((img, i) => (
|
|
447
|
-
<ImageBlock key={i} block={img} />
|
|
448
|
-
))}
|
|
449
|
-
</div>
|
|
450
|
-
</div>
|
|
451
|
-
);
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
if (message.role === "assistant") {
|
|
455
|
-
const am = message as AssistantMessage;
|
|
456
|
-
const blocks = am.content;
|
|
457
|
-
const modelLabel = am.provider && am.model
|
|
458
|
-
? modelNames?.[`${am.provider}:${am.model}`] ?? am.model
|
|
459
|
-
: am.model;
|
|
460
|
-
|
|
461
|
-
return (
|
|
462
|
-
<div style={{ marginBottom: 16 }}>
|
|
463
|
-
<div style={{
|
|
464
|
-
display: "flex",
|
|
465
|
-
alignItems: "center",
|
|
466
|
-
gap: 8,
|
|
467
|
-
marginBottom: 4,
|
|
468
|
-
}}>
|
|
469
|
-
<span style={{
|
|
470
|
-
fontSize: 13,
|
|
471
|
-
fontWeight: 600,
|
|
472
|
-
color: "var(--text, #1f2937)",
|
|
473
|
-
}}>
|
|
474
|
-
π {modelLabel && <span style={{ fontWeight: 400, color: "var(--text-muted, #6b7280)", fontSize: 12 }}>· {modelLabel}</span>}
|
|
475
|
-
</span>
|
|
476
|
-
{showTimestamp && message.timestamp && (
|
|
477
|
-
<span style={{ fontSize: 11, color: "var(--text-dim)" }}>
|
|
478
|
-
{formatTimestamp(message.timestamp)}
|
|
479
|
-
</span>
|
|
480
|
-
)}
|
|
481
|
-
{onFork && entryId && !isStreaming && (
|
|
482
|
-
<button
|
|
483
|
-
onClick={() => onFork(entryId)}
|
|
484
|
-
disabled={forking}
|
|
485
|
-
title="Fork from here"
|
|
486
|
-
style={{
|
|
487
|
-
background: "none",
|
|
488
|
-
border: "none",
|
|
489
|
-
cursor: forking ? "wait" : "pointer",
|
|
490
|
-
color: "var(--text-dim)",
|
|
491
|
-
padding: 0,
|
|
492
|
-
fontSize: 11,
|
|
493
|
-
}}
|
|
494
|
-
>
|
|
495
|
-
{forking ? "⏳" : "⎇"}
|
|
496
|
-
</button>
|
|
497
|
-
)}
|
|
498
|
-
</div>
|
|
499
|
-
<div style={{
|
|
500
|
-
padding: "0 2px",
|
|
501
|
-
fontSize: 14,
|
|
502
|
-
lineHeight: 1.7,
|
|
503
|
-
color: "var(--text, #1f2937)",
|
|
504
|
-
}}>
|
|
505
|
-
{blocks.map((block, i) => {
|
|
506
|
-
switch (block.type) {
|
|
507
|
-
case "text":
|
|
508
|
-
return <div key={i}>{renderMarkdown(block.text)}</div>;
|
|
509
|
-
case "thinking":
|
|
510
|
-
return <ThinkingBlock key={i} block={block} />;
|
|
511
|
-
case "toolCall":
|
|
512
|
-
return <ToolCallBlock key={i} block={block} />;
|
|
513
|
-
case "image":
|
|
514
|
-
return <ImageBlock key={i} block={block} />;
|
|
515
|
-
default:
|
|
516
|
-
return null;
|
|
517
|
-
}
|
|
518
|
-
})}
|
|
519
|
-
{am.errorMessage && (
|
|
520
|
-
<div style={{
|
|
521
|
-
marginTop: 8,
|
|
522
|
-
padding: "8px 12px",
|
|
523
|
-
borderRadius: 8,
|
|
524
|
-
background: "rgba(239,68,68,0.06)",
|
|
525
|
-
color: "#dc2626",
|
|
526
|
-
fontSize: 13,
|
|
527
|
-
}}>
|
|
528
|
-
{am.errorMessage}
|
|
529
|
-
</div>
|
|
530
|
-
)}
|
|
531
|
-
{am.usage && (
|
|
532
|
-
<div style={{
|
|
533
|
-
marginTop: 6,
|
|
534
|
-
fontSize: 11,
|
|
535
|
-
color: "var(--text-dim)",
|
|
536
|
-
display: "flex",
|
|
537
|
-
gap: 12,
|
|
538
|
-
}}>
|
|
539
|
-
<span>in: {am.usage.input.toLocaleString()}</span>
|
|
540
|
-
<span>out: {am.usage.output.toLocaleString()}</span>
|
|
541
|
-
{am.usage.cacheRead > 0 && <span>cache: {am.usage.cacheRead.toLocaleString()}</span>}
|
|
542
|
-
{am.usage.cost?.total ? <span>${am.usage.cost.total.toFixed(4)}</span> : null}
|
|
543
|
-
</div>
|
|
544
|
-
)}
|
|
545
|
-
</div>
|
|
546
|
-
</div>
|
|
547
|
-
);
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
if (message.role === "toolResult") {
|
|
551
|
-
return <ToolResultView message={message as ToolResultMessage} />;
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
if (message.role === "bashExecution") {
|
|
555
|
-
return <BashExecutionView message={message as BashExecutionMessage} />;
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
if (message.role === "custom") {
|
|
559
|
-
const cm = message as CustomMessage;
|
|
560
|
-
if (!cm.display) return null;
|
|
561
|
-
const text = typeof cm.content === "string" ? cm.content : extractText(cm.content);
|
|
562
|
-
if (cm.customType === "compaction") {
|
|
563
|
-
return (
|
|
564
|
-
<div style={{
|
|
565
|
-
margin: "12px 0",
|
|
566
|
-
padding: "8px 14px",
|
|
567
|
-
borderRadius: 8,
|
|
568
|
-
background: "var(--bg-panel, #f9fafb)",
|
|
569
|
-
border: "1px solid var(--border, #e5e7eb)",
|
|
570
|
-
fontSize: 13,
|
|
571
|
-
color: "var(--text-muted, #6b7280)",
|
|
572
|
-
fontStyle: "italic",
|
|
573
|
-
}}>
|
|
574
|
-
{text}
|
|
575
|
-
</div>
|
|
576
|
-
);
|
|
577
|
-
}
|
|
578
|
-
return (
|
|
579
|
-
<div style={{
|
|
580
|
-
margin: "8px 0",
|
|
581
|
-
padding: "8px 14px",
|
|
582
|
-
borderRadius: 8,
|
|
583
|
-
background: "var(--bg-panel, #f9fafb)",
|
|
584
|
-
fontSize: 13,
|
|
585
|
-
color: "var(--text-muted, #6b7280)",
|
|
586
|
-
}}>
|
|
587
|
-
{text}
|
|
588
|
-
</div>
|
|
589
|
-
);
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
return null;
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
export const MessageView = memo(MessageViewImpl);
|