@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,383 @@
1
+ import { useMemo, type ReactNode } from "react";
2
+
3
+ /**
4
+ * Lightweight markdown renderer for LLM output.
5
+ * Handles: code blocks, inline code, bold, italic, headers, links, lists, horizontal rules.
6
+ * NOT a full CommonMark parser — optimized for typical assistant message patterns.
7
+ *
8
+ * `streaming` renders a blinking caret at the tail of the last block so it
9
+ * appears to hug the final character instead of wrapping onto a new line
10
+ * after a block element (paragraph/list/code/…).
11
+ */
12
+ export function Markdown({
13
+ content,
14
+ highlightTerms,
15
+ streaming,
16
+ }: {
17
+ content: string;
18
+ highlightTerms?: string[];
19
+ streaming?: boolean;
20
+ }) {
21
+ const blocks = useMemo(() => parseBlocks(content), [content]);
22
+ const caret = streaming ? <StreamingCaret /> : null;
23
+
24
+ return (
25
+ <div className="text-sm text-foreground leading-relaxed space-y-2">
26
+ {blocks.map((block, i) => (
27
+ <Block
28
+ key={i}
29
+ block={block}
30
+ highlightTerms={highlightTerms}
31
+ caret={caret && i === blocks.length - 1 ? caret : null}
32
+ />
33
+ ))}
34
+ {blocks.length === 0 && caret}
35
+ </div>
36
+ );
37
+ }
38
+
39
+ function StreamingCaret() {
40
+ return (
41
+ <span
42
+ aria-hidden
43
+ className="inline-block w-[0.5em] h-[1em] ml-0.5 align-[-0.15em] bg-foreground/50 animate-pulse"
44
+ />
45
+ );
46
+ }
47
+
48
+ /* ------------------------------------------------------------------ */
49
+ /* Types */
50
+ /* ------------------------------------------------------------------ */
51
+
52
+ type BlockNode =
53
+ | { type: "code"; lang: string; content: string }
54
+ | { type: "heading"; level: number; content: string }
55
+ | { type: "hr" }
56
+ | { type: "list"; ordered: boolean; items: string[] }
57
+ | { type: "paragraph"; content: string };
58
+
59
+ /* ------------------------------------------------------------------ */
60
+ /* Block parser */
61
+ /* ------------------------------------------------------------------ */
62
+
63
+ function parseBlocks(text: string): BlockNode[] {
64
+ const lines = text.split("\n");
65
+ const blocks: BlockNode[] = [];
66
+ let i = 0;
67
+
68
+ while (i < lines.length) {
69
+ const line = lines[i];
70
+
71
+ // Fenced code block
72
+ const fenceMatch = line.match(/^```(\w*)/);
73
+ if (fenceMatch) {
74
+ const lang = fenceMatch[1] || "";
75
+ const codeLines: string[] = [];
76
+ i++;
77
+ while (i < lines.length && !lines[i].startsWith("```")) {
78
+ codeLines.push(lines[i]);
79
+ i++;
80
+ }
81
+ i++; // skip closing ```
82
+ blocks.push({ type: "code", lang, content: codeLines.join("\n") });
83
+ continue;
84
+ }
85
+
86
+ // Heading
87
+ const headingMatch = line.match(/^(#{1,4})\s+(.+)/);
88
+ if (headingMatch) {
89
+ blocks.push({
90
+ type: "heading",
91
+ level: headingMatch[1].length,
92
+ content: headingMatch[2],
93
+ });
94
+ i++;
95
+ continue;
96
+ }
97
+
98
+ // Horizontal rule
99
+ if (/^[-*_]{3,}\s*$/.test(line)) {
100
+ blocks.push({ type: "hr" });
101
+ i++;
102
+ continue;
103
+ }
104
+
105
+ // Unordered list
106
+ if (/^[-*+]\s/.test(line)) {
107
+ const items: string[] = [];
108
+ while (i < lines.length && /^[-*+]\s/.test(lines[i])) {
109
+ items.push(lines[i].replace(/^[-*+]\s/, ""));
110
+ i++;
111
+ }
112
+ blocks.push({ type: "list", ordered: false, items });
113
+ continue;
114
+ }
115
+
116
+ // Ordered list
117
+ if (/^\d+[.)]\s/.test(line)) {
118
+ const items: string[] = [];
119
+ while (i < lines.length && /^\d+[.)]\s/.test(lines[i])) {
120
+ items.push(lines[i].replace(/^\d+[.)]\s/, ""));
121
+ i++;
122
+ }
123
+ blocks.push({ type: "list", ordered: true, items });
124
+ continue;
125
+ }
126
+
127
+ // Empty line
128
+ if (line.trim() === "") {
129
+ i++;
130
+ continue;
131
+ }
132
+
133
+ // Paragraph — collect consecutive non-empty, non-special lines
134
+ const paraLines: string[] = [];
135
+ while (
136
+ i < lines.length &&
137
+ lines[i].trim() !== "" &&
138
+ !lines[i].match(/^```/) &&
139
+ !lines[i].match(/^#{1,4}\s/) &&
140
+ !lines[i].match(/^[-*+]\s/) &&
141
+ !lines[i].match(/^\d+[.)]\s/) &&
142
+ !lines[i].match(/^[-*_]{3,}\s*$/)
143
+ ) {
144
+ paraLines.push(lines[i]);
145
+ i++;
146
+ }
147
+ if (paraLines.length > 0) {
148
+ blocks.push({ type: "paragraph", content: paraLines.join("\n") });
149
+ }
150
+ }
151
+
152
+ return blocks;
153
+ }
154
+
155
+ /* ------------------------------------------------------------------ */
156
+ /* Block renderer */
157
+ /* ------------------------------------------------------------------ */
158
+
159
+ function Block({
160
+ block,
161
+ highlightTerms,
162
+ caret,
163
+ }: {
164
+ block: BlockNode;
165
+ highlightTerms?: string[];
166
+ caret?: ReactNode;
167
+ }) {
168
+ switch (block.type) {
169
+ case "code":
170
+ return (
171
+ <pre className="bg-secondary/60 border border-border px-3 py-2.5 text-xs font-mono leading-relaxed overflow-x-auto">
172
+ <code>
173
+ {block.content}
174
+ {caret}
175
+ </code>
176
+ </pre>
177
+ );
178
+
179
+ case "heading": {
180
+ const Tag = `h${Math.min(block.level, 4)}` as "h1" | "h2" | "h3" | "h4";
181
+ const sizes: Record<string, string> = {
182
+ h1: "text-base font-bold",
183
+ h2: "text-sm font-bold",
184
+ h3: "text-sm font-semibold",
185
+ h4: "text-sm font-medium",
186
+ };
187
+ return (
188
+ <Tag className={sizes[Tag]}>
189
+ <InlineContent text={block.content} highlightTerms={highlightTerms} />
190
+ {caret}
191
+ </Tag>
192
+ );
193
+ }
194
+
195
+ case "hr":
196
+ return (
197
+ <>
198
+ <hr className="border-border" />
199
+ {caret}
200
+ </>
201
+ );
202
+
203
+ case "list": {
204
+ const Tag = block.ordered ? "ol" : "ul";
205
+ const last = block.items.length - 1;
206
+ return (
207
+ <Tag
208
+ className={`space-y-0.5 ${block.ordered ? "list-decimal" : "list-disc"} pl-5 text-sm`}
209
+ >
210
+ {block.items.map((item, i) => (
211
+ <li key={i}>
212
+ <InlineContent text={item} highlightTerms={highlightTerms} />
213
+ {i === last ? caret : null}
214
+ </li>
215
+ ))}
216
+ </Tag>
217
+ );
218
+ }
219
+
220
+ case "paragraph":
221
+ return (
222
+ <p>
223
+ <InlineContent text={block.content} highlightTerms={highlightTerms} />
224
+ {caret}
225
+ </p>
226
+ );
227
+ }
228
+ }
229
+
230
+ /* ------------------------------------------------------------------ */
231
+ /* Inline parser + renderer */
232
+ /* ------------------------------------------------------------------ */
233
+
234
+ type InlineNode =
235
+ | { type: "text"; content: string }
236
+ | { type: "code"; content: string }
237
+ | { type: "bold"; content: string }
238
+ | { type: "italic"; content: string }
239
+ | { type: "link"; text: string; href: string }
240
+ | { type: "br" };
241
+
242
+ function parseInline(text: string): InlineNode[] {
243
+ const nodes: InlineNode[] = [];
244
+ // Pattern priority: code > link > bold > italic > bare URL > line break
245
+ const pattern =
246
+ /(`[^`]+`)|(\[([^\]]+)\]\(([^)]+)\))|(\*\*([^*]+)\*\*)|(\*([^*]+)\*)|(\bhttps?:\/\/[^\s<>)\]]+)|(\n)/g;
247
+ let lastIndex = 0;
248
+ let match: RegExpExecArray | null;
249
+
250
+ while ((match = pattern.exec(text)) !== null) {
251
+ if (match.index > lastIndex) {
252
+ nodes.push({ type: "text", content: text.slice(lastIndex, match.index) });
253
+ }
254
+
255
+ if (match[1]) {
256
+ // Inline code
257
+ nodes.push({ type: "code", content: match[1].slice(1, -1) });
258
+ } else if (match[2]) {
259
+ // [text](url) link
260
+ nodes.push({ type: "link", text: match[3], href: match[4] });
261
+ } else if (match[5]) {
262
+ // **bold**
263
+ nodes.push({ type: "bold", content: match[6] });
264
+ } else if (match[7]) {
265
+ // *italic*
266
+ nodes.push({ type: "italic", content: match[8] });
267
+ } else if (match[9]) {
268
+ // Bare URL
269
+ nodes.push({ type: "link", text: match[9], href: match[9] });
270
+ } else if (match[10]) {
271
+ // Line break within paragraph
272
+ nodes.push({ type: "br" });
273
+ }
274
+
275
+ lastIndex = match.index + match[0].length;
276
+ }
277
+
278
+ if (lastIndex < text.length) {
279
+ nodes.push({ type: "text", content: text.slice(lastIndex) });
280
+ }
281
+
282
+ return nodes;
283
+ }
284
+
285
+ function InlineContent({
286
+ text,
287
+ highlightTerms,
288
+ }: {
289
+ text: string;
290
+ highlightTerms?: string[];
291
+ }) {
292
+ const nodes = useMemo(() => parseInline(text), [text]);
293
+
294
+ return (
295
+ <>
296
+ {nodes.map((node, i) => {
297
+ switch (node.type) {
298
+ case "text":
299
+ return (
300
+ <HighlightedText
301
+ key={i}
302
+ text={node.content}
303
+ terms={highlightTerms}
304
+ />
305
+ );
306
+ case "code":
307
+ return (
308
+ <code
309
+ key={i}
310
+ className="bg-secondary/60 px-1.5 py-0.5 text-xs font-mono text-primary/90"
311
+ >
312
+ {node.content}
313
+ </code>
314
+ );
315
+ case "bold":
316
+ return (
317
+ <strong key={i} className="font-semibold">
318
+ <HighlightedText text={node.content} terms={highlightTerms} />
319
+ </strong>
320
+ );
321
+ case "italic":
322
+ return (
323
+ <em key={i}>
324
+ <HighlightedText text={node.content} terms={highlightTerms} />
325
+ </em>
326
+ );
327
+ case "link": {
328
+ // Security: only render http(s)/mailto links. Other schemes
329
+ // (javascript:, data:, vbscript:) are dropped to plain text so a
330
+ // crafted link in agent/message content can't execute on click.
331
+ const href = node.href.trim();
332
+ if (!/^(https?:|mailto:)/i.test(href)) {
333
+ return (
334
+ <HighlightedText
335
+ key={i}
336
+ text={node.text}
337
+ terms={highlightTerms}
338
+ />
339
+ );
340
+ }
341
+ return (
342
+ <a
343
+ key={i}
344
+ href={href}
345
+ target="_blank"
346
+ rel="noreferrer"
347
+ className="text-primary underline underline-offset-2 decoration-primary/30 hover:decoration-primary/60 transition-colors"
348
+ >
349
+ {node.text}
350
+ </a>
351
+ );
352
+ }
353
+ case "br":
354
+ return <br key={i} />;
355
+ }
356
+ })}
357
+ </>
358
+ );
359
+ }
360
+
361
+ /** Highlight search terms within a plain text string. */
362
+ function HighlightedText({ text, terms }: { text: string; terms?: string[] }) {
363
+ if (!terms || terms.length === 0) return <>{text}</>;
364
+
365
+ // Build a regex that matches any of the search terms (case-insensitive)
366
+ const escaped = terms.map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
367
+ const regex = new RegExp(`(${escaped.join("|")})`, "gi");
368
+ const parts = text.split(regex);
369
+
370
+ return (
371
+ <>
372
+ {parts.map((part, i) =>
373
+ regex.test(part) ? (
374
+ <mark key={i} className="bg-warning/30 text-warning px-0.5">
375
+ {part}
376
+ </mark>
377
+ ) : (
378
+ <span key={i}>{part}</span>
379
+ ),
380
+ )}
381
+ </>
382
+ );
383
+ }
@@ -0,0 +1,112 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ import { Brain, Eye, Gauge, Lightbulb, Wrench } from "lucide-react";
3
+ import { Spinner } from "@nastechai/ui/ui/components/spinner";
4
+ import { api } from "@/lib/api";
5
+ import type { ModelInfoResponse } from "@/lib/api";
6
+ import { formatTokenCount } from "@/lib/format";
7
+
8
+ interface ModelInfoCardProps {
9
+ /** Current model string from config state — used to detect changes */
10
+ currentModel: string;
11
+ /** Bumped after config saves to trigger re-fetch */
12
+ refreshKey?: number;
13
+ }
14
+
15
+ export function ModelInfoCard({
16
+ currentModel,
17
+ refreshKey = 0,
18
+ }: ModelInfoCardProps) {
19
+ const [info, setInfo] = useState<ModelInfoResponse | null>(null);
20
+ const [loading, setLoading] = useState(false);
21
+ const lastFetchKeyRef = useRef("");
22
+
23
+ useEffect(() => {
24
+ if (!currentModel) return;
25
+ // Re-fetch when model changes OR when refreshKey bumps (after save)
26
+ const fetchKey = `${currentModel}:${refreshKey}`;
27
+ if (fetchKey === lastFetchKeyRef.current) return;
28
+ lastFetchKeyRef.current = fetchKey;
29
+ setLoading(true);
30
+ api
31
+ .getModelInfo()
32
+ .then(setInfo)
33
+ .catch(() => setInfo(null))
34
+ .finally(() => setLoading(false));
35
+ }, [currentModel, refreshKey]);
36
+
37
+ if (loading) {
38
+ return (
39
+ <div className="flex items-center gap-2 py-2 text-xs text-muted-foreground">
40
+ <Spinner className="text-xs" />
41
+ Loading model info…
42
+ </div>
43
+ );
44
+ }
45
+
46
+ if (!info || !info.model || info.effective_context_length <= 0) return null;
47
+
48
+ const caps = info.capabilities;
49
+ const hasCaps = caps && Object.keys(caps).length > 0;
50
+
51
+ return (
52
+ <div className="border border-border/60 bg-muted/30 px-3 py-2.5 space-y-2">
53
+ <div className="flex items-center gap-4 text-xs">
54
+ <div className="flex items-center gap-1.5 text-muted-foreground">
55
+ <Gauge className="h-3.5 w-3.5" />
56
+ <span className="font-medium">Context Window</span>
57
+ </div>
58
+ <div className="flex items-center gap-2">
59
+ <span className="font-mono font-semibold text-foreground">
60
+ {formatTokenCount(info.effective_context_length)}
61
+ </span>
62
+ {info.config_context_length > 0 ? (
63
+ <span className="text-amber-500 text-xs">
64
+ (override — auto: {formatTokenCount(info.auto_context_length)})
65
+ </span>
66
+ ) : (
67
+ <span className="text-text-tertiary text-xs">
68
+ auto-detected
69
+ </span>
70
+ )}
71
+ </div>
72
+ </div>
73
+
74
+ {hasCaps && caps.max_output_tokens && caps.max_output_tokens > 0 && (
75
+ <div className="flex items-center gap-4 text-xs">
76
+ <div className="flex items-center gap-1.5 text-muted-foreground">
77
+ <Lightbulb className="h-3.5 w-3.5" />
78
+ <span className="font-medium">Max Output</span>
79
+ </div>
80
+ <span className="font-mono font-semibold text-foreground">
81
+ {formatTokenCount(caps.max_output_tokens)}
82
+ </span>
83
+ </div>
84
+ )}
85
+
86
+ {hasCaps && (
87
+ <div className="flex flex-wrap items-center gap-1.5 pt-0.5">
88
+ {caps.supports_tools && (
89
+ <span className="inline-flex items-center gap-1 bg-emerald-500/10 px-2 py-0.5 text-xs font-medium text-emerald-600 dark:text-emerald-400">
90
+ <Wrench className="h-2.5 w-2.5" /> Tools
91
+ </span>
92
+ )}
93
+ {caps.supports_vision && (
94
+ <span className="inline-flex items-center gap-1 bg-blue-500/10 px-2 py-0.5 text-xs font-medium text-blue-600 dark:text-blue-400">
95
+ <Eye className="h-2.5 w-2.5" /> Vision
96
+ </span>
97
+ )}
98
+ {caps.supports_reasoning && (
99
+ <span className="inline-flex items-center gap-1 bg-purple-500/10 px-2 py-0.5 text-xs font-medium text-purple-600 dark:text-purple-400">
100
+ <Brain className="h-2.5 w-2.5" /> Reasoning
101
+ </span>
102
+ )}
103
+ {caps.model_family && (
104
+ <span className="inline-flex items-center gap-1 bg-muted px-2 py-0.5 text-xs font-medium text-text-secondary">
105
+ {caps.model_family}
106
+ </span>
107
+ )}
108
+ </div>
109
+ )}
110
+ </div>
111
+ );
112
+ }