@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,150 @@
1
+ /**
2
+ * AuthWidget — sidebar "Logged in as …" affordance for the dashboard
3
+ * OAuth gate (Phase 7 of .nastech/plans/2026-05-21-dashboard-oauth-auth.md).
4
+ *
5
+ * Renders nothing in loopback / --insecure mode. In gated mode, fetches
6
+ * /api/auth/me on mount and surfaces:
7
+ *
8
+ * - the user_id (truncated to 14 chars + ellipsis) since the NasTech Portal
9
+ * contract V1 doesn't emit email/display_name claims (Contract Anchor
10
+ * C4 in the plan; the API responds with empty strings for those
11
+ * fields, so we use user_id as the display value)
12
+ * - the provider's display_name (looked up from /api/auth/providers,
13
+ * defaults to the bare provider key)
14
+ * - a logout button that POSTs /auth/logout and full-page-navigates to
15
+ * /login (the dashboard becomes inaccessible again)
16
+ *
17
+ * Failure modes:
18
+ * - 401 from /api/auth/me means we're not gated (or the gate is on but
19
+ * we have no cookie — in that case the gate's middleware would have
20
+ * redirected us before App.tsx renders, so we won't see this). The
21
+ * widget renders nothing.
22
+ * - Network error: shows a minimal "auth status unavailable" message
23
+ * so the user knows the widget tried.
24
+ */
25
+
26
+ import { useEffect, useState } from "react";
27
+ import { api, type AuthMeResponse } from "@/lib/api";
28
+ import { cn } from "@/lib/utils";
29
+ import { LogOut } from "lucide-react";
30
+
31
+ interface AuthWidgetProps {
32
+ className?: string;
33
+ }
34
+
35
+ /** Truncate ``user_id`` to fit a small UI without revealing the full
36
+ * opaque identifier. 14 chars is enough to disambiguate users in a
37
+ * small org and short enough to fit a single sidebar row. */
38
+ function truncateUserId(id: string): string {
39
+ if (id.length <= 14) return id;
40
+ return `${id.slice(0, 14)}…`;
41
+ }
42
+
43
+ export function AuthWidget({ className }: AuthWidgetProps) {
44
+ const [me, setMe] = useState<AuthMeResponse | null>(null);
45
+ const [hidden, setHidden] = useState(false);
46
+ const [error, setError] = useState<string | null>(null);
47
+
48
+ useEffect(() => {
49
+ let cancelled = false;
50
+ api
51
+ .getAuthMe()
52
+ .then((data) => {
53
+ if (cancelled) return;
54
+ setMe(data);
55
+ })
56
+ .catch((err: unknown) => {
57
+ if (cancelled) return;
58
+ // 401 from /api/auth/me means the gate isn't engaged in this
59
+ // process (loopback mode) — render nothing. fetchJSON throws an
60
+ // Error with the status code as a prefix; the global 401
61
+ // handler only redirects on the structured envelope, so a plain
62
+ // 401 from /api/auth/me with no envelope bubbles up here.
63
+ const msg = err instanceof Error ? err.message : String(err);
64
+ if (msg.startsWith("401:") || msg.startsWith("403:")) {
65
+ setHidden(true);
66
+ return;
67
+ }
68
+ setError("auth status unavailable");
69
+ });
70
+ return () => {
71
+ cancelled = true;
72
+ };
73
+ }, []);
74
+
75
+ if (hidden) return null;
76
+
77
+ if (error) {
78
+ return (
79
+ <div
80
+ className={cn(
81
+ "px-5 py-2 text-[0.65rem] tracking-[0.05em] text-muted-foreground/70",
82
+ className,
83
+ )}
84
+ >
85
+ {error}
86
+ </div>
87
+ );
88
+ }
89
+
90
+ if (!me) {
91
+ // Loading. Reserve the row height so the sidebar doesn't flicker
92
+ // when the data arrives.
93
+ return (
94
+ <div
95
+ className={cn(
96
+ "h-9 px-5 py-2 text-[0.65rem] text-muted-foreground/40",
97
+ className,
98
+ )}
99
+ aria-busy="true"
100
+ >
101
+
102
+ </div>
103
+ );
104
+ }
105
+
106
+ const handleLogout = () => {
107
+ void api.logout();
108
+ };
109
+
110
+ // Prefer display_name → email → truncated user_id. Contract V1 only
111
+ // populates user_id; the fallthroughs are forward-compat for a future
112
+ // Portal that adds a userinfo endpoint (OQ-C1 in the plan).
113
+ const label = me.display_name || me.email || truncateUserId(me.user_id);
114
+
115
+ return (
116
+ <div
117
+ className={cn(
118
+ "flex shrink-0 items-center justify-between gap-2",
119
+ "px-5 py-2",
120
+ "border-t border-current/10",
121
+ "text-[0.65rem] tracking-[0.05em]",
122
+ className,
123
+ )}
124
+ role="status"
125
+ aria-label={`Logged in as ${label}`}
126
+ >
127
+ <div className="flex min-w-0 flex-col">
128
+ <span className="truncate font-mono text-foreground/90" title={me.user_id}>
129
+ {label}
130
+ </span>
131
+ <span className="truncate text-muted-foreground/70">
132
+ via {me.provider}
133
+ </span>
134
+ </div>
135
+ <button
136
+ type="button"
137
+ onClick={handleLogout}
138
+ className={cn(
139
+ "shrink-0 rounded p-1.5 text-muted-foreground/70",
140
+ "transition-colors hover:bg-current/10 hover:text-foreground",
141
+ "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-current/40",
142
+ )}
143
+ aria-label="Log out"
144
+ title="Log out"
145
+ >
146
+ <LogOut className="h-3.5 w-3.5" />
147
+ </button>
148
+ </div>
149
+ );
150
+ }
@@ -0,0 +1,206 @@
1
+ import { Select, SelectOption } from "@nastechai/ui/ui/components/select";
2
+ import { Switch } from "@nastechai/ui/ui/components/switch";
3
+ import { Input } from "@nastechai/ui/ui/components/input";
4
+ import { Label } from "@nastechai/ui/ui/components/label";
5
+
6
+ function FieldHint({ schema, schemaKey }: { schema: Record<string, unknown>; schemaKey: string }) {
7
+ const keyPath = schemaKey.includes(".") ? schemaKey : "";
8
+ const description = schema.description ? String(schema.description) : "";
9
+
10
+ if (!keyPath && !description) return null;
11
+
12
+ return (
13
+ <div className="flex flex-col gap-0.5">
14
+ {keyPath && <span className="text-xs font-mono text-text-tertiary">{keyPath}</span>}
15
+ {description && <span className="text-xs text-text-secondary">{description}</span>}
16
+ </div>
17
+ );
18
+ }
19
+
20
+ function isRecord(value: unknown): value is Record<string, unknown> {
21
+ return typeof value === "object" && value !== null && !Array.isArray(value);
22
+ }
23
+
24
+ function formatScalar(value: unknown): string {
25
+ if (value === undefined || value === null) return "";
26
+ if (typeof value === "string") return value;
27
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
28
+ return JSON.stringify(value);
29
+ }
30
+
31
+ function NestedValueEditor({
32
+ fieldKey,
33
+ value,
34
+ onChange,
35
+ }: {
36
+ fieldKey: string;
37
+ value: unknown;
38
+ onChange: (v: unknown) => void;
39
+ }) {
40
+ if (isRecord(value)) {
41
+ return (
42
+ <div className="grid gap-2 border border-border p-2">
43
+ {Object.entries(value).map(([subKey, subVal]) => (
44
+ <div key={subKey} className="grid gap-1">
45
+ <Label className="text-xs text-muted-foreground">{subKey}</Label>
46
+ <NestedValueEditor
47
+ fieldKey={`${fieldKey}.${subKey}`}
48
+ value={subVal}
49
+ onChange={(next) => onChange({ ...value, [subKey]: next })}
50
+ />
51
+ </div>
52
+ ))}
53
+ </div>
54
+ );
55
+ }
56
+
57
+ if (Array.isArray(value)) {
58
+ return (
59
+ <div className="grid gap-2">
60
+ {value.map((item, index) => (
61
+ <div key={`${fieldKey}.${index}`} className="grid gap-1">
62
+ <Label className="text-xs text-muted-foreground">Item {index + 1}</Label>
63
+ <NestedValueEditor
64
+ fieldKey={`${fieldKey}.${index}`}
65
+ value={item}
66
+ onChange={(next) =>
67
+ onChange(value.map((existing, i) => (i === index ? next : existing)))
68
+ }
69
+ />
70
+ </div>
71
+ ))}
72
+ </div>
73
+ );
74
+ }
75
+
76
+ return (
77
+ <Input
78
+ value={formatScalar(value)}
79
+ onChange={(e) => onChange(e.target.value)}
80
+ className="text-xs"
81
+ />
82
+ );
83
+ }
84
+
85
+ export function AutoField({
86
+ schemaKey,
87
+ schema,
88
+ value,
89
+ onChange,
90
+ }: AutoFieldProps) {
91
+ const rawLabel = schemaKey.split(".").pop() ?? schemaKey;
92
+ const label = rawLabel.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
93
+
94
+ if (isRecord(value) || (Array.isArray(value) && value.some((item) => isRecord(item)))) {
95
+ return (
96
+ <div className="grid gap-3 border border-border p-3">
97
+ <Label className="text-xs font-medium">{label}</Label>
98
+ <FieldHint schema={schema} schemaKey={schemaKey} />
99
+ <NestedValueEditor fieldKey={schemaKey} value={value} onChange={onChange} />
100
+ </div>
101
+ );
102
+ }
103
+
104
+ if (schema.type === "boolean") {
105
+ return (
106
+ <div className="flex items-center justify-between gap-4">
107
+ <div className="flex flex-col gap-0.5">
108
+ <Label className="text-sm">{label}</Label>
109
+ <FieldHint schema={schema} schemaKey={schemaKey} />
110
+ </div>
111
+ <Switch checked={!!value} onCheckedChange={onChange} />
112
+ </div>
113
+ );
114
+ }
115
+
116
+ if (schema.type === "select") {
117
+ const options = (schema.options as string[]) ?? [];
118
+ return (
119
+ <div className="grid gap-1.5">
120
+ <Label className="text-sm">{label}</Label>
121
+ <FieldHint schema={schema} schemaKey={schemaKey} />
122
+ <Select value={String(value ?? "")} onValueChange={(v) => onChange(v)}>
123
+ {options.map((opt) => (
124
+ <SelectOption key={opt} value={opt}>
125
+ {opt || "(none)"}
126
+ </SelectOption>
127
+ ))}
128
+ </Select>
129
+ </div>
130
+ );
131
+ }
132
+
133
+ if (schema.type === "number") {
134
+ return (
135
+ <div className="grid gap-1.5">
136
+ <Label className="text-sm">{label}</Label>
137
+ <FieldHint schema={schema} schemaKey={schemaKey} />
138
+ <Input
139
+ type="number"
140
+ value={value === undefined || value === null ? "" : String(value)}
141
+ onChange={(e) => {
142
+ const raw = e.target.value;
143
+ if (raw === "") {
144
+ onChange(0);
145
+ return;
146
+ }
147
+ const n = Number(raw);
148
+ if (!Number.isNaN(n)) {
149
+ onChange(n);
150
+ }
151
+ }}
152
+ />
153
+ </div>
154
+ );
155
+ }
156
+
157
+ if (schema.type === "text") {
158
+ return (
159
+ <div className="grid gap-1.5">
160
+ <Label className="text-sm">{label}</Label>
161
+ <FieldHint schema={schema} schemaKey={schemaKey} />
162
+ <textarea
163
+ className="flex min-h-[80px] w-full border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
164
+ value={String(value ?? "")}
165
+ onChange={(e) => onChange(e.target.value)}
166
+ />
167
+ </div>
168
+ );
169
+ }
170
+
171
+ if (schema.type === "list") {
172
+ return (
173
+ <div className="grid gap-1.5">
174
+ <Label className="text-sm">{label}</Label>
175
+ <FieldHint schema={schema} schemaKey={schemaKey} />
176
+ <Input
177
+ value={Array.isArray(value) ? value.join(", ") : String(value ?? "")}
178
+ onChange={(e) =>
179
+ onChange(
180
+ e.target.value
181
+ .split(",")
182
+ .map((s) => s.trim())
183
+ .filter(Boolean),
184
+ )
185
+ }
186
+ placeholder="comma-separated values"
187
+ />
188
+ </div>
189
+ );
190
+ }
191
+
192
+ return (
193
+ <div className="grid gap-1.5">
194
+ <Label className="text-sm">{label}</Label>
195
+ <FieldHint schema={schema} schemaKey={schemaKey} />
196
+ <Input value={String(value ?? "")} onChange={(e) => onChange(e.target.value)} />
197
+ </div>
198
+ );
199
+ }
200
+
201
+ interface AutoFieldProps {
202
+ schemaKey: string;
203
+ schema: Record<string, unknown>;
204
+ value: unknown;
205
+ onChange: (v: unknown) => void;
206
+ }
@@ -0,0 +1,93 @@
1
+ import { useGpuTier } from "@nastechai/ui/hooks/use-gpu-tier";
2
+
3
+ import fillerBgUrl from "@nastechai/ui/assets/filler-bg0.webp";
4
+
5
+ /**
6
+ * Replicates the visual layer stack of `<Overlays dark />` from
7
+ * `@nastechai/ui` without pulling in its leva / gsap / three peer deps.
8
+ *
9
+ * See `design-language/src/ui/components/overlays/index.tsx` for the source of
10
+ * truth. Defaults match LENS_0 (the NasTech teal dark preset); the deep canvas
11
+ * and the warm vignette both read theme-switchable CSS custom properties so
12
+ * `ThemeProvider` can repaint the stack without remounting.
13
+ *
14
+ * z-1 bg = `var(--background-base)`, mix-blend-mode: difference
15
+ * z-2 bundled filler-bg WebP, inverted, opacity 0.033, difference
16
+ * z-99 warm top-left vignette (`var(--warm-glow)`), opacity 0.22, lighten
17
+ * z-101 noise grain (SVG, ~55% opacity × `--noise-opacity-mul`,
18
+ * color-dodge) — gated on GPU tier
19
+ *
20
+ * `useGpuTier` returns 0 when WebGL is unavailable, the renderer is a
21
+ * software rasterizer (SwiftShader/llvmpipe), or the user has
22
+ * `prefers-reduced-motion: reduce` set. We skip the animated noise layer
23
+ * in that case so low-power / accessibility-conscious sessions stay crisp,
24
+ * mirroring the DS `<Noise />` component's own opt-out.
25
+ */
26
+ export function Backdrop() {
27
+ const gpuTier = useGpuTier();
28
+
29
+ return (
30
+ <>
31
+ <div
32
+ aria-hidden
33
+ className="pointer-events-none fixed inset-0 z-[1]"
34
+ style={{
35
+ backgroundColor: "var(--background-base)",
36
+ mixBlendMode: "difference",
37
+ }}
38
+ />
39
+
40
+ <div
41
+ aria-hidden
42
+ className="pointer-events-none fixed inset-0 z-[2]"
43
+ style={
44
+ {
45
+ // Themes can override the filler background by setting
46
+ // `assets.bg` — the <img> hides itself when a CSS bg is set
47
+ // so the two don't double-darken. CSS var fallbacks keep the
48
+ // default behaviour unchanged when no theme customises these.
49
+ mixBlendMode:
50
+ "var(--component-backdrop-filler-blend-mode, difference)",
51
+ opacity: "var(--component-backdrop-filler-opacity, 0.033)",
52
+ backgroundImage: "var(--theme-asset-bg)",
53
+ backgroundSize: "var(--component-backdrop-background-size, cover)",
54
+ backgroundPosition:
55
+ "var(--component-backdrop-background-position, center)",
56
+ } as unknown as React.CSSProperties
57
+ }
58
+ >
59
+ <img
60
+ alt=""
61
+ className="h-[150dvh] w-auto min-w-[100dvw] object-cover object-top-left invert theme-default-filler"
62
+ fetchPriority="low"
63
+ src={fillerBgUrl}
64
+ />
65
+ </div>
66
+
67
+ <div
68
+ aria-hidden
69
+ className="pointer-events-none fixed inset-0 z-[99]"
70
+ style={{
71
+ background:
72
+ "radial-gradient(ellipse at 0% 0%, transparent 60%, var(--warm-glow) 100%)",
73
+ mixBlendMode: "lighten",
74
+ opacity: 0.22,
75
+ }}
76
+ />
77
+
78
+ {gpuTier > 0 && (
79
+ <div
80
+ aria-hidden
81
+ className="pointer-events-none fixed inset-0 z-[101]"
82
+ style={{
83
+ backgroundImage:
84
+ "url(\"data:image/svg+xml,%3Csvg viewBox='0 0 512 512' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' fill='%23eaeaea' filter='url(%23n)' opacity='0.6'/%3E%3C/svg%3E\")",
85
+ backgroundSize: "512px 512px",
86
+ mixBlendMode: "color-dodge",
87
+ opacity: "calc(0.55 * var(--noise-opacity-mul, 1))",
88
+ }}
89
+ />
90
+ )}
91
+ </>
92
+ );
93
+ }