@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.
Files changed (98) hide show
  1. package/eslint.config.js +23 -0
  2. package/index.html +24 -0
  3. package/package.json +54 -26
  4. package/package.json.bak +89 -0
  5. package/package.json.pub +88 -0
  6. package/src/App.tsx +1173 -0
  7. package/src/components/AuthWidget.tsx +150 -0
  8. package/src/components/AutoField.tsx +206 -0
  9. package/src/components/Backdrop.tsx +93 -0
  10. package/src/components/ChatSidebar.tsx +394 -0
  11. package/src/components/DeleteConfirmDialog.tsx +40 -0
  12. package/src/components/LanguageSwitcher.tsx +186 -0
  13. package/src/components/Markdown.tsx +383 -0
  14. package/src/components/ModelInfoCard.tsx +112 -0
  15. package/src/components/ModelPickerDialog.tsx +470 -0
  16. package/src/components/OAuthLoginModal.tsx +374 -0
  17. package/src/components/OAuthProvidersCard.tsx +287 -0
  18. package/src/components/PlatformsCard.tsx +97 -0
  19. package/src/components/ScheduleBuilder.tsx +273 -0
  20. package/src/components/SidebarFooter.tsx +42 -0
  21. package/src/components/SidebarStatusStrip.tsx +72 -0
  22. package/src/components/SlashPopover.tsx +171 -0
  23. package/src/components/ThemeSwitcher.tsx +243 -0
  24. package/src/components/ToolCall.tsx +228 -0
  25. package/src/components/ToolsetConfigDrawer.tsx +448 -0
  26. package/src/contexts/PageHeaderProvider.tsx +139 -0
  27. package/src/contexts/SystemActions.tsx +120 -0
  28. package/src/contexts/page-header-context.ts +12 -0
  29. package/src/contexts/system-actions-context.ts +18 -0
  30. package/src/contexts/usePageHeader.ts +10 -0
  31. package/src/contexts/useSystemActions.ts +15 -0
  32. package/src/hooks/useModalBehavior.ts +44 -0
  33. package/src/hooks/useSidebarStatus.ts +27 -0
  34. package/src/i18n/af.ts +702 -0
  35. package/src/i18n/context.tsx +123 -0
  36. package/src/i18n/de.ts +701 -0
  37. package/src/i18n/en.ts +708 -0
  38. package/src/i18n/es.ts +701 -0
  39. package/src/i18n/fr.ts +701 -0
  40. package/src/i18n/ga.ts +702 -0
  41. package/src/i18n/hu.ts +702 -0
  42. package/src/i18n/index.ts +2 -0
  43. package/src/i18n/it.ts +701 -0
  44. package/src/i18n/ja.ts +702 -0
  45. package/src/i18n/ko.ts +702 -0
  46. package/src/i18n/pt.ts +702 -0
  47. package/src/i18n/ru.ts +702 -0
  48. package/src/i18n/tr.ts +702 -0
  49. package/src/i18n/types.ts +710 -0
  50. package/src/i18n/uk.ts +702 -0
  51. package/src/i18n/zh-hant.ts +702 -0
  52. package/src/i18n/zh.ts +698 -0
  53. package/src/index.css +274 -0
  54. package/src/lib/api.ts +1585 -0
  55. package/src/lib/dashboard-flags.ts +15 -0
  56. package/src/lib/format.ts +9 -0
  57. package/src/lib/fuzzy.ts +192 -0
  58. package/src/lib/gatewayClient.ts +253 -0
  59. package/src/lib/nested.ts +23 -0
  60. package/src/lib/resolve-page-title.ts +41 -0
  61. package/src/lib/schedule.ts +382 -0
  62. package/src/lib/slashExec.ts +163 -0
  63. package/src/lib/utils.ts +35 -0
  64. package/src/main.tsx +25 -0
  65. package/src/pages/AnalyticsPage.tsx +601 -0
  66. package/src/pages/ChannelsPage.tsx +772 -0
  67. package/src/pages/ChatPage.tsx +889 -0
  68. package/src/pages/ConfigPage.tsx +660 -0
  69. package/src/pages/CronPage.tsx +524 -0
  70. package/src/pages/DocsPage.tsx +69 -0
  71. package/src/pages/EnvPage.tsx +918 -0
  72. package/src/pages/LogsPage.tsx +246 -0
  73. package/src/pages/McpPage.tsx +757 -0
  74. package/src/pages/ModelsPage.tsx +994 -0
  75. package/src/pages/PairingPage.tsx +276 -0
  76. package/src/pages/PluginsPage.tsx +580 -0
  77. package/src/pages/ProfilesPage.tsx +559 -0
  78. package/src/pages/SessionsPage.tsx +936 -0
  79. package/src/pages/SkillsPage.tsx +557 -0
  80. package/src/pages/SystemPage.tsx +1259 -0
  81. package/src/pages/WebhooksPage.tsx +483 -0
  82. package/src/plugins/PluginPage.tsx +64 -0
  83. package/src/plugins/index.ts +6 -0
  84. package/src/plugins/registry.ts +151 -0
  85. package/src/plugins/sdk.d.ts +160 -0
  86. package/src/plugins/slots.ts +199 -0
  87. package/src/plugins/types.ts +37 -0
  88. package/src/plugins/usePlugins.ts +133 -0
  89. package/src/themes/context.tsx +443 -0
  90. package/src/themes/fonts.ts +160 -0
  91. package/src/themes/index.ts +3 -0
  92. package/src/themes/presets.ts +477 -0
  93. package/src/themes/types.ts +187 -0
  94. package/tsconfig.app.json +34 -0
  95. package/tsconfig.json +7 -0
  96. package/tsconfig.node.json +26 -0
  97. package/vite.config.ts +124 -0
  98. 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
+ }