@grindxp/cli 0.1.6 → 0.1.8

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 (57) hide show
  1. package/dist/index.js +10 -7
  2. package/dist/web/client/assets/Copy.es-Bs4NgJu-.js +1 -0
  3. package/dist/web/client/assets/Sword.es-2Xm7T3t2.js +1 -0
  4. package/dist/web/client/assets/geist-cyrillic-wght-normal-CHSlOQsW.woff2 +0 -0
  5. package/dist/web/client/assets/geist-latin-ext-wght-normal-DMtmJ5ZE.woff2 +0 -0
  6. package/dist/web/client/assets/geist-latin-wght-normal-Dm3htQBi.woff2 +0 -0
  7. package/dist/web/client/assets/index-6XDcqRbL.js +42 -0
  8. package/dist/web/client/assets/index-BXM1N6tm.js +1 -0
  9. package/dist/web/client/assets/index-B_KMiE38.js +1 -0
  10. package/dist/web/client/assets/index-CGj2rOLm.js +1 -0
  11. package/dist/web/client/assets/index-CS5BuFbt.js +1 -0
  12. package/dist/web/client/assets/index-CYsASiu-.js +1 -0
  13. package/dist/web/client/assets/index-DAvwM0SX.js +1 -0
  14. package/dist/web/client/assets/index-DCBFp5DJ.js +1 -0
  15. package/dist/web/client/assets/index-DjKt1qNz.js +1 -0
  16. package/dist/web/client/assets/index-PIcFs1vr.js +1 -0
  17. package/dist/web/client/assets/instrument-serif-latin-400-italic-DKMiL14s.woff2 +0 -0
  18. package/dist/web/client/assets/instrument-serif-latin-400-italic-u__WvvIK.woff +0 -0
  19. package/dist/web/client/assets/instrument-serif-latin-400-normal-BVbkICAY.woff +0 -0
  20. package/dist/web/client/assets/instrument-serif-latin-400-normal-DnYpCC2O.woff2 +0 -0
  21. package/dist/web/client/assets/instrument-serif-latin-ext-400-italic-C9HzH3YL.woff2 +0 -0
  22. package/dist/web/client/assets/instrument-serif-latin-ext-400-italic-D7-lnxEk.woff +0 -0
  23. package/dist/web/client/assets/instrument-serif-latin-ext-400-normal-C2je3j2s.woff2 +0 -0
  24. package/dist/web/client/assets/instrument-serif-latin-ext-400-normal-CFCUzsTy.woff +0 -0
  25. package/dist/web/client/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  26. package/dist/web/client/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  27. package/dist/web/client/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  28. package/dist/web/client/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  29. package/dist/web/client/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  30. package/dist/web/client/assets/main-BI1EOhmt.js +18 -0
  31. package/dist/web/client/assets/styles-7TpWqjrh.css +1 -0
  32. package/dist/web/client/favicon.ico +0 -0
  33. package/dist/web/server/assets/_tanstack-start-manifest_v-B_rvI8DG.js +4 -0
  34. package/dist/web/server/assets/agent.functions-zpMkBrG3.js +19144 -0
  35. package/dist/web/server/assets/data.functions-9hSsMFx_.js +285 -0
  36. package/dist/web/server/assets/index-4SxmUYH6.js +14 -0
  37. package/dist/web/server/assets/index-BDL7hA7T.js +5924 -0
  38. package/dist/web/server/assets/index-BL8u2X7w.js +14 -0
  39. package/dist/web/server/assets/index-BRRsXrOi.js +14 -0
  40. package/dist/web/server/assets/index-BiD7uOOh.js +14 -0
  41. package/dist/web/server/assets/index-C09LXa7Z.js +4587 -0
  42. package/dist/web/server/assets/index-CJ_-TSqN.js +1426 -0
  43. package/dist/web/server/assets/index-D2fMUSdJ.js +477 -0
  44. package/dist/web/server/assets/index-D2yaimYL.js +14 -0
  45. package/dist/web/server/assets/index-D31yYLCV.js +2275 -0
  46. package/dist/web/server/assets/index-D3RUqTdb.js +14 -0
  47. package/dist/web/server/assets/index-D7z4dRpK.js +66 -0
  48. package/dist/web/server/assets/index-b30aLTKp.js +66 -0
  49. package/dist/web/server/assets/router-1koL9I3U.js +589 -0
  50. package/dist/web/server/assets/sessions-DOkG47Ex.js +403 -0
  51. package/dist/web/server/assets/start-HYkvq4Ni.js +4 -0
  52. package/dist/web/server/assets/token-W0NPKas8.js +86 -0
  53. package/dist/web/server/assets/token-util-1cB5CD6M.js +30 -0
  54. package/dist/web/server/assets/token-util-DA5xS0pj.js +451 -0
  55. package/dist/web/server/assets/vault.server-Ndu49yTf.js +19356 -0
  56. package/dist/web/server/server.js +4889 -0
  57. package/package.json +2 -4
@@ -0,0 +1,1426 @@
1
+ import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
+ import { a as createSsrRpc, c as cn, b as Route, l as loadConversationMessages, g as getConversations, d as appendPromptHistoryEntry, C as ChatMessagesSkeleton } from "./router-1koL9I3U.js";
3
+ import { useNavigate } from "@tanstack/react-router";
4
+ import { useState, useRef, useId, useEffect, useCallback, useEffectEvent } from "react";
5
+ import { c as createServerFn } from "../server.js";
6
+ import ReactMarkdown from "react-markdown";
7
+ import remarkGfm from "remark-gfm";
8
+ import { LightningIcon, CaretDownIcon, XIcon, HourglassIcon, FilePdfIcon, FileCodeIcon, FileArchiveIcon, FileTextIcon, FileIcon, TrashSimpleIcon, NoteIcon, PaperclipIcon, StopIcon, PaperPlaneTiltIcon, PlusIcon } from "@phosphor-icons/react";
9
+ import { motion, AnimatePresence } from "motion/react";
10
+ import { createPortal } from "react-dom";
11
+ import "@tanstack/react-query";
12
+ import "clsx";
13
+ import "tailwind-merge";
14
+ import "./vault.server-Ndu49yTf.js";
15
+ import "node:fs";
16
+ import "node:os";
17
+ import "node:path";
18
+ import "path";
19
+ import "fs";
20
+ import "child_process";
21
+ import "node:buffer";
22
+ import "events";
23
+ import "https";
24
+ import "http";
25
+ import "net";
26
+ import "tls";
27
+ import "crypto";
28
+ import "stream";
29
+ import "url";
30
+ import "zlib";
31
+ import "buffer";
32
+ import "node:crypto";
33
+ import "node:async_hooks";
34
+ import "node:stream";
35
+ import "@tanstack/react-router/ssr/server";
36
+ function validateSendInput(data) {
37
+ if (typeof data !== "object" || data === null) throw new Error("Invalid input");
38
+ const raw = data;
39
+ const message = raw["message"];
40
+ if (typeof message !== "string") throw new Error("Invalid input");
41
+ const conversationId = raw["conversationId"];
42
+ const rawAttachments = raw["attachments"];
43
+ const attachments = Array.isArray(rawAttachments) ? rawAttachments.filter((a) => typeof a === "object" && a !== null && typeof a["mime"] === "string" && typeof a["base64"] === "string") : void 0;
44
+ if (!message.trim() && (!attachments || attachments.length === 0)) {
45
+ throw new Error("message is required");
46
+ }
47
+ return {
48
+ message,
49
+ ...typeof conversationId === "string" ? {
50
+ conversationId
51
+ } : {},
52
+ ...attachments && attachments.length > 0 ? {
53
+ attachments
54
+ } : {}
55
+ };
56
+ }
57
+ const streamMessage = createServerFn({
58
+ method: "POST"
59
+ }).inputValidator(validateSendInput).handler(createSsrRpc("6856120823d177dc2bccc2b8c490fcf0742fdb07eb32f1567908fb7b7e9d60b8"));
60
+ const TOOL_LABELS = {
61
+ get_status: "Get status",
62
+ list_quests: "List quests",
63
+ create_quest: "Create quest",
64
+ complete_quest: "Complete quest",
65
+ abandon_quest: "Abandon quest",
66
+ start_timer: "Start timer",
67
+ stop_timer: "Stop timer",
68
+ get_timer: "Get timer",
69
+ analyze_patterns: "Analyze patterns",
70
+ suggest_quest: "Suggest quest",
71
+ list_forge_rules: "List forge rules",
72
+ list_forge_runs: "List forge runs",
73
+ create_forge_rule: "Create forge rule",
74
+ update_forge_rule: "Update forge rule",
75
+ run_forge_rule: "Run forge rule",
76
+ delete_forge_rule: "Delete forge rule",
77
+ fetch_url: "Fetch URL",
78
+ web_search: "Web search",
79
+ read_file: "Read file",
80
+ write_file: "Write file",
81
+ edit_file: "Edit file",
82
+ glob: "Glob",
83
+ grep: "Search",
84
+ bash: "Run command",
85
+ send_telegram_message: "Send Telegram",
86
+ get_integrations_status: "Check integrations",
87
+ get_calendar_events: "Get calendar events",
88
+ create_calendar_event: "Create calendar event",
89
+ update_calendar_event: "Update calendar event",
90
+ delete_calendar_event: "Delete calendar event",
91
+ get_emails: "Get emails",
92
+ send_email: "Send email"
93
+ };
94
+ function tryFormatJson(raw) {
95
+ try {
96
+ return JSON.stringify(JSON.parse(raw), null, 2);
97
+ } catch {
98
+ return raw;
99
+ }
100
+ }
101
+ function getDateKey(event) {
102
+ if (event.start.date) return event.start.date;
103
+ if (!event.start.dateTime) return "";
104
+ return new Intl.DateTimeFormat("en-CA", {
105
+ year: "numeric",
106
+ month: "2-digit",
107
+ day: "2-digit"
108
+ }).format(new Date(event.start.dateTime));
109
+ }
110
+ function formatDateLabel(dateKey) {
111
+ const parts = dateKey.split("-");
112
+ const date = new Date(Number(parts[0]), Number(parts[1]) - 1, Number(parts[2]));
113
+ return new Intl.DateTimeFormat("en-US", {
114
+ weekday: "short",
115
+ month: "short",
116
+ day: "numeric"
117
+ }).format(date).replace(",", "");
118
+ }
119
+ function formatTime(dt) {
120
+ if (!dt.dateTime) return "All day";
121
+ return new Intl.DateTimeFormat("en-US", {
122
+ hour: "numeric",
123
+ minute: "2-digit",
124
+ hour12: true
125
+ }).format(new Date(dt.dateTime));
126
+ }
127
+ function formatDuration(start, end) {
128
+ if (!start.dateTime || !end.dateTime) return "";
129
+ const ms = new Date(end.dateTime).getTime() - new Date(start.dateTime).getTime();
130
+ const totalMins = Math.round(ms / 6e4);
131
+ if (totalMins <= 0) return "";
132
+ const h = Math.floor(totalMins / 60);
133
+ const m = totalMins % 60;
134
+ if (h === 0) return `${m}m`;
135
+ if (m === 0) return `${h}h`;
136
+ return `${h}h ${m}m`;
137
+ }
138
+ function groupCalendarEvents(events) {
139
+ const map = /* @__PURE__ */ new Map();
140
+ for (const event of events) {
141
+ const key = getDateKey(event);
142
+ if (!key) continue;
143
+ const bucket = map.get(key);
144
+ if (bucket) bucket.push(event);
145
+ else map.set(key, [event]);
146
+ }
147
+ return Array.from(map.entries()).sort(([a], [b]) => a.localeCompare(b)).map(([dateKey, dayEvents]) => {
148
+ const sorted = [...dayEvents].sort((a, b) => {
149
+ const aAllDay = Boolean(a.start.date);
150
+ const bAllDay = Boolean(b.start.date);
151
+ if (aAllDay && !bAllDay) return -1;
152
+ if (!aAllDay && bAllDay) return 1;
153
+ if (a.start.dateTime && b.start.dateTime)
154
+ return new Date(a.start.dateTime).getTime() - new Date(b.start.dateTime).getTime();
155
+ return 0;
156
+ });
157
+ return {
158
+ dateKey,
159
+ label: formatDateLabel(dateKey),
160
+ events: sorted.map((e) => ({
161
+ id: e.id,
162
+ time: formatTime(e.start),
163
+ title: e.summary ?? "(no title)",
164
+ duration: Boolean(e.start.date) ? "" : formatDuration(e.start, e.end),
165
+ isAllDay: Boolean(e.start.date)
166
+ }))
167
+ };
168
+ });
169
+ }
170
+ function CalendarAgenda({ resultJson }) {
171
+ let parsed;
172
+ try {
173
+ parsed = JSON.parse(resultJson);
174
+ } catch {
175
+ return null;
176
+ }
177
+ if (!parsed.ok || !Array.isArray(parsed.events)) return null;
178
+ if (parsed.events.length === 0) {
179
+ return /* @__PURE__ */ jsx("p", { className: "py-2 text-xs text-muted-foreground italic", children: "No events in this period." });
180
+ }
181
+ const groups = groupCalendarEvents(parsed.events);
182
+ return /* @__PURE__ */ jsx("div", { className: "space-y-3 py-1", children: groups.map((group) => /* @__PURE__ */ jsxs("div", { children: [
183
+ /* @__PURE__ */ jsx("p", { className: "mb-1 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground", children: group.label }),
184
+ /* @__PURE__ */ jsx("div", { className: "space-y-0.5", children: group.events.map((event) => /* @__PURE__ */ jsxs("div", { className: "flex items-baseline gap-3 text-xs", children: [
185
+ /* @__PURE__ */ jsx(
186
+ "span",
187
+ {
188
+ className: cn(
189
+ "w-16 shrink-0 text-right font-mono tabular-nums",
190
+ event.isAllDay ? "text-muted-foreground/60 italic" : "text-muted-foreground"
191
+ ),
192
+ children: event.time
193
+ }
194
+ ),
195
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1 text-foreground/90", children: [
196
+ event.title,
197
+ event.duration && /* @__PURE__ */ jsxs("span", { className: "ml-1.5 text-muted-foreground", children: [
198
+ "· ",
199
+ event.duration
200
+ ] })
201
+ ] })
202
+ ] }, event.id)) })
203
+ ] }, group.dateKey)) });
204
+ }
205
+ function ToolCallCard({
206
+ toolName,
207
+ toolArgsJson,
208
+ toolResultJson,
209
+ status
210
+ }) {
211
+ const [isExpanded, setIsExpanded] = useState(false);
212
+ const label = TOOL_LABELS[toolName] ?? toolName;
213
+ const isPending = status === "pending";
214
+ const formattedArgs = tryFormatJson(toolArgsJson);
215
+ const formattedResult = toolResultJson != null ? tryFormatJson(toolResultJson) : null;
216
+ const isCalendarTool = toolName === "get_calendar_events";
217
+ return /* @__PURE__ */ jsxs(
218
+ "div",
219
+ {
220
+ className: cn(
221
+ "my-1 overflow-hidden rounded-r-lg border-l-2 bg-secondary/30",
222
+ isPending ? "border-grind-orange/60" : "border-grind-xp/40"
223
+ ),
224
+ children: [
225
+ /* @__PURE__ */ jsxs(
226
+ "button",
227
+ {
228
+ type: "button",
229
+ className: "flex w-full items-center gap-2 px-3 py-1.5 text-left transition-colors duration-100 hover:bg-secondary/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset",
230
+ onClick: () => setIsExpanded((prev) => !prev),
231
+ "aria-expanded": isExpanded,
232
+ "aria-controls": `tool-call-${toolName}-body`,
233
+ children: [
234
+ /* @__PURE__ */ jsx(
235
+ LightningIcon,
236
+ {
237
+ size: 12,
238
+ weight: "fill",
239
+ "aria-hidden": "true",
240
+ className: cn(
241
+ "shrink-0 transition-colors duration-150",
242
+ isPending ? "text-grind-orange animate-pulse" : "text-muted-foreground/60"
243
+ )
244
+ }
245
+ ),
246
+ /* @__PURE__ */ jsx(
247
+ "span",
248
+ {
249
+ className: cn(
250
+ "flex-1 font-mono text-xs",
251
+ isPending ? "text-foreground/70" : "text-muted-foreground"
252
+ ),
253
+ children: label
254
+ }
255
+ ),
256
+ isPending && /* @__PURE__ */ jsx("span", { className: "text-[10px] text-muted-foreground/60 tabular-nums", "aria-live": "polite", children: "running…" }),
257
+ !isPending && /* @__PURE__ */ jsx("span", { className: "text-[10px] text-grind-xp/80", children: "done" }),
258
+ /* @__PURE__ */ jsx(
259
+ CaretDownIcon,
260
+ {
261
+ size: 11,
262
+ "aria-hidden": "true",
263
+ className: cn(
264
+ "shrink-0 text-muted-foreground/50 transition-transform duration-150",
265
+ isExpanded ? "rotate-180" : ""
266
+ )
267
+ }
268
+ )
269
+ ]
270
+ }
271
+ ),
272
+ isExpanded && /* @__PURE__ */ jsxs(
273
+ "div",
274
+ {
275
+ id: `tool-call-${toolName}-body`,
276
+ className: "border-t border-border/30 px-3 py-2 space-y-2",
277
+ children: [
278
+ /* @__PURE__ */ jsxs("div", { children: [
279
+ /* @__PURE__ */ jsx("p", { className: "mb-1 text-[10px] font-medium uppercase tracking-wider text-muted-foreground/60", children: "Input" }),
280
+ /* @__PURE__ */ jsx("pre", { className: "overflow-x-auto rounded bg-background/60 p-2 font-mono text-[11px] text-foreground/70 leading-relaxed", children: formattedArgs })
281
+ ] }),
282
+ formattedResult != null && /* @__PURE__ */ jsxs("div", { children: [
283
+ /* @__PURE__ */ jsx("p", { className: "mb-1 text-[10px] font-medium uppercase tracking-wider text-muted-foreground/60", children: "Output" }),
284
+ isCalendarTool ? /* @__PURE__ */ jsx("div", { className: "rounded bg-background/60 px-3 py-2", children: /* @__PURE__ */ jsx(CalendarAgenda, { resultJson: toolResultJson }) }) : /* @__PURE__ */ jsx("pre", { className: "max-h-48 overflow-auto rounded bg-background/60 p-2 font-mono text-[11px] text-foreground/70 leading-relaxed", children: formattedResult })
285
+ ] })
286
+ ]
287
+ }
288
+ )
289
+ ]
290
+ }
291
+ );
292
+ }
293
+ const LIQUID_EASE = [0.22, 1, 0.36, 1];
294
+ const TRANSITION = { duration: 0.32, ease: LIQUID_EASE };
295
+ function ImageViewerOverlay({
296
+ src,
297
+ alt,
298
+ layoutId,
299
+ onClose
300
+ }) {
301
+ useEffect(() => {
302
+ const onKey = (e) => {
303
+ if (e.key === "Escape") onClose();
304
+ };
305
+ window.addEventListener("keydown", onKey);
306
+ return () => window.removeEventListener("keydown", onKey);
307
+ }, [onClose]);
308
+ return createPortal(
309
+ /* @__PURE__ */ jsx(
310
+ motion.div,
311
+ {
312
+ initial: { opacity: 0 },
313
+ animate: { opacity: 1 },
314
+ exit: { opacity: 0 },
315
+ transition: TRANSITION,
316
+ className: "fixed inset-0 z-50 flex cursor-zoom-out items-center justify-center overscroll-contain p-4 sm:p-8 bg-background/80",
317
+ onClick: onClose,
318
+ role: "dialog",
319
+ "aria-modal": "true",
320
+ "aria-label": `Fullscreen view of ${alt}`,
321
+ children: /* @__PURE__ */ jsx(
322
+ motion.span,
323
+ {
324
+ layoutId,
325
+ transition: TRANSITION,
326
+ className: "relative block max-h-full max-w-full overflow-hidden rounded-xl border border-border shadow-2xl",
327
+ children: /* @__PURE__ */ jsx(
328
+ "img",
329
+ {
330
+ src,
331
+ alt,
332
+ className: "max-h-[85vh] max-w-[90vw] w-auto object-contain block"
333
+ }
334
+ )
335
+ }
336
+ )
337
+ }
338
+ ),
339
+ document.body
340
+ );
341
+ }
342
+ function ImageViewer({
343
+ src,
344
+ alt,
345
+ children,
346
+ triggerClassName
347
+ }) {
348
+ const [isOpen, setIsOpen] = useState(false);
349
+ const triggerRef = useRef(null);
350
+ const layoutId = useId();
351
+ const close = () => {
352
+ setIsOpen(false);
353
+ triggerRef.current?.focus();
354
+ };
355
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
356
+ /* @__PURE__ */ jsx(
357
+ motion.span,
358
+ {
359
+ ref: triggerRef,
360
+ layoutId,
361
+ transition: TRANSITION,
362
+ role: "button",
363
+ tabIndex: 0,
364
+ "aria-label": `View ${alt} fullscreen`,
365
+ onClick: () => setIsOpen(true),
366
+ onKeyDown: (e) => (e.key === "Enter" || e.key === " ") && (e.preventDefault(), setIsOpen(true)),
367
+ className: cn(
368
+ "relative block cursor-zoom-in overflow-hidden",
369
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
370
+ isOpen && "invisible",
371
+ triggerClassName
372
+ ),
373
+ children
374
+ }
375
+ ),
376
+ /* @__PURE__ */ jsx(AnimatePresence, { onExitComplete: () => triggerRef.current?.focus(), children: isOpen && /* @__PURE__ */ jsx(ImageViewerOverlay, { src, alt, layoutId, onClose: close }) })
377
+ ] });
378
+ }
379
+ function TextViewerOverlay({
380
+ content,
381
+ filename,
382
+ onClose
383
+ }) {
384
+ useEffect(() => {
385
+ const onKey = (e) => {
386
+ if (e.key === "Escape") onClose();
387
+ };
388
+ window.addEventListener("keydown", onKey);
389
+ return () => window.removeEventListener("keydown", onKey);
390
+ }, [onClose]);
391
+ return createPortal(
392
+ /* @__PURE__ */ jsx(
393
+ motion.div,
394
+ {
395
+ initial: { opacity: 0 },
396
+ animate: { opacity: 1 },
397
+ exit: { opacity: 0 },
398
+ transition: TRANSITION,
399
+ className: "fixed inset-0 z-50 flex items-center justify-center overscroll-contain p-4 sm:p-8 bg-background/80",
400
+ onClick: onClose,
401
+ role: "dialog",
402
+ "aria-modal": "true",
403
+ "aria-label": `File viewer: ${filename}`,
404
+ children: /* @__PURE__ */ jsxs(
405
+ motion.div,
406
+ {
407
+ initial: { opacity: 0, scale: 0.96, y: 8 },
408
+ animate: { opacity: 1, scale: 1, y: 0 },
409
+ exit: { opacity: 0, scale: 0.96, y: 8 },
410
+ transition: TRANSITION,
411
+ className: "flex w-full max-w-3xl max-h-[85vh] flex-col overflow-hidden rounded-xl border border-border bg-card shadow-2xl",
412
+ onClick: (e) => e.stopPropagation(),
413
+ children: [
414
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 border-b border-border px-4 py-3 shrink-0", children: [
415
+ /* @__PURE__ */ jsx("span", { className: "truncate font-mono text-xs text-muted-foreground", children: filename }),
416
+ /* @__PURE__ */ jsx(
417
+ "button",
418
+ {
419
+ type: "button",
420
+ "aria-label": "Close file viewer",
421
+ onClick: onClose,
422
+ className: "flex shrink-0 items-center justify-center rounded-md p-1 text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
423
+ children: /* @__PURE__ */ jsx(XIcon, { size: 14, "aria-hidden": "true" })
424
+ }
425
+ )
426
+ ] }),
427
+ /* @__PURE__ */ jsx("pre", { className: "flex-1 overflow-auto px-4 py-3 font-mono text-xs text-foreground leading-relaxed whitespace-pre break-words", children: content })
428
+ ]
429
+ }
430
+ )
431
+ }
432
+ ),
433
+ document.body
434
+ );
435
+ }
436
+ function TextFileViewer({
437
+ base64,
438
+ filename,
439
+ children,
440
+ triggerClassName
441
+ }) {
442
+ const [isOpen, setIsOpen] = useState(false);
443
+ const triggerRef = useRef(null);
444
+ let decoded = "";
445
+ try {
446
+ decoded = atob(base64);
447
+ } catch {
448
+ decoded = "[Could not decode file content]";
449
+ }
450
+ const close = () => {
451
+ setIsOpen(false);
452
+ triggerRef.current?.focus();
453
+ };
454
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
455
+ /* @__PURE__ */ jsx(
456
+ "button",
457
+ {
458
+ ref: triggerRef,
459
+ type: "button",
460
+ "aria-label": `View ${filename}`,
461
+ onClick: () => setIsOpen(true),
462
+ className: triggerClassName,
463
+ children
464
+ }
465
+ ),
466
+ /* @__PURE__ */ jsx(AnimatePresence, { onExitComplete: () => triggerRef.current?.focus(), children: isOpen && /* @__PURE__ */ jsx(TextViewerOverlay, { content: decoded, filename, onClose: close }) })
467
+ ] });
468
+ }
469
+ function downloadAttachment(att) {
470
+ const link = document.createElement("a");
471
+ link.href = `data:${att.mime};base64,${att.base64}`;
472
+ link.download = att.filename ?? "attachment";
473
+ link.click();
474
+ }
475
+ function getAttachmentInteraction(mime) {
476
+ if (mime.startsWith("image/")) return "image";
477
+ if (mime.startsWith("text/") || mime === "application/json" || mime === "application/xml" || mime === "application/javascript" || mime === "application/x-javascript" || mime === "application/typescript" || mime.includes("+json") || mime.includes("+xml"))
478
+ return "text";
479
+ return "download";
480
+ }
481
+ function FileTypeIcon({ mime, size = 14 }) {
482
+ if (mime === "application/pdf") return /* @__PURE__ */ jsx(FilePdfIcon, { size, "aria-hidden": "true" });
483
+ if (mime.startsWith("text/") || mime.includes("json") || mime.includes("xml") || mime.includes("javascript"))
484
+ return /* @__PURE__ */ jsx(FileCodeIcon, { size, "aria-hidden": "true" });
485
+ if (mime.includes("zip") || mime.includes("tar") || mime.includes("archive") || mime.includes("compressed"))
486
+ return /* @__PURE__ */ jsx(FileArchiveIcon, { size, "aria-hidden": "true" });
487
+ if (mime.startsWith("text/")) return /* @__PURE__ */ jsx(FileTextIcon, { size, "aria-hidden": "true" });
488
+ return /* @__PURE__ */ jsx(FileIcon, { size, "aria-hidden": "true" });
489
+ }
490
+ function AttachmentChip({ attachment }) {
491
+ const filename = attachment.filename ?? "attachment";
492
+ const interaction = getAttachmentInteraction(attachment.mime);
493
+ const src = `data:${attachment.mime};base64,${attachment.base64}`;
494
+ if (interaction === "image") {
495
+ return /* @__PURE__ */ jsx(
496
+ ImageViewer,
497
+ {
498
+ src,
499
+ alt: filename,
500
+ triggerClassName: "h-16 w-16 flex-shrink-0 rounded-lg border border-border",
501
+ children: /* @__PURE__ */ jsx("img", { src, alt: filename, className: "h-full w-full object-cover" })
502
+ }
503
+ );
504
+ }
505
+ const chipClass = cn(
506
+ "flex items-center gap-1.5 rounded-lg border border-border bg-secondary/60 px-2.5 py-1.5 text-xs text-muted-foreground",
507
+ "transition-colors duration-150 hover:bg-secondary hover:text-foreground",
508
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
509
+ );
510
+ if (interaction === "text") {
511
+ return /* @__PURE__ */ jsxs(TextFileViewer, { base64: attachment.base64, filename, triggerClassName: chipClass, children: [
512
+ /* @__PURE__ */ jsx(FileTypeIcon, { mime: attachment.mime }),
513
+ /* @__PURE__ */ jsx("span", { className: "max-w-[120px] truncate", children: filename })
514
+ ] });
515
+ }
516
+ return /* @__PURE__ */ jsxs(
517
+ "button",
518
+ {
519
+ type: "button",
520
+ "aria-label": `Download ${filename}`,
521
+ onClick: () => downloadAttachment(attachment),
522
+ className: chipClass,
523
+ children: [
524
+ /* @__PURE__ */ jsx(FileTypeIcon, { mime: attachment.mime }),
525
+ /* @__PURE__ */ jsx("span", { className: "max-w-[120px] truncate", children: filename })
526
+ ]
527
+ }
528
+ );
529
+ }
530
+ function UserBubble({ message }) {
531
+ const hasAttachments = message.attachments && message.attachments.length > 0;
532
+ return /* @__PURE__ */ jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsxs("div", { className: cn("max-w-[75%]", message.queued && "opacity-50"), children: [
533
+ hasAttachments && /* @__PURE__ */ jsx("div", { className: "mb-1.5 flex flex-wrap justify-end gap-1.5", children: message.attachments.map((att, i) => /* @__PURE__ */ jsx(AttachmentChip, { attachment: att }, i)) }),
534
+ message.content && /* @__PURE__ */ jsx("div", { className: "rounded-2xl rounded-tr-sm bg-grind-elevated px-4 py-2.5 text-sm text-foreground leading-relaxed whitespace-pre-wrap break-words", children: message.content }),
535
+ message.queued && /* @__PURE__ */ jsxs("div", { className: "mt-1 flex items-center justify-end gap-1 pr-1", children: [
536
+ /* @__PURE__ */ jsx(HourglassIcon, { size: 10, "aria-hidden": "true", className: "text-muted-foreground" }),
537
+ /* @__PURE__ */ jsx("span", { className: "text-[10px] text-muted-foreground", children: "queued" })
538
+ ] })
539
+ ] }) });
540
+ }
541
+ function AssistantBubble({ message }) {
542
+ const hasContent = message.content.trim().length > 0;
543
+ const hasTools = message.toolCalls.length > 0;
544
+ return /* @__PURE__ */ jsx("div", { className: "flex justify-start", children: /* @__PURE__ */ jsxs("div", { className: "max-w-[85%] w-full space-y-1", children: [
545
+ hasTools && /* @__PURE__ */ jsx("div", { className: "space-y-1", children: message.toolCalls.map((tc) => /* @__PURE__ */ jsx(
546
+ ToolCallCard,
547
+ {
548
+ toolName: tc.toolName,
549
+ toolArgsJson: tc.toolArgsJson,
550
+ status: tc.status,
551
+ ...tc.toolResultJson !== void 0 ? { toolResultJson: tc.toolResultJson } : {}
552
+ },
553
+ tc.id
554
+ )) }),
555
+ (hasContent || message.isStreaming) && /* @__PURE__ */ jsxs("div", { className: "rounded-2xl rounded-tl-sm bg-card px-4 py-2.5 text-sm text-foreground leading-relaxed border border-border/50", children: [
556
+ /* @__PURE__ */ jsx(MarkdownContent, { content: message.content }),
557
+ message.isStreaming && /* @__PURE__ */ jsx(
558
+ "span",
559
+ {
560
+ "aria-label": "AI is typing",
561
+ className: "ml-0.5 inline-block h-[1em] w-[2px] bg-grind-orange motion-safe:animate-blink align-middle"
562
+ }
563
+ )
564
+ ] }),
565
+ !hasContent && !hasTools && message.isStreaming && /* @__PURE__ */ jsx("div", { className: "rounded-2xl rounded-tl-sm bg-card px-4 py-2.5 border border-border/50", children: /* @__PURE__ */ jsx("span", { "aria-label": "AI is thinking", className: "flex gap-1", children: [0, 1, 2].map((i) => /* @__PURE__ */ jsx(
566
+ "span",
567
+ {
568
+ "aria-hidden": "true",
569
+ className: "h-1.5 w-1.5 rounded-full bg-muted-foreground motion-safe:animate-pulse",
570
+ style: { animationDelay: `${i * 150}ms` }
571
+ },
572
+ i
573
+ )) }) })
574
+ ] }) });
575
+ }
576
+ function MarkdownContent({ content }) {
577
+ return /* @__PURE__ */ jsx(
578
+ ReactMarkdown,
579
+ {
580
+ remarkPlugins: [remarkGfm],
581
+ components: {
582
+ p: ({ children }) => /* @__PURE__ */ jsx("p", { className: "mb-3 last:mb-0 leading-relaxed", children }),
583
+ h1: ({ children }) => /* @__PURE__ */ jsx("h1", { className: "text-base font-bold mt-4 mb-2 first:mt-0", children }),
584
+ h2: ({ children }) => /* @__PURE__ */ jsx("h2", { className: "text-sm font-bold mt-3 mb-1.5 first:mt-0", children }),
585
+ h3: ({ children }) => /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold mt-2 mb-1 first:mt-0", children }),
586
+ strong: ({ children }) => /* @__PURE__ */ jsx("strong", { className: "font-semibold", children }),
587
+ em: ({ children }) => /* @__PURE__ */ jsx("em", { className: "italic", children }),
588
+ del: ({ children }) => /* @__PURE__ */ jsx("del", { className: "line-through text-muted-foreground", children }),
589
+ ul: ({ children }) => /* @__PURE__ */ jsx("ul", { className: "mb-3 last:mb-0 space-y-0.5 pl-4 list-disc", children }),
590
+ ol: ({ children }) => /* @__PURE__ */ jsx("ol", { className: "mb-3 last:mb-0 space-y-0.5 pl-4 list-decimal", children }),
591
+ li: ({ children }) => /* @__PURE__ */ jsx("li", { className: "leading-relaxed", children }),
592
+ blockquote: ({ children }) => /* @__PURE__ */ jsx("blockquote", { className: "border-l-2 border-border pl-3 my-2 text-muted-foreground italic", children }),
593
+ hr: () => /* @__PURE__ */ jsx("hr", { className: "my-3 border-border" }),
594
+ a: ({ href, children }) => /* @__PURE__ */ jsxs(
595
+ "a",
596
+ {
597
+ href,
598
+ target: "_blank",
599
+ rel: "noopener noreferrer",
600
+ className: "rounded-sm text-primary underline underline-offset-2 hover:opacity-80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
601
+ children: [
602
+ children,
603
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: " (opens in new tab)" })
604
+ ]
605
+ }
606
+ ),
607
+ code: ({ className, children, ...props }) => {
608
+ const isBlock = "data-language" in props || className?.startsWith("language-");
609
+ if (isBlock) {
610
+ return /* @__PURE__ */ jsx("code", { className: cn("block", className), children });
611
+ }
612
+ return /* @__PURE__ */ jsx("code", { className: "rounded px-1 py-0.5 bg-muted font-mono text-[0.8em]", children });
613
+ },
614
+ pre: ({ children }) => /* @__PURE__ */ jsx("pre", { className: "my-2 overflow-x-auto rounded-lg bg-muted px-3 py-2.5 text-xs font-mono leading-relaxed", children }),
615
+ table: ({ children }) => /* @__PURE__ */ jsx("div", { className: "my-2 overflow-x-auto", children: /* @__PURE__ */ jsx("table", { className: "w-full border-collapse text-xs", children }) }),
616
+ thead: ({ children }) => /* @__PURE__ */ jsx("thead", { className: "border-b border-border", children }),
617
+ tbody: ({ children }) => /* @__PURE__ */ jsx("tbody", { children }),
618
+ tr: ({ children }) => /* @__PURE__ */ jsx("tr", { className: "border-b border-border/50 last:border-0", children }),
619
+ th: ({ children }) => /* @__PURE__ */ jsx("th", { className: "px-2 py-1.5 text-left font-semibold text-foreground", children }),
620
+ td: ({ children }) => /* @__PURE__ */ jsx("td", { className: "px-2 py-1.5 text-muted-foreground", children })
621
+ },
622
+ children: content
623
+ }
624
+ );
625
+ }
626
+ function MessageBubble({ message }) {
627
+ if (message.role === "user") return /* @__PURE__ */ jsx(UserBubble, { message });
628
+ return /* @__PURE__ */ jsx(AssistantBubble, { message });
629
+ }
630
+ const PASTE_LINE_THRESHOLD = 5;
631
+ function ChatInput({
632
+ value,
633
+ onChange,
634
+ onSubmit,
635
+ onStop,
636
+ onCancelQueue,
637
+ isStreaming,
638
+ hasPendingMessage = false,
639
+ pendingMessagePreview,
640
+ disabled,
641
+ placeholder = "Message the Companion…",
642
+ initialHistory
643
+ }) {
644
+ const textareaRef = useRef(null);
645
+ const fileInputRef = useRef(null);
646
+ const [pasteBlobs, setPasteBlobs] = useState([]);
647
+ const [expandedBlobId, setExpandedBlobId] = useState(null);
648
+ const [attachments, setAttachments] = useState([]);
649
+ const historyRef = useRef(initialHistory ?? []);
650
+ const historyDraftRef = useRef("");
651
+ const [historyNavPos, setHistoryNavPos] = useState(null);
652
+ useEffect(() => {
653
+ const ta = textareaRef.current;
654
+ if (!ta) return;
655
+ ta.style.height = "auto";
656
+ ta.style.height = `${Math.min(ta.scrollHeight, 144)}px`;
657
+ }, [value]);
658
+ const hasContent = value.trim().length > 0 || pasteBlobs.length > 0 || attachments.length > 0;
659
+ const canQueue = isStreaming && !hasPendingMessage && hasContent;
660
+ const isDisabled = (disabled ?? false) || isStreaming && hasPendingMessage;
661
+ const canSend = !isStreaming && hasContent && !isDisabled;
662
+ const showStop = isStreaming && !canQueue;
663
+ function navigateHistory(direction) {
664
+ const history = historyRef.current;
665
+ if (direction === "up") {
666
+ if (history.length === 0) return;
667
+ const curIdx = historyNavPos !== null ? historyNavPos.i - 1 : -1;
668
+ const nextIdx = curIdx === -1 ? history.length - 1 : Math.max(0, curIdx - 1);
669
+ if (nextIdx === curIdx) return;
670
+ if (curIdx === -1) historyDraftRef.current = value;
671
+ setHistoryNavPos({ i: nextIdx + 1, n: history.length });
672
+ onChange(history[nextIdx]);
673
+ } else {
674
+ if (historyNavPos === null) return;
675
+ const nextIdx = historyNavPos.i;
676
+ if (nextIdx >= history.length) {
677
+ setHistoryNavPos(null);
678
+ onChange(historyDraftRef.current);
679
+ } else {
680
+ setHistoryNavPos({ i: nextIdx + 1, n: history.length });
681
+ onChange(history[nextIdx]);
682
+ }
683
+ }
684
+ }
685
+ function handleKeyDown(e) {
686
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
687
+ e.preventDefault();
688
+ if (canSend || canQueue) handleSubmit();
689
+ return;
690
+ }
691
+ if (e.key === "ArrowUp") {
692
+ const ta = textareaRef.current;
693
+ if (!ta) return;
694
+ const onFirstLine = !value.slice(0, ta.selectionStart).includes("\n");
695
+ if (onFirstLine && historyRef.current.length > 0) {
696
+ e.preventDefault();
697
+ navigateHistory("up");
698
+ }
699
+ return;
700
+ }
701
+ if (e.key === "ArrowDown" && historyNavPos !== null) {
702
+ e.preventDefault();
703
+ navigateHistory("down");
704
+ }
705
+ }
706
+ function handleChange(e) {
707
+ if (historyNavPos !== null) {
708
+ historyDraftRef.current = "";
709
+ setHistoryNavPos(null);
710
+ }
711
+ onChange(e.target.value);
712
+ }
713
+ function handleSubmit() {
714
+ const trimmed = value.trim();
715
+ if (!trimmed && pasteBlobs.length === 0 && attachments.length === 0) return;
716
+ const parts = [...pasteBlobs.map((b) => b.content), ...trimmed ? [trimmed] : []];
717
+ const combined = parts.join("\n\n");
718
+ if (trimmed) {
719
+ const h = historyRef.current;
720
+ if (h[h.length - 1] !== trimmed) {
721
+ historyRef.current = [...h, trimmed].slice(-100);
722
+ }
723
+ }
724
+ historyDraftRef.current = "";
725
+ setHistoryNavPos(null);
726
+ setPasteBlobs([]);
727
+ setExpandedBlobId(null);
728
+ const submittedAttachments = attachments;
729
+ setAttachments([]);
730
+ onChange("");
731
+ onSubmit(combined, submittedAttachments);
732
+ }
733
+ const handlePaste = useCallback(async (e) => {
734
+ const items = Array.from(e.clipboardData.items);
735
+ const imageItem = items.find((item) => item.type.startsWith("image/"));
736
+ if (imageItem) {
737
+ e.preventDefault();
738
+ const file = imageItem.getAsFile();
739
+ if (!file) return;
740
+ const reader = new FileReader();
741
+ reader.onload = () => {
742
+ const dataUrl = reader.result;
743
+ const commaIdx = dataUrl.indexOf(",");
744
+ const base64 = commaIdx !== -1 ? dataUrl.slice(commaIdx + 1) : dataUrl;
745
+ setAttachments((prev) => [
746
+ ...prev,
747
+ {
748
+ id: crypto.randomUUID(),
749
+ filename: file.name || "clipboard",
750
+ mime: file.type,
751
+ base64
752
+ }
753
+ ]);
754
+ };
755
+ reader.readAsDataURL(file);
756
+ return;
757
+ }
758
+ const text = e.clipboardData.getData("text");
759
+ if (!text) return;
760
+ const lines = text.split("\n");
761
+ if (lines.length < PASTE_LINE_THRESHOLD) return;
762
+ e.preventDefault();
763
+ setPasteBlobs((prev) => [
764
+ ...prev,
765
+ { id: crypto.randomUUID(), content: text, lineCount: lines.length }
766
+ ]);
767
+ }, []);
768
+ const processFiles = useCallback((files) => {
769
+ if (!files || files.length === 0) return;
770
+ Array.from(files).forEach((file) => {
771
+ const reader = new FileReader();
772
+ reader.onload = () => {
773
+ const dataUrl = reader.result;
774
+ const commaIdx = dataUrl.indexOf(",");
775
+ const base64 = commaIdx !== -1 ? dataUrl.slice(commaIdx + 1) : dataUrl;
776
+ setAttachments((prev) => [
777
+ ...prev,
778
+ {
779
+ id: crypto.randomUUID(),
780
+ filename: file.name,
781
+ mime: file.type || "application/octet-stream",
782
+ base64
783
+ }
784
+ ]);
785
+ };
786
+ reader.readAsDataURL(file);
787
+ });
788
+ }, []);
789
+ function handleFormSubmit(e) {
790
+ e.preventDefault();
791
+ if (showStop) {
792
+ onStop?.();
793
+ } else {
794
+ handleSubmit();
795
+ }
796
+ }
797
+ const hintText = historyNavPos ? `↑↓ history ${historyNavPos.i}/${historyNavPos.n} · ↓ newer · ⌘ Return to send` : historyRef.current.length > 0 ? "↑ history · ⌘ Return to send · Enter for newline" : "⌘ Return to send · Enter for newline";
798
+ const hasAttachmentRow = pasteBlobs.length > 0 || attachments.length > 0;
799
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5", children: [
800
+ hasPendingMessage && pendingMessagePreview && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 rounded-lg border border-border bg-card px-3 py-1.5", children: [
801
+ /* @__PURE__ */ jsx("span", { className: "text-[10px] text-muted-foreground shrink-0", children: "queued" }),
802
+ /* @__PURE__ */ jsx("span", { className: "flex-1 min-w-0 truncate text-xs text-foreground/70", children: pendingMessagePreview }),
803
+ /* @__PURE__ */ jsx(
804
+ "button",
805
+ {
806
+ type: "button",
807
+ onClick: onCancelQueue,
808
+ "aria-label": "Cancel queued message",
809
+ className: "shrink-0 rounded p-0.5 text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
810
+ children: /* @__PURE__ */ jsx(XIcon, { size: 12, "aria-hidden": "true" })
811
+ }
812
+ )
813
+ ] }),
814
+ /* @__PURE__ */ jsxs(
815
+ "form",
816
+ {
817
+ onSubmit: handleFormSubmit,
818
+ className: "flex flex-col rounded-xl border border-border bg-card has-[textarea:focus-visible]:border-ring",
819
+ children: [
820
+ hasAttachmentRow && /* @__PURE__ */ jsxs("div", { className: "px-3 pt-4 pb-1 flex flex-col gap-1.5", children: [
821
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-4 overflow-visible", children: [
822
+ attachments.map((att) => {
823
+ const interaction = getAttachmentInteraction(att.mime);
824
+ const src = `data:${att.mime};base64,${att.base64}`;
825
+ const removeBtn = /* @__PURE__ */ jsx(
826
+ "button",
827
+ {
828
+ type: "button",
829
+ "aria-label": `Remove attachment ${att.filename}`,
830
+ onClick: () => setAttachments((prev) => prev.filter((a) => a.id !== att.id)),
831
+ className: "absolute -top-2 -right-2 flex items-center justify-center w-7 h-7 rounded-md bg-background border border-border text-foreground/70 hover:text-foreground transition-colors [touch-action:manipulation] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
832
+ children: /* @__PURE__ */ jsx(TrashSimpleIcon, { size: 13, "aria-hidden": "true" })
833
+ }
834
+ );
835
+ if (interaction === "image") {
836
+ return /* @__PURE__ */ jsxs("div", { className: "relative flex-shrink-0 overflow-visible", children: [
837
+ /* @__PURE__ */ jsx(
838
+ ImageViewer,
839
+ {
840
+ src,
841
+ alt: att.filename,
842
+ triggerClassName: "block w-14 h-14 rounded-lg border border-border overflow-hidden",
843
+ children: /* @__PURE__ */ jsx("img", { src, alt: att.filename, className: "w-full h-full object-cover" })
844
+ }
845
+ ),
846
+ removeBtn
847
+ ] }, att.id);
848
+ }
849
+ const chipBase = "w-14 h-14 flex flex-col items-center justify-center gap-1 px-1 rounded-lg border border-border bg-secondary/60";
850
+ if (interaction === "text") {
851
+ return /* @__PURE__ */ jsxs("div", { className: "relative flex-shrink-0 overflow-visible", children: [
852
+ /* @__PURE__ */ jsxs(
853
+ TextFileViewer,
854
+ {
855
+ base64: att.base64,
856
+ filename: att.filename,
857
+ triggerClassName: cn(
858
+ chipBase,
859
+ "transition-colors hover:bg-secondary cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
860
+ ),
861
+ children: [
862
+ /* @__PURE__ */ jsx(FileIcon, { size: 20, className: "text-muted-foreground", "aria-hidden": "true" }),
863
+ /* @__PURE__ */ jsx("span", { className: "text-[9px] text-muted-foreground text-center leading-tight line-clamp-2 break-all", children: att.filename })
864
+ ]
865
+ }
866
+ ),
867
+ removeBtn
868
+ ] }, att.id);
869
+ }
870
+ return /* @__PURE__ */ jsxs("div", { className: "relative flex-shrink-0 overflow-visible", children: [
871
+ /* @__PURE__ */ jsxs(
872
+ "button",
873
+ {
874
+ type: "button",
875
+ "aria-label": `Download ${att.filename}`,
876
+ onClick: () => downloadAttachment(att),
877
+ className: cn(
878
+ chipBase,
879
+ "transition-colors hover:bg-secondary cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
880
+ ),
881
+ children: [
882
+ /* @__PURE__ */ jsx(FileIcon, { size: 20, className: "text-muted-foreground", "aria-hidden": "true" }),
883
+ /* @__PURE__ */ jsx("span", { className: "text-[9px] text-muted-foreground text-center leading-tight line-clamp-2 break-all", children: att.filename })
884
+ ]
885
+ }
886
+ ),
887
+ removeBtn
888
+ ] }, att.id);
889
+ }),
890
+ pasteBlobs.map((blob) => {
891
+ const isExpanded = expandedBlobId === blob.id;
892
+ const previewLine = blob.content.split("\n").find((l) => l.trim()) ?? "";
893
+ return /* @__PURE__ */ jsxs(
894
+ "div",
895
+ {
896
+ className: "relative flex-shrink-0 self-start min-w-[120px] max-w-[200px] overflow-visible",
897
+ children: [
898
+ /* @__PURE__ */ jsxs(
899
+ "button",
900
+ {
901
+ type: "button",
902
+ "aria-label": isExpanded ? "Collapse preview" : "Expand preview",
903
+ "aria-expanded": isExpanded,
904
+ onClick: () => setExpandedBlobId(isExpanded ? null : blob.id),
905
+ className: "flex w-full flex-col gap-0.5 rounded-lg border border-border bg-secondary/60 py-1.5 pl-2 pr-5 text-left transition-colors hover:bg-secondary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
906
+ children: [
907
+ /* @__PURE__ */ jsxs("div", { className: "flex w-full items-center gap-1 text-xs text-muted-foreground", children: [
908
+ /* @__PURE__ */ jsx(NoteIcon, { size: 11, "aria-hidden": "true", className: "shrink-0" }),
909
+ /* @__PURE__ */ jsxs("span", { className: "shrink-0", children: [
910
+ blob.lineCount,
911
+ " lines"
912
+ ] }),
913
+ /* @__PURE__ */ jsx(
914
+ CaretDownIcon,
915
+ {
916
+ size: 10,
917
+ "aria-hidden": "true",
918
+ className: cn(
919
+ "shrink-0 transition-transform duration-150",
920
+ isExpanded && "rotate-180"
921
+ )
922
+ }
923
+ )
924
+ ] }),
925
+ previewLine && /* @__PURE__ */ jsx("span", { className: "truncate text-[10px] leading-tight text-muted-foreground/70", children: previewLine })
926
+ ]
927
+ }
928
+ ),
929
+ /* @__PURE__ */ jsx(
930
+ "button",
931
+ {
932
+ type: "button",
933
+ "aria-label": "Remove pasted text",
934
+ onClick: () => {
935
+ if (expandedBlobId === blob.id) setExpandedBlobId(null);
936
+ setPasteBlobs((prev) => prev.filter((b) => b.id !== blob.id));
937
+ },
938
+ className: "absolute -top-2 -right-2 flex h-7 w-7 items-center justify-center rounded-md bg-background border border-border text-foreground/70 hover:text-foreground transition-colors [touch-action:manipulation] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
939
+ children: /* @__PURE__ */ jsx(TrashSimpleIcon, { size: 13, "aria-hidden": "true" })
940
+ }
941
+ )
942
+ ]
943
+ },
944
+ blob.id
945
+ );
946
+ })
947
+ ] }),
948
+ expandedBlobId && (() => {
949
+ const blob = pasteBlobs.find((b) => b.id === expandedBlobId);
950
+ return blob ? /* @__PURE__ */ jsx("pre", { className: "max-h-48 overflow-y-auto rounded-md border border-border bg-background/60 px-3 py-2 font-mono text-xs text-foreground whitespace-pre-wrap break-words", children: blob.content }) : null;
951
+ })()
952
+ ] }),
953
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 p-2", children: [
954
+ /* @__PURE__ */ jsx(
955
+ "input",
956
+ {
957
+ ref: fileInputRef,
958
+ type: "file",
959
+ multiple: true,
960
+ className: "sr-only",
961
+ tabIndex: -1,
962
+ "aria-hidden": "true",
963
+ onChange: (e) => {
964
+ processFiles(e.target.files);
965
+ e.target.value = "";
966
+ }
967
+ }
968
+ ),
969
+ /* @__PURE__ */ jsx(
970
+ "button",
971
+ {
972
+ type: "button",
973
+ "aria-label": "Attach files",
974
+ disabled: isDisabled,
975
+ onClick: () => fileInputRef.current?.click(),
976
+ className: cn(
977
+ "flex h-8 w-8 shrink-0 items-center justify-center rounded-lg transition-[background-color,color] duration-150",
978
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
979
+ "text-muted-foreground hover:bg-secondary hover:text-foreground",
980
+ "disabled:cursor-not-allowed disabled:opacity-40"
981
+ ),
982
+ children: /* @__PURE__ */ jsx(PaperclipIcon, { size: 15, "aria-hidden": "true" })
983
+ }
984
+ ),
985
+ /* @__PURE__ */ jsx("label", { htmlFor: "chat-message-input", className: "sr-only", children: "Message the Companion" }),
986
+ /* @__PURE__ */ jsx(
987
+ "textarea",
988
+ {
989
+ id: "chat-message-input",
990
+ ref: textareaRef,
991
+ name: "message",
992
+ rows: 1,
993
+ value,
994
+ onChange: handleChange,
995
+ onKeyDown: handleKeyDown,
996
+ onPaste: handlePaste,
997
+ disabled: isDisabled,
998
+ placeholder,
999
+ autoComplete: "off",
1000
+ spellCheck: true,
1001
+ "aria-label": "Message the Companion",
1002
+ "aria-multiline": "true",
1003
+ className: cn(
1004
+ "min-h-[36px] flex-1 resize-none bg-transparent px-2 py-1.5 text-sm text-foreground placeholder:text-muted-foreground",
1005
+ "focus-visible:outline-none",
1006
+ "disabled:cursor-not-allowed disabled:opacity-50"
1007
+ ),
1008
+ style: { maxHeight: "144px", overflowY: "auto" }
1009
+ }
1010
+ ),
1011
+ /* @__PURE__ */ jsx(
1012
+ "button",
1013
+ {
1014
+ type: "submit",
1015
+ "aria-label": showStop ? "Stop generating" : canQueue ? "Queue message" : "Send message",
1016
+ disabled: !showStop && !canSend && !canQueue,
1017
+ className: cn(
1018
+ "flex h-8 w-8 shrink-0 items-center justify-center rounded-lg transition-[background-color,transform] duration-150",
1019
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
1020
+ showStop ? "bg-destructive/80 text-white hover:bg-destructive" : canSend || canQueue ? "bg-grind-orange text-white hover:bg-grind-orange/90 active:scale-95" : "cursor-not-allowed bg-secondary text-muted-foreground"
1021
+ ),
1022
+ children: showStop ? /* @__PURE__ */ jsx(StopIcon, { size: 14, weight: "fill", "aria-hidden": "true" }) : /* @__PURE__ */ jsx(PaperPlaneTiltIcon, { size: 14, weight: "fill", "aria-hidden": "true" })
1023
+ }
1024
+ )
1025
+ ] })
1026
+ ]
1027
+ }
1028
+ ),
1029
+ /* @__PURE__ */ jsx("p", { "aria-hidden": "true", className: "text-center text-[10px] text-muted-foreground", children: hintText }),
1030
+ historyNavPos !== null && /* @__PURE__ */ jsxs("span", { className: "sr-only", "aria-live": "polite", "aria-atomic": "true", children: [
1031
+ "History entry ",
1032
+ historyNavPos.i,
1033
+ " of ",
1034
+ historyNavPos.n
1035
+ ] })
1036
+ ] });
1037
+ }
1038
+ function formatConvTime(ts) {
1039
+ const now = Date.now();
1040
+ const diff = now - ts;
1041
+ const days = Math.floor(diff / 864e5);
1042
+ if (days === 0) return "Today";
1043
+ if (days === 1) return "Yesterday";
1044
+ if (days < 7) return `${days}d ago`;
1045
+ return new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric" }).format(new Date(ts));
1046
+ }
1047
+ function getConvTitle(conv) {
1048
+ if (conv.title) return conv.title;
1049
+ return `Chat ${new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }).format(new Date(conv.createdAt))}`;
1050
+ }
1051
+ function ConversationSidebar({
1052
+ conversations,
1053
+ activeId,
1054
+ onSelect,
1055
+ onNew
1056
+ }) {
1057
+ return /* @__PURE__ */ jsxs(
1058
+ "aside",
1059
+ {
1060
+ className: "flex w-52 shrink-0 flex-col border-r border-border bg-sidebar",
1061
+ "aria-label": "Conversations",
1062
+ children: [
1063
+ /* @__PURE__ */ jsx("div", { className: "flex h-14 items-center justify-end px-3", children: /* @__PURE__ */ jsxs(
1064
+ "button",
1065
+ {
1066
+ type: "button",
1067
+ onClick: onNew,
1068
+ className: "flex items-center gap-2 rounded-md px-3 py-2 text-sm text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sidebar-ring",
1069
+ "aria-label": "Start new conversation",
1070
+ children: [
1071
+ /* @__PURE__ */ jsx(PlusIcon, { size: 15, "aria-hidden": "true" }),
1072
+ "New Chat"
1073
+ ]
1074
+ }
1075
+ ) }),
1076
+ /* @__PURE__ */ jsx("nav", { className: "flex-1 overflow-y-auto px-2 pt-2 pb-3", "aria-label": "Conversation history", children: conversations.length === 0 ? /* @__PURE__ */ jsx("p", { className: "px-3 py-4 text-center text-xs text-muted-foreground", children: "No conversations yet" }) : /* @__PURE__ */ jsx("ul", { role: "list", className: "space-y-0.5", children: conversations.map((conv) => {
1077
+ const isActive = conv.id === activeId;
1078
+ const title = getConvTitle(conv);
1079
+ const timeLabel = formatConvTime(conv.updatedAt);
1080
+ return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
1081
+ "button",
1082
+ {
1083
+ type: "button",
1084
+ onClick: () => onSelect(conv.id),
1085
+ className: cn(
1086
+ "flex w-full flex-col gap-0.5 rounded-md px-3 py-2 text-left",
1087
+ isActive ? "bg-sidebar-accent text-sidebar-primary [&]:hover:bg-sidebar-accent [&]:hover:text-sidebar-primary" : "text-sidebar-foreground/70 hover:bg-sidebar-accent/60 hover:text-sidebar-foreground",
1088
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sidebar-ring"
1089
+ ),
1090
+ "aria-current": isActive ? "page" : void 0,
1091
+ "aria-label": `${title}, ${timeLabel}`,
1092
+ children: [
1093
+ /* @__PURE__ */ jsx("span", { className: "truncate text-xs font-medium leading-tight", children: title }),
1094
+ /* @__PURE__ */ jsx("span", { className: "text-[10px] text-muted-foreground", children: timeLabel })
1095
+ ]
1096
+ }
1097
+ ) }, conv.id);
1098
+ }) }) })
1099
+ ]
1100
+ }
1101
+ );
1102
+ }
1103
+ const SUGGESTED_PROMPTS = ["What quests do I have active right now?", "Log a 45-minute chest workout", "What should I focus on today?", "Create a new daily quest for morning reading"];
1104
+ function EmptyState({
1105
+ onPrompt,
1106
+ companion
1107
+ }) {
1108
+ const name = companion.name ?? "Companion";
1109
+ const emoji = companion.emoji ?? "⚡";
1110
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-col items-center justify-center gap-6 p-8 text-center", children: [
1111
+ /* @__PURE__ */ jsx("div", { className: "flex h-16 w-16 items-center justify-center rounded-full border border-border bg-card", children: /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "text-2xl", children: emoji }) }),
1112
+ /* @__PURE__ */ jsxs("div", { children: [
1113
+ /* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold text-foreground", children: name }),
1114
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: "Your AI grind partner. Ask anything about your quests, skills, or goals." })
1115
+ ] }),
1116
+ /* @__PURE__ */ jsx("ul", { className: "flex flex-col gap-2 w-full max-w-sm", "aria-label": "Suggested prompts", children: SUGGESTED_PROMPTS.map((prompt) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("button", { type: "button", onClick: () => onPrompt(prompt), className: "w-full rounded-lg border border-border bg-card px-4 py-2.5 text-left text-sm text-foreground/80 [touch-action:manipulation] transition-colors duration-150 hover:border-ring/40 hover:bg-secondary hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", children: prompt }) }, prompt)) })
1117
+ ] });
1118
+ }
1119
+ function ChatPage() {
1120
+ const {
1121
+ conversations: initialConversations,
1122
+ companion,
1123
+ promptHistory
1124
+ } = Route.useLoaderData();
1125
+ const search = Route.useSearch();
1126
+ const navigate = useNavigate({
1127
+ from: "/app/chat/"
1128
+ });
1129
+ const [conversations, setConversations] = useState(initialConversations);
1130
+ const [activeConvId, setActiveConvId] = useState(search.c);
1131
+ const [messages, setMessages] = useState([]);
1132
+ const [inputValue, setInputValue] = useState("");
1133
+ const [isStreaming, setIsStreaming] = useState(false);
1134
+ const [isLoadingHistory, setIsLoadingHistory] = useState(() => !!search.c);
1135
+ const [pendingMessage, setPendingMessage] = useState(null);
1136
+ const messagesContainerRef = useRef(null);
1137
+ const stoppedRef = useRef(false);
1138
+ const pendingFiredRef = useRef(false);
1139
+ const scrollToBottom = useCallback((behavior) => {
1140
+ const el = messagesContainerRef.current;
1141
+ if (!el) return;
1142
+ const resolved = behavior ?? (window.matchMedia("(prefers-reduced-motion: reduce)").matches ? "instant" : "smooth");
1143
+ el.scrollTo({
1144
+ top: el.scrollHeight,
1145
+ behavior: resolved
1146
+ });
1147
+ }, []);
1148
+ const onConversationChanged = useEffectEvent((convId) => {
1149
+ if (!convId) {
1150
+ setMessages([]);
1151
+ setActiveConvId(void 0);
1152
+ return;
1153
+ }
1154
+ if (isStreaming) return;
1155
+ setActiveConvId(convId);
1156
+ setIsLoadingHistory(true);
1157
+ setMessages([]);
1158
+ loadConversationMessages({
1159
+ data: {
1160
+ conversationId: convId
1161
+ }
1162
+ }).then((stored) => {
1163
+ const loaded = stored.map((m) => {
1164
+ if (m.role === "user") {
1165
+ return {
1166
+ role: "user",
1167
+ id: m.id,
1168
+ content: m.content,
1169
+ ...m.attachments && m.attachments.length > 0 ? {
1170
+ attachments: m.attachments
1171
+ } : {}
1172
+ };
1173
+ }
1174
+ return {
1175
+ role: "assistant",
1176
+ id: m.id,
1177
+ content: m.content,
1178
+ toolCalls: [],
1179
+ isStreaming: false
1180
+ };
1181
+ });
1182
+ setMessages(loaded);
1183
+ requestAnimationFrame(() => scrollToBottom("instant"));
1184
+ }).catch(() => {
1185
+ setMessages([]);
1186
+ }).finally(() => {
1187
+ setIsLoadingHistory(false);
1188
+ });
1189
+ });
1190
+ useEffect(() => {
1191
+ onConversationChanged(search.c);
1192
+ }, [search.c]);
1193
+ const handleSelectConversation = useCallback((id) => {
1194
+ navigate({
1195
+ search: {
1196
+ c: id
1197
+ }
1198
+ });
1199
+ }, [navigate]);
1200
+ const handleNewConversation = useCallback(() => {
1201
+ navigate({
1202
+ search: {
1203
+ c: void 0
1204
+ }
1205
+ });
1206
+ }, [navigate]);
1207
+ const executeSend = useCallback(
1208
+ // eslint-disable-next-line @typescript-eslint/no-shadow
1209
+ async (msg, attachments = []) => {
1210
+ stoppedRef.current = false;
1211
+ const userMsgId = crypto.randomUUID();
1212
+ const assistantMsgId = crypto.randomUUID();
1213
+ const userAttachments = attachments.length > 0 ? attachments.map((a) => ({
1214
+ mime: a.mime,
1215
+ base64: a.base64,
1216
+ filename: a.filename
1217
+ })) : void 0;
1218
+ setMessages((prev) => [...prev, {
1219
+ role: "user",
1220
+ id: userMsgId,
1221
+ content: msg,
1222
+ ...userAttachments ? {
1223
+ attachments: userAttachments
1224
+ } : {}
1225
+ }, {
1226
+ role: "assistant",
1227
+ id: assistantMsgId,
1228
+ content: "",
1229
+ toolCalls: [],
1230
+ isStreaming: true
1231
+ }]);
1232
+ requestAnimationFrame(() => scrollToBottom());
1233
+ setIsStreaming(true);
1234
+ try {
1235
+ const sendData = {
1236
+ message: msg
1237
+ };
1238
+ if (activeConvId) sendData.conversationId = activeConvId;
1239
+ if (attachments.length > 0) {
1240
+ sendData.attachments = attachments.map((a) => ({
1241
+ mime: a.mime,
1242
+ base64: a.base64
1243
+ }));
1244
+ }
1245
+ const stream = await streamMessage({
1246
+ data: sendData
1247
+ });
1248
+ for await (const event of stream) {
1249
+ if (stoppedRef.current) break;
1250
+ if (event.type === "conversation-id") {
1251
+ setActiveConvId(event.conversationId);
1252
+ navigate({
1253
+ search: {
1254
+ c: event.conversationId
1255
+ },
1256
+ replace: true
1257
+ });
1258
+ } else if (event.type === "text-delta") {
1259
+ setMessages((prev) => prev.map((m) => m.id === assistantMsgId && m.role === "assistant" ? {
1260
+ ...m,
1261
+ content: m.content + event.text
1262
+ } : m));
1263
+ } else if (event.type === "tool-call") {
1264
+ const newTc = {
1265
+ id: crypto.randomUUID(),
1266
+ toolName: event.toolName,
1267
+ toolArgsJson: event.toolArgsJson,
1268
+ status: "pending"
1269
+ };
1270
+ setMessages((prev) => prev.map((m) => m.id === assistantMsgId && m.role === "assistant" ? {
1271
+ ...m,
1272
+ toolCalls: [...m.toolCalls, newTc]
1273
+ } : m));
1274
+ } else if (event.type === "tool-result") {
1275
+ const resultToolName = event.toolName;
1276
+ const resultJson = event.toolResultJson;
1277
+ setMessages((prev) => prev.map((m) => {
1278
+ if (m.id !== assistantMsgId || m.role !== "assistant") return m;
1279
+ let lastPendingIdx = -1;
1280
+ for (let i = m.toolCalls.length - 1; i >= 0; i--) {
1281
+ const tc = m.toolCalls[i];
1282
+ if (tc && tc.toolName === resultToolName && tc.status === "pending") {
1283
+ lastPendingIdx = i;
1284
+ break;
1285
+ }
1286
+ }
1287
+ if (lastPendingIdx === -1) return m;
1288
+ const existingTc = m.toolCalls[lastPendingIdx];
1289
+ if (!existingTc) return m;
1290
+ const updatedTcs = [...m.toolCalls];
1291
+ updatedTcs[lastPendingIdx] = {
1292
+ ...existingTc,
1293
+ toolResultJson: resultJson,
1294
+ status: "complete"
1295
+ };
1296
+ return {
1297
+ ...m,
1298
+ toolCalls: updatedTcs
1299
+ };
1300
+ }));
1301
+ } else if (event.type === "error") {
1302
+ setMessages((prev) => prev.map((m) => m.id === assistantMsgId && m.role === "assistant" ? {
1303
+ ...m,
1304
+ content: `⚠️ ${event.error}`,
1305
+ isStreaming: false
1306
+ } : m));
1307
+ setIsStreaming(false);
1308
+ return;
1309
+ } else if (event.type === "done") {
1310
+ setMessages((prev) => prev.map((m) => m.id === assistantMsgId && m.role === "assistant" ? {
1311
+ ...m,
1312
+ isStreaming: false
1313
+ } : m));
1314
+ setIsStreaming(false);
1315
+ }
1316
+ }
1317
+ } catch (err) {
1318
+ setMessages((prev) => prev.map((m) => m.role === "assistant" && m.isStreaming ? {
1319
+ ...m,
1320
+ content: `⚠️ ${String(err)}`,
1321
+ isStreaming: false
1322
+ } : m));
1323
+ setIsStreaming(false);
1324
+ }
1325
+ try {
1326
+ const updated = await getConversations();
1327
+ setConversations(updated);
1328
+ } catch {
1329
+ }
1330
+ },
1331
+ [activeConvId, navigate, scrollToBottom]
1332
+ );
1333
+ const triggerExecuteSend = useEffectEvent((msg) => {
1334
+ void executeSend(msg, []);
1335
+ });
1336
+ useEffect(() => {
1337
+ if (!isStreaming && pendingMessage) {
1338
+ if (pendingFiredRef.current) {
1339
+ pendingFiredRef.current = false;
1340
+ return;
1341
+ }
1342
+ pendingFiredRef.current = true;
1343
+ setPendingMessage(null);
1344
+ setMessages((prev) => prev.filter((m) => !(m.role === "user" && m.queued)));
1345
+ triggerExecuteSend(pendingMessage);
1346
+ }
1347
+ }, [isStreaming, pendingMessage]);
1348
+ const queueOrSend = useCallback((msg, attachments = []) => {
1349
+ if (!msg.trim() && attachments.length === 0) return;
1350
+ if (isStreaming) {
1351
+ if (pendingMessage !== null) return;
1352
+ setPendingMessage(msg);
1353
+ const queuedAttachments = attachments.length > 0 ? attachments.map((a) => ({
1354
+ mime: a.mime,
1355
+ base64: a.base64,
1356
+ filename: a.filename
1357
+ })) : void 0;
1358
+ setMessages((prev) => [...prev, {
1359
+ role: "user",
1360
+ id: crypto.randomUUID(),
1361
+ content: msg,
1362
+ queued: true,
1363
+ ...queuedAttachments ? {
1364
+ attachments: queuedAttachments
1365
+ } : {}
1366
+ }]);
1367
+ requestAnimationFrame(() => scrollToBottom());
1368
+ return;
1369
+ }
1370
+ void executeSend(msg, attachments);
1371
+ }, [isStreaming, pendingMessage, executeSend, scrollToBottom]);
1372
+ const handleSend = useCallback((text, attachments = []) => {
1373
+ const msg = text.trim();
1374
+ if (!msg && attachments.length === 0) return;
1375
+ if (msg) void appendPromptHistoryEntry({
1376
+ data: {
1377
+ content: msg
1378
+ }
1379
+ }).catch(() => {
1380
+ });
1381
+ queueOrSend(msg, attachments);
1382
+ }, [queueOrSend]);
1383
+ const handleStop = useCallback(() => {
1384
+ stoppedRef.current = true;
1385
+ setIsStreaming(false);
1386
+ setPendingMessage(null);
1387
+ pendingFiredRef.current = false;
1388
+ setMessages((prev) => prev.map((m) => m.role === "assistant" && m.isStreaming ? {
1389
+ ...m,
1390
+ isStreaming: false
1391
+ } : m).filter((m) => !(m.role === "user" && m.queued)));
1392
+ }, []);
1393
+ const handleCancelQueue = useCallback(() => {
1394
+ setPendingMessage(null);
1395
+ pendingFiredRef.current = false;
1396
+ setMessages((prev) => prev.filter((m) => !(m.role === "user" && m.queued)));
1397
+ }, []);
1398
+ const handlePromptClick = useCallback((prompt) => {
1399
+ queueOrSend(prompt);
1400
+ }, [queueOrSend]);
1401
+ const showEmptyState = messages.length === 0 && !isLoadingHistory;
1402
+ return /* @__PURE__ */ jsxs("div", { className: "flex h-full overflow-hidden", children: [
1403
+ /* @__PURE__ */ jsx("div", { className: "hidden md:flex", children: /* @__PURE__ */ jsx(ConversationSidebar, { conversations, activeId: activeConvId, onSelect: handleSelectConversation, onNew: handleNewConversation }) }),
1404
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-col overflow-hidden", children: [
1405
+ /* @__PURE__ */ jsxs("header", { "aria-label": "Chat header", className: "flex h-14 shrink-0 items-center gap-3 border-b border-border px-5", children: [
1406
+ /* @__PURE__ */ jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-full bg-grind-orange/10 border border-grind-orange/20", children: /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "text-base", children: companion.emoji ?? "⚡" }) }),
1407
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-col", children: [
1408
+ /* @__PURE__ */ jsx("span", { className: "truncate text-sm font-medium text-foreground", children: companion.name ?? "Companion" }),
1409
+ /* @__PURE__ */ jsx("span", { className: "text-[10px] text-muted-foreground", children: "AI-powered grind partner" })
1410
+ ] })
1411
+ ] }),
1412
+ /* @__PURE__ */ jsxs("div", { ref: messagesContainerRef, className: "flex-1 overflow-y-auto px-4 py-4 space-y-3", role: "log", "aria-label": "Chat messages", "aria-live": "polite", "aria-atomic": "false", children: [
1413
+ isLoadingHistory && /* @__PURE__ */ jsx(ChatMessagesSkeleton, {}),
1414
+ showEmptyState && !isLoadingHistory && /* @__PURE__ */ jsx(EmptyState, { onPrompt: handlePromptClick, companion }),
1415
+ messages.map((message) => /* @__PURE__ */ jsx(MessageBubble, { message }, message.id)),
1416
+ /* @__PURE__ */ jsx("div", { "aria-hidden": "true" })
1417
+ ] }),
1418
+ /* @__PURE__ */ jsx("div", { className: "shrink-0 border-t border-border p-3", children: /* @__PURE__ */ jsx(ChatInput, { value: inputValue, onChange: setInputValue, onSubmit: handleSend, onStop: handleStop, onCancelQueue: handleCancelQueue, isStreaming, hasPendingMessage: pendingMessage !== null, initialHistory: promptHistory, ...pendingMessage ? {
1419
+ pendingMessagePreview: pendingMessage
1420
+ } : {} }) })
1421
+ ] })
1422
+ ] });
1423
+ }
1424
+ export {
1425
+ ChatPage as component
1426
+ };