arcie 0.1.4 → 0.1.6

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 (64) hide show
  1. package/dist/agent/index.d.ts +1 -1
  2. package/dist/build-TWNCPXBU.js +82 -0
  3. package/dist/build-TWNCPXBU.js.map +1 -0
  4. package/dist/channels/index.d.ts +1 -1
  5. package/dist/{chunk-2ZSKZYWK.js → chunk-4WSILP75.js} +37 -164
  6. package/dist/chunk-4WSILP75.js.map +1 -0
  7. package/dist/chunk-HVBDOHTL.js +148 -0
  8. package/dist/chunk-HVBDOHTL.js.map +1 -0
  9. package/dist/chunk-HWD2Z2CH.js +742 -0
  10. package/dist/chunk-HWD2Z2CH.js.map +1 -0
  11. package/dist/chunk-KVSX4MLK.js +139 -0
  12. package/dist/chunk-KVSX4MLK.js.map +1 -0
  13. package/dist/chunk-N5R3TEEZ.js +15 -0
  14. package/dist/chunk-N5R3TEEZ.js.map +1 -0
  15. package/dist/chunk-SGWQZZ2A.js +54 -0
  16. package/dist/chunk-SGWQZZ2A.js.map +1 -0
  17. package/dist/chunk-XOFWQUOO.js +30 -0
  18. package/dist/chunk-XOFWQUOO.js.map +1 -0
  19. package/dist/cli/index.d.ts +7 -0
  20. package/dist/cli/index.js +156 -231
  21. package/dist/cli/index.js.map +1 -1
  22. package/dist/context/index.d.ts +1 -1
  23. package/dist/dev-ZZDVOWUZ.js +1967 -0
  24. package/dist/dev-ZZDVOWUZ.js.map +1 -0
  25. package/dist/hooks/index.d.ts +1 -1
  26. package/dist/{index-b4NcDl3m.d.ts → index-CElLRVTP.d.ts} +1 -1
  27. package/dist/index.d.ts +4 -4
  28. package/dist/index.js +4 -2
  29. package/dist/index.js.map +1 -1
  30. package/dist/init-5F7CIVLN.js +822 -0
  31. package/dist/init-5F7CIVLN.js.map +1 -0
  32. package/dist/instructions/index.d.ts +1 -1
  33. package/dist/runner/index.d.ts +2 -2
  34. package/dist/runner/index.js +2 -1
  35. package/dist/scaffold-web-chat-OQL5C2GK.js +7 -0
  36. package/dist/scaffold-web-chat-OQL5C2GK.js.map +1 -0
  37. package/dist/schedules/index.d.ts +1 -1
  38. package/dist/skills/index.d.ts +1 -1
  39. package/dist/tools/index.d.ts +1 -1
  40. package/dist/{types-BxOQTzx1.d.ts → types-DwxwdSa2.d.ts} +1 -1
  41. package/package.json +1 -1
  42. package/templates/default/.env.local +12 -0
  43. package/templates/web-chat/.env.local.example +4 -0
  44. package/templates/web-chat/README.md +51 -0
  45. package/templates/web-chat/app/api/chat/route.ts +43 -0
  46. package/templates/web-chat/app/globals.css +65 -0
  47. package/templates/web-chat/app/layout.tsx +15 -0
  48. package/templates/web-chat/app/page.tsx +9 -0
  49. package/templates/web-chat/components/chat.tsx +219 -0
  50. package/templates/web-chat/components/input-bar.tsx +181 -0
  51. package/templates/web-chat/components/message.tsx +123 -0
  52. package/templates/web-chat/components/tool-call.tsx +79 -0
  53. package/templates/web-chat/components/ui/button.tsx +46 -0
  54. package/templates/web-chat/components/ui/input.tsx +19 -0
  55. package/templates/web-chat/components.json +20 -0
  56. package/templates/web-chat/lib/stream.ts +41 -0
  57. package/templates/web-chat/lib/types.ts +62 -0
  58. package/templates/web-chat/lib/utils.ts +6 -0
  59. package/templates/web-chat/next.config.mjs +6 -0
  60. package/templates/web-chat/package.json +33 -0
  61. package/templates/web-chat/postcss.config.mjs +6 -0
  62. package/templates/web-chat/tailwind.config.ts +61 -0
  63. package/templates/web-chat/tsconfig.json +23 -0
  64. package/dist/chunk-2ZSKZYWK.js.map +0 -1
@@ -0,0 +1,181 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { ArrowUp, Paperclip, StopCircle, Trash2, X } from "lucide-react";
5
+ import { cn } from "@/lib/utils";
6
+
7
+ interface InputBarProps {
8
+ onSend(message: string, files?: File[]): void;
9
+ onStop(): void;
10
+ onClear?(): void;
11
+ streaming: boolean;
12
+ disabled?: boolean;
13
+ hasMessages?: boolean;
14
+ }
15
+
16
+ export function InputBar({
17
+ onSend,
18
+ onStop,
19
+ onClear,
20
+ streaming,
21
+ disabled,
22
+ hasMessages,
23
+ }: InputBarProps) {
24
+ const [value, setValue] = React.useState("");
25
+ const [files, setFiles] = React.useState<File[]>([]);
26
+ const textareaRef = React.useRef<HTMLTextAreaElement>(null);
27
+ const fileInputRef = React.useRef<HTMLInputElement>(null);
28
+
29
+ const submit = () => {
30
+ const trimmed = value.trim();
31
+ if ((trimmed.length === 0 && files.length === 0) || streaming || disabled) return;
32
+ onSend(trimmed, files.length > 0 ? files : undefined);
33
+ setValue("");
34
+ setFiles([]);
35
+ if (textareaRef.current) textareaRef.current.style.height = "auto";
36
+ if (fileInputRef.current) fileInputRef.current.value = "";
37
+ };
38
+
39
+ const onKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
40
+ if (event.key === "Enter" && !event.shiftKey) {
41
+ event.preventDefault();
42
+ submit();
43
+ }
44
+ };
45
+
46
+ const onChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
47
+ setValue(event.target.value);
48
+ const el = event.target;
49
+ el.style.height = "auto";
50
+ el.style.height = `${Math.min(el.scrollHeight, 160)}px`;
51
+ };
52
+
53
+ const onFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
54
+ const next = event.target.files;
55
+ if (next === null) return;
56
+ setFiles((prev) => [...prev, ...Array.from(next)]);
57
+ };
58
+
59
+ const removeFile = (index: number) => {
60
+ setFiles((prev) => prev.filter((_, i) => i !== index));
61
+ };
62
+
63
+ const openFilePicker = () => {
64
+ fileInputRef.current?.click();
65
+ };
66
+
67
+ return (
68
+ <div className="shrink-0 bg-transparent px-4 pt-4 pb-6">
69
+ <div className="mx-auto w-full max-w-3xl">
70
+ <div
71
+ className={cn(
72
+ "relative flex flex-col rounded-2xl border border-border/40 bg-muted/10 backdrop-blur-md p-3 transition-all",
73
+ "hover:border-border/60 hover:bg-muted/20",
74
+ "focus-within:border-primary/45 focus-within:ring-1 focus-within:ring-primary/20",
75
+ )}
76
+ >
77
+ {files.length > 0 && (
78
+ <div className="flex flex-wrap gap-1.5 mb-2">
79
+ {files.map((file, index) => (
80
+ <div
81
+ key={`${file.name}-${index}`}
82
+ className="flex items-center gap-1.5 px-2 py-1 rounded-lg bg-card/60 text-[11px] font-medium text-foreground border border-border/30"
83
+ >
84
+ <Paperclip className="h-3 w-3 text-muted-foreground/70" />
85
+ <span className="max-w-[160px] truncate">{file.name}</span>
86
+ <button
87
+ type="button"
88
+ onClick={() => removeFile(index)}
89
+ className="text-muted-foreground/50 hover:text-foreground transition-colors"
90
+ title="Remove attachment"
91
+ >
92
+ <X className="h-3 w-3" />
93
+ </button>
94
+ </div>
95
+ ))}
96
+ </div>
97
+ )}
98
+
99
+ <textarea
100
+ ref={textareaRef}
101
+ value={value}
102
+ onChange={onChange}
103
+ onKeyDown={onKeyDown}
104
+ rows={2}
105
+ placeholder={streaming ? "…" : "Ask a question..."}
106
+ disabled={disabled}
107
+ className={cn(
108
+ "max-h-40 min-h-[48px] w-full resize-none bg-transparent py-1.5 text-xs",
109
+ "placeholder:text-muted-foreground/50 focus:outline-none leading-relaxed",
110
+ "disabled:cursor-not-allowed disabled:opacity-50",
111
+ )}
112
+ />
113
+
114
+ <div className="flex items-center justify-between pt-2.5 mt-2 select-none">
115
+ <div className="flex items-center gap-1.5">
116
+ <input
117
+ ref={fileInputRef}
118
+ type="file"
119
+ multiple
120
+ onChange={onFileChange}
121
+ className="hidden"
122
+ />
123
+ <button
124
+ type="button"
125
+ onClick={openFilePicker}
126
+ disabled={streaming}
127
+ className={cn(
128
+ "h-8 w-8 flex items-center justify-center rounded-full text-muted-foreground/60 transition-colors",
129
+ "hover:bg-muted/30 hover:text-foreground",
130
+ "disabled:cursor-not-allowed disabled:opacity-40",
131
+ )}
132
+ aria-label="Attach files"
133
+ title="Attach files"
134
+ >
135
+ <Paperclip className="h-4 w-4" />
136
+ </button>
137
+ </div>
138
+
139
+ <div className="flex items-center gap-2 shrink-0">
140
+ {hasMessages && onClear && (
141
+ <button
142
+ type="button"
143
+ onClick={onClear}
144
+ className="h-8 w-8 flex items-center justify-center rounded-full text-muted-foreground/60 hover:bg-muted/30 hover:text-foreground transition-colors"
145
+ aria-label="New chat"
146
+ title="New chat"
147
+ >
148
+ <Trash2 className="h-4 w-4" />
149
+ </button>
150
+ )}
151
+ {streaming ? (
152
+ <button
153
+ type="button"
154
+ onClick={onStop}
155
+ className="h-8 w-8 flex items-center justify-center rounded-full bg-foreground text-background hover:bg-foreground/90 transition-colors"
156
+ aria-label="Stop generation"
157
+ >
158
+ <StopCircle className="h-3.5 w-3.5 fill-current" />
159
+ </button>
160
+ ) : (
161
+ <button
162
+ type="button"
163
+ onClick={submit}
164
+ disabled={(value.trim().length === 0 && files.length === 0) || disabled}
165
+ className={cn(
166
+ "h-8 w-8 flex items-center justify-center rounded-full bg-foreground text-background transition-colors",
167
+ "hover:bg-foreground/90",
168
+ "disabled:cursor-not-allowed disabled:opacity-40",
169
+ )}
170
+ aria-label="Send message"
171
+ >
172
+ <ArrowUp className="h-3.5 w-3.5" />
173
+ </button>
174
+ )}
175
+ </div>
176
+ </div>
177
+ </div>
178
+ </div>
179
+ </div>
180
+ );
181
+ }
@@ -0,0 +1,123 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import ReactMarkdown from "react-markdown";
5
+ import remarkGfm from "remark-gfm";
6
+ import { AlertTriangle, Copy, RotateCcw } from "lucide-react";
7
+ import { cn } from "@/lib/utils";
8
+ import type { UiMessage } from "@/lib/types";
9
+ import { ToolCall } from "@/components/tool-call";
10
+
11
+ interface MessageProps {
12
+ message: UiMessage;
13
+ isLast?: boolean;
14
+ onCopy?(text: string): void;
15
+ onRegenerate?(): void;
16
+ }
17
+
18
+ export function Message({ message, isLast, onCopy, onRegenerate }: MessageProps) {
19
+ const isUser = message.role === "user";
20
+
21
+ if (isUser) {
22
+ return (
23
+ <div className="flex flex-col px-4 items-end">
24
+ <div className="max-w-[85%] rounded-2xl rounded-tr-sm bg-primary px-3.5 py-2 text-primary-foreground shadow-sm">
25
+ <p className="text-xs font-medium leading-relaxed whitespace-pre-wrap">
26
+ {message.content}
27
+ </p>
28
+ </div>
29
+ </div>
30
+ );
31
+ }
32
+
33
+ return (
34
+ <div className="flex flex-col px-4 items-start">
35
+ <div className="w-full space-y-2">
36
+ {message.errored && (
37
+ <div className="mb-2 flex items-center gap-2 text-xs text-destructive">
38
+ <AlertTriangle className="h-3 w-3" />
39
+ <span>Error</span>
40
+ </div>
41
+ )}
42
+
43
+ {message.reasoning && (
44
+ <details className="text-[11px] text-muted-foreground/70 border border-border/20 rounded-lg px-2.5 py-1.5 bg-muted/10">
45
+ <summary className="cursor-pointer select-none font-medium">thinking</summary>
46
+ <div className="mt-1.5 whitespace-pre-wrap italic leading-relaxed">
47
+ {message.reasoning}
48
+ </div>
49
+ </details>
50
+ )}
51
+
52
+ {message.streaming && message.content.length === 0 && !message.toolCalls?.length ? (
53
+ <span className="inline-flex items-center gap-[3px]">
54
+ <span
55
+ className="w-1.5 h-1.5 rounded-full bg-muted-foreground/40 animate-bounce"
56
+ style={{ animationDelay: "0ms" }}
57
+ />
58
+ <span
59
+ className="w-1.5 h-1.5 rounded-full bg-muted-foreground/40 animate-bounce"
60
+ style={{ animationDelay: "150ms" }}
61
+ />
62
+ <span
63
+ className="w-1.5 h-1.5 rounded-full bg-muted-foreground/40 animate-bounce"
64
+ style={{ animationDelay: "300ms" }}
65
+ />
66
+ </span>
67
+ ) : (
68
+ message.content.length > 0 && (
69
+ <div
70
+ className={cn(
71
+ "prose prose-sm prose-zinc max-w-none dark:prose-invert",
72
+ "prose-p:my-2 prose-p:leading-relaxed prose-p:text-xs",
73
+ "prose-pre:my-2 prose-pre:text-xs",
74
+ "prose-code:text-xs prose-code:bg-muted prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-code:before:content-none prose-code:after:content-none",
75
+ message.errored && "text-destructive",
76
+ )}
77
+ >
78
+ <ReactMarkdown remarkPlugins={[remarkGfm]}>{message.content}</ReactMarkdown>
79
+ </div>
80
+ )
81
+ )}
82
+
83
+ {message.toolCalls && message.toolCalls.length > 0 && (
84
+ <div className="flex flex-col gap-1.5">
85
+ {message.toolCalls.map((call) => (
86
+ <ToolCall key={call.callId} call={call} />
87
+ ))}
88
+ </div>
89
+ )}
90
+
91
+ {!message.streaming && message.content.length > 0 && (
92
+ <div className="flex flex-wrap items-center gap-1.5 pt-1">
93
+ {message.latencyMs !== undefined && (
94
+ <span className="h-5 rounded border border-border/30 bg-muted/5 px-1.5 font-mono text-[9px] text-muted-foreground/70">
95
+ {message.latencyMs}ms
96
+ </span>
97
+ )}
98
+ {onCopy && (
99
+ <button
100
+ type="button"
101
+ onClick={() => onCopy(message.content)}
102
+ className="h-6 w-6 flex items-center justify-center rounded text-muted-foreground/60 hover:text-foreground hover:bg-muted/30 transition-colors"
103
+ title="Copy to clipboard"
104
+ >
105
+ <Copy className="h-3 w-3" />
106
+ </button>
107
+ )}
108
+ {isLast && onRegenerate && (
109
+ <button
110
+ type="button"
111
+ onClick={onRegenerate}
112
+ className="h-6 w-6 flex items-center justify-center rounded text-muted-foreground/60 hover:text-foreground hover:bg-muted/30 transition-colors"
113
+ title="Regenerate response"
114
+ >
115
+ <RotateCcw className="h-3 w-3" />
116
+ </button>
117
+ )}
118
+ </div>
119
+ )}
120
+ </div>
121
+ </div>
122
+ );
123
+ }
@@ -0,0 +1,79 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { CheckCircle2, Loader2, XCircle, HelpCircle } from "lucide-react";
5
+ import { cn } from "@/lib/utils";
6
+ import type { UiToolCall } from "@/lib/types";
7
+
8
+ function summarize(input: unknown, max = 60): string {
9
+ if (input === undefined || input === null) return "";
10
+ if (typeof input !== "object") return String(input).slice(0, max);
11
+ if (Array.isArray(input)) return `[${input.length}]`;
12
+ const entries = Object.entries(input as Record<string, unknown>);
13
+ if (entries.length === 0) return "";
14
+ const parts = entries.map(([key, value]) => {
15
+ if (typeof value === "string") return `${key}="${value.slice(0, 24)}"`;
16
+ if (typeof value === "object") {
17
+ return `${key}=${Array.isArray(value) ? `[${value.length}]` : "{…}"}`;
18
+ }
19
+ return `${key}=${String(value)}`;
20
+ });
21
+ const joined = parts.join(", ");
22
+ return joined.length > max ? `${joined.slice(0, max - 1)}…` : joined;
23
+ }
24
+
25
+ function resultText(call: UiToolCall): string | undefined {
26
+ if (call.status === "error") return call.errorMessage;
27
+ if (call.status === "approval") return call.errorMessage ?? "awaiting approval";
28
+ if (call.output === undefined || call.output === null) return undefined;
29
+ if (typeof call.output === "string") {
30
+ return call.output.split("\n").find((line) => line.trim().length > 0) ?? undefined;
31
+ }
32
+ if (typeof call.output !== "object") return String(call.output);
33
+ const record = call.output as Record<string, unknown>;
34
+ for (const key of ["result", "text", "message", "summary", "value", "output"]) {
35
+ const value = record[key];
36
+ if (value !== undefined && typeof value !== "object") return String(value);
37
+ }
38
+ return undefined;
39
+ }
40
+
41
+ export function ToolCall({ call }: { call: UiToolCall }) {
42
+ const args = summarize(call.input);
43
+ const result = resultText(call);
44
+ return (
45
+ <div
46
+ className={cn(
47
+ "flex flex-col gap-1 rounded-lg border border-border/30 bg-muted/10 px-3 py-2 text-xs",
48
+ )}
49
+ >
50
+ <div className="flex items-center gap-2">
51
+ <StatusGlyph status={call.status} />
52
+ <span className="font-mono font-medium text-[11px]">{call.name}</span>
53
+ {args.length > 0 && (
54
+ <span className="truncate font-mono text-[10px] text-muted-foreground/70">{args}</span>
55
+ )}
56
+ </div>
57
+ {result !== undefined && (
58
+ <div className="ml-6 truncate text-[10px] text-muted-foreground/70">
59
+ → <span className="font-mono">{result}</span>
60
+ </div>
61
+ )}
62
+ </div>
63
+ );
64
+ }
65
+
66
+ function StatusGlyph({ status }: { status: UiToolCall["status"] }) {
67
+ switch (status) {
68
+ case "running":
69
+ return <Loader2 className="h-3.5 w-3.5 animate-spin text-muted-foreground" />;
70
+ case "done":
71
+ return <CheckCircle2 className="h-3.5 w-3.5 text-emerald-500" />;
72
+ case "error":
73
+ return <XCircle className="h-3.5 w-3.5 text-destructive" />;
74
+ case "approval":
75
+ return <HelpCircle className="h-3.5 w-3.5 text-amber-500" />;
76
+ default:
77
+ return null;
78
+ }
79
+ }
@@ -0,0 +1,46 @@
1
+ import * as React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+ import { cn } from "@/lib/utils";
4
+
5
+ const buttonVariants = cva(
6
+ "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
7
+ {
8
+ variants: {
9
+ variant: {
10
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
11
+ destructive:
12
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
13
+ outline:
14
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
15
+ secondary:
16
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
17
+ ghost: "hover:bg-accent hover:text-accent-foreground",
18
+ link: "text-primary underline-offset-4 hover:underline",
19
+ },
20
+ size: {
21
+ default: "h-10 px-4 py-2",
22
+ sm: "h-9 rounded-md px-3",
23
+ lg: "h-11 rounded-md px-8",
24
+ icon: "h-10 w-10",
25
+ },
26
+ },
27
+ defaultVariants: { variant: "default", size: "default" },
28
+ },
29
+ );
30
+
31
+ export interface ButtonProps
32
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
33
+ VariantProps<typeof buttonVariants> {}
34
+
35
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
36
+ ({ className, variant, size, ...props }, ref) => (
37
+ <button
38
+ className={cn(buttonVariants({ variant, size, className }))}
39
+ ref={ref}
40
+ {...props}
41
+ />
42
+ ),
43
+ );
44
+ Button.displayName = "Button";
45
+
46
+ export { Button, buttonVariants };
@@ -0,0 +1,19 @@
1
+ import * as React from "react";
2
+ import { cn } from "@/lib/utils";
3
+
4
+ const Input = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>(
5
+ ({ className, type, ...props }, ref) => (
6
+ <input
7
+ type={type}
8
+ className={cn(
9
+ "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
10
+ className,
11
+ )}
12
+ ref={ref}
13
+ {...props}
14
+ />
15
+ ),
16
+ );
17
+ Input.displayName = "Input";
18
+
19
+ export { Input };
@@ -0,0 +1,20 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "default",
4
+ "rsc": true,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "tailwind.config.ts",
8
+ "css": "app/globals.css",
9
+ "baseColor": "zinc",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "aliases": {
14
+ "components": "@/components",
15
+ "utils": "@/lib/utils",
16
+ "ui": "@/components/ui",
17
+ "lib": "@/lib",
18
+ "hooks": "@/hooks"
19
+ }
20
+ }
@@ -0,0 +1,41 @@
1
+ import type { ArcieStreamEvent } from "./types";
2
+
3
+ /**
4
+ * Reads an NDJSON stream from `response.body` and yields decoded events.
5
+ * Tolerates partial lines split across chunks and skips malformed rows so
6
+ * one bad event doesn't stop the stream.
7
+ */
8
+ export async function* readArcieStream(
9
+ response: Response,
10
+ ): AsyncIterable<ArcieStreamEvent> {
11
+ if (response.body === null) return;
12
+ const reader = response.body.getReader();
13
+ const decoder = new TextDecoder();
14
+ let buffer = "";
15
+
16
+ while (true) {
17
+ const { done, value } = await reader.read();
18
+ if (done) break;
19
+ buffer += decoder.decode(value, { stream: true });
20
+ const lines = buffer.split("\n");
21
+ buffer = lines.pop() ?? "";
22
+ for (const line of lines) {
23
+ const trimmed = line.trim();
24
+ if (trimmed.length === 0) continue;
25
+ try {
26
+ yield JSON.parse(trimmed) as ArcieStreamEvent;
27
+ } catch {
28
+ // Ignore malformed lines; a partial chunk may still finish next round.
29
+ }
30
+ }
31
+ }
32
+
33
+ const tail = buffer.trim();
34
+ if (tail.length > 0) {
35
+ try {
36
+ yield JSON.parse(tail) as ArcieStreamEvent;
37
+ } catch {
38
+ /* ignore */
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,62 @@
1
+ export type ChatRole = "user" | "assistant";
2
+
3
+ export interface UiMessage {
4
+ id: string;
5
+ role: ChatRole;
6
+ content: string;
7
+ toolCalls?: UiToolCall[];
8
+ reasoning?: string;
9
+ streaming?: boolean;
10
+ errored?: boolean;
11
+ latencyMs?: number;
12
+ }
13
+
14
+ export interface UiToolCall {
15
+ callId: string;
16
+ name: string;
17
+ input?: unknown;
18
+ output?: unknown;
19
+ status: "running" | "done" | "error" | "approval";
20
+ errorMessage?: string;
21
+ }
22
+
23
+ export type ArcieStreamEvent =
24
+ | { type: "session.started"; data: { sessionId: string } }
25
+ | { type: "turn.started"; data: { turnId: string } }
26
+ | { type: "message.received"; data: { message: string; turnId: string } }
27
+ | {
28
+ type: "message.appended";
29
+ data: { delta: string; textSoFar: string; turnId: string; stepIndex: number };
30
+ }
31
+ | {
32
+ type: "message.completed";
33
+ data: { text: string | null; turnId: string; stepIndex: number };
34
+ }
35
+ | {
36
+ type: "reasoning.appended";
37
+ data: { delta: string; soFar: string; turnId: string; stepIndex: number };
38
+ }
39
+ | {
40
+ type: "reasoning.completed";
41
+ data: { text: string; turnId: string; stepIndex: number };
42
+ }
43
+ | {
44
+ type: "tool.started";
45
+ data: { name: string; input: unknown; callId: string; turnId: string };
46
+ }
47
+ | {
48
+ type: "tool.completed";
49
+ data: {
50
+ name: string;
51
+ output: unknown;
52
+ callId: string;
53
+ status: string;
54
+ error?: { code: string; message: string };
55
+ turnId: string;
56
+ };
57
+ }
58
+ | {
59
+ type: "step.failed" | "turn.failed" | "session.failed";
60
+ data: { code: string; message: string };
61
+ }
62
+ | { type: string; data: unknown };
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
@@ -0,0 +1,6 @@
1
+ /** @type {import('next').NextConfig} */
2
+ const nextConfig = {
3
+ devIndicators: false,
4
+ };
5
+
6
+ export default nextConfig;
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "web-chat",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev --port 3001",
7
+ "build": "next build",
8
+ "start": "next start --port 3001",
9
+ "lint": "next lint"
10
+ },
11
+ "dependencies": {
12
+ "class-variance-authority": "^0.7.0",
13
+ "clsx": "^2.1.1",
14
+ "lucide-react": "^0.475.0",
15
+ "next": "^15.1.0",
16
+ "react": "^19.0.0",
17
+ "react-dom": "^19.0.0",
18
+ "react-markdown": "^9.0.1",
19
+ "remark-gfm": "^4.0.0",
20
+ "tailwind-merge": "^2.5.5",
21
+ "tailwindcss-animate": "^1.0.7"
22
+ },
23
+ "devDependencies": {
24
+ "@tailwindcss/typography": "^0.5.15",
25
+ "@types/node": "^22.10.0",
26
+ "@types/react": "^19.0.0",
27
+ "@types/react-dom": "^19.0.0",
28
+ "autoprefixer": "^10.4.20",
29
+ "postcss": "^8.4.49",
30
+ "tailwindcss": "^3.4.17",
31
+ "typescript": "^5.7.2"
32
+ }
33
+ }
@@ -0,0 +1,6 @@
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ };