@nastechai/agent 0.16.0 → 0.17.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.
- package/eslint.config.js +23 -0
- package/index.html +24 -0
- package/package.json +54 -26
- package/package.json.bak +89 -0
- package/package.json.pub +88 -0
- package/src/App.tsx +1173 -0
- package/src/components/AuthWidget.tsx +150 -0
- package/src/components/AutoField.tsx +206 -0
- package/src/components/Backdrop.tsx +93 -0
- package/src/components/ChatSidebar.tsx +394 -0
- package/src/components/DeleteConfirmDialog.tsx +40 -0
- package/src/components/LanguageSwitcher.tsx +186 -0
- package/src/components/Markdown.tsx +383 -0
- package/src/components/ModelInfoCard.tsx +112 -0
- package/src/components/ModelPickerDialog.tsx +470 -0
- package/src/components/OAuthLoginModal.tsx +374 -0
- package/src/components/OAuthProvidersCard.tsx +287 -0
- package/src/components/PlatformsCard.tsx +97 -0
- package/src/components/ScheduleBuilder.tsx +273 -0
- package/src/components/SidebarFooter.tsx +42 -0
- package/src/components/SidebarStatusStrip.tsx +72 -0
- package/src/components/SlashPopover.tsx +171 -0
- package/src/components/ThemeSwitcher.tsx +243 -0
- package/src/components/ToolCall.tsx +228 -0
- package/src/components/ToolsetConfigDrawer.tsx +448 -0
- package/src/contexts/PageHeaderProvider.tsx +139 -0
- package/src/contexts/SystemActions.tsx +120 -0
- package/src/contexts/page-header-context.ts +12 -0
- package/src/contexts/system-actions-context.ts +18 -0
- package/src/contexts/usePageHeader.ts +10 -0
- package/src/contexts/useSystemActions.ts +15 -0
- package/src/hooks/useModalBehavior.ts +44 -0
- package/src/hooks/useSidebarStatus.ts +27 -0
- package/src/i18n/af.ts +702 -0
- package/src/i18n/context.tsx +123 -0
- package/src/i18n/de.ts +701 -0
- package/src/i18n/en.ts +708 -0
- package/src/i18n/es.ts +701 -0
- package/src/i18n/fr.ts +701 -0
- package/src/i18n/ga.ts +702 -0
- package/src/i18n/hu.ts +702 -0
- package/src/i18n/index.ts +2 -0
- package/src/i18n/it.ts +701 -0
- package/src/i18n/ja.ts +702 -0
- package/src/i18n/ko.ts +702 -0
- package/src/i18n/pt.ts +702 -0
- package/src/i18n/ru.ts +702 -0
- package/src/i18n/tr.ts +702 -0
- package/src/i18n/types.ts +710 -0
- package/src/i18n/uk.ts +702 -0
- package/src/i18n/zh-hant.ts +702 -0
- package/src/i18n/zh.ts +698 -0
- package/src/index.css +274 -0
- package/src/lib/api.ts +1585 -0
- package/src/lib/dashboard-flags.ts +15 -0
- package/src/lib/format.ts +9 -0
- package/src/lib/fuzzy.ts +192 -0
- package/src/lib/gatewayClient.ts +253 -0
- package/src/lib/nested.ts +23 -0
- package/src/lib/resolve-page-title.ts +41 -0
- package/src/lib/schedule.ts +382 -0
- package/src/lib/slashExec.ts +163 -0
- package/src/lib/utils.ts +35 -0
- package/src/main.tsx +25 -0
- package/src/pages/AnalyticsPage.tsx +601 -0
- package/src/pages/ChannelsPage.tsx +772 -0
- package/src/pages/ChatPage.tsx +889 -0
- package/src/pages/ConfigPage.tsx +660 -0
- package/src/pages/CronPage.tsx +524 -0
- package/src/pages/DocsPage.tsx +69 -0
- package/src/pages/EnvPage.tsx +918 -0
- package/src/pages/LogsPage.tsx +246 -0
- package/src/pages/McpPage.tsx +757 -0
- package/src/pages/ModelsPage.tsx +994 -0
- package/src/pages/PairingPage.tsx +276 -0
- package/src/pages/PluginsPage.tsx +580 -0
- package/src/pages/ProfilesPage.tsx +559 -0
- package/src/pages/SessionsPage.tsx +936 -0
- package/src/pages/SkillsPage.tsx +557 -0
- package/src/pages/SystemPage.tsx +1259 -0
- package/src/pages/WebhooksPage.tsx +483 -0
- package/src/plugins/PluginPage.tsx +64 -0
- package/src/plugins/index.ts +6 -0
- package/src/plugins/registry.ts +151 -0
- package/src/plugins/sdk.d.ts +160 -0
- package/src/plugins/slots.ts +199 -0
- package/src/plugins/types.ts +37 -0
- package/src/plugins/usePlugins.ts +133 -0
- package/src/themes/context.tsx +443 -0
- package/src/themes/fonts.ts +160 -0
- package/src/themes/index.ts +3 -0
- package/src/themes/presets.ts +477 -0
- package/src/themes/types.ts +187 -0
- package/tsconfig.app.json +34 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +26 -0
- package/vite.config.ts +124 -0
- package/vite.config.ts.timestamp-1780999102396-af6b77b30ebd8.mjs +105 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { createPortal } from "react-dom";
|
|
3
|
+
import { Palette, Check } from "lucide-react";
|
|
4
|
+
import { Button } from "@nastechai/ui/ui/components/button";
|
|
5
|
+
import { ListItem } from "@nastechai/ui/ui/components/list-item";
|
|
6
|
+
import { BottomSheet } from "@nastechai/ui/ui/components/bottom-sheet";
|
|
7
|
+
import { Typography } from "@nastechai/ui/ui/components/typography/index";
|
|
8
|
+
import { useBelowBreakpoint } from "@nastechai/ui/hooks/use-below-breakpoint";
|
|
9
|
+
import { BUILTIN_THEMES, useTheme } from "@/themes";
|
|
10
|
+
import type { DashboardTheme, ThemeListEntry } from "@/themes";
|
|
11
|
+
import { useI18n } from "@/i18n";
|
|
12
|
+
import { cn } from "@/lib/utils";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Compact theme picker mounted next to the language switcher in the header.
|
|
16
|
+
* Each dropdown row shows a 3-stop swatch (background / midground / warm
|
|
17
|
+
* glow) so users can preview the palette before committing. User-defined
|
|
18
|
+
* themes from `~/.nastech/dashboard-themes/*.yaml` use their API-provided
|
|
19
|
+
* definitions so they show real palette swatches just like built-ins.
|
|
20
|
+
*
|
|
21
|
+
* When placed at the bottom of a container (e.g. the sidebar rail), pass
|
|
22
|
+
* `dropUp` so the menu opens above the trigger instead of clipping below
|
|
23
|
+
* the viewport. On viewports below the `sm` breakpoint, `dropUp` uses a
|
|
24
|
+
* bottom sheet portaled to `document.body` so the picker is not clipped by
|
|
25
|
+
* the sidebar (same idea as a responsive Drawer).
|
|
26
|
+
*/
|
|
27
|
+
export function ThemeSwitcher({ collapsed = false, dropUp = false }: ThemeSwitcherProps) {
|
|
28
|
+
const { themeName, availableThemes, setTheme } = useTheme();
|
|
29
|
+
const { t } = useI18n();
|
|
30
|
+
const [open, setOpen] = useState(false);
|
|
31
|
+
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
32
|
+
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
33
|
+
const narrowViewport = useBelowBreakpoint(640);
|
|
34
|
+
const useMobileSheet = Boolean(dropUp && narrowViewport);
|
|
35
|
+
|
|
36
|
+
const close = useCallback(() => setOpen(false), []);
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (!open) return;
|
|
40
|
+
const onKey = (e: KeyboardEvent) => {
|
|
41
|
+
if (e.key === "Escape") close();
|
|
42
|
+
};
|
|
43
|
+
document.addEventListener("keydown", onKey);
|
|
44
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
45
|
+
}, [open, close]);
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (!open || useMobileSheet) return;
|
|
49
|
+
const onMouseDown = (e: MouseEvent) => {
|
|
50
|
+
const target = e.target as Node;
|
|
51
|
+
if (wrapperRef.current?.contains(target)) return;
|
|
52
|
+
if (dropdownRef.current?.contains(target)) return;
|
|
53
|
+
close();
|
|
54
|
+
};
|
|
55
|
+
document.addEventListener("mousedown", onMouseDown);
|
|
56
|
+
return () => document.removeEventListener("mousedown", onMouseDown);
|
|
57
|
+
}, [open, close, useMobileSheet]);
|
|
58
|
+
|
|
59
|
+
const current = availableThemes.find((th) => th.name === themeName);
|
|
60
|
+
const label = current?.label ?? themeName;
|
|
61
|
+
const sheetTitle = t.theme?.title ?? "Theme";
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div ref={wrapperRef} className="relative">
|
|
65
|
+
<Button
|
|
66
|
+
ghost
|
|
67
|
+
size={collapsed ? "icon" : undefined}
|
|
68
|
+
onClick={() => setOpen((o) => !o)}
|
|
69
|
+
className={cn(
|
|
70
|
+
collapsed
|
|
71
|
+
? "text-text-secondary hover:text-foreground hover:bg-transparent"
|
|
72
|
+
: "px-2 py-1 normal-case tracking-normal font-normal text-xs text-text-secondary hover:text-foreground",
|
|
73
|
+
)}
|
|
74
|
+
title={`${t.theme?.switchTheme ?? "Switch theme"}: ${label}`}
|
|
75
|
+
aria-label={t.theme?.switchTheme ?? "Switch theme"}
|
|
76
|
+
aria-expanded={open}
|
|
77
|
+
aria-haspopup="listbox"
|
|
78
|
+
>
|
|
79
|
+
<span className="inline-flex items-center gap-1.5">
|
|
80
|
+
<Palette className="h-3.5 w-3.5" />
|
|
81
|
+
|
|
82
|
+
{!collapsed && (
|
|
83
|
+
<Typography
|
|
84
|
+
mondwest
|
|
85
|
+
className="hidden sm:inline text-display tracking-wide text-xs"
|
|
86
|
+
>
|
|
87
|
+
{label}
|
|
88
|
+
</Typography>
|
|
89
|
+
)}
|
|
90
|
+
</span>
|
|
91
|
+
</Button>
|
|
92
|
+
|
|
93
|
+
{useMobileSheet && (
|
|
94
|
+
<BottomSheet
|
|
95
|
+
backdropDismissLabel={t.common.close}
|
|
96
|
+
onClose={close}
|
|
97
|
+
open={open}
|
|
98
|
+
title={sheetTitle}
|
|
99
|
+
>
|
|
100
|
+
<div aria-label={sheetTitle} role="listbox">
|
|
101
|
+
<ThemeSwitcherOptions
|
|
102
|
+
availableThemes={availableThemes}
|
|
103
|
+
close={close}
|
|
104
|
+
setTheme={setTheme}
|
|
105
|
+
themeName={themeName}
|
|
106
|
+
/>
|
|
107
|
+
</div>
|
|
108
|
+
</BottomSheet>
|
|
109
|
+
)}
|
|
110
|
+
|
|
111
|
+
{open && !useMobileSheet && (() => {
|
|
112
|
+
const rect = wrapperRef.current?.getBoundingClientRect();
|
|
113
|
+
const dropdown = (
|
|
114
|
+
<div
|
|
115
|
+
ref={dropdownRef}
|
|
116
|
+
aria-label={sheetTitle}
|
|
117
|
+
className={cn(
|
|
118
|
+
"min-w-[240px] max-h-[70dvh] overflow-y-auto",
|
|
119
|
+
"border border-current/20 bg-background-base/95 backdrop-blur-sm",
|
|
120
|
+
"shadow-[0_12px_32px_-8px_rgba(0,0,0,0.6)]",
|
|
121
|
+
dropUp ? "fixed z-[100]" : "absolute z-50 right-0 top-full mt-1",
|
|
122
|
+
)}
|
|
123
|
+
role="listbox"
|
|
124
|
+
style={
|
|
125
|
+
dropUp && rect
|
|
126
|
+
? { bottom: window.innerHeight - rect.top + 4, left: rect.left }
|
|
127
|
+
: undefined
|
|
128
|
+
}
|
|
129
|
+
>
|
|
130
|
+
<div className="border-b border-current/20 px-3 py-2">
|
|
131
|
+
<Typography
|
|
132
|
+
mondwest
|
|
133
|
+
className="text-display text-xs tracking-[0.12em] text-text-tertiary"
|
|
134
|
+
>
|
|
135
|
+
{sheetTitle}
|
|
136
|
+
</Typography>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<ThemeSwitcherOptions
|
|
140
|
+
availableThemes={availableThemes}
|
|
141
|
+
close={close}
|
|
142
|
+
setTheme={setTheme}
|
|
143
|
+
themeName={themeName}
|
|
144
|
+
/>
|
|
145
|
+
</div>
|
|
146
|
+
);
|
|
147
|
+
return dropUp ? createPortal(dropdown, document.body) : dropdown;
|
|
148
|
+
})()}
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function ThemeSwitcherOptions({
|
|
154
|
+
availableThemes,
|
|
155
|
+
close,
|
|
156
|
+
setTheme,
|
|
157
|
+
themeName,
|
|
158
|
+
}: ThemeSwitcherOptionsProps) {
|
|
159
|
+
return (
|
|
160
|
+
<>
|
|
161
|
+
{availableThemes.map((th) => {
|
|
162
|
+
const isActive = th.name === themeName;
|
|
163
|
+
const paletteTheme = BUILTIN_THEMES[th.name] ?? th.definition;
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<ListItem
|
|
167
|
+
active={isActive}
|
|
168
|
+
aria-selected={isActive}
|
|
169
|
+
className="gap-3"
|
|
170
|
+
key={th.name}
|
|
171
|
+
onClick={() => {
|
|
172
|
+
setTheme(th.name);
|
|
173
|
+
close();
|
|
174
|
+
}}
|
|
175
|
+
role="option"
|
|
176
|
+
>
|
|
177
|
+
{paletteTheme ? (
|
|
178
|
+
<ThemeSwatch theme={paletteTheme} />
|
|
179
|
+
) : (
|
|
180
|
+
<PlaceholderSwatch />
|
|
181
|
+
)}
|
|
182
|
+
|
|
183
|
+
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
|
|
184
|
+
<Typography
|
|
185
|
+
mondwest
|
|
186
|
+
className="truncate text-display text-xs tracking-wide"
|
|
187
|
+
>
|
|
188
|
+
{th.label}
|
|
189
|
+
</Typography>
|
|
190
|
+
{th.description && (
|
|
191
|
+
<Typography className="truncate text-xs tracking-normal text-text-tertiary">
|
|
192
|
+
{th.description}
|
|
193
|
+
</Typography>
|
|
194
|
+
)}
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
<Check
|
|
198
|
+
className={cn(
|
|
199
|
+
"h-3 w-3 shrink-0 text-midground",
|
|
200
|
+
isActive ? "opacity-100" : "opacity-0",
|
|
201
|
+
)}
|
|
202
|
+
/>
|
|
203
|
+
</ListItem>
|
|
204
|
+
);
|
|
205
|
+
})}
|
|
206
|
+
</>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function ThemeSwatch({ theme }: { theme: DashboardTheme }) {
|
|
211
|
+
const { background, midground, warmGlow } = theme.palette;
|
|
212
|
+
return (
|
|
213
|
+
<div
|
|
214
|
+
aria-hidden
|
|
215
|
+
className="flex h-4 w-9 shrink-0 overflow-hidden border border-current/20"
|
|
216
|
+
>
|
|
217
|
+
<span className="flex-1" style={{ background: background.hex }} />
|
|
218
|
+
<span className="flex-1" style={{ background: midground.hex }} />
|
|
219
|
+
<span className="flex-1" style={{ background: warmGlow }} />
|
|
220
|
+
</div>
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function PlaceholderSwatch() {
|
|
225
|
+
return (
|
|
226
|
+
<div
|
|
227
|
+
aria-hidden
|
|
228
|
+
className="h-4 w-9 shrink-0 border border-dashed border-current/20"
|
|
229
|
+
/>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
interface ThemeSwitcherOptionsProps {
|
|
234
|
+
availableThemes: ThemeListEntry[];
|
|
235
|
+
close: () => void;
|
|
236
|
+
setTheme: (name: string) => void;
|
|
237
|
+
themeName: string;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
interface ThemeSwitcherProps {
|
|
241
|
+
collapsed?: boolean;
|
|
242
|
+
dropUp?: boolean;
|
|
243
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { ListItem } from "@nastechai/ui/ui/components/list-item";
|
|
2
|
+
import {
|
|
3
|
+
AlertCircle,
|
|
4
|
+
Check,
|
|
5
|
+
ChevronDown,
|
|
6
|
+
ChevronRight,
|
|
7
|
+
Zap,
|
|
8
|
+
} from "lucide-react";
|
|
9
|
+
import { useEffect, useState } from "react";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Expandable tool call row — the web equivalent of Ink's ToolTrail node.
|
|
13
|
+
*
|
|
14
|
+
* Renders one `tool.start` + `tool.complete` pair (plus any `tool.progress`
|
|
15
|
+
* in between) as a single collapsible item in the transcript:
|
|
16
|
+
*
|
|
17
|
+
* ▸ ● read_file(path=/foo) 2.3s
|
|
18
|
+
*
|
|
19
|
+
* Click the header to reveal a preformatted body with context (args), the
|
|
20
|
+
* streaming preview (while running), and the final summary or error. Error
|
|
21
|
+
* rows auto-expand so failures aren't silently collapsed.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export interface ToolEntry {
|
|
25
|
+
kind: "tool";
|
|
26
|
+
id: string;
|
|
27
|
+
tool_id: string;
|
|
28
|
+
name: string;
|
|
29
|
+
context?: string;
|
|
30
|
+
preview?: string;
|
|
31
|
+
summary?: string;
|
|
32
|
+
error?: string;
|
|
33
|
+
inline_diff?: string;
|
|
34
|
+
status: "running" | "done" | "error";
|
|
35
|
+
startedAt: number;
|
|
36
|
+
completedAt?: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const STATUS_TONE: Record<ToolEntry["status"], string> = {
|
|
40
|
+
running: "border-primary/40 bg-primary/[0.04]",
|
|
41
|
+
done: "border-border bg-muted/20",
|
|
42
|
+
error: "border-destructive/50 bg-destructive/[0.04]",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const BULLET_TONE: Record<ToolEntry["status"], string> = {
|
|
46
|
+
running: "text-primary",
|
|
47
|
+
done: "text-primary/80",
|
|
48
|
+
error: "text-destructive",
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const TICK_MS = 500;
|
|
52
|
+
|
|
53
|
+
export function ToolCall({ tool }: { tool: ToolEntry }) {
|
|
54
|
+
// `open` is derived: errors default-expanded, everything else collapsed.
|
|
55
|
+
// `null` means "follow the default"; any explicit bool is the user's override.
|
|
56
|
+
// This lets a running tool flip to expanded automatically when it errors,
|
|
57
|
+
// without mirroring state in an effect.
|
|
58
|
+
const [userOverride, setUserOverride] = useState<boolean | null>(null);
|
|
59
|
+
const open = userOverride ?? tool.status === "error";
|
|
60
|
+
|
|
61
|
+
// Tick `now` while the tool is running so the elapsed label updates live.
|
|
62
|
+
const [now, setNow] = useState(() => Date.now());
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (tool.status !== "running") return;
|
|
65
|
+
const id = window.setInterval(() => setNow(() => Date.now()), TICK_MS);
|
|
66
|
+
return () => window.clearInterval(id);
|
|
67
|
+
}, [tool.status]);
|
|
68
|
+
|
|
69
|
+
// Historical tools (hydrated from session.resume) signal missing timestamps
|
|
70
|
+
// with `startedAt === 0`; we hide the elapsed badge for those rather than
|
|
71
|
+
// rendering a misleading "0ms".
|
|
72
|
+
const hasTimestamps = tool.startedAt > 0;
|
|
73
|
+
const elapsed = hasTimestamps
|
|
74
|
+
? fmtElapsed((tool.completedAt ?? now) - tool.startedAt)
|
|
75
|
+
: null;
|
|
76
|
+
|
|
77
|
+
const hasBody = !!(
|
|
78
|
+
tool.context ||
|
|
79
|
+
tool.preview ||
|
|
80
|
+
tool.summary ||
|
|
81
|
+
tool.error ||
|
|
82
|
+
tool.inline_diff
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const Chevron = open ? ChevronDown : ChevronRight;
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<div
|
|
89
|
+
className={`rounded-md border overflow-hidden ${STATUS_TONE[tool.status]}`}
|
|
90
|
+
>
|
|
91
|
+
<ListItem
|
|
92
|
+
onClick={() => setUserOverride(!open)}
|
|
93
|
+
disabled={!hasBody}
|
|
94
|
+
aria-expanded={open}
|
|
95
|
+
className="px-2.5 py-1.5 text-xs hover:bg-foreground/2 disabled:cursor-default"
|
|
96
|
+
>
|
|
97
|
+
{hasBody ? (
|
|
98
|
+
<Chevron className="h-3 w-3 shrink-0 text-muted-foreground" />
|
|
99
|
+
) : (
|
|
100
|
+
<span className="w-3 shrink-0" />
|
|
101
|
+
)}
|
|
102
|
+
|
|
103
|
+
<Zap className={`h-3 w-3 shrink-0 ${BULLET_TONE[tool.status]}`} />
|
|
104
|
+
|
|
105
|
+
<span className="font-mono font-medium shrink-0">{tool.name}</span>
|
|
106
|
+
|
|
107
|
+
<span className="font-mono text-text-secondary truncate min-w-0 flex-1">
|
|
108
|
+
{tool.context ?? ""}
|
|
109
|
+
</span>
|
|
110
|
+
|
|
111
|
+
{tool.status === "running" && (
|
|
112
|
+
<span
|
|
113
|
+
className="inline-block h-2 w-2 rounded-full bg-primary animate-pulse shrink-0"
|
|
114
|
+
title="running"
|
|
115
|
+
/>
|
|
116
|
+
)}
|
|
117
|
+
{tool.status === "error" && (
|
|
118
|
+
<AlertCircle
|
|
119
|
+
className="h-3 w-3 shrink-0 text-destructive"
|
|
120
|
+
aria-label="error"
|
|
121
|
+
/>
|
|
122
|
+
)}
|
|
123
|
+
{tool.status === "done" && (
|
|
124
|
+
<Check
|
|
125
|
+
className="h-3 w-3 shrink-0 text-primary/80"
|
|
126
|
+
aria-label="done"
|
|
127
|
+
/>
|
|
128
|
+
)}
|
|
129
|
+
|
|
130
|
+
{elapsed && (
|
|
131
|
+
<span className="font-mono text-xs text-text-tertiary tabular-nums shrink-0">
|
|
132
|
+
{elapsed}
|
|
133
|
+
</span>
|
|
134
|
+
)}
|
|
135
|
+
</ListItem>
|
|
136
|
+
|
|
137
|
+
{open && hasBody && (
|
|
138
|
+
<div className="border-t border-border/60 px-3 py-2 space-y-2 text-xs font-mono">
|
|
139
|
+
{tool.context && <Section label="context">{tool.context}</Section>}
|
|
140
|
+
|
|
141
|
+
{tool.preview && tool.status === "running" && (
|
|
142
|
+
<Section label="streaming">
|
|
143
|
+
{tool.preview}
|
|
144
|
+
<span className="inline-block w-1.5 h-3 align-middle bg-foreground/40 ml-0.5 animate-pulse" />
|
|
145
|
+
</Section>
|
|
146
|
+
)}
|
|
147
|
+
|
|
148
|
+
{tool.inline_diff && (
|
|
149
|
+
<Section label="diff">
|
|
150
|
+
<pre className="whitespace-pre overflow-x-auto text-[0.7rem] leading-snug">
|
|
151
|
+
{colorizeDiff(tool.inline_diff)}
|
|
152
|
+
</pre>
|
|
153
|
+
</Section>
|
|
154
|
+
)}
|
|
155
|
+
|
|
156
|
+
{tool.summary && (
|
|
157
|
+
<Section label="result">
|
|
158
|
+
<span className="text-foreground/90 whitespace-pre-wrap">
|
|
159
|
+
{tool.summary}
|
|
160
|
+
</span>
|
|
161
|
+
</Section>
|
|
162
|
+
)}
|
|
163
|
+
|
|
164
|
+
{tool.error && (
|
|
165
|
+
<Section label="error" tone="error">
|
|
166
|
+
<span className="text-destructive whitespace-pre-wrap">
|
|
167
|
+
{tool.error}
|
|
168
|
+
</span>
|
|
169
|
+
</Section>
|
|
170
|
+
)}
|
|
171
|
+
</div>
|
|
172
|
+
)}
|
|
173
|
+
</div>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function Section({
|
|
178
|
+
label,
|
|
179
|
+
children,
|
|
180
|
+
tone,
|
|
181
|
+
}: {
|
|
182
|
+
label: string;
|
|
183
|
+
children: React.ReactNode;
|
|
184
|
+
tone?: "error";
|
|
185
|
+
}) {
|
|
186
|
+
return (
|
|
187
|
+
<div className="flex gap-3">
|
|
188
|
+
<span
|
|
189
|
+
className={`text-display font-mondwest tracking-wider text-xs shrink-0 w-20 pt-0.5 ${
|
|
190
|
+
tone === "error" ? "text-destructive" : "text-text-tertiary"
|
|
191
|
+
}`}
|
|
192
|
+
>
|
|
193
|
+
{label}
|
|
194
|
+
</span>
|
|
195
|
+
|
|
196
|
+
<div className="flex-1 min-w-0 text-muted-foreground">{children}</div>
|
|
197
|
+
</div>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function fmtElapsed(ms: number): string {
|
|
202
|
+
const sec = Math.max(0, ms) / 1000;
|
|
203
|
+
if (sec < 1) return `${Math.round(ms)}ms`;
|
|
204
|
+
if (sec < 10) return `${sec.toFixed(1)}s`;
|
|
205
|
+
if (sec < 60) return `${Math.round(sec)}s`;
|
|
206
|
+
|
|
207
|
+
const m = Math.floor(sec / 60);
|
|
208
|
+
const s = Math.round(sec % 60);
|
|
209
|
+
return s ? `${m}m ${s}s` : `${m}m`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Colorize unified-diff lines for the inline diff section. */
|
|
213
|
+
function colorizeDiff(diff: string): React.ReactNode {
|
|
214
|
+
return diff.split("\n").map((line, i) => (
|
|
215
|
+
<div key={i} className={diffLineClass(line)}>
|
|
216
|
+
{line || "\u00A0"}
|
|
217
|
+
</div>
|
|
218
|
+
));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function diffLineClass(line: string): string {
|
|
222
|
+
if (line.startsWith("+") && !line.startsWith("+++"))
|
|
223
|
+
return "text-emerald-500 dark:text-emerald-400";
|
|
224
|
+
if (line.startsWith("-") && !line.startsWith("---"))
|
|
225
|
+
return "text-destructive";
|
|
226
|
+
if (line.startsWith("@@")) return "text-primary";
|
|
227
|
+
return "text-text-secondary";
|
|
228
|
+
}
|