@workerdeck/ui 0.7.0 → 0.10.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.
- package/README.md +44 -3
- package/build/index.d.mts +767 -21
- package/build/index.mjs +3268 -348
- package/build/index.mjs.map +1 -1
- package/package.json +5 -4
- package/src/components/agent/CodeEditor.tsx +300 -0
- package/src/components/agent/Composer.tsx +379 -56
- package/src/components/agent/ContextDialog.tsx +99 -0
- package/src/components/agent/EditorTabs.tsx +165 -0
- package/src/components/agent/FileTree.tsx +287 -0
- package/src/components/agent/FileViewer.tsx +148 -0
- package/src/components/agent/HostFilesDialog.tsx +218 -0
- package/src/components/agent/McpDialog.tsx +363 -0
- package/src/components/agent/ModelSelect.tsx +34 -6
- package/src/components/agent/PermissionModeSelect.tsx +99 -22
- package/src/components/agent/PermissionPrompt.tsx +72 -6
- package/src/components/agent/PromptTokenText.tsx +39 -0
- package/src/components/agent/SessionEmptyState.tsx +65 -0
- package/src/components/agent/SessionInfoDialog.tsx +163 -0
- package/src/components/agent/SessionPanel.tsx +380 -40
- package/src/components/agent/SessionWorkspace.tsx +282 -0
- package/src/components/agent/SkillsDialog.tsx +195 -0
- package/src/components/agent/StatusBar.tsx +85 -18
- package/src/components/agent/ToolCallCard.tsx +80 -4
- package/src/components/agent/Transcript.tsx +109 -12
- package/src/components/agent/UsageDialog.tsx +168 -0
- package/src/components/prompt-area/prompt-area-engine.ts +53 -0
- package/src/components/prompt-area/types.ts +15 -0
- package/src/components/prompt-area/use-prompt-area.ts +20 -0
- package/src/components/ui/CopyButton.tsx +8 -1
- package/src/components/ui/Dialog.tsx +92 -0
- package/src/components/ui/Menu.tsx +55 -0
- package/src/components/ui/Splitter.tsx +133 -0
- package/src/components/ui/Tooltip.tsx +22 -5
- package/src/index.ts +53 -1
- package/src/lib/clipboard.ts +56 -0
- package/src/lib/format.ts +48 -0
- package/src/lib/tool-icon.ts +74 -0
package/build/index.mjs
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
|
|
1
|
+
import { Fragment, forwardRef, memo, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import { cva } from "class-variance-authority";
|
|
3
3
|
import { clsx } from "clsx";
|
|
4
4
|
import { extendTailwindMerge } from "tailwind-merge";
|
|
5
|
-
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
6
6
|
import { Select as Select$1 } from "@base-ui/react/select";
|
|
7
|
-
import { ArrowDown, ArrowUp, Brain, Check, ChevronDown, ChevronsUpDown, Clock, Copy, Download, FileText, LoaderCircle, MessageCircleQuestion,
|
|
7
|
+
import { ArrowDown, ArrowDownCircle, ArrowUp, AtSign, Brain, ChartPie, Check, CheckSquare, ChevronDown, ChevronLeft, ChevronRight, ChevronsUpDown, ClipboardList, Clock, Code, Copy, Download, File, FileDiff, FileText, FileWarning, Folder, FolderOpen, FolderSearch, FolderTree, Gauge, Globe, Hand, Image, Info, Link2, LoaderCircle, MessageCircleQuestion, MessageSquareText, MoreHorizontal, PanelLeftClose, PanelLeftOpen, Paperclip, PencilLine, Plug, Power, PowerOff, Puzzle, RefreshCw, RotateCcw, RotateCw, Search, ShieldCheck, SlashSquare, Sparkles, Square, SquarePen, Terminal, Trash2, TriangleAlert, UsersRound, WifiOff, Wrench, X, Zap } from "lucide-react";
|
|
8
8
|
import { AlertDialog as AlertDialog$1 } from "@base-ui/react/alert-dialog";
|
|
9
|
+
import { Menu as Menu$1 } from "@base-ui/react/menu";
|
|
10
|
+
import { Dialog as Dialog$1 } from "@base-ui/react/dialog";
|
|
9
11
|
import { Tooltip } from "@base-ui/react/tooltip";
|
|
10
12
|
import { Toaster as Toaster$1, toast } from "sonner";
|
|
11
|
-
import {
|
|
12
|
-
import { useClaudeSession, useToolCallHost } from "@workerdeck/react";
|
|
13
|
+
import { PROTOCOL_VERSION } from "@workerdeck/protocol";
|
|
14
|
+
import { currentText, isDirty, rateLimitWindows, scanPromptTokens, useAttachments, useClaudeSession, useHostFileRoots, useHostFileSearch, useHostFileTree, useOpenFiles, useSessionInfo, useToolCallHost } from "@workerdeck/react";
|
|
13
15
|
import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom";
|
|
14
16
|
import { Streamdown } from "streamdown";
|
|
17
|
+
import { createPortal } from "react-dom";
|
|
15
18
|
//#region src/lib/utils.ts
|
|
16
19
|
const twMerge = extendTailwindMerge({ extend: { classGroups: { "font-size": [{ text: [
|
|
17
20
|
"display-xl",
|
|
@@ -207,6 +210,86 @@ const AlertDialogDescription = ({ className, ...props }) => /* @__PURE__ */ jsx(
|
|
|
207
210
|
...props
|
|
208
211
|
});
|
|
209
212
|
//#endregion
|
|
213
|
+
//#region src/components/ui/Menu.tsx
|
|
214
|
+
const Menu = Menu$1.Root;
|
|
215
|
+
const MenuTrigger = Menu$1.Trigger;
|
|
216
|
+
const MenuContent = ({ className, align = "end", side = "bottom", sideOffset = 6, ...props }) => /* @__PURE__ */ jsx(Menu$1.Portal, { children: /* @__PURE__ */ jsx(Menu$1.Positioner, {
|
|
217
|
+
align,
|
|
218
|
+
side,
|
|
219
|
+
sideOffset,
|
|
220
|
+
className: "isolate z-60 outline-none",
|
|
221
|
+
children: /* @__PURE__ */ jsx(Menu$1.Popup, {
|
|
222
|
+
"data-slot": "menu-content",
|
|
223
|
+
className: cn("min-w-48 rounded-md border border-border bg-surface p-1 text-fg-1 shadow-(--shadow-lg) outline-none", "transition-[opacity,transform] duration-(--motion-base)", "data-starting-style:scale-95 data-starting-style:opacity-0", "data-ending-style:scale-95 data-ending-style:opacity-0", className),
|
|
224
|
+
...props
|
|
225
|
+
})
|
|
226
|
+
}) });
|
|
227
|
+
const MenuItem = ({ className, destructive, ...props }) => /* @__PURE__ */ jsx(Menu$1.Item, {
|
|
228
|
+
"data-slot": "menu-item",
|
|
229
|
+
className: cn("flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-body-sm outline-none select-none", "data-highlighted:bg-surface-hover", destructive ? "text-danger" : "text-text", className),
|
|
230
|
+
...props
|
|
231
|
+
});
|
|
232
|
+
const MenuSeparator = ({ className, ...props }) => /* @__PURE__ */ jsx(Menu$1.Separator, {
|
|
233
|
+
className: cn("my-1 h-px bg-border", className),
|
|
234
|
+
...props
|
|
235
|
+
});
|
|
236
|
+
//#endregion
|
|
237
|
+
//#region src/components/ui/Dialog.tsx
|
|
238
|
+
const Dialog = Dialog$1.Root;
|
|
239
|
+
const DialogTrigger = Dialog$1.Trigger;
|
|
240
|
+
const DialogClose = Dialog$1.Close;
|
|
241
|
+
/**
|
|
242
|
+
* A dismissible panel, sized for reading rather than confirming — the web
|
|
243
|
+
* counterpart of the iOS app's detail sheets (context, usage, session info, MCP).
|
|
244
|
+
*
|
|
245
|
+
* Taller than {@link AlertDialogContent} and scrollable inside, because these
|
|
246
|
+
* carry lists whose length is the engine's business, not the layout's.
|
|
247
|
+
*/
|
|
248
|
+
const DialogContent = ({ className, children, size = "md", ...props }) => /* @__PURE__ */ jsxs(Dialog$1.Portal, { children: [/* @__PURE__ */ jsx(Dialog$1.Backdrop, { className: cn("fixed inset-0 z-70 bg-black/40 backdrop-blur-[1px]", "transition-opacity duration-(--motion-base)", "data-starting-style:opacity-0 data-ending-style:opacity-0") }), /* @__PURE__ */ jsx(Dialog$1.Popup, {
|
|
249
|
+
"data-slot": "dialog-content",
|
|
250
|
+
className: cn("fixed top-1/2 left-1/2 z-70 flex max-h-[min(42rem,calc(100dvh-3rem))] -translate-x-1/2 -translate-y-1/2 flex-col", size === "sm" && "w-[min(24rem,calc(100vw-2rem))]", size === "md" && "w-[min(32rem,calc(100vw-2rem))]", size === "lg" && "w-[min(46rem,calc(100vw-2rem))]", "rounded-lg border border-border bg-surface shadow-(--shadow-lg) outline-none", "transition-[opacity,transform] duration-(--motion-base)", "data-starting-style:scale-95 data-starting-style:opacity-0", "data-ending-style:scale-95 data-ending-style:opacity-0", className),
|
|
251
|
+
...props,
|
|
252
|
+
children
|
|
253
|
+
})] });
|
|
254
|
+
/** Title row with the close button, pinned above the scrolling body. */
|
|
255
|
+
const DialogHeader = ({ title, description, actions }) => /* @__PURE__ */ jsxs("div", {
|
|
256
|
+
className: "flex items-start gap-2 border-b border-border px-4 py-3",
|
|
257
|
+
children: [
|
|
258
|
+
/* @__PURE__ */ jsxs("div", {
|
|
259
|
+
className: "min-w-0 flex-1",
|
|
260
|
+
children: [/* @__PURE__ */ jsx(Dialog$1.Title, {
|
|
261
|
+
className: "truncate text-body-sm font-semibold text-text",
|
|
262
|
+
children: title
|
|
263
|
+
}), description ? /* @__PURE__ */ jsx(Dialog$1.Description, {
|
|
264
|
+
className: "mt-0.5 text-label text-fg-4",
|
|
265
|
+
children: description
|
|
266
|
+
}) : null]
|
|
267
|
+
}),
|
|
268
|
+
actions,
|
|
269
|
+
/* @__PURE__ */ jsx(Dialog$1.Close, {
|
|
270
|
+
"aria-label": "Close",
|
|
271
|
+
className: "-mr-1 flex size-6 shrink-0 items-center justify-center rounded-md text-fg-3 transition-colors outline-none hover:bg-surface-hover hover:text-fg-1",
|
|
272
|
+
children: /* @__PURE__ */ jsx(X, { className: "size-3.5" })
|
|
273
|
+
})
|
|
274
|
+
]
|
|
275
|
+
});
|
|
276
|
+
/** The scrolling region under the header. */
|
|
277
|
+
const DialogBody = ({ className, ...props }) => /* @__PURE__ */ jsx("div", {
|
|
278
|
+
className: cn("min-h-0 flex-1 overflow-y-auto p-4", className),
|
|
279
|
+
...props
|
|
280
|
+
});
|
|
281
|
+
/** A label/value row — the shape every one of these panels is mostly made of. */
|
|
282
|
+
const DialogRow = ({ label, children, mono }) => /* @__PURE__ */ jsxs("div", {
|
|
283
|
+
className: "flex items-baseline justify-between gap-4 py-1.5",
|
|
284
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
285
|
+
className: "shrink-0 text-label text-fg-3",
|
|
286
|
+
children: label
|
|
287
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
288
|
+
className: cn("min-w-0 truncate text-right text-body-sm text-fg-1", mono && "font-mono text-label"),
|
|
289
|
+
children
|
|
290
|
+
})]
|
|
291
|
+
});
|
|
292
|
+
//#endregion
|
|
210
293
|
//#region src/components/ui/Tooltip.tsx
|
|
211
294
|
const TooltipProvider = Tooltip.Provider;
|
|
212
295
|
const TooltipContent = ({ className, side = "top", sideOffset = 6, ...props }) => /* @__PURE__ */ jsx(Tooltip.Portal, { children: /* @__PURE__ */ jsx(Tooltip.Positioner, {
|
|
@@ -219,12 +302,22 @@ const TooltipContent = ({ className, side = "top", sideOffset = 6, ...props }) =
|
|
|
219
302
|
...props
|
|
220
303
|
})
|
|
221
304
|
}) });
|
|
222
|
-
/**
|
|
223
|
-
|
|
305
|
+
/**
|
|
306
|
+
* Convenience wrapper: `<Tip content="..."><Button/></Tip>`.
|
|
307
|
+
*
|
|
308
|
+
* Pass `render` when the trigger must *be* an element you already have — a tab, a
|
|
309
|
+
* row — rather than something wrapped in a span. The default span is fine beside
|
|
310
|
+
* a button but would break any layout that styles its own children (a flex tab
|
|
311
|
+
* strip gets an extra box between the container and its items).
|
|
312
|
+
*/
|
|
313
|
+
function Tip({ content, render, side, children }) {
|
|
224
314
|
return /* @__PURE__ */ jsxs(Tooltip.Root, { children: [/* @__PURE__ */ jsx(Tooltip.Trigger, {
|
|
225
|
-
render: /* @__PURE__ */ jsx("span", { className: "inline-flex" }),
|
|
315
|
+
render: render ?? /* @__PURE__ */ jsx("span", { className: "inline-flex" }),
|
|
226
316
|
children
|
|
227
|
-
}), /* @__PURE__ */ jsx(TooltipContent, {
|
|
317
|
+
}), /* @__PURE__ */ jsx(TooltipContent, {
|
|
318
|
+
side,
|
|
319
|
+
children: content
|
|
320
|
+
})] });
|
|
228
321
|
}
|
|
229
322
|
//#endregion
|
|
230
323
|
//#region src/components/ui/Sonner.tsx
|
|
@@ -243,6 +336,53 @@ function Toaster() {
|
|
|
243
336
|
});
|
|
244
337
|
}
|
|
245
338
|
//#endregion
|
|
339
|
+
//#region src/lib/clipboard.ts
|
|
340
|
+
/**
|
|
341
|
+
* Copy text to the clipboard, on origins where the modern API does not exist.
|
|
342
|
+
*
|
|
343
|
+
* `navigator.clipboard` is gated on a **secure context**: HTTPS, or localhost.
|
|
344
|
+
* A WorkerDeck dashboard reached the way it is meant to be reached — plain HTTP
|
|
345
|
+
* on a LAN address, from a laptop or a phone — is neither, so `navigator.clipboard`
|
|
346
|
+
* is `undefined` there and touching `.writeText` throws outright. That is the
|
|
347
|
+
* normal deployment, not an edge case, which is why this falls back rather than
|
|
348
|
+
* feature-detecting into a disabled button.
|
|
349
|
+
*
|
|
350
|
+
* The fallback is `document.execCommand('copy')` over an off-screen textarea.
|
|
351
|
+
* It is deprecated and it is also the only thing that works here; every browser
|
|
352
|
+
* still implements it. Returns whether the text actually landed, so a caller can
|
|
353
|
+
* avoid claiming success it did not have.
|
|
354
|
+
*/
|
|
355
|
+
async function copyText(value) {
|
|
356
|
+
try {
|
|
357
|
+
if (navigator.clipboard?.writeText) {
|
|
358
|
+
await navigator.clipboard.writeText(value);
|
|
359
|
+
return true;
|
|
360
|
+
}
|
|
361
|
+
} catch {}
|
|
362
|
+
return legacyCopy(value);
|
|
363
|
+
}
|
|
364
|
+
function legacyCopy(value) {
|
|
365
|
+
if (typeof document === "undefined") return false;
|
|
366
|
+
const textarea = document.createElement("textarea");
|
|
367
|
+
textarea.value = value;
|
|
368
|
+
textarea.setAttribute("readonly", "");
|
|
369
|
+
textarea.style.position = "fixed";
|
|
370
|
+
textarea.style.top = "-9999px";
|
|
371
|
+
textarea.style.opacity = "0";
|
|
372
|
+
document.body.appendChild(textarea);
|
|
373
|
+
const previous = document.activeElement;
|
|
374
|
+
try {
|
|
375
|
+
textarea.select();
|
|
376
|
+
textarea.setSelectionRange(0, value.length);
|
|
377
|
+
return document.execCommand("copy");
|
|
378
|
+
} catch {
|
|
379
|
+
return false;
|
|
380
|
+
} finally {
|
|
381
|
+
document.body.removeChild(textarea);
|
|
382
|
+
if (previous instanceof HTMLElement) previous.focus();
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
//#endregion
|
|
246
386
|
//#region src/components/ui/CopyButton.tsx
|
|
247
387
|
function CopyButton({ value, className, variant = "ghost", size = "icon-sm", ...props }) {
|
|
248
388
|
const [copied, setCopied] = useState(false);
|
|
@@ -252,7 +392,8 @@ function CopyButton({ value, className, variant = "ghost", size = "icon-sm", ...
|
|
|
252
392
|
"aria-label": "Copy",
|
|
253
393
|
className: cn("text-fg-3", className),
|
|
254
394
|
onClick: () => {
|
|
255
|
-
|
|
395
|
+
copyText(value).then((ok) => {
|
|
396
|
+
if (!ok) return;
|
|
256
397
|
setCopied(true);
|
|
257
398
|
setTimeout(() => setCopied(false), 1500);
|
|
258
399
|
});
|
|
@@ -290,6 +431,81 @@ function CodeBlock({ code, label, copyable = true, className }) {
|
|
|
290
431
|
});
|
|
291
432
|
}
|
|
292
433
|
//#endregion
|
|
434
|
+
//#region src/components/ui/Splitter.tsx
|
|
435
|
+
/**
|
|
436
|
+
* A draggable pane divider.
|
|
437
|
+
*
|
|
438
|
+
* Hand-rolled rather than depended on: `@base-ui/react` ships no splitter, the
|
|
439
|
+
* behaviour is a hundred lines of pointer events, and this repo's instinct at
|
|
440
|
+
* this layer is to own it (the composer is vendored for the same reason).
|
|
441
|
+
*
|
|
442
|
+
* Pointer capture is what makes it survive a fast drag — without it the pointer
|
|
443
|
+
* leaves the 5px bar within a frame and the moves go to whatever is underneath,
|
|
444
|
+
* which for this layout is an iframe-free but still selection-happy code pane.
|
|
445
|
+
* The drag origin is captured on pointerdown and every move is measured against
|
|
446
|
+
* it, so the pane cannot drift relative to the cursor over a long drag the way
|
|
447
|
+
* per-move deltas do once clamping is involved.
|
|
448
|
+
*
|
|
449
|
+
* Keyboard-operable and announced as a separator, because a pane you can only
|
|
450
|
+
* size by dragging is a pane some people cannot size.
|
|
451
|
+
*/
|
|
452
|
+
function Splitter({ orientation, value, onValueChange, min, max, step = 16, inverted, "aria-label": label, className }) {
|
|
453
|
+
const drag = useRef(null);
|
|
454
|
+
const vertical = orientation === "vertical";
|
|
455
|
+
const clamp = useCallback((next) => Math.min(max, Math.max(min, next)), [min, max]);
|
|
456
|
+
const onPointerDown = (event) => {
|
|
457
|
+
if (event.button !== 0) return;
|
|
458
|
+
event.preventDefault();
|
|
459
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
460
|
+
drag.current = {
|
|
461
|
+
origin: vertical ? event.clientX : event.clientY,
|
|
462
|
+
start: value
|
|
463
|
+
};
|
|
464
|
+
};
|
|
465
|
+
const onPointerMove = (event) => {
|
|
466
|
+
const state = drag.current;
|
|
467
|
+
if (!state) return;
|
|
468
|
+
const delta = (vertical ? event.clientX : event.clientY) - state.origin;
|
|
469
|
+
onValueChange(clamp(state.start + (inverted ? -delta : delta)));
|
|
470
|
+
};
|
|
471
|
+
const endDrag = (event) => {
|
|
472
|
+
if (!drag.current) return;
|
|
473
|
+
drag.current = null;
|
|
474
|
+
if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId);
|
|
475
|
+
};
|
|
476
|
+
const onKeyDown = (event) => {
|
|
477
|
+
const grow = vertical ? "ArrowRight" : "ArrowDown";
|
|
478
|
+
const shrink = vertical ? "ArrowLeft" : "ArrowUp";
|
|
479
|
+
const direction = inverted ? -1 : 1;
|
|
480
|
+
if (event.key === grow) onValueChange(clamp(value + step * direction));
|
|
481
|
+
else if (event.key === shrink) onValueChange(clamp(value - step * direction));
|
|
482
|
+
else if (event.key === "Home") onValueChange(min);
|
|
483
|
+
else if (event.key === "End") onValueChange(max);
|
|
484
|
+
else return;
|
|
485
|
+
event.preventDefault();
|
|
486
|
+
};
|
|
487
|
+
return /* @__PURE__ */ jsx("div", {
|
|
488
|
+
"data-slot": "splitter",
|
|
489
|
+
role: "separator",
|
|
490
|
+
tabIndex: 0,
|
|
491
|
+
"aria-label": label,
|
|
492
|
+
"aria-orientation": orientation,
|
|
493
|
+
"aria-valuenow": Math.round(value),
|
|
494
|
+
"aria-valuemin": min,
|
|
495
|
+
"aria-valuemax": max,
|
|
496
|
+
onPointerDown,
|
|
497
|
+
onPointerMove,
|
|
498
|
+
onPointerUp: endDrag,
|
|
499
|
+
onPointerCancel: endDrag,
|
|
500
|
+
onKeyDown,
|
|
501
|
+
className: cn("group relative shrink-0 touch-none bg-border transition-colors", "hover:bg-border-strong focus-visible:bg-accent focus-visible:outline-none", vertical ? "w-px cursor-col-resize" : "h-px cursor-row-resize", className),
|
|
502
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
503
|
+
"aria-hidden": true,
|
|
504
|
+
className: cn("absolute", vertical ? "-inset-x-[3px] inset-y-0" : "-inset-y-[3px] inset-x-0")
|
|
505
|
+
})
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
//#endregion
|
|
293
509
|
//#region src/components/ui/ProgressRing.tsx
|
|
294
510
|
/** Tiny SVG progress circle; stroke color comes from `currentColor` so callers set it
|
|
295
511
|
* via text color classes (e.g. warning/danger past thresholds). */
|
|
@@ -519,6 +735,58 @@ function resolveChip(segments, activeTrigger, chip) {
|
|
|
519
735
|
};
|
|
520
736
|
}
|
|
521
737
|
/**
|
|
738
|
+
* Replaces the active trigger's range with **plain text** instead of a chip.
|
|
739
|
+
*
|
|
740
|
+
* The sibling of {@link resolveChip}, for suggestions that are a typing aid
|
|
741
|
+
* rather than a token: what lands in the document is ordinary editable text the
|
|
742
|
+
* user is expected to finish and change, and it must not look or behave like a
|
|
743
|
+
* resolved chip — no immutability, no trigger character, nothing for a consumer
|
|
744
|
+
* to parse back out. The caret is left at the end of the inserted text so typing
|
|
745
|
+
* continues from there.
|
|
746
|
+
*/
|
|
747
|
+
function resolveText(segments, activeTrigger, text) {
|
|
748
|
+
const triggerStart = activeTrigger.startOffset;
|
|
749
|
+
const triggerEnd = triggerStart + 1 + activeTrigger.query.length;
|
|
750
|
+
const newSegments = [];
|
|
751
|
+
let offset = 0;
|
|
752
|
+
let cursorOffset = triggerStart + text.length;
|
|
753
|
+
for (const seg of segments) {
|
|
754
|
+
if (seg.type === "chip") {
|
|
755
|
+
const chipEnd = offset + `${seg.trigger}${seg.displayText}`.length;
|
|
756
|
+
if (chipEnd <= triggerStart || offset >= triggerEnd) newSegments.push(seg);
|
|
757
|
+
offset = chipEnd;
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
const textStart = offset;
|
|
761
|
+
const textEnd = offset + seg.text.length;
|
|
762
|
+
if (textEnd <= triggerStart || textStart >= triggerEnd) newSegments.push(seg);
|
|
763
|
+
else {
|
|
764
|
+
const before = seg.text.slice(0, Math.max(0, triggerStart - textStart));
|
|
765
|
+
const after = seg.text.slice(Math.min(seg.text.length, triggerEnd - textStart));
|
|
766
|
+
if (before) newSegments.push({
|
|
767
|
+
type: "text",
|
|
768
|
+
text: before
|
|
769
|
+
});
|
|
770
|
+
newSegments.push({
|
|
771
|
+
type: "text",
|
|
772
|
+
text
|
|
773
|
+
});
|
|
774
|
+
if (after) newSegments.push({
|
|
775
|
+
type: "text",
|
|
776
|
+
text: after
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
offset = textEnd;
|
|
780
|
+
}
|
|
781
|
+
const merged = mergeAdjacentTextSegments(newSegments);
|
|
782
|
+
const total = segmentsToPlainText(merged).length;
|
|
783
|
+
if (cursorOffset > total) cursorOffset = total;
|
|
784
|
+
return {
|
|
785
|
+
segments: merged,
|
|
786
|
+
cursorOffset
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
522
790
|
* Removes a chip at the given segment index and merges adjacent text segments.
|
|
523
791
|
*
|
|
524
792
|
* @param segments - Current document segments
|
|
@@ -2908,6 +3176,20 @@ function usePromptArea({ value, onChange, triggers = [], disabled = false, onSub
|
|
|
2908
3176
|
const selectSuggestionInternal = useCallback((suggestion) => {
|
|
2909
3177
|
if (!activeTrigger) return;
|
|
2910
3178
|
const segments = readSegmentsFromDOM();
|
|
3179
|
+
const asText = activeTrigger.config.insertAsText?.(suggestion);
|
|
3180
|
+
if (asText !== void 0) {
|
|
3181
|
+
const inserted = resolveText(segments, activeTrigger, asText);
|
|
3182
|
+
events.pushUndo(segments);
|
|
3183
|
+
onChange(inserted.segments);
|
|
3184
|
+
renderSegmentsToDOM(inserted.segments);
|
|
3185
|
+
const editor = editorRef.current;
|
|
3186
|
+
if (editor) setCursorAtOffset(editor, inserted.cursorOffset);
|
|
3187
|
+
dismissTrigger();
|
|
3188
|
+
setTimeout(() => {
|
|
3189
|
+
editorRef.current?.focus();
|
|
3190
|
+
}, 0);
|
|
3191
|
+
return;
|
|
3192
|
+
}
|
|
2911
3193
|
const displayText = activeTrigger.config.onSelect?.(suggestion) ?? suggestion.label;
|
|
2912
3194
|
const chipData = {
|
|
2913
3195
|
value: suggestion.value,
|
|
@@ -3522,8 +3804,8 @@ function Svg({ className, children }) {
|
|
|
3522
3804
|
children
|
|
3523
3805
|
});
|
|
3524
3806
|
}
|
|
3525
|
-
const FileBody = /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("path", { d: "M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" }), /* @__PURE__ */ jsx("path", { d: "M14 2v5a1 1 0 0 0 1 1h5" })] });
|
|
3526
|
-
const File = ({ className }) => /* @__PURE__ */ jsx(Svg, {
|
|
3807
|
+
const FileBody = /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("path", { d: "M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" }), /* @__PURE__ */ jsx("path", { d: "M14 2v5a1 1 0 0 0 1 1h5" })] });
|
|
3808
|
+
const File$1 = ({ className }) => /* @__PURE__ */ jsx(Svg, {
|
|
3527
3809
|
className,
|
|
3528
3810
|
children: FileBody
|
|
3529
3811
|
});
|
|
@@ -3589,7 +3871,7 @@ const FILE_ICONS = {
|
|
|
3589
3871
|
spreadsheet: FileSpreadsheet,
|
|
3590
3872
|
code: FileCode,
|
|
3591
3873
|
image: ImageIcon,
|
|
3592
|
-
default: File
|
|
3874
|
+
default: File$1
|
|
3593
3875
|
};
|
|
3594
3876
|
/** Format bytes into a human-readable string. */
|
|
3595
3877
|
function formatFileSize(bytes) {
|
|
@@ -4057,134 +4339,1144 @@ function getChipsByTrigger(segments, trigger) {
|
|
|
4057
4339
|
return segments.filter((seg) => seg.type === "chip" && seg.trigger === trigger);
|
|
4058
4340
|
}
|
|
4059
4341
|
//#endregion
|
|
4342
|
+
//#region src/lib/format.ts
|
|
4343
|
+
function formatCost(usd) {
|
|
4344
|
+
if (usd === void 0 || Number.isNaN(usd)) return "—";
|
|
4345
|
+
if (usd === 0) return "$0.00";
|
|
4346
|
+
if (usd < .01) return "<$0.01";
|
|
4347
|
+
return `$${usd.toFixed(2)}`;
|
|
4348
|
+
}
|
|
4349
|
+
function formatDuration(ms) {
|
|
4350
|
+
if (ms < 1e3) return `${Math.round(ms)}ms`;
|
|
4351
|
+
const s = ms / 1e3;
|
|
4352
|
+
if (s < 60) return `${s.toFixed(1)}s`;
|
|
4353
|
+
return `${Math.floor(s / 60)}m ${Math.round(s % 60)}s`;
|
|
4354
|
+
}
|
|
4355
|
+
/** Compact token count, Claude Code-style: 850 → "850", 359_000 → "359.0k", 1_200_000 → "1.2M". */
|
|
4356
|
+
function formatTokens(tokens) {
|
|
4357
|
+
if (tokens >= 1e6) return `${(tokens / 1e6).toFixed(1)}M`;
|
|
4358
|
+
if (tokens >= 1e3) return `${(tokens / 1e3).toFixed(1)}k`;
|
|
4359
|
+
return String(Math.round(tokens));
|
|
4360
|
+
}
|
|
4361
|
+
function formatBytes(bytes) {
|
|
4362
|
+
if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
4363
|
+
if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
4364
|
+
return `${bytes} B`;
|
|
4365
|
+
}
|
|
4366
|
+
/** Countdown to an epoch-ms deadline: "2h 18m", "12m", "<1m"; "now" once passed. */
|
|
4367
|
+
function formatCountdown(untilEpochMs, now = Date.now()) {
|
|
4368
|
+
const remaining = untilEpochMs - now;
|
|
4369
|
+
if (remaining <= 0) return "now";
|
|
4370
|
+
const minutes = Math.ceil(remaining / 6e4);
|
|
4371
|
+
if (minutes < 1) return "<1m";
|
|
4372
|
+
if (minutes < 60) return `${minutes}m`;
|
|
4373
|
+
const days = Math.floor(minutes / 1440);
|
|
4374
|
+
if (days >= 1) return `${days}d ${Math.floor(minutes % 1440 / 60)}h`;
|
|
4375
|
+
return `${Math.floor(minutes / 60)}h ${minutes % 60}m`;
|
|
4376
|
+
}
|
|
4377
|
+
function formatRelativeTime(epochMs, now = Date.now()) {
|
|
4378
|
+
if (!epochMs) return "—";
|
|
4379
|
+
const diff = Math.max(0, now - epochMs);
|
|
4380
|
+
const s = Math.floor(diff / 1e3);
|
|
4381
|
+
if (s < 60) return "just now";
|
|
4382
|
+
const m = Math.floor(s / 60);
|
|
4383
|
+
if (m < 60) return `${m}m ago`;
|
|
4384
|
+
const h = Math.floor(m / 60);
|
|
4385
|
+
if (h < 24) return `${h}h ago`;
|
|
4386
|
+
return `${Math.floor(h / 24)}d ago`;
|
|
4387
|
+
}
|
|
4388
|
+
/**
|
|
4389
|
+
* Human label for a rate-limit window key, compact: 'five_hour' → "5h",
|
|
4390
|
+
* 'seven_day_opus' → "7d opus". The per-model suffix is an open set — the CLI
|
|
4391
|
+
* adds buckets as plans gain them — so it is rewritten rather than enumerated.
|
|
4392
|
+
*/
|
|
4393
|
+
function formatRateLimitWindow(key) {
|
|
4394
|
+
if (key === "five_hour") return "5h";
|
|
4395
|
+
if (key === "seven_day") return "7d";
|
|
4396
|
+
const spaced = key.replaceAll("_", " ");
|
|
4397
|
+
return key.startsWith("seven_day_") ? `7d ${spaced.slice(10)}` : spaced;
|
|
4398
|
+
}
|
|
4399
|
+
/** The same key spelled out, where there is room: 'five_hour' → "5-hour
|
|
4400
|
+
* session", 'seven_day_fable' → "Weekly · Fable". */
|
|
4401
|
+
function formatRateLimitWindowLong(key) {
|
|
4402
|
+
if (key === "five_hour") return "5-hour session";
|
|
4403
|
+
if (key === "seven_day") return "Weekly";
|
|
4404
|
+
if (key === "seven_day_oauth_apps") return "Weekly · apps";
|
|
4405
|
+
const capitalize = (s) => s.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
4406
|
+
if (!key.startsWith("seven_day_")) return capitalize(key.replaceAll("_", " "));
|
|
4407
|
+
return `Weekly · ${capitalize(key.slice(10).replaceAll("_", " "))}`;
|
|
4408
|
+
}
|
|
4409
|
+
/**
|
|
4410
|
+
* How long a rate-limit window is, in seconds — the denominator behind the pace
|
|
4411
|
+
* marker. Derived from the key rather than reported: the CLI sends a reset time
|
|
4412
|
+
* and a percentage, never a duration. `undefined` for a window whose key doesn't
|
|
4413
|
+
* say, and the marker is then simply not drawn rather than guessed.
|
|
4414
|
+
*/
|
|
4415
|
+
function rateLimitWindowSeconds(key) {
|
|
4416
|
+
if (key === "five_hour") return 5 * 3600;
|
|
4417
|
+
if (key.startsWith("seven_day")) return 7 * 86400;
|
|
4418
|
+
}
|
|
4419
|
+
/** "8 secs ago" / "3 mins ago" — a freshness line finer-grained than
|
|
4420
|
+
* {@link formatRelativeTime}, because a poll that just landed should say so. */
|
|
4421
|
+
function formatAgoPrecise(epochMs, now = Date.now()) {
|
|
4422
|
+
const seconds = Math.max(0, Math.floor((now - epochMs) / 1e3));
|
|
4423
|
+
if (seconds < 60) return `${seconds} sec${seconds === 1 ? "" : "s"} ago`;
|
|
4424
|
+
if (seconds < 3600) {
|
|
4425
|
+
const minutes = Math.floor(seconds / 60);
|
|
4426
|
+
return `${minutes} min${minutes === 1 ? "" : "s"} ago`;
|
|
4427
|
+
}
|
|
4428
|
+
const hours = Math.floor(seconds / 3600);
|
|
4429
|
+
return `${hours} hour${hours === 1 ? "" : "s"} ago`;
|
|
4430
|
+
}
|
|
4431
|
+
/** Compact one-line preview of a tool input for card headers. */
|
|
4432
|
+
function toolInputPreview(input, max = 80) {
|
|
4433
|
+
if (input === null || input === void 0) return "";
|
|
4434
|
+
if (typeof input === "object") {
|
|
4435
|
+
const o = input;
|
|
4436
|
+
const primary = o.command ?? o.file_path ?? o.path ?? o.url ?? o.pattern ?? o.query ?? o.description;
|
|
4437
|
+
if (typeof primary === "string") return primary.length > max ? primary.slice(0, max - 1) + "…" : primary;
|
|
4438
|
+
}
|
|
4439
|
+
const text = JSON.stringify(input) ?? "";
|
|
4440
|
+
return text.length > max ? text.slice(0, max - 1) + "…" : text;
|
|
4441
|
+
}
|
|
4442
|
+
//#endregion
|
|
4060
4443
|
//#region src/components/agent/Composer.tsx
|
|
4061
4444
|
/** CLI names may carry display annotations (e.g. "foo (MCP)") the parser rejects. */
|
|
4062
4445
|
const cleanName = (name) => name.replace(/\s*\(MCP\)$/i, "");
|
|
4063
|
-
/**
|
|
4064
|
-
*
|
|
4065
|
-
*
|
|
4066
|
-
*
|
|
4067
|
-
|
|
4446
|
+
/**
|
|
4447
|
+
* What a picked skill types into the composer.
|
|
4448
|
+
*
|
|
4449
|
+
* The engine's own `defaultPrompt` when it declared one — it knows what its
|
|
4450
|
+
* skill wants to be asked — and otherwise `$name`, which is codex's native way
|
|
4451
|
+
* of referring to a skill in prompt text: its `skill-creator` documents the form
|
|
4452
|
+
* (`Use $skill-x at /path/to/skill-x to solve problem y`) and its own bundled
|
|
4453
|
+
* prompts are written that way ("Use $pdf to …"). Spelling it the way the engine
|
|
4454
|
+
* spells it beats paraphrasing into "Use the X skill to".
|
|
4455
|
+
*
|
|
4456
|
+
* Either way it ends in a space so the caret lands ready for the rest of the
|
|
4457
|
+
* sentence, and either way it is ordinary text: nothing here is submitted, and
|
|
4458
|
+
* nothing is parsed back out.
|
|
4459
|
+
*/
|
|
4460
|
+
function skillPrompt(skill) {
|
|
4461
|
+
const base = skill.defaultPrompt?.trim() || `$${skill.name}`;
|
|
4462
|
+
return /\s$/.test(base) ? base : base + " ";
|
|
4463
|
+
}
|
|
4464
|
+
/** Ranks a haystack set against the typed query: 2 for a prefix hit, 1 for a
|
|
4465
|
+
* substring, 0 for no match. Shared so commands and skills sort as one list
|
|
4466
|
+
* rather than two concatenated ones. */
|
|
4467
|
+
function matchScore(query, haystacks) {
|
|
4468
|
+
const needle = query.toLowerCase();
|
|
4469
|
+
const lowered = haystacks.map((s) => s.toLowerCase());
|
|
4470
|
+
if (lowered.some((h) => h.startsWith(needle))) return 2;
|
|
4471
|
+
return lowered.some((h) => h.includes(needle)) ? 1 : 0;
|
|
4472
|
+
}
|
|
4473
|
+
/**
|
|
4474
|
+
* Framed prompt input built on prompt-area's contentEditable.
|
|
4475
|
+
*
|
|
4476
|
+
* Three completions ride the same field and behave nothing alike. `/` is the
|
|
4477
|
+
* CLI's command list and `$` is the engine's skill list — both local, so they
|
|
4478
|
+
* filter completely and instantly; `@` is a search against the host filesystem,
|
|
4479
|
+
* debounced and abortable so a fast typist makes one request rather than eight.
|
|
4480
|
+
*
|
|
4481
|
+
* `/` and `$` are separate keys rather than one merged menu, and that mirrors
|
|
4482
|
+
* the engines themselves: codex completes skills on `$` and reserves `/` for
|
|
4483
|
+
* commands. The behaviours differ too — a command resolves to a **chip**,
|
|
4484
|
+
* because the CLI really does parse `/name` out of the message, while a skill
|
|
4485
|
+
* resolves to plain editable **text**, because no engine parses `$name` as
|
|
4486
|
+
* syntax; it is prose the model reads. Rendering them alike would promise
|
|
4487
|
+
* something that does not happen.
|
|
4488
|
+
*
|
|
4489
|
+
* Files can arrive three ways — the paperclip, a drop, or a paste — because on a
|
|
4490
|
+
* desktop all three are things people already do, and the upload starts the
|
|
4491
|
+
* moment one lands rather than at send time.
|
|
4492
|
+
*/
|
|
4493
|
+
function Composer({ onSend, onInterrupt, busy, disabled, placeholder = "Message the agent…", commands, skills, onSearchFiles, attachments, toolbar, className, ref }) {
|
|
4068
4494
|
const { bind, plainText, isEmpty, clear, focus } = usePromptAreaState();
|
|
4495
|
+
const fileInput = useRef(null);
|
|
4496
|
+
const [dragging, setDragging] = useState(false);
|
|
4497
|
+
useImperativeHandle(ref, () => ({ insertText: (text) => {
|
|
4498
|
+
const prefix = plainText.length > 0 && !/\s$/.test(plainText) ? " " : "";
|
|
4499
|
+
bind.ref.current?.appendText(prefix + text);
|
|
4500
|
+
focus();
|
|
4501
|
+
} }), [
|
|
4502
|
+
bind.ref,
|
|
4503
|
+
plainText,
|
|
4504
|
+
focus
|
|
4505
|
+
]);
|
|
4069
4506
|
const triggers = useMemo(() => {
|
|
4070
|
-
|
|
4071
|
-
const
|
|
4072
|
-
|
|
4073
|
-
const
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4507
|
+
const configured = [];
|
|
4508
|
+
const usableSkills = (skills ?? []).filter((s) => s.enabled);
|
|
4509
|
+
if (commands && commands.length > 0) {
|
|
4510
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4511
|
+
const unique = commands.flatMap((c) => {
|
|
4512
|
+
const name = cleanName(c.name);
|
|
4513
|
+
if (seen.has(name)) return [];
|
|
4514
|
+
seen.add(name);
|
|
4515
|
+
return [{
|
|
4516
|
+
...c,
|
|
4517
|
+
name
|
|
4518
|
+
}];
|
|
4519
|
+
});
|
|
4520
|
+
configured.push(commandTrigger({
|
|
4521
|
+
onSearch: (query) => {
|
|
4522
|
+
const scored = [];
|
|
4523
|
+
for (const c of unique) {
|
|
4524
|
+
const score = matchScore(query, [
|
|
4525
|
+
c.name,
|
|
4526
|
+
...c.aliases ?? [],
|
|
4527
|
+
...c.name.split(":")
|
|
4528
|
+
]);
|
|
4529
|
+
if (score === 0) continue;
|
|
4530
|
+
scored.push({
|
|
4531
|
+
score,
|
|
4532
|
+
suggestion: {
|
|
4533
|
+
value: c.name,
|
|
4534
|
+
label: `/${c.name}${c.argumentHint ? ` ${c.argumentHint}` : ""}`,
|
|
4535
|
+
description: c.description
|
|
4536
|
+
}
|
|
4537
|
+
});
|
|
4538
|
+
}
|
|
4539
|
+
scored.sort((a, b) => b.score - a.score);
|
|
4540
|
+
return scored.map(({ suggestion }) => suggestion);
|
|
4541
|
+
},
|
|
4542
|
+
onSelect: (suggestion) => suggestion.value,
|
|
4543
|
+
chipClassName: "font-mono"
|
|
4544
|
+
}));
|
|
4545
|
+
}
|
|
4546
|
+
if (usableSkills.length > 0) configured.push(commandTrigger({
|
|
4547
|
+
char: "$",
|
|
4548
|
+
accessibilityLabel: "skill",
|
|
4082
4549
|
onSearch: (query) => {
|
|
4083
|
-
const
|
|
4084
|
-
const
|
|
4085
|
-
const
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
score
|
|
4090
|
-
|
|
4091
|
-
|
|
4550
|
+
const scored = [];
|
|
4551
|
+
for (const skill of usableSkills) {
|
|
4552
|
+
const score = matchScore(query, [skill.name, ...skill.name.split(/[-:_]/)]);
|
|
4553
|
+
if (score === 0) continue;
|
|
4554
|
+
const summary = skill.shortDescription ?? skill.description;
|
|
4555
|
+
scored.push({
|
|
4556
|
+
score,
|
|
4557
|
+
suggestion: {
|
|
4558
|
+
value: skill.name,
|
|
4559
|
+
label: skill.displayName ?? skill.name,
|
|
4560
|
+
description: summary ? `Skill · ${summary}` : "Skill · inserts a message you can edit",
|
|
4561
|
+
icon: /* @__PURE__ */ jsx(Sparkles, { className: "size-3.5 text-fg-3" })
|
|
4562
|
+
}
|
|
4563
|
+
});
|
|
4564
|
+
}
|
|
4092
4565
|
scored.sort((a, b) => b.score - a.score);
|
|
4093
|
-
return scored.map(({
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4566
|
+
return scored.map(({ suggestion }) => suggestion);
|
|
4567
|
+
},
|
|
4568
|
+
insertAsText: (suggestion) => {
|
|
4569
|
+
const skill = usableSkills.find((s) => s.name === suggestion.value);
|
|
4570
|
+
return skill ? skillPrompt(skill) : `$${suggestion.value} `;
|
|
4571
|
+
}
|
|
4572
|
+
}));
|
|
4573
|
+
if (onSearchFiles) configured.push(mentionTrigger({
|
|
4574
|
+
searchDebounceMs: 150,
|
|
4575
|
+
onSearch: async (query, options) => {
|
|
4576
|
+
return (await onSearchFiles(query, options)).map((match) => ({
|
|
4577
|
+
value: match.relative,
|
|
4578
|
+
label: match.relative,
|
|
4579
|
+
description: match.path
|
|
4097
4580
|
}));
|
|
4098
4581
|
},
|
|
4099
4582
|
onSelect: (suggestion) => suggestion.value,
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4583
|
+
chipStyle: "inline",
|
|
4584
|
+
chipClassName: "font-mono",
|
|
4585
|
+
emptyMessage: "No matching files"
|
|
4586
|
+
}));
|
|
4587
|
+
return configured.length > 0 ? configured : void 0;
|
|
4588
|
+
}, [
|
|
4589
|
+
commands,
|
|
4590
|
+
skills,
|
|
4591
|
+
onSearchFiles
|
|
4592
|
+
]);
|
|
4593
|
+
const staged = attachments?.items ?? [];
|
|
4594
|
+
const canSend = !disabled && (!isEmpty || staged.length > 0) && !attachments?.uploading && !attachments?.hasFailure;
|
|
4103
4595
|
const submit = () => {
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4596
|
+
if (!canSend) return;
|
|
4597
|
+
onSend(plainText.trim(), attachments?.readyIds ?? []);
|
|
4598
|
+
attachments?.clear();
|
|
4107
4599
|
clear();
|
|
4108
4600
|
focus();
|
|
4109
4601
|
};
|
|
4110
|
-
const
|
|
4602
|
+
const pick = (files) => {
|
|
4603
|
+
if (files && files.length > 0) attachments?.add(files);
|
|
4604
|
+
};
|
|
4111
4605
|
return /* @__PURE__ */ jsxs("div", {
|
|
4112
4606
|
"data-slot": "composer",
|
|
4113
4607
|
className: cn("px-3 pb-3", className),
|
|
4114
4608
|
children: [/* @__PURE__ */ jsxs("div", {
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
"
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4609
|
+
onDragOver: (e) => {
|
|
4610
|
+
if (attachments && !attachments.disabled) {
|
|
4611
|
+
e.preventDefault();
|
|
4612
|
+
setDragging(true);
|
|
4613
|
+
}
|
|
4614
|
+
},
|
|
4615
|
+
onDragLeave: () => setDragging(false),
|
|
4616
|
+
onDrop: (e) => {
|
|
4617
|
+
if (!attachments || attachments.disabled) return;
|
|
4618
|
+
e.preventDefault();
|
|
4619
|
+
setDragging(false);
|
|
4620
|
+
pick(e.dataTransfer.files);
|
|
4621
|
+
},
|
|
4622
|
+
className: cn("mx-auto w-full max-w-3xl overflow-hidden rounded-lg border border-border bg-bg shadow-(--shadow-xs)", "transition-colors focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/30", dragging && "border-ring ring-2 ring-ring/30", disabled && "opacity-60"),
|
|
4623
|
+
children: [
|
|
4624
|
+
staged.length > 0 && attachments ? /* @__PURE__ */ jsx(AttachmentStrip, { attachments }) : null,
|
|
4625
|
+
/* @__PURE__ */ jsx(PromptArea, {
|
|
4626
|
+
...bind,
|
|
4627
|
+
triggers,
|
|
4628
|
+
onSubmit: submit,
|
|
4629
|
+
disabled,
|
|
4630
|
+
placeholder: disabled ? "Session ended" : placeholder,
|
|
4631
|
+
minHeight: 28,
|
|
4632
|
+
maxHeight: 192,
|
|
4633
|
+
"aria-label": "Message the agent",
|
|
4634
|
+
className: "px-3 pt-2.5 pb-1 text-body-sm text-text",
|
|
4635
|
+
onImagePaste: (file) => attachments?.add([file])
|
|
4636
|
+
}),
|
|
4637
|
+
/* @__PURE__ */ jsxs("div", {
|
|
4638
|
+
className: "flex items-center justify-between gap-2 px-2 pb-2",
|
|
4639
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
4640
|
+
className: "flex min-w-0 items-center gap-1",
|
|
4641
|
+
children: [attachments && !attachments.disabled ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("input", {
|
|
4642
|
+
ref: fileInput,
|
|
4643
|
+
type: "file",
|
|
4644
|
+
multiple: true,
|
|
4645
|
+
accept: attachments.accept || void 0,
|
|
4646
|
+
className: "hidden",
|
|
4647
|
+
onChange: (e) => {
|
|
4648
|
+
pick(e.target.files);
|
|
4649
|
+
e.target.value = "";
|
|
4650
|
+
}
|
|
4651
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
4652
|
+
variant: "ghost",
|
|
4653
|
+
size: "icon-sm",
|
|
4654
|
+
"aria-label": "Attach files",
|
|
4655
|
+
disabled,
|
|
4656
|
+
onClick: () => fileInput.current?.click(),
|
|
4657
|
+
children: /* @__PURE__ */ jsx(Paperclip, { className: "size-4" })
|
|
4658
|
+
})] }) : null, toolbar]
|
|
4659
|
+
}), busy && !canSend ? /* @__PURE__ */ jsx(Button, {
|
|
4660
|
+
variant: "outline",
|
|
4661
|
+
size: "icon-sm",
|
|
4662
|
+
"aria-label": "Interrupt",
|
|
4663
|
+
className: "rounded-full",
|
|
4664
|
+
onClick: onInterrupt,
|
|
4665
|
+
children: /* @__PURE__ */ jsx(Square, { className: "size-3" })
|
|
4666
|
+
}) : /* @__PURE__ */ jsx(Button, {
|
|
4667
|
+
size: "icon-sm",
|
|
4668
|
+
"aria-label": "Send",
|
|
4669
|
+
className: "rounded-full",
|
|
4670
|
+
disabled: !canSend,
|
|
4671
|
+
onClick: submit,
|
|
4672
|
+
children: /* @__PURE__ */ jsx(ArrowUp, { className: "size-4" })
|
|
4673
|
+
})]
|
|
4674
|
+
})
|
|
4675
|
+
]
|
|
4676
|
+
}), attachments?.error ? /* @__PURE__ */ jsxs("div", {
|
|
4677
|
+
className: "mx-auto mt-1 flex w-full max-w-3xl items-center gap-2 text-label text-danger",
|
|
4678
|
+
children: [
|
|
4679
|
+
/* @__PURE__ */ jsx(TriangleAlert, { className: "size-3 shrink-0" }),
|
|
4680
|
+
/* @__PURE__ */ jsx("span", {
|
|
4681
|
+
className: "min-w-0 flex-1",
|
|
4682
|
+
children: attachments.error
|
|
4683
|
+
}),
|
|
4684
|
+
/* @__PURE__ */ jsx("button", {
|
|
4685
|
+
type: "button",
|
|
4686
|
+
onClick: attachments.dismissError,
|
|
4687
|
+
"aria-label": "Dismiss",
|
|
4688
|
+
className: "shrink-0 opacity-70 hover:opacity-100",
|
|
4689
|
+
children: /* @__PURE__ */ jsx(X, { className: "size-3" })
|
|
4690
|
+
})
|
|
4691
|
+
]
|
|
4692
|
+
}) : /* @__PURE__ */ jsx("div", {
|
|
4148
4693
|
className: "mx-auto mt-1 w-full max-w-3xl text-center text-label text-fg-4",
|
|
4149
4694
|
children: "Enter to send · Shift+Enter for a new line"
|
|
4150
4695
|
})]
|
|
4151
4696
|
});
|
|
4152
4697
|
}
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4698
|
+
/** Staged files as a scrolling row of chips above the field. The thumbnail is
|
|
4699
|
+
* the local blob, so nothing here waits on the network; the upload's state rides
|
|
4700
|
+
* on top of it and the ✕ takes it back off. */
|
|
4701
|
+
function AttachmentStrip({ attachments }) {
|
|
4702
|
+
return /* @__PURE__ */ jsx("div", {
|
|
4703
|
+
className: "flex gap-2 overflow-x-auto border-b border-border px-2 py-2",
|
|
4704
|
+
children: attachments.items.map((item) => /* @__PURE__ */ jsx(AttachmentChip, {
|
|
4705
|
+
item,
|
|
4706
|
+
onRetry: () => attachments.retry(item.key),
|
|
4707
|
+
onRemove: () => attachments.remove(item.key)
|
|
4708
|
+
}, item.key))
|
|
4709
|
+
});
|
|
4164
4710
|
}
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4711
|
+
function AttachmentChip({ item, onRetry, onRemove }) {
|
|
4712
|
+
const failed = item.status === "failed";
|
|
4713
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
4714
|
+
className: "group relative shrink-0",
|
|
4715
|
+
title: failed ? `${item.name} — ${item.error}` : `${item.name} · ${formatBytes(item.bytes)}`,
|
|
4716
|
+
children: [
|
|
4717
|
+
/* @__PURE__ */ jsx("div", {
|
|
4718
|
+
className: cn("flex size-14 items-center justify-center overflow-hidden rounded-md border border-border bg-surface", failed && "border-danger/50"),
|
|
4719
|
+
children: item.previewUrl ? /* @__PURE__ */ jsx("img", {
|
|
4720
|
+
src: item.previewUrl,
|
|
4721
|
+
alt: item.name,
|
|
4722
|
+
className: "size-full object-cover"
|
|
4723
|
+
}) : /* @__PURE__ */ jsxs("div", {
|
|
4724
|
+
className: "flex flex-col items-center gap-0.5 text-fg-3",
|
|
4725
|
+
children: [/* @__PURE__ */ jsx(FileText, { className: "size-4" }), /* @__PURE__ */ jsx("span", {
|
|
4726
|
+
className: "max-w-12 truncate text-[9px] font-semibold uppercase",
|
|
4727
|
+
children: extensionOf(item.name)
|
|
4728
|
+
})]
|
|
4729
|
+
})
|
|
4730
|
+
}),
|
|
4731
|
+
item.status === "uploading" ? /* @__PURE__ */ jsx("div", {
|
|
4732
|
+
className: "absolute inset-0 flex items-center justify-center rounded-md bg-black/35",
|
|
4733
|
+
children: /* @__PURE__ */ jsx(Spinner, { className: "size-4 text-white" })
|
|
4734
|
+
}) : null,
|
|
4735
|
+
failed ? /* @__PURE__ */ jsx("button", {
|
|
4736
|
+
type: "button",
|
|
4737
|
+
onClick: onRetry,
|
|
4738
|
+
"aria-label": `Retry ${item.name}`,
|
|
4739
|
+
className: "absolute inset-0 flex items-center justify-center rounded-md bg-black/45 text-warning",
|
|
4740
|
+
children: /* @__PURE__ */ jsx(RotateCw, { className: "size-4" })
|
|
4741
|
+
}) : null,
|
|
4742
|
+
/* @__PURE__ */ jsx("button", {
|
|
4743
|
+
type: "button",
|
|
4744
|
+
onClick: onRemove,
|
|
4745
|
+
"aria-label": `Remove ${item.name}`,
|
|
4746
|
+
className: "absolute -top-1 -right-1 flex size-4 items-center justify-center rounded-full border border-border bg-surface text-fg-3 shadow-(--shadow-xs) hover:text-fg-1",
|
|
4747
|
+
children: /* @__PURE__ */ jsx(X, { className: "size-2.5" })
|
|
4748
|
+
})
|
|
4749
|
+
]
|
|
4750
|
+
});
|
|
4751
|
+
}
|
|
4752
|
+
const extensionOf = (name) => {
|
|
4753
|
+
const dot = name.lastIndexOf(".");
|
|
4754
|
+
return dot > 0 ? name.slice(dot + 1).toUpperCase() : "FILE";
|
|
4755
|
+
};
|
|
4756
|
+
//#endregion
|
|
4757
|
+
//#region src/components/agent/ContextDialog.tsx
|
|
4758
|
+
/** The CLI reports category colors as its own theme token names ('inactive',
|
|
4759
|
+
* 'promptBorder', ...), not CSS colors — only pass through what CSS can render. */
|
|
4760
|
+
const cssColor$1 = (color) => typeof CSS !== "undefined" && CSS.supports("color", color) ? color : void 0;
|
|
4761
|
+
const usageTint$1 = (pct) => pct >= 90 ? "bg-danger" : pct >= 70 ? "bg-warning" : "bg-accent";
|
|
4762
|
+
/**
|
|
4763
|
+
* What is in the model's context window right now, category by category.
|
|
4764
|
+
*
|
|
4765
|
+
* One of the three panels the status bar opens. Context, usage and session info
|
|
4766
|
+
* are different questions asked at different moments, so they are different
|
|
4767
|
+
* screens rather than one "details" list you scroll past two answers to reach.
|
|
4768
|
+
*/
|
|
4769
|
+
function ContextDialog({ usage, open, onOpenChange }) {
|
|
4770
|
+
return /* @__PURE__ */ jsx(Dialog, {
|
|
4771
|
+
open,
|
|
4772
|
+
onOpenChange,
|
|
4773
|
+
children: /* @__PURE__ */ jsxs(DialogContent, { children: [/* @__PURE__ */ jsx(DialogHeader, {
|
|
4774
|
+
title: "Context",
|
|
4775
|
+
description: usage?.model
|
|
4776
|
+
}), /* @__PURE__ */ jsx(DialogBody, { children: !usage ? /* @__PURE__ */ jsx("p", {
|
|
4777
|
+
className: "py-6 text-center text-body-sm text-fg-4",
|
|
4778
|
+
children: "No reading yet — the context window is measured after a turn completes."
|
|
4779
|
+
}) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
4780
|
+
/* @__PURE__ */ jsxs("div", {
|
|
4781
|
+
className: "flex items-baseline justify-between gap-4",
|
|
4782
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
4783
|
+
className: "text-label text-fg-3",
|
|
4784
|
+
children: "Used"
|
|
4785
|
+
}), /* @__PURE__ */ jsxs("span", {
|
|
4786
|
+
className: "font-mono text-body-sm text-fg-1",
|
|
4787
|
+
children: [
|
|
4788
|
+
formatTokens(usage.totalTokens),
|
|
4789
|
+
" / ",
|
|
4790
|
+
formatTokens(usage.maxTokens),
|
|
4791
|
+
" ·",
|
|
4792
|
+
" ",
|
|
4793
|
+
usage.percentage.toFixed(0),
|
|
4794
|
+
"%"
|
|
4795
|
+
]
|
|
4796
|
+
})]
|
|
4797
|
+
}),
|
|
4798
|
+
/* @__PURE__ */ jsx("div", {
|
|
4799
|
+
className: "mt-2 h-2 overflow-hidden rounded-full bg-border",
|
|
4800
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
4801
|
+
className: cn("h-full rounded-full", usageTint$1(usage.percentage)),
|
|
4802
|
+
style: { width: `${Math.min(100, Math.max(2, usage.percentage))}%` }
|
|
4803
|
+
})
|
|
4804
|
+
}),
|
|
4805
|
+
usage.categories.length > 0 ? /* @__PURE__ */ jsxs("div", {
|
|
4806
|
+
className: "mt-5",
|
|
4807
|
+
children: [/* @__PURE__ */ jsx("h3", {
|
|
4808
|
+
className: "text-label font-medium text-fg-3",
|
|
4809
|
+
children: "Breakdown"
|
|
4810
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
4811
|
+
className: "mt-2 flex flex-col gap-3",
|
|
4812
|
+
children: usage.categories.map((category) => {
|
|
4813
|
+
const share = category.tokens / Math.max(usage.maxTokens, 1) * 100;
|
|
4814
|
+
const color = cssColor$1(category.color);
|
|
4815
|
+
return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
|
|
4816
|
+
className: "flex items-baseline justify-between gap-3",
|
|
4817
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
4818
|
+
className: "flex min-w-0 items-center gap-2",
|
|
4819
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
4820
|
+
className: "size-2 shrink-0 rounded-full bg-fg-4",
|
|
4821
|
+
style: color ? { backgroundColor: color } : void 0
|
|
4822
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
4823
|
+
className: "truncate text-body-sm text-fg-1",
|
|
4824
|
+
children: category.name
|
|
4825
|
+
})]
|
|
4826
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
4827
|
+
className: "shrink-0 font-mono text-label text-fg-3",
|
|
4828
|
+
children: formatTokens(category.tokens)
|
|
4829
|
+
})]
|
|
4830
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
4831
|
+
className: "mt-1 h-1 overflow-hidden rounded-full bg-border",
|
|
4832
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
4833
|
+
className: "h-full rounded-full bg-fg-4",
|
|
4834
|
+
style: {
|
|
4835
|
+
width: `${Math.min(100, share)}%`,
|
|
4836
|
+
backgroundColor: color
|
|
4837
|
+
}
|
|
4838
|
+
})
|
|
4839
|
+
})] }, category.name);
|
|
4840
|
+
})
|
|
4841
|
+
})]
|
|
4842
|
+
}) : null
|
|
4843
|
+
] }) })] })
|
|
4844
|
+
});
|
|
4845
|
+
}
|
|
4846
|
+
//#endregion
|
|
4847
|
+
//#region src/components/agent/HostFilesDialog.tsx
|
|
4848
|
+
/**
|
|
4849
|
+
* Browse the project the session is working in.
|
|
4850
|
+
*
|
|
4851
|
+
* Deliberately rooted at the session's cwd rather than at the server's
|
|
4852
|
+
* `hostFiles.roots`: the roots are the *security* boundary (the server enforces
|
|
4853
|
+
* them on every request), but what someone wants while watching an agent is this
|
|
4854
|
+
* project's tree. Read-only — writing is a separate server opt-in and not
|
|
4855
|
+
* something a session viewer should be doing behind the agent's back.
|
|
4856
|
+
*/
|
|
4857
|
+
function HostFilesDialog({ client, cwd, open, onOpenChange }) {
|
|
4858
|
+
const [path, setPath] = useState(cwd);
|
|
4859
|
+
const [entries, setEntries] = useState([]);
|
|
4860
|
+
const [truncated, setTruncated] = useState(false);
|
|
4861
|
+
const [query, setQuery] = useState("");
|
|
4862
|
+
const [matches, setMatches] = useState();
|
|
4863
|
+
const [file, setFile] = useState();
|
|
4864
|
+
const [loading, setLoading] = useState(false);
|
|
4865
|
+
const [error, setError] = useState();
|
|
4866
|
+
const list = useCallback(async (target) => {
|
|
4867
|
+
setLoading(true);
|
|
4868
|
+
setError(void 0);
|
|
4869
|
+
try {
|
|
4870
|
+
const response = await client.listHostDir(target);
|
|
4871
|
+
setPath(response.path);
|
|
4872
|
+
setEntries(response.entries);
|
|
4873
|
+
setTruncated(response.truncated ?? false);
|
|
4874
|
+
} catch (e) {
|
|
4875
|
+
setError(e instanceof Error ? e.message : "Could not read that directory");
|
|
4876
|
+
} finally {
|
|
4877
|
+
setLoading(false);
|
|
4878
|
+
}
|
|
4879
|
+
}, [client]);
|
|
4880
|
+
useEffect(() => {
|
|
4881
|
+
if (!open) return;
|
|
4882
|
+
setFile(void 0);
|
|
4883
|
+
setQuery("");
|
|
4884
|
+
setMatches(void 0);
|
|
4885
|
+
setError(void 0);
|
|
4886
|
+
if (cwd) list(cwd);
|
|
4887
|
+
}, [
|
|
4888
|
+
open,
|
|
4889
|
+
cwd,
|
|
4890
|
+
list
|
|
4891
|
+
]);
|
|
4892
|
+
useEffect(() => {
|
|
4893
|
+
if (!open || !cwd) return;
|
|
4894
|
+
const q = query.trim();
|
|
4895
|
+
if (!q) {
|
|
4896
|
+
setMatches(void 0);
|
|
4897
|
+
return;
|
|
4898
|
+
}
|
|
4899
|
+
const timer = setTimeout(() => {
|
|
4900
|
+
client.findHostFiles(cwd, q, 40).then((response) => setMatches(response.matches)).catch(() => setMatches([]));
|
|
4901
|
+
}, 150);
|
|
4902
|
+
return () => clearTimeout(timer);
|
|
4903
|
+
}, [
|
|
4904
|
+
client,
|
|
4905
|
+
cwd,
|
|
4906
|
+
query,
|
|
4907
|
+
open
|
|
4908
|
+
]);
|
|
4909
|
+
const openFile = async (target) => {
|
|
4910
|
+
setLoading(true);
|
|
4911
|
+
setError(void 0);
|
|
4912
|
+
try {
|
|
4913
|
+
const response = await client.readHostFile(target);
|
|
4914
|
+
setFile({
|
|
4915
|
+
path: response.path,
|
|
4916
|
+
bytes: response.bytes,
|
|
4917
|
+
content: response.encoding === "utf8" ? response.content : "(binary file — not shown)"
|
|
4918
|
+
});
|
|
4919
|
+
} catch (e) {
|
|
4920
|
+
setError(e instanceof Error ? e.message : "Could not read that file");
|
|
4921
|
+
} finally {
|
|
4922
|
+
setLoading(false);
|
|
4923
|
+
}
|
|
4924
|
+
};
|
|
4925
|
+
const parent = path && cwd && path !== cwd ? path.slice(0, path.lastIndexOf("/")) || "/" : void 0;
|
|
4926
|
+
const shown = matches ?? entries;
|
|
4927
|
+
return /* @__PURE__ */ jsx(Dialog, {
|
|
4928
|
+
open,
|
|
4929
|
+
onOpenChange,
|
|
4930
|
+
children: /* @__PURE__ */ jsxs(DialogContent, {
|
|
4931
|
+
size: "lg",
|
|
4932
|
+
children: [/* @__PURE__ */ jsx(DialogHeader, {
|
|
4933
|
+
title: file ? file.path.split("/").pop() : "Files",
|
|
4934
|
+
description: file ? file.path : path,
|
|
4935
|
+
actions: file ? /* @__PURE__ */ jsxs(Button, {
|
|
4936
|
+
variant: "ghost",
|
|
4937
|
+
size: "xs",
|
|
4938
|
+
onClick: () => setFile(void 0),
|
|
4939
|
+
children: [/* @__PURE__ */ jsx(ChevronLeft, { className: "size-3.5" }), "Back"]
|
|
4940
|
+
}) : parent ? /* @__PURE__ */ jsxs(Button, {
|
|
4941
|
+
variant: "ghost",
|
|
4942
|
+
size: "xs",
|
|
4943
|
+
onClick: () => void list(parent),
|
|
4944
|
+
children: [/* @__PURE__ */ jsx(ChevronLeft, { className: "size-3.5" }), "Up"]
|
|
4945
|
+
}) : null
|
|
4946
|
+
}), /* @__PURE__ */ jsxs(DialogBody, {
|
|
4947
|
+
className: "flex flex-col gap-3",
|
|
4948
|
+
children: [error ? /* @__PURE__ */ jsx("div", {
|
|
4949
|
+
className: "rounded-md bg-danger-bg px-3 py-2 text-body-sm text-danger",
|
|
4950
|
+
children: error
|
|
4951
|
+
}) : null, file ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("p", {
|
|
4952
|
+
className: "text-label text-fg-4",
|
|
4953
|
+
children: formatBytes(file.bytes)
|
|
4954
|
+
}), /* @__PURE__ */ jsx(CodeBlock, {
|
|
4955
|
+
code: file.content,
|
|
4956
|
+
label: file.path.split("/").pop()
|
|
4957
|
+
})] }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
4958
|
+
/* @__PURE__ */ jsxs("div", {
|
|
4959
|
+
className: "relative",
|
|
4960
|
+
children: [/* @__PURE__ */ jsx(Search, { className: "absolute top-1/2 left-2 size-3.5 -translate-y-1/2 text-fg-4" }), /* @__PURE__ */ jsx(Input, {
|
|
4961
|
+
value: query,
|
|
4962
|
+
onChange: (e) => setQuery(e.target.value),
|
|
4963
|
+
placeholder: "Search this project…",
|
|
4964
|
+
className: "pl-7",
|
|
4965
|
+
spellCheck: false
|
|
4966
|
+
})]
|
|
4967
|
+
}),
|
|
4968
|
+
loading && shown.length === 0 ? /* @__PURE__ */ jsx("div", {
|
|
4969
|
+
className: "py-6 text-center",
|
|
4970
|
+
children: /* @__PURE__ */ jsx(Spinner, { className: "size-4 text-fg-4" })
|
|
4971
|
+
}) : shown.length === 0 ? /* @__PURE__ */ jsx("p", {
|
|
4972
|
+
className: "py-6 text-center text-body-sm text-fg-4",
|
|
4973
|
+
children: matches ? "No matching files." : "This directory is empty."
|
|
4974
|
+
}) : /* @__PURE__ */ jsx("ul", {
|
|
4975
|
+
className: "flex flex-col",
|
|
4976
|
+
children: matches ? matches.map((match) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("button", {
|
|
4977
|
+
type: "button",
|
|
4978
|
+
onClick: () => void openFile(match.path),
|
|
4979
|
+
className: "flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left transition-colors hover:bg-surface-hover",
|
|
4980
|
+
children: [/* @__PURE__ */ jsx(File, { className: "size-3.5 shrink-0 text-fg-4" }), /* @__PURE__ */ jsx("span", {
|
|
4981
|
+
className: "min-w-0 flex-1 truncate font-mono text-label text-fg-1",
|
|
4982
|
+
children: match.relative
|
|
4983
|
+
})]
|
|
4984
|
+
}) }, match.path)) : entries.map((entry) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("button", {
|
|
4985
|
+
type: "button",
|
|
4986
|
+
onClick: () => entry.type === "dir" ? void list(entry.path) : void openFile(entry.path),
|
|
4987
|
+
className: "flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left transition-colors hover:bg-surface-hover",
|
|
4988
|
+
children: [
|
|
4989
|
+
/* @__PURE__ */ jsx(EntryIcon$1, { type: entry.type }),
|
|
4990
|
+
/* @__PURE__ */ jsxs("span", {
|
|
4991
|
+
className: "min-w-0 flex-1 truncate font-mono text-label text-fg-1",
|
|
4992
|
+
children: [entry.name, entry.type === "dir" ? "/" : ""]
|
|
4993
|
+
}),
|
|
4994
|
+
entry.bytes !== void 0 ? /* @__PURE__ */ jsx("span", {
|
|
4995
|
+
className: "shrink-0 text-label text-fg-4",
|
|
4996
|
+
children: formatBytes(entry.bytes)
|
|
4997
|
+
}) : null
|
|
4998
|
+
]
|
|
4999
|
+
}) }, entry.path))
|
|
5000
|
+
}),
|
|
5001
|
+
truncated && !matches ? /* @__PURE__ */ jsx("p", {
|
|
5002
|
+
className: "text-label text-fg-4",
|
|
5003
|
+
children: "More entries than the server will return — use the search box."
|
|
5004
|
+
}) : null
|
|
5005
|
+
] })]
|
|
5006
|
+
})]
|
|
5007
|
+
})
|
|
5008
|
+
});
|
|
5009
|
+
}
|
|
5010
|
+
/** A symlink is reported as itself and never silently resolved — following it is
|
|
5011
|
+
* the next request's problem, and that request is refused if it escapes the roots. */
|
|
5012
|
+
function EntryIcon$1({ type }) {
|
|
5013
|
+
if (type === "dir") return /* @__PURE__ */ jsx(Folder, { className: "size-3.5 shrink-0 text-accent" });
|
|
5014
|
+
if (type === "symlink") return /* @__PURE__ */ jsx(Link2, { className: "size-3.5 shrink-0 text-fg-4" });
|
|
5015
|
+
return /* @__PURE__ */ jsx(File, { className: "size-3.5 shrink-0 text-fg-4" });
|
|
5016
|
+
}
|
|
5017
|
+
//#endregion
|
|
5018
|
+
//#region src/components/agent/McpDialog.tsx
|
|
5019
|
+
/** The engine's status vocabulary is open — anything unrecognised renders
|
|
5020
|
+
* neutrally rather than being forced into one of these. */
|
|
5021
|
+
const STATUS_VARIANT = {
|
|
5022
|
+
connected: "success",
|
|
5023
|
+
failed: "danger",
|
|
5024
|
+
"needs-auth": "warning",
|
|
5025
|
+
pending: "info",
|
|
5026
|
+
disabled: "neutral"
|
|
5027
|
+
};
|
|
5028
|
+
/**
|
|
5029
|
+
* The session's MCP servers, at the CLI's own `/mcp` depth: servers → one server
|
|
5030
|
+
* → its tools → one tool, with Reconnect / Enable / Disable where they apply.
|
|
5031
|
+
*
|
|
5032
|
+
* Two things vary by engine rather than being fixed here. **The actions** exist
|
|
5033
|
+
* only where the engine has them (`canManageServers`): codex reports rich status
|
|
5034
|
+
* but has no per-server reconnect or toggle, so its panel is read-only. And
|
|
5035
|
+
* **tool parameters** appear only where the engine reports a schema — codex
|
|
5036
|
+
* returns each tool's full JSON Schema, the Agent SDK returns none at all, so
|
|
5037
|
+
* the tool view either renders it or says why it can't, rather than leaving a
|
|
5038
|
+
* silent gap or claiming the absence is universal.
|
|
5039
|
+
*/
|
|
5040
|
+
function McpDialog({ client, sessionId, open, onOpenChange, canManageServers = true }) {
|
|
5041
|
+
const [servers, setServers] = useState();
|
|
5042
|
+
const [error, setError] = useState();
|
|
5043
|
+
const [loading, setLoading] = useState(false);
|
|
5044
|
+
const [busyServer, setBusyServer] = useState();
|
|
5045
|
+
const [selectedServer, setSelectedServer] = useState();
|
|
5046
|
+
const [selectedTool, setSelectedTool] = useState();
|
|
5047
|
+
const load = useCallback(async () => {
|
|
5048
|
+
if (!sessionId) return;
|
|
5049
|
+
setLoading(true);
|
|
5050
|
+
setError(void 0);
|
|
5051
|
+
try {
|
|
5052
|
+
setServers(await client.listMcpServers(sessionId));
|
|
5053
|
+
} catch (e) {
|
|
5054
|
+
setError(e instanceof Error ? e.message : "Could not read MCP status");
|
|
5055
|
+
} finally {
|
|
5056
|
+
setLoading(false);
|
|
5057
|
+
}
|
|
5058
|
+
}, [client, sessionId]);
|
|
5059
|
+
useEffect(() => {
|
|
5060
|
+
if (!open) return;
|
|
5061
|
+
setSelectedServer(void 0);
|
|
5062
|
+
setSelectedTool(void 0);
|
|
5063
|
+
load();
|
|
5064
|
+
}, [open, load]);
|
|
5065
|
+
const act = async (name, action) => {
|
|
5066
|
+
if (!sessionId) return;
|
|
5067
|
+
setBusyServer(name);
|
|
5068
|
+
setError(void 0);
|
|
5069
|
+
try {
|
|
5070
|
+
setServers(await client.mcpServerAction(sessionId, name, action));
|
|
5071
|
+
} catch (e) {
|
|
5072
|
+
setError(e instanceof Error ? e.message : `Could not ${action} ${name}`);
|
|
5073
|
+
} finally {
|
|
5074
|
+
setBusyServer(void 0);
|
|
5075
|
+
}
|
|
5076
|
+
};
|
|
5077
|
+
const server = servers?.find((s) => s.name === selectedServer);
|
|
5078
|
+
const tool = server?.tools?.find((t) => t.name === selectedTool);
|
|
5079
|
+
return /* @__PURE__ */ jsx(Dialog, {
|
|
5080
|
+
open,
|
|
5081
|
+
onOpenChange,
|
|
5082
|
+
children: /* @__PURE__ */ jsxs(DialogContent, { children: [/* @__PURE__ */ jsx(DialogHeader, {
|
|
5083
|
+
title: tool?.name ?? server?.name ?? "MCP servers",
|
|
5084
|
+
description: tool ? `${server?.name} tool` : server ? server.serverInfo?.name : void 0,
|
|
5085
|
+
actions: selectedServer ? /* @__PURE__ */ jsxs(Button, {
|
|
5086
|
+
variant: "ghost",
|
|
5087
|
+
size: "xs",
|
|
5088
|
+
onClick: () => selectedTool ? setSelectedTool(void 0) : setSelectedServer(void 0),
|
|
5089
|
+
children: [/* @__PURE__ */ jsx(ChevronLeft, { className: "size-3.5" }), "Back"]
|
|
5090
|
+
}) : /* @__PURE__ */ jsxs(Button, {
|
|
5091
|
+
variant: "ghost",
|
|
5092
|
+
size: "xs",
|
|
5093
|
+
onClick: () => void load(),
|
|
5094
|
+
disabled: loading,
|
|
5095
|
+
children: [loading ? /* @__PURE__ */ jsx(Spinner, { className: "size-3 text-current" }) : /* @__PURE__ */ jsx(RotateCw, { className: "size-3" }), "Refresh"]
|
|
5096
|
+
})
|
|
5097
|
+
}), /* @__PURE__ */ jsxs(DialogBody, { children: [error ? /* @__PURE__ */ jsx("div", {
|
|
5098
|
+
className: "mb-3 rounded-md bg-danger-bg px-3 py-2 text-body-sm text-danger",
|
|
5099
|
+
children: error
|
|
5100
|
+
}) : null, tool ? /* @__PURE__ */ jsx(ToolView, { tool }) : server ? /* @__PURE__ */ jsx(ServerView, {
|
|
5101
|
+
server,
|
|
5102
|
+
busy: busyServer === server.name,
|
|
5103
|
+
canManage: canManageServers,
|
|
5104
|
+
onAct: (action) => void act(server.name, action),
|
|
5105
|
+
onSelectTool: setSelectedTool
|
|
5106
|
+
}) : error && !servers ? null : /* @__PURE__ */ jsx(ServerList, {
|
|
5107
|
+
servers,
|
|
5108
|
+
loading,
|
|
5109
|
+
onSelect: setSelectedServer
|
|
5110
|
+
})] })] })
|
|
5111
|
+
});
|
|
5112
|
+
}
|
|
5113
|
+
function ServerList({ servers, loading, onSelect }) {
|
|
5114
|
+
if (loading && !servers) return /* @__PURE__ */ jsx("div", {
|
|
5115
|
+
className: "py-6 text-center",
|
|
5116
|
+
children: /* @__PURE__ */ jsx(Spinner, { className: "size-4 text-fg-4" })
|
|
5117
|
+
});
|
|
5118
|
+
if (!servers?.length) return /* @__PURE__ */ jsx("p", {
|
|
5119
|
+
className: "py-6 text-center text-body-sm text-fg-4",
|
|
5120
|
+
children: "No MCP servers configured for this session."
|
|
5121
|
+
});
|
|
5122
|
+
return /* @__PURE__ */ jsx("div", {
|
|
5123
|
+
className: "flex flex-col gap-4",
|
|
5124
|
+
children: [...new Set(servers.map((s) => s.scope ?? "other"))].map((scope) => /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h3", {
|
|
5125
|
+
className: "text-label font-medium text-fg-3 capitalize",
|
|
5126
|
+
children: scope
|
|
5127
|
+
}), /* @__PURE__ */ jsx("ul", {
|
|
5128
|
+
className: "mt-1 flex flex-col",
|
|
5129
|
+
children: servers.filter((s) => (s.scope ?? "other") === scope).map((s) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("button", {
|
|
5130
|
+
type: "button",
|
|
5131
|
+
onClick: () => onSelect(s.name),
|
|
5132
|
+
className: "flex w-full items-center gap-2 rounded-md px-2 py-2 text-left transition-colors hover:bg-surface-hover",
|
|
5133
|
+
children: [
|
|
5134
|
+
/* @__PURE__ */ jsx("span", {
|
|
5135
|
+
className: "min-w-0 flex-1 truncate text-body-sm text-fg-1",
|
|
5136
|
+
children: s.name
|
|
5137
|
+
}),
|
|
5138
|
+
s.tools?.length ? /* @__PURE__ */ jsxs("span", {
|
|
5139
|
+
className: "shrink-0 text-label text-fg-4",
|
|
5140
|
+
children: [s.tools.length, " tools"]
|
|
5141
|
+
}) : null,
|
|
5142
|
+
/* @__PURE__ */ jsx(Badge, {
|
|
5143
|
+
variant: STATUS_VARIANT[s.status] ?? "neutral",
|
|
5144
|
+
dot: true,
|
|
5145
|
+
className: "shrink-0",
|
|
5146
|
+
children: s.status
|
|
5147
|
+
}),
|
|
5148
|
+
/* @__PURE__ */ jsx(ChevronRight, { className: "size-3.5 shrink-0 text-fg-4" })
|
|
5149
|
+
]
|
|
5150
|
+
}) }, s.name))
|
|
5151
|
+
})] }, scope))
|
|
5152
|
+
});
|
|
5153
|
+
}
|
|
5154
|
+
function ServerView({ server, busy, canManage, onAct, onSelectTool }) {
|
|
5155
|
+
const disabled = server.status === "disabled";
|
|
5156
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
5157
|
+
className: "flex flex-col gap-4",
|
|
5158
|
+
children: [
|
|
5159
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
5160
|
+
/* @__PURE__ */ jsx(DialogRow, {
|
|
5161
|
+
label: "Status",
|
|
5162
|
+
children: /* @__PURE__ */ jsx(Badge, {
|
|
5163
|
+
variant: STATUS_VARIANT[server.status] ?? "neutral",
|
|
5164
|
+
dot: true,
|
|
5165
|
+
children: server.status
|
|
5166
|
+
})
|
|
5167
|
+
}),
|
|
5168
|
+
server.transport ? /* @__PURE__ */ jsx(DialogRow, {
|
|
5169
|
+
label: "Transport",
|
|
5170
|
+
children: server.transport
|
|
5171
|
+
}) : null,
|
|
5172
|
+
server.scope ? /* @__PURE__ */ jsx(DialogRow, {
|
|
5173
|
+
label: "Scope",
|
|
5174
|
+
children: server.scope
|
|
5175
|
+
}) : null,
|
|
5176
|
+
server.serverInfo ? /* @__PURE__ */ jsxs(DialogRow, {
|
|
5177
|
+
label: "Server",
|
|
5178
|
+
children: [
|
|
5179
|
+
server.serverInfo.name,
|
|
5180
|
+
" ",
|
|
5181
|
+
server.serverInfo.version
|
|
5182
|
+
]
|
|
5183
|
+
}) : null,
|
|
5184
|
+
server.command ? /* @__PURE__ */ jsx(DialogRow, {
|
|
5185
|
+
label: "Command",
|
|
5186
|
+
mono: true,
|
|
5187
|
+
children: [server.command, ...server.args ?? []].join(" ")
|
|
5188
|
+
}) : null,
|
|
5189
|
+
server.url ? /* @__PURE__ */ jsx(DialogRow, {
|
|
5190
|
+
label: "URL",
|
|
5191
|
+
mono: true,
|
|
5192
|
+
children: server.url
|
|
5193
|
+
}) : null
|
|
5194
|
+
] }),
|
|
5195
|
+
server.error ? /* @__PURE__ */ jsx("div", {
|
|
5196
|
+
className: "rounded-md bg-danger-bg px-3 py-2 text-body-sm break-words text-danger",
|
|
5197
|
+
children: server.error
|
|
5198
|
+
}) : null,
|
|
5199
|
+
canManage ? /* @__PURE__ */ jsxs("div", {
|
|
5200
|
+
className: "flex gap-2",
|
|
5201
|
+
children: [/* @__PURE__ */ jsxs(Button, {
|
|
5202
|
+
variant: "outline",
|
|
5203
|
+
size: "sm",
|
|
5204
|
+
disabled: busy,
|
|
5205
|
+
onClick: () => onAct("reconnect"),
|
|
5206
|
+
children: [busy ? /* @__PURE__ */ jsx(Spinner, { className: "size-3 text-current" }) : /* @__PURE__ */ jsx(RotateCw, { className: "size-3" }), "Reconnect"]
|
|
5207
|
+
}), /* @__PURE__ */ jsxs(Button, {
|
|
5208
|
+
variant: "outline",
|
|
5209
|
+
size: "sm",
|
|
5210
|
+
disabled: busy,
|
|
5211
|
+
onClick: () => onAct(disabled ? "enable" : "disable"),
|
|
5212
|
+
children: [disabled ? /* @__PURE__ */ jsx(Power, { className: "size-3" }) : /* @__PURE__ */ jsx(PowerOff, { className: "size-3" }), disabled ? "Enable" : "Disable"]
|
|
5213
|
+
})]
|
|
5214
|
+
}) : null,
|
|
5215
|
+
/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h3", {
|
|
5216
|
+
className: "text-label font-medium text-fg-3",
|
|
5217
|
+
children: "Tools"
|
|
5218
|
+
}), !server.tools?.length ? /* @__PURE__ */ jsx("p", {
|
|
5219
|
+
className: "py-2 text-body-sm text-fg-4",
|
|
5220
|
+
children: server.status === "connected" ? "This server exposes no tools." : "Tools are listed once the server connects."
|
|
5221
|
+
}) : /* @__PURE__ */ jsx("ul", {
|
|
5222
|
+
className: "mt-1 flex flex-col",
|
|
5223
|
+
children: server.tools.map((t) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("button", {
|
|
5224
|
+
type: "button",
|
|
5225
|
+
onClick: () => onSelectTool(t.name),
|
|
5226
|
+
className: "flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left transition-colors hover:bg-surface-hover",
|
|
5227
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
5228
|
+
className: "min-w-0 flex-1 truncate font-mono text-label text-fg-1",
|
|
5229
|
+
children: t.name
|
|
5230
|
+
}), /* @__PURE__ */ jsx(ChevronRight, { className: "size-3.5 shrink-0 text-fg-4" })]
|
|
5231
|
+
}) }, t.name))
|
|
5232
|
+
})] })
|
|
5233
|
+
]
|
|
5234
|
+
});
|
|
5235
|
+
}
|
|
5236
|
+
function ToolView({ tool }) {
|
|
5237
|
+
const annotations = tool.annotations;
|
|
5238
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
5239
|
+
className: "flex flex-col gap-3",
|
|
5240
|
+
children: [
|
|
5241
|
+
tool.description ? /* @__PURE__ */ jsx("p", {
|
|
5242
|
+
className: "text-body-sm whitespace-pre-wrap text-fg-2",
|
|
5243
|
+
children: tool.description
|
|
5244
|
+
}) : /* @__PURE__ */ jsx("p", {
|
|
5245
|
+
className: "text-body-sm text-fg-4",
|
|
5246
|
+
children: "This tool carries no description."
|
|
5247
|
+
}),
|
|
5248
|
+
annotations ? /* @__PURE__ */ jsxs("div", {
|
|
5249
|
+
className: "flex flex-wrap gap-1.5",
|
|
5250
|
+
children: [
|
|
5251
|
+
annotations.readOnly ? /* @__PURE__ */ jsx(Badge, {
|
|
5252
|
+
variant: "success",
|
|
5253
|
+
children: "read-only"
|
|
5254
|
+
}) : null,
|
|
5255
|
+
annotations.destructive ? /* @__PURE__ */ jsx(Badge, {
|
|
5256
|
+
variant: "danger",
|
|
5257
|
+
children: "destructive"
|
|
5258
|
+
}) : null,
|
|
5259
|
+
annotations.openWorld ? /* @__PURE__ */ jsx(Badge, {
|
|
5260
|
+
variant: "warning",
|
|
5261
|
+
children: "open world"
|
|
5262
|
+
}) : null
|
|
5263
|
+
]
|
|
5264
|
+
}) : null,
|
|
5265
|
+
tool.inputSchema !== void 0 ? /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h3", {
|
|
5266
|
+
className: "text-label font-medium text-fg-3",
|
|
5267
|
+
children: "Parameters"
|
|
5268
|
+
}), /* @__PURE__ */ jsx("pre", {
|
|
5269
|
+
className: "mt-1 max-h-64 overflow-auto rounded-md bg-surface px-3 py-2 text-label text-fg-2",
|
|
5270
|
+
children: safeSchema(tool.inputSchema)
|
|
5271
|
+
})] }) : /* @__PURE__ */ jsx("p", {
|
|
5272
|
+
className: cn("text-label text-fg-4"),
|
|
5273
|
+
children: "Parameters aren’t available: this engine names and describes each tool but reports no input schema."
|
|
5274
|
+
})
|
|
5275
|
+
]
|
|
5276
|
+
});
|
|
5277
|
+
}
|
|
5278
|
+
/** The schema is an opaque JSON document from another process — pretty-print it,
|
|
5279
|
+
* and never let an unserializable value take the dialog down with it. */
|
|
5280
|
+
function safeSchema(schema) {
|
|
5281
|
+
try {
|
|
5282
|
+
return JSON.stringify(schema, null, 2);
|
|
5283
|
+
} catch {
|
|
5284
|
+
return String(schema);
|
|
5285
|
+
}
|
|
5286
|
+
}
|
|
5287
|
+
//#endregion
|
|
5288
|
+
//#region src/components/agent/SkillsDialog.tsx
|
|
5289
|
+
/** Where the skill came from. The engine's set is open, so an unrecognised
|
|
5290
|
+
* scope renders as itself rather than being forced into a bucket. */
|
|
5291
|
+
const SCOPE_LABEL = {
|
|
5292
|
+
user: "Personal",
|
|
5293
|
+
repo: "This project",
|
|
5294
|
+
system: "System",
|
|
5295
|
+
admin: "Managed"
|
|
5296
|
+
};
|
|
5297
|
+
/**
|
|
5298
|
+
* What this session's engine can do beyond its own tools: the skills it found,
|
|
5299
|
+
* grouped by where they came from, with one drilled-down view each.
|
|
5300
|
+
*
|
|
5301
|
+
* The framing matters more here than in most panels. A skill is **not** a
|
|
5302
|
+
* command — the model decides to use one by reading its description, and there
|
|
5303
|
+
* is no wire syntax that invokes it. So this screen is a *discovery* surface,
|
|
5304
|
+
* and the one action it offers ("Use this skill") is honest about being a
|
|
5305
|
+
* drafting aid: it types a message for the operator to edit and send.
|
|
5306
|
+
*
|
|
5307
|
+
* Fed from the session's `skills` event rather than a REST route, because that
|
|
5308
|
+
* is the channel the engine refreshes on its own when a skill changes on disk.
|
|
5309
|
+
*/
|
|
5310
|
+
function SkillsDialog({ skills, open, onOpenChange, onUse }) {
|
|
5311
|
+
const [selected, setSelected] = useState();
|
|
5312
|
+
const skill = skills?.find((s) => s.name === selected);
|
|
5313
|
+
return /* @__PURE__ */ jsx(Dialog, {
|
|
5314
|
+
open,
|
|
5315
|
+
onOpenChange: (next) => {
|
|
5316
|
+
if (!next) setSelected(void 0);
|
|
5317
|
+
onOpenChange(next);
|
|
5318
|
+
},
|
|
5319
|
+
children: /* @__PURE__ */ jsxs(DialogContent, { children: [/* @__PURE__ */ jsx(DialogHeader, {
|
|
5320
|
+
title: skill ? skill.displayName ?? skill.name : "Skills",
|
|
5321
|
+
description: skill ? skill.name !== (skill.displayName ?? skill.name) ? skill.name : void 0 : "Capabilities the agent can choose to use. Not commands — the model picks them from their descriptions.",
|
|
5322
|
+
actions: skill ? /* @__PURE__ */ jsxs(Button, {
|
|
5323
|
+
variant: "ghost",
|
|
5324
|
+
size: "xs",
|
|
5325
|
+
onClick: () => setSelected(void 0),
|
|
5326
|
+
children: [/* @__PURE__ */ jsx(ChevronLeft, { className: "size-3.5" }), "Back"]
|
|
5327
|
+
}) : void 0
|
|
5328
|
+
}), /* @__PURE__ */ jsx(DialogBody, { children: skill ? /* @__PURE__ */ jsx(SkillView, {
|
|
5329
|
+
skill,
|
|
5330
|
+
onUse,
|
|
5331
|
+
onUsed: () => onOpenChange(false)
|
|
5332
|
+
}) : /* @__PURE__ */ jsx(SkillList, {
|
|
5333
|
+
skills,
|
|
5334
|
+
onSelect: setSelected
|
|
5335
|
+
}) })] })
|
|
5336
|
+
});
|
|
5337
|
+
}
|
|
5338
|
+
function SkillList({ skills, onSelect }) {
|
|
5339
|
+
if (!skills) return /* @__PURE__ */ jsx("p", {
|
|
5340
|
+
className: "py-6 text-center text-body-sm text-fg-4",
|
|
5341
|
+
children: "Skills are listed once the session connects — send a message first."
|
|
5342
|
+
});
|
|
5343
|
+
if (skills.length === 0) return /* @__PURE__ */ jsx("p", {
|
|
5344
|
+
className: "py-6 text-center text-body-sm text-fg-4",
|
|
5345
|
+
children: "This session found no skills."
|
|
5346
|
+
});
|
|
5347
|
+
return /* @__PURE__ */ jsx("div", {
|
|
5348
|
+
className: "flex flex-col gap-4",
|
|
5349
|
+
children: [...new Set(skills.map((s) => s.scope ?? "other"))].map((scope) => /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h3", {
|
|
5350
|
+
className: "text-label font-medium text-fg-3",
|
|
5351
|
+
children: SCOPE_LABEL[scope] ?? scope
|
|
5352
|
+
}), /* @__PURE__ */ jsx("ul", {
|
|
5353
|
+
className: "mt-1 flex flex-col",
|
|
5354
|
+
children: skills.filter((s) => (s.scope ?? "other") === scope).map((s) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("button", {
|
|
5355
|
+
type: "button",
|
|
5356
|
+
onClick: () => onSelect(s.name),
|
|
5357
|
+
className: "flex w-full items-start gap-2 rounded-md px-2 py-2 text-left transition-colors hover:bg-surface-hover",
|
|
5358
|
+
children: [
|
|
5359
|
+
/* @__PURE__ */ jsxs("span", {
|
|
5360
|
+
className: "min-w-0 flex-1",
|
|
5361
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
5362
|
+
className: "block truncate text-body-sm text-fg-1",
|
|
5363
|
+
children: s.displayName ?? s.name
|
|
5364
|
+
}), s.shortDescription ?? s.description ? /* @__PURE__ */ jsx("span", {
|
|
5365
|
+
className: "block truncate text-label text-fg-4",
|
|
5366
|
+
children: s.shortDescription ?? s.description
|
|
5367
|
+
}) : null]
|
|
5368
|
+
}),
|
|
5369
|
+
!s.enabled ? /* @__PURE__ */ jsx(Badge, {
|
|
5370
|
+
variant: "neutral",
|
|
5371
|
+
className: "mt-0.5 shrink-0",
|
|
5372
|
+
children: "off"
|
|
5373
|
+
}) : null,
|
|
5374
|
+
/* @__PURE__ */ jsx(ChevronRight, { className: "mt-0.5 size-3.5 shrink-0 text-fg-4" })
|
|
5375
|
+
]
|
|
5376
|
+
}) }, s.name))
|
|
5377
|
+
})] }, scope))
|
|
5378
|
+
});
|
|
5379
|
+
}
|
|
5380
|
+
function SkillView({ skill, onUse, onUsed }) {
|
|
5381
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
5382
|
+
className: "flex flex-col gap-4",
|
|
5383
|
+
children: [
|
|
5384
|
+
/* @__PURE__ */ jsxs("div", { children: [skill.scope ? /* @__PURE__ */ jsx(DialogRow, {
|
|
5385
|
+
label: "Scope",
|
|
5386
|
+
children: SCOPE_LABEL[skill.scope] ?? skill.scope
|
|
5387
|
+
}) : null, /* @__PURE__ */ jsx(DialogRow, {
|
|
5388
|
+
label: "Status",
|
|
5389
|
+
children: /* @__PURE__ */ jsx(Badge, {
|
|
5390
|
+
variant: skill.enabled ? "success" : "neutral",
|
|
5391
|
+
dot: true,
|
|
5392
|
+
children: skill.enabled ? "enabled" : "disabled"
|
|
5393
|
+
})
|
|
5394
|
+
})] }),
|
|
5395
|
+
skill.description ? /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h3", {
|
|
5396
|
+
className: "text-label font-medium text-fg-3",
|
|
5397
|
+
children: "Description"
|
|
5398
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
5399
|
+
className: "mt-1 text-body-sm whitespace-pre-wrap text-fg-2",
|
|
5400
|
+
children: skill.description
|
|
5401
|
+
})] }) : /* @__PURE__ */ jsx("p", {
|
|
5402
|
+
className: "text-body-sm text-fg-4",
|
|
5403
|
+
children: "This skill carries no description."
|
|
5404
|
+
}),
|
|
5405
|
+
onUse && skill.enabled ? /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx(Button, {
|
|
5406
|
+
variant: "outline",
|
|
5407
|
+
size: "sm",
|
|
5408
|
+
onClick: () => {
|
|
5409
|
+
onUse(skill);
|
|
5410
|
+
onUsed();
|
|
5411
|
+
},
|
|
5412
|
+
children: "Use this skill"
|
|
5413
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
5414
|
+
className: "mt-1.5 text-label text-fg-4",
|
|
5415
|
+
children: "Writes an opening message into the composer for you to edit — it isn’t sent, and there is no command that runs a skill directly."
|
|
5416
|
+
})] }) : null
|
|
5417
|
+
]
|
|
5418
|
+
});
|
|
5419
|
+
}
|
|
5420
|
+
//#endregion
|
|
5421
|
+
//#region src/components/agent/ModelSelect.tsx
|
|
5422
|
+
/** The CLI's supportedModels list leads with a "Default (recommended)" row whose value
|
|
5423
|
+
* is a sentinel, not a model id — selecting it means "clear the override". */
|
|
5424
|
+
const isDefaultOption = (value) => value === "default";
|
|
5425
|
+
/** Everything before a '[1m]'-style context-window suffix. */
|
|
5426
|
+
const dropVariant = (id) => id.replace(/\[.*\]$/, "");
|
|
5427
|
+
/** 'claude-opus-4-8' → "opus", 'sonnet' → "sonnet". The vendor prefix and the
|
|
5428
|
+
* version tail are dropped; what is left is the name a person would say. */
|
|
5429
|
+
function family(id) {
|
|
5430
|
+
const parts = id.toLowerCase().split("-");
|
|
5431
|
+
if (parts[0] === "claude") parts.shift();
|
|
5432
|
+
return parts[0] ?? "";
|
|
5433
|
+
}
|
|
5434
|
+
/**
|
|
5435
|
+
* Whether a row is the one naming `model`.
|
|
5436
|
+
*
|
|
5437
|
+
* Three passes, narrowest first, because the rows and the id a session *reports*
|
|
5438
|
+
* are written differently: the rows are aliases ('opus[1m]', 'sonnet',
|
|
5439
|
+
* 'claude-fable-5[1m]') and a session reports a resolved wire id
|
|
5440
|
+
* ('claude-opus-5[1m]'). `resolvedModel` is the server's own answer to this and
|
|
5441
|
+
* wins when present; the family fallback covers a server that doesn't send it,
|
|
5442
|
+
* which is the difference between the chip reading "Opus 5" and reading
|
|
5443
|
+
* `claude-opus-5[1m]`. Kept identical to the iOS client's `ModelOption.matches`.
|
|
5444
|
+
*/
|
|
5445
|
+
function optionMatches(option, model) {
|
|
5446
|
+
if (model === option.value || model === option.resolvedModel) return true;
|
|
5447
|
+
const stripped = dropVariant(model);
|
|
5448
|
+
if (option.resolvedModel) return stripped === dropVariant(option.resolvedModel);
|
|
5449
|
+
const token = family(stripped);
|
|
5450
|
+
return token !== "" && token === family(dropVariant(option.value));
|
|
5451
|
+
}
|
|
5452
|
+
/** Find the option matching a (possibly decorated/aliased) session model id. */
|
|
5453
|
+
function matchModel(models, model) {
|
|
5454
|
+
if (!model) return void 0;
|
|
5455
|
+
return models.filter((m) => !isDefaultOption(m.value)).find((m) => optionMatches(m, model));
|
|
5456
|
+
}
|
|
5457
|
+
/** Compact model switcher for the composer toolbar; fed by the `capabilities` event.
|
|
5458
|
+
* Rows render CLI-style: bold display name with the model's description beneath. */
|
|
5459
|
+
function ModelSelect({ models, model, onModelChange, variant = "toolbar", disabled, className }) {
|
|
5460
|
+
const selected = matchModel(models, model) ?? (model ? void 0 : models.find((m) => isDefaultOption(m.value)));
|
|
5461
|
+
return /* @__PURE__ */ jsxs(Select, {
|
|
5462
|
+
items: models.map((m) => ({
|
|
5463
|
+
value: m.value,
|
|
5464
|
+
label: m.displayName
|
|
5465
|
+
})),
|
|
5466
|
+
value: selected?.value ?? null,
|
|
5467
|
+
onValueChange: (value) => {
|
|
5468
|
+
if (typeof value !== "string" || value === selected?.value) return;
|
|
5469
|
+
onModelChange(isDefaultOption(value) ? void 0 : value);
|
|
5470
|
+
},
|
|
5471
|
+
disabled,
|
|
5472
|
+
children: [/* @__PURE__ */ jsx(SelectTrigger, {
|
|
5473
|
+
"aria-label": "Model",
|
|
5474
|
+
className: cn(variant === "toolbar" && "h-6 max-w-56 border-transparent bg-transparent text-fg-3 hover:bg-surface-hover", className),
|
|
5475
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
5476
|
+
className: cn("truncate", variant === "toolbar" && "font-mono text-label"),
|
|
5477
|
+
children: /* @__PURE__ */ jsx(SelectValue, { placeholder: model ?? "model" })
|
|
5478
|
+
})
|
|
5479
|
+
}), /* @__PURE__ */ jsx(SelectContent, {
|
|
4188
5480
|
className: "min-w-72",
|
|
4189
5481
|
children: models.map((m) => /* @__PURE__ */ jsxs(SelectItem, {
|
|
4190
5482
|
value: m.value,
|
|
@@ -4201,44 +5493,67 @@ function ModelSelect({ models, model, onModelChange, variant = "toolbar", disabl
|
|
|
4201
5493
|
}
|
|
4202
5494
|
//#endregion
|
|
4203
5495
|
//#region src/components/agent/PermissionModeSelect.tsx
|
|
4204
|
-
/**
|
|
5496
|
+
/**
|
|
5497
|
+
* The modes surfaced across UI surfaces (session creation, in-session switcher),
|
|
5498
|
+
* ordered by how much of the approval gate they give away.
|
|
5499
|
+
*
|
|
5500
|
+
* Notably `default` is **"Manual"**: the wire value is `default`, but calling it
|
|
5501
|
+
* that in the UI conflates a real mode (ask me every time) with "whatever the
|
|
5502
|
+
* server picked", which is the one confusion a mode chip exists to avoid. The
|
|
5503
|
+
* naming, the icons and the summaries are shared with the iOS app on purpose —
|
|
5504
|
+
* the two surfaces should read as the same list.
|
|
5505
|
+
*/
|
|
4205
5506
|
const PERMISSION_MODES = [
|
|
4206
5507
|
{
|
|
4207
5508
|
value: "default",
|
|
4208
|
-
label: "
|
|
4209
|
-
|
|
5509
|
+
label: "Manual",
|
|
5510
|
+
shortLabel: "Manual",
|
|
5511
|
+
description: "Always ask before making changes",
|
|
5512
|
+
icon: Hand
|
|
4210
5513
|
},
|
|
4211
5514
|
{
|
|
4212
5515
|
value: "acceptEdits",
|
|
4213
|
-
label: "
|
|
4214
|
-
|
|
5516
|
+
label: "Accept edits",
|
|
5517
|
+
shortLabel: "Edits",
|
|
5518
|
+
description: "Automatically accept all file edits",
|
|
5519
|
+
icon: Code
|
|
4215
5520
|
},
|
|
4216
5521
|
{
|
|
4217
5522
|
value: "plan",
|
|
4218
|
-
label: "
|
|
4219
|
-
|
|
5523
|
+
label: "Plan",
|
|
5524
|
+
shortLabel: "Plan",
|
|
5525
|
+
description: "Create a plan before making changes",
|
|
5526
|
+
icon: ClipboardList
|
|
4220
5527
|
},
|
|
4221
5528
|
{
|
|
4222
5529
|
value: "auto",
|
|
4223
|
-
label: "
|
|
4224
|
-
|
|
5530
|
+
label: "Auto",
|
|
5531
|
+
shortLabel: "Auto",
|
|
5532
|
+
description: "Claude handles permission decisions",
|
|
5533
|
+
icon: Zap
|
|
4225
5534
|
},
|
|
4226
5535
|
{
|
|
4227
5536
|
value: "dontAsk",
|
|
4228
|
-
label: "
|
|
4229
|
-
|
|
5537
|
+
label: "Don't ask",
|
|
5538
|
+
shortLabel: "Don't ask",
|
|
5539
|
+
description: "Never ask — deny anything not pre-approved",
|
|
5540
|
+
icon: ShieldCheck
|
|
4230
5541
|
},
|
|
4231
5542
|
{
|
|
4232
5543
|
value: "bypassPermissions",
|
|
4233
|
-
label: "
|
|
4234
|
-
|
|
5544
|
+
label: "Bypass permissions",
|
|
5545
|
+
shortLabel: "Bypass",
|
|
5546
|
+
description: "Skip every approval — the agent is unsupervised",
|
|
5547
|
+
icon: TriangleAlert,
|
|
4235
5548
|
dangerous: true
|
|
4236
5549
|
}
|
|
4237
5550
|
];
|
|
5551
|
+
const permissionModeMeta = (mode) => PERMISSION_MODES.find((m) => m.value === mode);
|
|
4238
5552
|
/** Permission-mode switcher: compact in the composer toolbar, field-sized in forms. */
|
|
4239
|
-
function PermissionModeSelect({ mode, onModeChange, modes, variant = "toolbar", disabled, className }) {
|
|
5553
|
+
function PermissionModeSelect({ mode, onModeChange, modes, canBypass, variant = "toolbar", disabled, className }) {
|
|
4240
5554
|
const dangerous = mode === "bypassPermissions";
|
|
4241
5555
|
const offered = modes ? PERMISSION_MODES.filter((m) => modes.includes(m.value)) : PERMISSION_MODES;
|
|
5556
|
+
const unavailable = (meta) => meta.value === "bypassPermissions" && canBypass === false;
|
|
4242
5557
|
return /* @__PURE__ */ jsxs(Select, {
|
|
4243
5558
|
items: offered.map((m) => ({
|
|
4244
5559
|
value: m.value,
|
|
@@ -4253,46 +5568,131 @@ function PermissionModeSelect({ mode, onModeChange, modes, variant = "toolbar",
|
|
|
4253
5568
|
"aria-label": "Permission mode",
|
|
4254
5569
|
className: cn(variant === "toolbar" && "h-6 max-w-44 border-transparent bg-transparent hover:bg-surface-hover", dangerous ? "text-danger" : variant === "toolbar" ? "text-fg-3" : void 0, className),
|
|
4255
5570
|
children: /* @__PURE__ */ jsx("span", {
|
|
4256
|
-
className: cn("truncate", variant === "toolbar" && "
|
|
5571
|
+
className: cn("truncate", variant === "toolbar" && "text-label"),
|
|
4257
5572
|
children: /* @__PURE__ */ jsx(SelectValue, { placeholder: "permissions" })
|
|
4258
5573
|
})
|
|
4259
5574
|
}), /* @__PURE__ */ jsx(SelectContent, {
|
|
4260
|
-
className: "min-w-
|
|
4261
|
-
children: offered.map((m) =>
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
5575
|
+
className: "min-w-64",
|
|
5576
|
+
children: offered.map((m) => {
|
|
5577
|
+
const blocked = unavailable(m);
|
|
5578
|
+
return /* @__PURE__ */ jsxs(SelectItem, {
|
|
5579
|
+
value: m.value,
|
|
5580
|
+
disabled: blocked,
|
|
5581
|
+
children: [/* @__PURE__ */ jsx(SelectItemText, { children: /* @__PURE__ */ jsxs("span", {
|
|
5582
|
+
className: "flex items-center gap-2",
|
|
5583
|
+
children: [/* @__PURE__ */ jsx(m.icon, { className: cn("size-3.5 shrink-0", m.dangerous ? "text-danger" : "text-fg-3") }), /* @__PURE__ */ jsx("span", {
|
|
5584
|
+
className: cn("font-medium", m.dangerous && "text-danger"),
|
|
5585
|
+
children: m.label
|
|
5586
|
+
})]
|
|
5587
|
+
}) }), /* @__PURE__ */ jsx("span", {
|
|
5588
|
+
className: cn("pl-5.5 text-label", blocked ? "text-fg-4" : m.dangerous ? "text-danger/80" : "text-fg-4"),
|
|
5589
|
+
children: blocked ? "Only available to a session started in this mode" : m.description
|
|
5590
|
+
})]
|
|
5591
|
+
}, m.value);
|
|
5592
|
+
})
|
|
4271
5593
|
})]
|
|
4272
5594
|
});
|
|
4273
5595
|
}
|
|
4274
5596
|
//#endregion
|
|
5597
|
+
//#region src/lib/tool-icon.ts
|
|
5598
|
+
/**
|
|
5599
|
+
* An icon per tool, so a transcript can be skimmed by shape rather than read.
|
|
5600
|
+
*
|
|
5601
|
+
* The same mapping the iOS app makes, in lucide's vocabulary rather than SF
|
|
5602
|
+
* Symbols — the two clients should be recognisably showing the same thing. An
|
|
5603
|
+
* unknown tool falls back to a wrench, and an MCP tool (`mcp__server__name`) to
|
|
5604
|
+
* the puzzle piece the MCP screens use, because "which server is this from" is
|
|
5605
|
+
* the useful thing to see at a glance.
|
|
5606
|
+
*/
|
|
5607
|
+
function toolIcon(toolName) {
|
|
5608
|
+
switch (toolName) {
|
|
5609
|
+
case "Bash":
|
|
5610
|
+
case "BashOutput":
|
|
5611
|
+
case "KillShell": return Terminal;
|
|
5612
|
+
case "Read": return FileText;
|
|
5613
|
+
case "Write": return SquarePen;
|
|
5614
|
+
case "Edit":
|
|
5615
|
+
case "MultiEdit":
|
|
5616
|
+
case "NotebookEdit": return PencilLine;
|
|
5617
|
+
case "Glob": return FolderSearch;
|
|
5618
|
+
case "Grep": return Search;
|
|
5619
|
+
case "WebFetch": return ArrowDownCircle;
|
|
5620
|
+
case "WebSearch": return Globe;
|
|
5621
|
+
case "Task":
|
|
5622
|
+
case "Agent": return UsersRound;
|
|
5623
|
+
case "TodoWrite": return CheckSquare;
|
|
5624
|
+
case "Skill": return Sparkles;
|
|
5625
|
+
case "AskUserQuestion": return MessageCircleQuestion;
|
|
5626
|
+
case "CodexCommand": return Terminal;
|
|
5627
|
+
case "CodexFileChange": return FileDiff;
|
|
5628
|
+
case "CodexWebSearch": return Globe;
|
|
5629
|
+
case "CodexImageGeneration":
|
|
5630
|
+
case "CodexImageView": return Image;
|
|
5631
|
+
default: return toolName.startsWith("mcp__") ? Puzzle : Wrench;
|
|
5632
|
+
}
|
|
5633
|
+
}
|
|
5634
|
+
//#endregion
|
|
4275
5635
|
//#region src/components/agent/PermissionPrompt.tsx
|
|
5636
|
+
/**
|
|
5637
|
+
* Generic allow/deny prompt for a pending permission request.
|
|
5638
|
+
*
|
|
5639
|
+
* Three outcomes, not two: denying usually means "not that, try something else",
|
|
5640
|
+
* so plain Deny lets the turn continue (with an optional reason the agent reads)
|
|
5641
|
+
* while "Deny & stop" also interrupts.
|
|
5642
|
+
*
|
|
5643
|
+
* The heading is whatever the engine authored — `title`, else `displayName`.
|
|
5644
|
+
* Composing "wants to use {tool}" instead would be wrong for codex, where an
|
|
5645
|
+
* approval is usually an *escalation after a sandbox refusal* and the runner has
|
|
5646
|
+
* already written the sentence that says so.
|
|
5647
|
+
*/
|
|
4276
5648
|
function PermissionPrompt({ request, onApprove, onDeny, className }) {
|
|
4277
5649
|
const [showInput, setShowInput] = useState(false);
|
|
4278
|
-
|
|
5650
|
+
const [denying, setDenying] = useState(false);
|
|
5651
|
+
const [reason, setReason] = useState("");
|
|
5652
|
+
const deny = (interrupt) => {
|
|
5653
|
+
const message = reason.trim();
|
|
5654
|
+
onDeny(request.id, message || void 0, interrupt || void 0);
|
|
5655
|
+
setReason("");
|
|
5656
|
+
setDenying(false);
|
|
5657
|
+
};
|
|
5658
|
+
const summary = toolInputPreview(request.input);
|
|
5659
|
+
const ToolIcon = toolIcon(request.toolName);
|
|
5660
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
4279
5661
|
"data-slot": "permission-prompt",
|
|
4280
5662
|
className: cn("rounded-lg border border-warning/40 bg-warning-bg p-3", className),
|
|
4281
|
-
children: /* @__PURE__ */ jsxs("div", {
|
|
5663
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
4282
5664
|
className: "flex items-start gap-2.5",
|
|
4283
5665
|
children: [
|
|
4284
|
-
/* @__PURE__ */ jsx(
|
|
5666
|
+
/* @__PURE__ */ jsx(Hand, { className: "mt-0.5 size-4 shrink-0 text-warning" }),
|
|
4285
5667
|
/* @__PURE__ */ jsxs("div", {
|
|
4286
5668
|
className: "min-w-0 flex-1",
|
|
4287
5669
|
children: [
|
|
4288
5670
|
/* @__PURE__ */ jsx("div", {
|
|
4289
5671
|
className: "text-body-sm font-medium text-fg-1",
|
|
4290
|
-
children: request.title ??
|
|
5672
|
+
children: request.title ?? request.displayName ?? "Permission needed"
|
|
4291
5673
|
}),
|
|
4292
5674
|
request.description ? /* @__PURE__ */ jsx("div", {
|
|
4293
5675
|
className: "mt-0.5 text-label text-fg-3",
|
|
4294
5676
|
children: request.description
|
|
4295
5677
|
}) : null,
|
|
5678
|
+
request.decisionReason ? /* @__PURE__ */ jsx("div", {
|
|
5679
|
+
className: "mt-0.5 text-label text-fg-4",
|
|
5680
|
+
children: request.decisionReason
|
|
5681
|
+
}) : null,
|
|
5682
|
+
/* @__PURE__ */ jsxs("div", {
|
|
5683
|
+
className: "mt-1.5 flex min-w-0 items-center gap-2",
|
|
5684
|
+
children: [
|
|
5685
|
+
/* @__PURE__ */ jsx(ToolIcon, { className: "size-3 shrink-0 text-fg-3" }),
|
|
5686
|
+
/* @__PURE__ */ jsx("span", {
|
|
5687
|
+
className: "shrink-0 font-mono text-label font-medium text-fg-2",
|
|
5688
|
+
children: request.toolName
|
|
5689
|
+
}),
|
|
5690
|
+
summary ? /* @__PURE__ */ jsx("span", {
|
|
5691
|
+
className: "min-w-0 truncate font-mono text-label text-fg-4",
|
|
5692
|
+
children: summary
|
|
5693
|
+
}) : null
|
|
5694
|
+
]
|
|
5695
|
+
}),
|
|
4296
5696
|
/* @__PURE__ */ jsxs("button", {
|
|
4297
5697
|
type: "button",
|
|
4298
5698
|
className: "mt-1 font-mono text-label text-fg-3 underline-offset-2 hover:underline",
|
|
@@ -4311,20 +5711,56 @@ function PermissionPrompt({ request, onApprove, onDeny, className }) {
|
|
|
4311
5711
|
]
|
|
4312
5712
|
}),
|
|
4313
5713
|
/* @__PURE__ */ jsxs("div", {
|
|
4314
|
-
className: "flex shrink-0 gap-1.5",
|
|
4315
|
-
children: [
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
5714
|
+
className: "flex shrink-0 flex-wrap justify-end gap-1.5",
|
|
5715
|
+
children: [
|
|
5716
|
+
/* @__PURE__ */ jsx(Button, {
|
|
5717
|
+
size: "sm",
|
|
5718
|
+
onClick: () => onApprove(request.id),
|
|
5719
|
+
children: "Allow"
|
|
5720
|
+
}),
|
|
5721
|
+
/* @__PURE__ */ jsx(Button, {
|
|
5722
|
+
size: "sm",
|
|
5723
|
+
variant: "outline",
|
|
5724
|
+
onClick: () => setDenying((v) => !v),
|
|
5725
|
+
children: "Deny"
|
|
5726
|
+
}),
|
|
5727
|
+
/* @__PURE__ */ jsx(Button, {
|
|
5728
|
+
size: "sm",
|
|
5729
|
+
variant: "outline",
|
|
5730
|
+
onClick: () => deny(true),
|
|
5731
|
+
children: "Deny & stop"
|
|
5732
|
+
})
|
|
5733
|
+
]
|
|
4325
5734
|
})
|
|
4326
5735
|
]
|
|
4327
|
-
})
|
|
5736
|
+
}), denying ? /* @__PURE__ */ jsxs("div", {
|
|
5737
|
+
className: "mt-2.5 flex items-center gap-2 border-t border-warning/30 pt-2.5",
|
|
5738
|
+
children: [
|
|
5739
|
+
/* @__PURE__ */ jsx(Input, {
|
|
5740
|
+
autoFocus: true,
|
|
5741
|
+
value: reason,
|
|
5742
|
+
onChange: (e) => setReason(e.target.value),
|
|
5743
|
+
onKeyDown: (e) => {
|
|
5744
|
+
if (e.key === "Enter") deny(false);
|
|
5745
|
+
if (e.key === "Escape") setDenying(false);
|
|
5746
|
+
},
|
|
5747
|
+
placeholder: "Reason (optional) — the agent reads this and can try something else",
|
|
5748
|
+
className: "h-7 flex-1"
|
|
5749
|
+
}),
|
|
5750
|
+
/* @__PURE__ */ jsx(Button, {
|
|
5751
|
+
size: "sm",
|
|
5752
|
+
variant: "outline",
|
|
5753
|
+
onClick: () => setDenying(false),
|
|
5754
|
+
children: "Cancel"
|
|
5755
|
+
}),
|
|
5756
|
+
/* @__PURE__ */ jsx(Button, {
|
|
5757
|
+
size: "sm",
|
|
5758
|
+
variant: "destructive",
|
|
5759
|
+
onClick: () => deny(false),
|
|
5760
|
+
children: "Deny"
|
|
5761
|
+
})
|
|
5762
|
+
]
|
|
5763
|
+
}) : null]
|
|
4328
5764
|
});
|
|
4329
5765
|
}
|
|
4330
5766
|
//#endregion
|
|
@@ -4488,62 +5924,170 @@ function QuestionPrompt({ request, onAnswer, onDismiss, className }) {
|
|
|
4488
5924
|
});
|
|
4489
5925
|
}
|
|
4490
5926
|
//#endregion
|
|
4491
|
-
//#region src/
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
}
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
5927
|
+
//#region src/components/agent/SessionInfoDialog.tsx
|
|
5928
|
+
/**
|
|
5929
|
+
* What this session *is*: engine, profile, model, mode, where it runs, which
|
|
5930
|
+
* credentials it found, and the files it has handed over.
|
|
5931
|
+
*
|
|
5932
|
+
* The identity half of the session's facts. Context and usage have their own
|
|
5933
|
+
* panels — they change every turn and are consulted mid-run, while everything
|
|
5934
|
+
* here is fixed at creation and looked up once.
|
|
5935
|
+
*/
|
|
5936
|
+
function SessionInfoDialog({ state, client, sessionId, open, onOpenChange }) {
|
|
5937
|
+
const session = state.session;
|
|
5938
|
+
const mode = state.permissionMode ? permissionModeMeta(state.permissionMode) : void 0;
|
|
5939
|
+
return /* @__PURE__ */ jsx(Dialog, {
|
|
5940
|
+
open,
|
|
5941
|
+
onOpenChange,
|
|
5942
|
+
children: /* @__PURE__ */ jsxs(DialogContent, { children: [/* @__PURE__ */ jsx(DialogHeader, {
|
|
5943
|
+
title: "Session info",
|
|
5944
|
+
description: session?.title
|
|
5945
|
+
}), /* @__PURE__ */ jsx(DialogBody, { children: /* @__PURE__ */ jsxs("div", {
|
|
5946
|
+
className: "flex flex-col divide-y divide-border",
|
|
5947
|
+
children: [
|
|
5948
|
+
/* @__PURE__ */ jsxs("div", {
|
|
5949
|
+
className: "pb-2",
|
|
5950
|
+
children: [
|
|
5951
|
+
/* @__PURE__ */ jsx(DialogRow, {
|
|
5952
|
+
label: "Engine",
|
|
5953
|
+
children: state.engine ?? "claude"
|
|
5954
|
+
}),
|
|
5955
|
+
session?.profile ? /* @__PURE__ */ jsx(DialogRow, {
|
|
5956
|
+
label: "Profile",
|
|
5957
|
+
children: session.profile
|
|
5958
|
+
}) : null,
|
|
5959
|
+
state.model ? /* @__PURE__ */ jsx(DialogRow, {
|
|
5960
|
+
label: "Model",
|
|
5961
|
+
mono: true,
|
|
5962
|
+
children: state.model
|
|
5963
|
+
}) : null,
|
|
5964
|
+
mode ? /* @__PURE__ */ jsx(DialogRow, {
|
|
5965
|
+
label: "Permission mode",
|
|
5966
|
+
children: mode.label
|
|
5967
|
+
}) : null,
|
|
5968
|
+
session?.apiKeySource ? /* @__PURE__ */ jsx(DialogRow, {
|
|
5969
|
+
label: "Credentials",
|
|
5970
|
+
children: session.apiKeySource
|
|
5971
|
+
}) : null
|
|
5972
|
+
]
|
|
5973
|
+
}),
|
|
5974
|
+
/* @__PURE__ */ jsxs("div", {
|
|
5975
|
+
className: "py-2",
|
|
5976
|
+
children: [
|
|
5977
|
+
state.cwd ? /* @__PURE__ */ jsx(CopyRow, {
|
|
5978
|
+
label: "Working directory",
|
|
5979
|
+
value: state.cwd
|
|
5980
|
+
}) : null,
|
|
5981
|
+
state.sdkSessionId ? /* @__PURE__ */ jsx(CopyRow, {
|
|
5982
|
+
label: "Engine session id",
|
|
5983
|
+
value: state.sdkSessionId
|
|
5984
|
+
}) : null,
|
|
5985
|
+
session ? /* @__PURE__ */ jsx(CopyRow, {
|
|
5986
|
+
label: "Gateway session id",
|
|
5987
|
+
value: session.id
|
|
5988
|
+
}) : null
|
|
5989
|
+
]
|
|
5990
|
+
}),
|
|
5991
|
+
/* @__PURE__ */ jsxs("div", {
|
|
5992
|
+
className: "py-2",
|
|
5993
|
+
children: [
|
|
5994
|
+
session?.createdAt ? /* @__PURE__ */ jsx(DialogRow, {
|
|
5995
|
+
label: "Started",
|
|
5996
|
+
children: formatRelativeTime(session.createdAt)
|
|
5997
|
+
}) : null,
|
|
5998
|
+
session?.numTurns !== void 0 ? /* @__PURE__ */ jsx(DialogRow, {
|
|
5999
|
+
label: "Turns",
|
|
6000
|
+
children: session.numTurns
|
|
6001
|
+
}) : null,
|
|
6002
|
+
/* @__PURE__ */ jsx(DialogRow, {
|
|
6003
|
+
label: "Cost",
|
|
6004
|
+
mono: true,
|
|
6005
|
+
children: formatCost(state.totalCostUsd)
|
|
6006
|
+
})
|
|
6007
|
+
]
|
|
6008
|
+
}),
|
|
6009
|
+
state.capabilities.vfs ? /* @__PURE__ */ jsx(SessionFiles, {
|
|
6010
|
+
client,
|
|
6011
|
+
sessionId,
|
|
6012
|
+
open
|
|
6013
|
+
}) : null
|
|
6014
|
+
]
|
|
6015
|
+
}) })] })
|
|
6016
|
+
});
|
|
4525
6017
|
}
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
6018
|
+
/** Long ids are for pasting elsewhere, so give them a copy target. */
|
|
6019
|
+
function CopyRow({ label, value }) {
|
|
6020
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
6021
|
+
className: "flex items-start justify-between gap-2 py-1.5",
|
|
6022
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
6023
|
+
className: "min-w-0 flex-1",
|
|
6024
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
6025
|
+
className: "text-label text-fg-3",
|
|
6026
|
+
children: label
|
|
6027
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
6028
|
+
className: "mt-0.5 font-mono text-label break-all text-fg-1",
|
|
6029
|
+
children: value
|
|
6030
|
+
})]
|
|
6031
|
+
}), /* @__PURE__ */ jsx(CopyButton, {
|
|
6032
|
+
value,
|
|
6033
|
+
"aria-label": `Copy ${label.toLowerCase()}`
|
|
6034
|
+
})]
|
|
6035
|
+
});
|
|
4536
6036
|
}
|
|
4537
|
-
/**
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
if (
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
6037
|
+
/** Files the agent produced inside the session's VFS. Fetched when the panel
|
|
6038
|
+
* opens — this is not a live list and nothing pushes changes to it. */
|
|
6039
|
+
function SessionFiles({ client, sessionId, open }) {
|
|
6040
|
+
const [files, setFiles] = useState();
|
|
6041
|
+
const [loading, setLoading] = useState(false);
|
|
6042
|
+
useEffect(() => {
|
|
6043
|
+
if (!open || !sessionId) return;
|
|
6044
|
+
let cancelled = false;
|
|
6045
|
+
setLoading(true);
|
|
6046
|
+
client.listSessionFiles(sessionId).then((list) => {
|
|
6047
|
+
if (!cancelled) setFiles(list);
|
|
6048
|
+
}).catch(() => {
|
|
6049
|
+
if (!cancelled) setFiles([]);
|
|
6050
|
+
}).finally(() => {
|
|
6051
|
+
if (!cancelled) setLoading(false);
|
|
6052
|
+
});
|
|
6053
|
+
return () => {
|
|
6054
|
+
cancelled = true;
|
|
6055
|
+
};
|
|
6056
|
+
}, [
|
|
6057
|
+
client,
|
|
6058
|
+
sessionId,
|
|
6059
|
+
open
|
|
6060
|
+
]);
|
|
6061
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
6062
|
+
className: "pt-3",
|
|
6063
|
+
children: [/* @__PURE__ */ jsx("h3", {
|
|
6064
|
+
className: "text-label font-medium text-fg-3",
|
|
6065
|
+
children: "Files"
|
|
6066
|
+
}), loading ? /* @__PURE__ */ jsx("div", {
|
|
6067
|
+
className: "py-3 text-center",
|
|
6068
|
+
children: /* @__PURE__ */ jsx(Spinner, { className: "size-4 text-fg-4" })
|
|
6069
|
+
}) : !files?.length ? /* @__PURE__ */ jsx("p", {
|
|
6070
|
+
className: "py-2 text-body-sm text-fg-4",
|
|
6071
|
+
children: "Nothing delivered yet."
|
|
6072
|
+
}) : /* @__PURE__ */ jsx("ul", {
|
|
6073
|
+
className: "mt-1 flex flex-col",
|
|
6074
|
+
children: files.map((file) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("a", {
|
|
6075
|
+
href: sessionId ? client.sessionFileUrl(sessionId, file.path) : void 0,
|
|
6076
|
+
className: "flex items-center gap-2 rounded-md px-1 py-1.5 hover:bg-surface-hover",
|
|
6077
|
+
children: [
|
|
6078
|
+
/* @__PURE__ */ jsx(Download, { className: "size-3.5 shrink-0 text-fg-4" }),
|
|
6079
|
+
/* @__PURE__ */ jsx("span", {
|
|
6080
|
+
className: "min-w-0 flex-1 truncate font-mono text-label text-fg-1",
|
|
6081
|
+
children: file.path
|
|
6082
|
+
}),
|
|
6083
|
+
/* @__PURE__ */ jsx("span", {
|
|
6084
|
+
className: "shrink-0 text-label text-fg-4",
|
|
6085
|
+
children: formatBytes(file.bytes)
|
|
6086
|
+
})
|
|
6087
|
+
]
|
|
6088
|
+
}) }, file.path))
|
|
6089
|
+
})]
|
|
6090
|
+
});
|
|
4547
6091
|
}
|
|
4548
6092
|
//#endregion
|
|
4549
6093
|
//#region src/components/agent/status.ts
|
|
@@ -4685,40 +6229,68 @@ function RateLimitMeter({ label, info, now }) {
|
|
|
4685
6229
|
})
|
|
4686
6230
|
});
|
|
4687
6231
|
}
|
|
4688
|
-
|
|
6232
|
+
/** A gauge that leads somewhere. `undefined` handler leaves it inert, so an
|
|
6233
|
+
* embedder that mounts no panels doesn't get buttons that do nothing. */
|
|
6234
|
+
function Slot({ onClick, hint, children }) {
|
|
6235
|
+
if (!onClick) return /* @__PURE__ */ jsx(Fragment$1, { children });
|
|
6236
|
+
return /* @__PURE__ */ jsx("button", {
|
|
6237
|
+
type: "button",
|
|
6238
|
+
onClick,
|
|
6239
|
+
"aria-label": hint,
|
|
6240
|
+
className: "rounded-md px-1 py-0.5 transition-colors outline-none hover:bg-surface-hover focus-visible:bg-surface-hover",
|
|
6241
|
+
children
|
|
6242
|
+
});
|
|
6243
|
+
}
|
|
6244
|
+
function StatusBar({ state, connected, connection, onOpenStatus, onOpenContext, onOpenUsage, actions, className }) {
|
|
4689
6245
|
const meta = STATUS_META[state.status];
|
|
4690
6246
|
const now = useNow();
|
|
4691
6247
|
const session = state.rateLimits?.five_hour;
|
|
4692
6248
|
const weekly = state.rateLimits?.seven_day;
|
|
6249
|
+
const link = connection ?? (connected === false ? "reconnecting" : "live");
|
|
4693
6250
|
return /* @__PURE__ */ jsxs("div", {
|
|
4694
6251
|
"data-slot": "status-bar",
|
|
4695
|
-
className: cn("flex items-center gap-
|
|
6252
|
+
className: cn("flex items-center gap-2 border-b border-border bg-surface px-3 py-1.5", className),
|
|
4696
6253
|
children: [
|
|
4697
|
-
/* @__PURE__ */
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
children:
|
|
6254
|
+
/* @__PURE__ */ jsx(Slot, {
|
|
6255
|
+
onClick: onOpenStatus,
|
|
6256
|
+
hint: "Session info",
|
|
6257
|
+
children: link === "live" ? /* @__PURE__ */ jsxs(Badge, {
|
|
6258
|
+
variant: meta.variant,
|
|
6259
|
+
dot: !meta.busy,
|
|
6260
|
+
children: [meta.busy ? /* @__PURE__ */ jsx(Spinner, { className: "size-3 text-current" }) : null, meta.label]
|
|
6261
|
+
}) : /* @__PURE__ */ jsxs(Badge, {
|
|
6262
|
+
variant: link === "offline" ? "danger" : "warning",
|
|
6263
|
+
dot: false,
|
|
6264
|
+
children: [link === "offline" ? /* @__PURE__ */ jsx(WifiOff, { className: "size-3 text-current" }) : /* @__PURE__ */ jsx(RefreshCw, { className: "size-3 animate-spin text-current" }), link === "offline" ? "Offline" : "Reconnecting…"]
|
|
6265
|
+
})
|
|
4701
6266
|
}),
|
|
4702
|
-
state.contextUsage ? /* @__PURE__ */ jsx(
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
now
|
|
6267
|
+
state.capabilities.contextUsage && state.contextUsage ? /* @__PURE__ */ jsx(Slot, {
|
|
6268
|
+
onClick: onOpenContext,
|
|
6269
|
+
hint: "Context breakdown",
|
|
6270
|
+
children: /* @__PURE__ */ jsx(ContextMeter, { usage: state.contextUsage })
|
|
4707
6271
|
}) : null,
|
|
4708
|
-
weekly ? /* @__PURE__ */ jsx(
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
6272
|
+
session || weekly ? /* @__PURE__ */ jsx(Slot, {
|
|
6273
|
+
onClick: onOpenUsage,
|
|
6274
|
+
hint: "Plan usage",
|
|
6275
|
+
children: /* @__PURE__ */ jsxs("span", {
|
|
6276
|
+
className: "inline-flex items-center gap-2",
|
|
6277
|
+
children: [session ? /* @__PURE__ */ jsx(RateLimitMeter, {
|
|
6278
|
+
label: "Session",
|
|
6279
|
+
info: session,
|
|
6280
|
+
now
|
|
6281
|
+
}) : null, weekly ? /* @__PURE__ */ jsx(RateLimitMeter, {
|
|
6282
|
+
label: "Weekly",
|
|
6283
|
+
info: weekly,
|
|
6284
|
+
now
|
|
6285
|
+
}) : null]
|
|
6286
|
+
})
|
|
4712
6287
|
}) : null,
|
|
4713
6288
|
/* @__PURE__ */ jsx("span", { className: "flex-1" }),
|
|
4714
|
-
!connected ? /* @__PURE__ */ jsxs("span", {
|
|
4715
|
-
className: "inline-flex items-center gap-1 text-label text-warning",
|
|
4716
|
-
children: [/* @__PURE__ */ jsx(WifiOff, { className: "size-3" }), " reconnecting…"]
|
|
4717
|
-
}) : null,
|
|
4718
6289
|
/* @__PURE__ */ jsx("span", {
|
|
4719
6290
|
className: "font-mono text-label text-fg-3",
|
|
4720
6291
|
children: formatCost(state.totalCostUsd)
|
|
4721
|
-
})
|
|
6292
|
+
}),
|
|
6293
|
+
actions
|
|
4722
6294
|
]
|
|
4723
6295
|
});
|
|
4724
6296
|
}
|
|
@@ -4829,6 +6401,40 @@ function MessageContent({ className, ...props }) {
|
|
|
4829
6401
|
});
|
|
4830
6402
|
}
|
|
4831
6403
|
//#endregion
|
|
6404
|
+
//#region src/components/agent/PromptTokenText.tsx
|
|
6405
|
+
/**
|
|
6406
|
+
* A sent message, with its `@file` and `/command` tokens styled the way the CLI
|
|
6407
|
+
* writes them: monospace and tinted, no background — the bubble already has one,
|
|
6408
|
+
* and a second fill inside it reads as a button.
|
|
6409
|
+
*
|
|
6410
|
+
* Literal text, never markdown: what was typed is what was sent. The one pass
|
|
6411
|
+
* over it is this, so a message reads the same after sending as it did in the
|
|
6412
|
+
* composer. Two tokens, two meanings — a file is a reference, a command is an
|
|
6413
|
+
* action — so they are told apart by hue rather than by shape alone.
|
|
6414
|
+
*/
|
|
6415
|
+
function PromptTokenText({ text, className }) {
|
|
6416
|
+
const tokens = scanPromptTokens(text);
|
|
6417
|
+
if (tokens.length === 0) return /* @__PURE__ */ jsx("span", {
|
|
6418
|
+
className,
|
|
6419
|
+
children: text
|
|
6420
|
+
});
|
|
6421
|
+
const parts = [];
|
|
6422
|
+
let cursor = 0;
|
|
6423
|
+
for (const [index, token] of tokens.entries()) {
|
|
6424
|
+
if (token.start > cursor) parts.push(text.slice(cursor, token.start));
|
|
6425
|
+
parts.push(/* @__PURE__ */ jsx("span", {
|
|
6426
|
+
className: cn("font-mono", token.kind === "file" ? "text-accent" : "text-info"),
|
|
6427
|
+
children: token.text
|
|
6428
|
+
}, `${token.start}-${index}`));
|
|
6429
|
+
cursor = token.end;
|
|
6430
|
+
}
|
|
6431
|
+
if (cursor < text.length) parts.push(text.slice(cursor));
|
|
6432
|
+
return /* @__PURE__ */ jsx("span", {
|
|
6433
|
+
className,
|
|
6434
|
+
children: parts.map((part, index) => /* @__PURE__ */ jsx(Fragment, { children: part }, index))
|
|
6435
|
+
});
|
|
6436
|
+
}
|
|
6437
|
+
//#endregion
|
|
4832
6438
|
//#region src/components/agent/Response.tsx
|
|
4833
6439
|
/** Markdown renderer for assistant output — streaming-safe via streamdown, code
|
|
4834
6440
|
* highlighted with shiki (dual theme follows [data-theme] through the dark: variant). */
|
|
@@ -4885,8 +6491,73 @@ function Reasoning({ children, isStreaming = false, defaultOpen, className }) {
|
|
|
4885
6491
|
});
|
|
4886
6492
|
}
|
|
4887
6493
|
//#endregion
|
|
6494
|
+
//#region src/components/agent/SessionEmptyState.tsx
|
|
6495
|
+
/**
|
|
6496
|
+
* What a session shows before it has said anything: where the agent is sitting,
|
|
6497
|
+
* and what the composer accepts beyond prose.
|
|
6498
|
+
*
|
|
6499
|
+
* Every hint is conditional on the affordance actually existing — an engine
|
|
6500
|
+
* without slash commands, or a gateway without host files, is not told about
|
|
6501
|
+
* them. Deliberately no project name (the header already carries it) and no
|
|
6502
|
+
* brand mark (its geometry is inlined in several places already and they are
|
|
6503
|
+
* meant to stay identical).
|
|
6504
|
+
*/
|
|
6505
|
+
function SessionEmptyState({ cwd, hasCommands, hasSkills, canBrowseFiles, className }) {
|
|
6506
|
+
const hints = [
|
|
6507
|
+
{
|
|
6508
|
+
icon: MessageSquareText,
|
|
6509
|
+
text: "Tell the agent what to do."
|
|
6510
|
+
},
|
|
6511
|
+
...hasCommands ? [{
|
|
6512
|
+
icon: SlashSquare,
|
|
6513
|
+
text: "Type / for the CLI’s slash commands."
|
|
6514
|
+
}] : [],
|
|
6515
|
+
...hasSkills ? [{
|
|
6516
|
+
icon: Sparkles,
|
|
6517
|
+
text: "Type $ to draft a message for one of the agent’s skills."
|
|
6518
|
+
}] : [],
|
|
6519
|
+
...canBrowseFiles ? [{
|
|
6520
|
+
icon: AtSign,
|
|
6521
|
+
text: "Type @ to search this project’s files."
|
|
6522
|
+
}] : []
|
|
6523
|
+
];
|
|
6524
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
6525
|
+
className: cn("flex flex-col items-center gap-4 py-10", className),
|
|
6526
|
+
children: [
|
|
6527
|
+
/* @__PURE__ */ jsx("div", {
|
|
6528
|
+
className: "flex size-12 items-center justify-center rounded-xl bg-surface text-fg-3",
|
|
6529
|
+
children: /* @__PURE__ */ jsx(Terminal, { className: "size-5" })
|
|
6530
|
+
}),
|
|
6531
|
+
cwd ? /* @__PURE__ */ jsx("p", {
|
|
6532
|
+
className: "max-w-full truncate font-mono text-label text-fg-4",
|
|
6533
|
+
children: cwd
|
|
6534
|
+
}) : null,
|
|
6535
|
+
/* @__PURE__ */ jsx("ul", {
|
|
6536
|
+
className: "w-full max-w-sm divide-y divide-border overflow-hidden rounded-lg bg-surface",
|
|
6537
|
+
children: hints.map((hint) => /* @__PURE__ */ jsxs("li", {
|
|
6538
|
+
className: "flex items-center gap-3 px-3.5 py-2.5",
|
|
6539
|
+
children: [/* @__PURE__ */ jsx(hint.icon, { className: "size-4 shrink-0 text-fg-4" }), /* @__PURE__ */ jsx("span", {
|
|
6540
|
+
className: "text-body-sm text-fg-3",
|
|
6541
|
+
children: hint.text
|
|
6542
|
+
})]
|
|
6543
|
+
}, hint.text))
|
|
6544
|
+
})
|
|
6545
|
+
]
|
|
6546
|
+
});
|
|
6547
|
+
}
|
|
6548
|
+
//#endregion
|
|
4888
6549
|
//#region src/components/agent/ToolCallCard.tsx
|
|
4889
6550
|
const RESULT_PREVIEW_CHARS = 2e3;
|
|
6551
|
+
/** Codex's built-in image tools name a host path rather than sending bytes —
|
|
6552
|
+
* an event log carries references, never base64. Rendering the picture means
|
|
6553
|
+
* reading that path back through the gateway's host-file route. */
|
|
6554
|
+
const IMAGE_TOOLS = new Set(["CodexImageGeneration", "CodexImageView"]);
|
|
6555
|
+
const imagePathOf = (item) => {
|
|
6556
|
+
if (!IMAGE_TOOLS.has(item.name)) return void 0;
|
|
6557
|
+
const input = item.input;
|
|
6558
|
+
const path = input?.savedPath ?? input?.path;
|
|
6559
|
+
return typeof path === "string" ? path : void 0;
|
|
6560
|
+
};
|
|
4890
6561
|
/** Badge per execution state. `pending`/`deferred` mean the work is happening
|
|
4891
6562
|
* somewhere else (this tab's sandbox, a queue) — distinct from the model still
|
|
4892
6563
|
* running the call itself. */
|
|
@@ -4917,12 +6588,14 @@ const STATE_BADGE = {
|
|
|
4917
6588
|
busy: false
|
|
4918
6589
|
}
|
|
4919
6590
|
};
|
|
4920
|
-
function ToolCallCard({ item, className }) {
|
|
6591
|
+
function ToolCallCard({ item, hostImage, className }) {
|
|
4921
6592
|
const [open, setOpen] = useState(false);
|
|
4922
6593
|
const [fullResult, setFullResult] = useState(false);
|
|
6594
|
+
const imagePath = imagePathOf(item);
|
|
4923
6595
|
const status = item.status ?? (item.result === void 0 ? "running" : "settled");
|
|
4924
6596
|
const badge = STATE_BADGE[status];
|
|
4925
6597
|
const isError = status === "failed" || item.result?.isError === true;
|
|
6598
|
+
const Icon = toolIcon(item.name);
|
|
4926
6599
|
const resultText = item.result?.text ?? "";
|
|
4927
6600
|
const truncated = !fullResult && resultText.length > RESULT_PREVIEW_CHARS;
|
|
4928
6601
|
const shownResult = truncated ? resultText.slice(0, RESULT_PREVIEW_CHARS) : resultText;
|
|
@@ -4930,59 +6603,102 @@ function ToolCallCard({ item, className }) {
|
|
|
4930
6603
|
"data-slot": "tool-call",
|
|
4931
6604
|
"data-state": status,
|
|
4932
6605
|
className: cn("w-full overflow-hidden rounded-lg border border-border bg-surface", className),
|
|
4933
|
-
children: [
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
badge.
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
6606
|
+
children: [
|
|
6607
|
+
/* @__PURE__ */ jsxs("button", {
|
|
6608
|
+
type: "button",
|
|
6609
|
+
onClick: () => setOpen((v) => !v),
|
|
6610
|
+
className: cn("flex w-full items-center gap-2 px-3 py-2 text-left transition-colors outline-none", "hover:bg-surface-hover focus-visible:bg-surface-hover"),
|
|
6611
|
+
children: [
|
|
6612
|
+
/* @__PURE__ */ jsx(Icon, { className: "size-3.5 shrink-0 text-fg-3" }),
|
|
6613
|
+
/* @__PURE__ */ jsx("span", {
|
|
6614
|
+
className: "shrink-0 font-mono text-body-sm font-medium text-fg-1",
|
|
6615
|
+
children: item.name
|
|
6616
|
+
}),
|
|
6617
|
+
item.backend && item.backend !== "server" ? /* @__PURE__ */ jsx(Badge, {
|
|
6618
|
+
variant: "neutral",
|
|
6619
|
+
className: "shrink-0",
|
|
6620
|
+
children: item.backend
|
|
6621
|
+
}) : null,
|
|
6622
|
+
/* @__PURE__ */ jsx("span", {
|
|
6623
|
+
className: "min-w-0 flex-1 truncate font-mono text-label text-fg-4",
|
|
6624
|
+
children: toolInputPreview(item.input)
|
|
6625
|
+
}),
|
|
6626
|
+
/* @__PURE__ */ jsxs(Badge, {
|
|
6627
|
+
variant: badge.variant,
|
|
6628
|
+
dot: !badge.busy,
|
|
6629
|
+
className: "shrink-0 gap-1",
|
|
6630
|
+
children: [
|
|
6631
|
+
badge.busy ? /* @__PURE__ */ jsx(Spinner, { className: "size-3 text-current" }) : null,
|
|
6632
|
+
status === "deferred" ? /* @__PURE__ */ jsx(Clock, { className: "size-3 text-current" }) : null,
|
|
6633
|
+
badge.label
|
|
6634
|
+
]
|
|
6635
|
+
}),
|
|
6636
|
+
/* @__PURE__ */ jsx(ChevronDown, { className: cn("size-3.5 shrink-0 text-fg-4 transition-transform", open && "rotate-180") })
|
|
6637
|
+
]
|
|
6638
|
+
}),
|
|
6639
|
+
imagePath && hostImage ? /* @__PURE__ */ jsx(HostImage, {
|
|
6640
|
+
path: imagePath,
|
|
6641
|
+
load: hostImage
|
|
6642
|
+
}) : null,
|
|
6643
|
+
open ? /* @__PURE__ */ jsxs("div", {
|
|
6644
|
+
className: "flex flex-col gap-2 border-t border-border p-2.5",
|
|
6645
|
+
children: [
|
|
6646
|
+
/* @__PURE__ */ jsx(CodeBlock, {
|
|
6647
|
+
code: JSON.stringify(item.input, null, 2),
|
|
6648
|
+
label: "Parameters"
|
|
6649
|
+
}),
|
|
6650
|
+
item.logs?.length ? /* @__PURE__ */ jsx(CodeBlock, {
|
|
6651
|
+
code: item.logs.join("\n"),
|
|
6652
|
+
label: "Logs"
|
|
6653
|
+
}) : null,
|
|
6654
|
+
item.result !== void 0 ? /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx(CodeBlock, {
|
|
6655
|
+
code: shownResult || "(empty result)",
|
|
6656
|
+
label: isError ? "Error" : "Result",
|
|
6657
|
+
className: cn(isError && "border-danger/40 [&_pre]:text-danger")
|
|
6658
|
+
}), truncated ? /* @__PURE__ */ jsxs("button", {
|
|
6659
|
+
type: "button",
|
|
6660
|
+
className: "mt-1 text-label text-fg-3 underline-offset-2 hover:underline",
|
|
6661
|
+
onClick: () => setFullResult(true),
|
|
6662
|
+
children: [
|
|
6663
|
+
"Show all ",
|
|
6664
|
+
resultText.length.toLocaleString(),
|
|
6665
|
+
" chars"
|
|
6666
|
+
]
|
|
6667
|
+
}) : null] }) : null
|
|
6668
|
+
]
|
|
6669
|
+
}) : null
|
|
6670
|
+
]
|
|
6671
|
+
});
|
|
6672
|
+
}
|
|
6673
|
+
/**
|
|
6674
|
+
* A picture that lives on the host, fetched through the gateway's host-file
|
|
6675
|
+
* route and shown inline.
|
|
6676
|
+
*
|
|
6677
|
+
* Silent on failure by design: a path outside the server's allowed roots is the
|
|
6678
|
+
* *expected* case for codex's default save location, and the card's result text
|
|
6679
|
+
* already names where the file went. An error banner over that would be noise
|
|
6680
|
+
* about a thing the operator can fix in one line of config.
|
|
6681
|
+
*/
|
|
6682
|
+
function HostImage({ path, load }) {
|
|
6683
|
+
const [src, setSrc] = useState();
|
|
6684
|
+
useEffect(() => {
|
|
6685
|
+
let cancelled = false;
|
|
6686
|
+
setSrc(void 0);
|
|
6687
|
+
load(path).then((url) => {
|
|
6688
|
+
if (!cancelled) setSrc(url);
|
|
6689
|
+
}).catch(() => {});
|
|
6690
|
+
return () => {
|
|
6691
|
+
cancelled = true;
|
|
6692
|
+
};
|
|
6693
|
+
}, [path, load]);
|
|
6694
|
+
if (!src) return null;
|
|
6695
|
+
return /* @__PURE__ */ jsx("div", {
|
|
6696
|
+
className: "border-t border-border p-2.5",
|
|
6697
|
+
children: /* @__PURE__ */ jsx("img", {
|
|
6698
|
+
src,
|
|
6699
|
+
alt: path.split("/").pop() ?? "Generated image",
|
|
6700
|
+
className: "max-h-96 w-auto max-w-full rounded-md border border-border"
|
|
6701
|
+
})
|
|
4986
6702
|
});
|
|
4987
6703
|
}
|
|
4988
6704
|
//#endregion
|
|
@@ -4990,22 +6706,31 @@ function ToolCallCard({ item, className }) {
|
|
|
4990
6706
|
function TurnResultRow({ item }) {
|
|
4991
6707
|
return /* @__PURE__ */ jsxs("div", {
|
|
4992
6708
|
"data-slot": "turn-result",
|
|
4993
|
-
className: "
|
|
4994
|
-
children: [
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
item.isError ?
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
6709
|
+
className: "py-1",
|
|
6710
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
6711
|
+
className: "flex items-center gap-2",
|
|
6712
|
+
children: [
|
|
6713
|
+
/* @__PURE__ */ jsx("div", { className: "h-px flex-1 bg-border" }),
|
|
6714
|
+
/* @__PURE__ */ jsxs("span", {
|
|
6715
|
+
className: cn("font-mono text-label", item.isError ? "text-danger" : "text-fg-4"),
|
|
6716
|
+
children: [
|
|
6717
|
+
item.isError ? item.subtype : "turn done",
|
|
6718
|
+
" · ",
|
|
6719
|
+
formatDuration(item.durationMs),
|
|
6720
|
+
" ·",
|
|
6721
|
+
" ",
|
|
6722
|
+
formatCost(item.totalCostUsd)
|
|
6723
|
+
]
|
|
6724
|
+
}),
|
|
6725
|
+
/* @__PURE__ */ jsx("div", { className: "h-px flex-1 bg-border" })
|
|
6726
|
+
]
|
|
6727
|
+
}), item.errors?.length ? /* @__PURE__ */ jsx("ul", {
|
|
6728
|
+
className: "mt-1 flex flex-col gap-0.5 text-center",
|
|
6729
|
+
children: item.errors.map((message, index) => /* @__PURE__ */ jsx("li", {
|
|
6730
|
+
className: "text-label break-words text-danger",
|
|
6731
|
+
children: message
|
|
6732
|
+
}, index))
|
|
6733
|
+
}) : null]
|
|
5009
6734
|
});
|
|
5010
6735
|
}
|
|
5011
6736
|
function NoticeRow({ item }) {
|
|
@@ -5015,11 +6740,14 @@ function NoticeRow({ item }) {
|
|
|
5015
6740
|
children: item.text
|
|
5016
6741
|
});
|
|
5017
6742
|
}
|
|
5018
|
-
function TranscriptItemView({ item, fileUrl }) {
|
|
6743
|
+
function TranscriptItemView({ item, fileUrl, attachmentUrl, hostImage }) {
|
|
5019
6744
|
switch (item.kind) {
|
|
5020
|
-
case "user": return /* @__PURE__ */
|
|
6745
|
+
case "user": return /* @__PURE__ */ jsxs(Message, {
|
|
5021
6746
|
from: "user",
|
|
5022
|
-
children: /* @__PURE__ */ jsx(
|
|
6747
|
+
children: [item.attachments?.length ? /* @__PURE__ */ jsx(SentAttachments, {
|
|
6748
|
+
attachments: item.attachments,
|
|
6749
|
+
attachmentUrl
|
|
6750
|
+
}) : null, item.text ? /* @__PURE__ */ jsx(MessageContent, { children: /* @__PURE__ */ jsx(PromptTokenText, { text: item.text }) }) : null]
|
|
5023
6751
|
});
|
|
5024
6752
|
case "assistant_text": return /* @__PURE__ */ jsx(Message, {
|
|
5025
6753
|
from: "assistant",
|
|
@@ -5032,7 +6760,10 @@ function TranscriptItemView({ item, fileUrl }) {
|
|
|
5032
6760
|
isStreaming: item.id === "streaming-thinking",
|
|
5033
6761
|
children: item.text
|
|
5034
6762
|
});
|
|
5035
|
-
case "tool_call": return /* @__PURE__ */ jsx(ToolCallCard, {
|
|
6763
|
+
case "tool_call": return /* @__PURE__ */ jsx(ToolCallCard, {
|
|
6764
|
+
item,
|
|
6765
|
+
hostImage
|
|
6766
|
+
});
|
|
5036
6767
|
case "turn_result": return /* @__PURE__ */ jsx(TurnResultRow, { item });
|
|
5037
6768
|
case "notice": return /* @__PURE__ */ jsx(NoticeRow, { item });
|
|
5038
6769
|
case "file_delivered": return /* @__PURE__ */ jsx(FileCard, {
|
|
@@ -5052,32 +6783,204 @@ function showLoader(state) {
|
|
|
5052
6783
|
if (last.kind === "thinking" && last.id === "streaming-thinking") return false;
|
|
5053
6784
|
return last.kind !== "turn_result" || state.status === "running";
|
|
5054
6785
|
}
|
|
5055
|
-
|
|
6786
|
+
/** Files sent with a message: thumbnails for images, named chips for the rest.
|
|
6787
|
+
* References only — the bytes are fetched from the gateway. */
|
|
6788
|
+
function SentAttachments({ attachments, attachmentUrl }) {
|
|
6789
|
+
return /* @__PURE__ */ jsx("div", {
|
|
6790
|
+
className: "mb-1 flex flex-wrap justify-end gap-1.5",
|
|
6791
|
+
children: attachments.map((attachment) => {
|
|
6792
|
+
const href = attachmentUrl?.(attachment.id);
|
|
6793
|
+
return attachment.mediaType.startsWith("image/") && href ? /* @__PURE__ */ jsx("img", {
|
|
6794
|
+
src: href,
|
|
6795
|
+
alt: attachment.name,
|
|
6796
|
+
className: "size-20 rounded-md border border-border object-cover"
|
|
6797
|
+
}, attachment.id) : /* @__PURE__ */ jsx("span", {
|
|
6798
|
+
className: "rounded-full border border-border bg-surface px-2.5 py-1 text-body-xs text-fg-3",
|
|
6799
|
+
children: attachment.name
|
|
6800
|
+
}, attachment.id);
|
|
6801
|
+
})
|
|
6802
|
+
});
|
|
6803
|
+
}
|
|
6804
|
+
/** Rows produced inside a subagent (`parentToolUseId != null`) are stepped in
|
|
6805
|
+
* behind a rule, so a Task's own output reads as belonging to the tool call
|
|
6806
|
+
* above it rather than as the main thread carrying on. */
|
|
6807
|
+
function nestedClass(item) {
|
|
6808
|
+
return "parentToolUseId" in item && item.parentToolUseId != null ? "border-l-2 border-border pl-3" : void 0;
|
|
6809
|
+
}
|
|
6810
|
+
function Transcript({ state, fileUrl, attachmentUrl, canBrowseFiles, hostImage, className }) {
|
|
5056
6811
|
return /* @__PURE__ */ jsxs(Conversation, {
|
|
5057
6812
|
className,
|
|
5058
|
-
children: [/* @__PURE__ */ jsxs(ConversationContent, { children: [state.items.length === 0 && state.status !== "starting" ? /* @__PURE__ */ jsx(
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
6813
|
+
children: [/* @__PURE__ */ jsxs(ConversationContent, { children: [state.items.length === 0 && state.status !== "starting" ? /* @__PURE__ */ jsx(SessionEmptyState, {
|
|
6814
|
+
cwd: state.cwd,
|
|
6815
|
+
hasCommands: !!state.commands?.length,
|
|
6816
|
+
hasSkills: !!state.skills?.some((s) => s.enabled),
|
|
6817
|
+
canBrowseFiles
|
|
6818
|
+
}) : state.items.map((item) => /* @__PURE__ */ jsx("div", {
|
|
6819
|
+
className: nestedClass(item),
|
|
6820
|
+
children: /* @__PURE__ */ jsx(TranscriptItemView, {
|
|
6821
|
+
item,
|
|
6822
|
+
fileUrl,
|
|
6823
|
+
attachmentUrl,
|
|
6824
|
+
hostImage
|
|
6825
|
+
})
|
|
5064
6826
|
}, `${item.kind}:${item.id}`)), showLoader(state) ? /* @__PURE__ */ jsx(Loader, { label: state.status === "starting" ? "Starting session…" : void 0 }) : null] }), /* @__PURE__ */ jsx(ConversationScrollButton, {})]
|
|
5065
6827
|
});
|
|
5066
6828
|
}
|
|
5067
6829
|
//#endregion
|
|
6830
|
+
//#region src/components/agent/UsageDialog.tsx
|
|
6831
|
+
/** Ticking clock — the countdowns and the pace markers both move with it, and a
|
|
6832
|
+
* minute is the finest resolution either of them prints. */
|
|
6833
|
+
function useMinuteClock(open) {
|
|
6834
|
+
const [now, setNow] = useState(() => Date.now());
|
|
6835
|
+
useEffect(() => {
|
|
6836
|
+
if (!open) return;
|
|
6837
|
+
setNow(Date.now());
|
|
6838
|
+
const timer = setInterval(() => setNow(Date.now()), 6e4);
|
|
6839
|
+
return () => clearInterval(timer);
|
|
6840
|
+
}, [open]);
|
|
6841
|
+
return now;
|
|
6842
|
+
}
|
|
6843
|
+
const usageTint = (pct) => pct >= 90 ? "bg-danger" : pct >= 70 ? "bg-warning" : "bg-accent";
|
|
6844
|
+
/**
|
|
6845
|
+
* The plan's rate-limit windows, spelled out: how much of each is used, how that
|
|
6846
|
+
* compares to the pace that would spend the window exactly, and when it resets.
|
|
6847
|
+
*
|
|
6848
|
+
* The pace marker is the point. A bar alone says "17% used", which is only
|
|
6849
|
+
* alarming or reassuring once you know how far into the week you are — so every
|
|
6850
|
+
* window draws a tick at the elapsed share of its duration. Left of the tick is
|
|
6851
|
+
* under budget, right of it is ahead of it. The duration comes from the window
|
|
6852
|
+
* key (5h, 7d) because the CLI reports a reset time and a percentage and never a
|
|
6853
|
+
* duration; a window whose key doesn't say gets no marker rather than a guessed one.
|
|
6854
|
+
*/
|
|
6855
|
+
function UsageDialog({ rateLimits, subscriptionType, engine, totalCostUsd, updatedAt, open, onOpenChange }) {
|
|
6856
|
+
const now = useMinuteClock(open);
|
|
6857
|
+
return /* @__PURE__ */ jsx(Dialog, {
|
|
6858
|
+
open,
|
|
6859
|
+
onOpenChange,
|
|
6860
|
+
children: /* @__PURE__ */ jsxs(DialogContent, { children: [/* @__PURE__ */ jsx(DialogHeader, {
|
|
6861
|
+
title: "Usage",
|
|
6862
|
+
description: engine === "claude" ? "Claude Code" : engine,
|
|
6863
|
+
actions: subscriptionType ? /* @__PURE__ */ jsx(Badge, {
|
|
6864
|
+
variant: "accent",
|
|
6865
|
+
className: "mt-0.5 shrink-0 capitalize",
|
|
6866
|
+
children: subscriptionType
|
|
6867
|
+
}) : null
|
|
6868
|
+
}), /* @__PURE__ */ jsxs(DialogBody, { children: [
|
|
6869
|
+
rateLimits.length === 0 ? /* @__PURE__ */ jsx("p", {
|
|
6870
|
+
className: "py-6 text-center text-body-sm text-fg-4",
|
|
6871
|
+
children: engine === "claude" ? "This session reports no plan windows — API-key sessions have none, and a subscription session reports them once a turn has run." : `Plan windows are a claude.ai subscription thing; this session runs on the ${engine} engine.`
|
|
6872
|
+
}) : /* @__PURE__ */ jsx("div", {
|
|
6873
|
+
className: "flex flex-col gap-5",
|
|
6874
|
+
children: rateLimits.map(({ key, info }) => /* @__PURE__ */ jsx(UsageWindow, {
|
|
6875
|
+
windowKey: key,
|
|
6876
|
+
info,
|
|
6877
|
+
now
|
|
6878
|
+
}, key))
|
|
6879
|
+
}),
|
|
6880
|
+
/* @__PURE__ */ jsxs("div", {
|
|
6881
|
+
className: "mt-5 flex items-baseline justify-between gap-4 border-t border-border pt-3",
|
|
6882
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
6883
|
+
className: "text-label text-fg-3",
|
|
6884
|
+
children: "This session has cost"
|
|
6885
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
6886
|
+
className: "font-mono text-body-sm text-fg-1",
|
|
6887
|
+
children: formatCost(totalCostUsd)
|
|
6888
|
+
})]
|
|
6889
|
+
}),
|
|
6890
|
+
updatedAt ? /* @__PURE__ */ jsxs("p", {
|
|
6891
|
+
className: "mt-2 text-label text-fg-4",
|
|
6892
|
+
children: ["Updated ", formatAgoPrecise(updatedAt, now)]
|
|
6893
|
+
}) : null
|
|
6894
|
+
] })] })
|
|
6895
|
+
});
|
|
6896
|
+
}
|
|
6897
|
+
function UsageWindow({ windowKey, info, now }) {
|
|
6898
|
+
const utilization = info.utilization ?? 0;
|
|
6899
|
+
const resetsAtMs = info.resetsAt !== void 0 ? info.resetsAt * 1e3 : void 0;
|
|
6900
|
+
const duration = rateLimitWindowSeconds(windowKey);
|
|
6901
|
+
const remaining = resetsAtMs !== void 0 ? (resetsAtMs - now) / 1e3 : void 0;
|
|
6902
|
+
const pace = duration !== void 0 && remaining !== void 0 && remaining > 0 && remaining < duration ? (duration - remaining) / duration : void 0;
|
|
6903
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
6904
|
+
/* @__PURE__ */ jsxs("div", {
|
|
6905
|
+
className: "flex items-baseline justify-between gap-3",
|
|
6906
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
6907
|
+
className: "truncate text-body-sm text-fg-1",
|
|
6908
|
+
children: formatRateLimitWindowLong(windowKey)
|
|
6909
|
+
}), /* @__PURE__ */ jsxs("span", {
|
|
6910
|
+
className: cn("shrink-0 font-mono text-body-sm font-medium", info.status === "rejected" ? "text-danger" : "text-fg-1"),
|
|
6911
|
+
children: [utilization.toFixed(0), "% used"]
|
|
6912
|
+
})]
|
|
6913
|
+
}),
|
|
6914
|
+
/* @__PURE__ */ jsxs("div", {
|
|
6915
|
+
className: "relative mt-2 h-2 rounded-full bg-border",
|
|
6916
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
6917
|
+
className: cn("h-full rounded-full", usageTint(utilization)),
|
|
6918
|
+
style: { width: `${Math.min(100, Math.max(2, utilization))}%` }
|
|
6919
|
+
}), pace !== void 0 ? /* @__PURE__ */ jsx("span", {
|
|
6920
|
+
"aria-hidden": true,
|
|
6921
|
+
title: "Spent evenly, usage would be here by now",
|
|
6922
|
+
className: "absolute -top-1 h-4 w-0.5 -translate-x-1/2 rounded-full bg-fg-1",
|
|
6923
|
+
style: { left: `${Math.min(100, Math.max(0, pace * 100))}%` }
|
|
6924
|
+
}) : null]
|
|
6925
|
+
}),
|
|
6926
|
+
/* @__PURE__ */ jsxs("div", {
|
|
6927
|
+
className: "mt-1.5 flex items-center gap-3 text-label text-fg-4",
|
|
6928
|
+
children: [
|
|
6929
|
+
resetsAtMs !== void 0 ? /* @__PURE__ */ jsxs("span", {
|
|
6930
|
+
className: "inline-flex items-center gap-1",
|
|
6931
|
+
children: [
|
|
6932
|
+
/* @__PURE__ */ jsx(RotateCcw, { className: "size-3" }),
|
|
6933
|
+
" Resets in ",
|
|
6934
|
+
formatCountdown(resetsAtMs, now)
|
|
6935
|
+
]
|
|
6936
|
+
}) : null,
|
|
6937
|
+
info.isUsingOverage ? /* @__PURE__ */ jsx("span", {
|
|
6938
|
+
className: "text-warning",
|
|
6939
|
+
children: "overage"
|
|
6940
|
+
}) : null,
|
|
6941
|
+
info.status === "rejected" ? /* @__PURE__ */ jsx("span", {
|
|
6942
|
+
className: "text-danger",
|
|
6943
|
+
children: "limit reached"
|
|
6944
|
+
}) : null
|
|
6945
|
+
]
|
|
6946
|
+
})
|
|
6947
|
+
] });
|
|
6948
|
+
}
|
|
6949
|
+
//#endregion
|
|
5068
6950
|
//#region src/components/agent/SessionPanel.tsx
|
|
5069
6951
|
/**
|
|
5070
6952
|
* The all-in-one embeddable session surface: status bar, streaming transcript,
|
|
5071
6953
|
* permission prompts, composer. Attaches via useClaudeSession; remount (key) to switch
|
|
5072
6954
|
* sessions.
|
|
6955
|
+
*
|
|
6956
|
+
* Every affordance is gated on the session's **capability record** rather than on
|
|
6957
|
+
* the engine name — an absent capability hides the control instead of offering
|
|
6958
|
+
* one that can only fail.
|
|
5073
6959
|
*/
|
|
5074
6960
|
function SessionPanel({ client, sessionId, header, className }) {
|
|
5075
6961
|
const [protocolError, setProtocolError] = useState(void 0);
|
|
5076
|
-
const
|
|
6962
|
+
const [panel, setPanel] = useState();
|
|
6963
|
+
const { state, connection, protocolMismatch, models, effectiveModel, handle, send, approve, deny, interrupt, setModel, setPermissionMode, reconnectNow } = useClaudeSession(client, sessionId, { onProtocolError: setProtocolError });
|
|
5077
6964
|
useEffect(() => setProtocolError(void 0), [sessionId]);
|
|
6965
|
+
useEffect(() => {
|
|
6966
|
+
const onVisible = () => {
|
|
6967
|
+
if (document.visibilityState === "visible") reconnectNow();
|
|
6968
|
+
};
|
|
6969
|
+
document.addEventListener("visibilitychange", onVisible);
|
|
6970
|
+
return () => document.removeEventListener("visibilitychange", onVisible);
|
|
6971
|
+
}, [reconnectNow]);
|
|
5078
6972
|
useToolCallHost(handle);
|
|
6973
|
+
const capabilities = state.capabilities;
|
|
5079
6974
|
const busy = state.status === "running" || state.status === "awaiting_approval";
|
|
5080
6975
|
const ended = state.status === "failed" || state.status === "closed";
|
|
6976
|
+
const attachments = useAttachments(client, sessionId, {
|
|
6977
|
+
capabilities,
|
|
6978
|
+
engine: state.engine
|
|
6979
|
+
});
|
|
6980
|
+
const hostFiles = useHostFileSearch(client, state.cwd);
|
|
6981
|
+
const windows = useMemo(() => rateLimitWindows(state), [state]);
|
|
6982
|
+
const hostImage = useHostImage(client, sessionId, state.producedFiles);
|
|
6983
|
+
const composerRef = useRef(null);
|
|
5081
6984
|
const commands = useMemo(() => {
|
|
5082
6985
|
if (!state.commands) return void 0;
|
|
5083
6986
|
if (state.commands.some((c) => c.name === "model")) return state.commands;
|
|
@@ -5087,52 +6990,95 @@ function SessionPanel({ client, sessionId, header, className }) {
|
|
|
5087
6990
|
argumentHint: "<model>"
|
|
5088
6991
|
}, ...state.commands];
|
|
5089
6992
|
}, [state.commands]);
|
|
5090
|
-
const handleSend = (text) => {
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
6993
|
+
const handleSend = (text, attachmentIds) => {
|
|
6994
|
+
if (attachmentIds.length === 0) {
|
|
6995
|
+
const modelCommand = /^\/model\s+(\S+)$/.exec(text);
|
|
6996
|
+
if (modelCommand) {
|
|
6997
|
+
setModel(modelCommand[1]);
|
|
6998
|
+
return;
|
|
6999
|
+
}
|
|
7000
|
+
if (capabilities.mcpStatus && text.trim() === "/mcp") {
|
|
7001
|
+
setPanel("mcp");
|
|
7002
|
+
return;
|
|
7003
|
+
}
|
|
5095
7004
|
}
|
|
5096
|
-
send(text);
|
|
7005
|
+
send(text, attachmentIds);
|
|
5097
7006
|
};
|
|
7007
|
+
const actionsMenu = /* @__PURE__ */ jsxs(Menu, { children: [/* @__PURE__ */ jsx(MenuTrigger, { render: /* @__PURE__ */ jsx(Button, {
|
|
7008
|
+
variant: "ghost",
|
|
7009
|
+
size: "icon-sm",
|
|
7010
|
+
"aria-label": "Session actions",
|
|
7011
|
+
children: /* @__PURE__ */ jsx(MoreHorizontal, { className: "size-4" })
|
|
7012
|
+
}) }), /* @__PURE__ */ jsxs(MenuContent, { children: [
|
|
7013
|
+
capabilities.contextUsage ? /* @__PURE__ */ jsxs(MenuItem, {
|
|
7014
|
+
onClick: () => setPanel("context"),
|
|
7015
|
+
children: [/* @__PURE__ */ jsx(ChartPie, { className: "size-3.5 text-fg-3" }), " Context"]
|
|
7016
|
+
}) : null,
|
|
7017
|
+
capabilities.rateLimits ? /* @__PURE__ */ jsxs(MenuItem, {
|
|
7018
|
+
onClick: () => setPanel("usage"),
|
|
7019
|
+
children: [/* @__PURE__ */ jsx(Gauge, { className: "size-3.5 text-fg-3" }), " Usage"]
|
|
7020
|
+
}) : null,
|
|
7021
|
+
/* @__PURE__ */ jsxs(MenuItem, {
|
|
7022
|
+
onClick: () => setPanel("info"),
|
|
7023
|
+
children: [/* @__PURE__ */ jsx(Info, { className: "size-3.5 text-fg-3" }), " Session info"]
|
|
7024
|
+
}),
|
|
7025
|
+
capabilities.mcpStatus ? /* @__PURE__ */ jsxs(MenuItem, {
|
|
7026
|
+
onClick: () => setPanel("mcp"),
|
|
7027
|
+
children: [/* @__PURE__ */ jsx(Plug, { className: "size-3.5 text-fg-3" }), " MCP servers"]
|
|
7028
|
+
}) : null,
|
|
7029
|
+
capabilities.skillsList ? /* @__PURE__ */ jsxs(MenuItem, {
|
|
7030
|
+
onClick: () => setPanel("skills"),
|
|
7031
|
+
children: [/* @__PURE__ */ jsx(Sparkles, { className: "size-3.5 text-fg-3" }), " Skills"]
|
|
7032
|
+
}) : null,
|
|
7033
|
+
hostFiles.available ? /* @__PURE__ */ jsxs(MenuItem, {
|
|
7034
|
+
onClick: () => setPanel("files"),
|
|
7035
|
+
children: [/* @__PURE__ */ jsx(FolderTree, { className: "size-3.5 text-fg-3" }), " Project files"]
|
|
7036
|
+
}) : null
|
|
7037
|
+
] })] });
|
|
7038
|
+
const headerTakesActions = typeof header === "function";
|
|
5098
7039
|
return /* @__PURE__ */ jsxs("div", {
|
|
5099
7040
|
"data-slot": "session-panel",
|
|
5100
7041
|
className: cn("flex h-full min-h-0 flex-col overflow-hidden bg-bg", className),
|
|
5101
7042
|
children: [
|
|
5102
|
-
header,
|
|
7043
|
+
headerTakesActions ? header({ actions: actionsMenu }) : header,
|
|
5103
7044
|
/* @__PURE__ */ jsx(StatusBar, {
|
|
5104
7045
|
state,
|
|
5105
|
-
|
|
7046
|
+
connection,
|
|
7047
|
+
onOpenStatus: () => setPanel("info"),
|
|
7048
|
+
onOpenContext: () => setPanel("context"),
|
|
7049
|
+
onOpenUsage: () => setPanel("usage"),
|
|
7050
|
+
actions: headerTakesActions ? void 0 : actionsMenu
|
|
5106
7051
|
}),
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
children:
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
})]
|
|
5122
|
-
})
|
|
7052
|
+
protocolMismatch !== void 0 ? /* @__PURE__ */ jsxs(Notice, {
|
|
7053
|
+
level: "warning",
|
|
7054
|
+
children: [
|
|
7055
|
+
"Server speaks protocol v",
|
|
7056
|
+
protocolMismatch,
|
|
7057
|
+
", this build renders v",
|
|
7058
|
+
PROTOCOL_VERSION,
|
|
7059
|
+
". Some events may not render."
|
|
7060
|
+
]
|
|
7061
|
+
}) : null,
|
|
7062
|
+
protocolError ? /* @__PURE__ */ jsx(Notice, {
|
|
7063
|
+
level: "error",
|
|
7064
|
+
onDismiss: () => setProtocolError(void 0),
|
|
7065
|
+
children: protocolError
|
|
5123
7066
|
}) : null,
|
|
5124
7067
|
/* @__PURE__ */ jsx(Transcript, {
|
|
5125
7068
|
state,
|
|
5126
|
-
fileUrl: sessionId ? (path) => client.sessionFileUrl(sessionId, path) : void 0
|
|
7069
|
+
fileUrl: sessionId ? (path) => client.sessionFileUrl(sessionId, path) : void 0,
|
|
7070
|
+
attachmentUrl: sessionId ? (id) => client.attachmentUrl(sessionId, id) : void 0,
|
|
7071
|
+
canBrowseFiles: hostFiles.available,
|
|
7072
|
+
hostImage
|
|
5127
7073
|
}),
|
|
5128
|
-
state.pendingApprovals.length > 0 ? /* @__PURE__ */ jsx("div", {
|
|
7074
|
+
capabilities.interactiveApprovals && state.pendingApprovals.length > 0 ? /* @__PURE__ */ jsx("div", {
|
|
5129
7075
|
className: "px-3 pb-2",
|
|
5130
7076
|
children: /* @__PURE__ */ jsx("div", {
|
|
5131
7077
|
className: "mx-auto flex w-full max-w-3xl flex-col gap-2",
|
|
5132
7078
|
children: state.pendingApprovals.map((request) => request.toolName === "AskUserQuestion" && parseUserQuestions(request.input).length > 0 ? /* @__PURE__ */ jsx(QuestionPrompt, {
|
|
5133
7079
|
request,
|
|
5134
7080
|
onAnswer: approve,
|
|
5135
|
-
onDismiss: deny
|
|
7081
|
+
onDismiss: (id) => deny(id, "Question dismissed by user")
|
|
5136
7082
|
}, request.id) : /* @__PURE__ */ jsx(PermissionPrompt, {
|
|
5137
7083
|
request,
|
|
5138
7084
|
onApprove: approve,
|
|
@@ -5141,26 +7087,1000 @@ function SessionPanel({ client, sessionId, header, className }) {
|
|
|
5141
7087
|
})
|
|
5142
7088
|
}) : null,
|
|
5143
7089
|
/* @__PURE__ */ jsx(Composer, {
|
|
7090
|
+
ref: composerRef,
|
|
5144
7091
|
onSend: handleSend,
|
|
5145
7092
|
onInterrupt: interrupt,
|
|
5146
7093
|
busy,
|
|
5147
7094
|
disabled: ended || !sessionId,
|
|
5148
|
-
commands,
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
7095
|
+
commands: capabilities.slashCommands ? commands : void 0,
|
|
7096
|
+
skills: capabilities.skillsList ? state.skills : void 0,
|
|
7097
|
+
attachments,
|
|
7098
|
+
onSearchFiles: hostFiles.available ? (query, options) => hostFiles.search(query, {
|
|
7099
|
+
...options,
|
|
7100
|
+
limit: 8
|
|
7101
|
+
}) : void 0,
|
|
7102
|
+
toolbar: /* @__PURE__ */ jsxs(Fragment$1, { children: [models.length ? /* @__PURE__ */ jsx(ModelSelect, {
|
|
7103
|
+
models,
|
|
7104
|
+
model: effectiveModel,
|
|
5152
7105
|
onModelChange: setModel,
|
|
5153
7106
|
disabled: ended
|
|
5154
7107
|
}) : null, state.permissionMode ? /* @__PURE__ */ jsx(PermissionModeSelect, {
|
|
5155
7108
|
mode: state.permissionMode,
|
|
5156
7109
|
onModeChange: setPermissionMode,
|
|
5157
|
-
modes:
|
|
7110
|
+
modes: capabilities.permissionModes,
|
|
7111
|
+
canBypass: state.session?.canBypassPermissions,
|
|
5158
7112
|
disabled: ended
|
|
5159
7113
|
}) : null] })
|
|
7114
|
+
}),
|
|
7115
|
+
/* @__PURE__ */ jsx(SessionInfoDialog, {
|
|
7116
|
+
state,
|
|
7117
|
+
client,
|
|
7118
|
+
sessionId,
|
|
7119
|
+
open: panel === "info",
|
|
7120
|
+
onOpenChange: (next) => setPanel(next ? "info" : void 0)
|
|
7121
|
+
}),
|
|
7122
|
+
/* @__PURE__ */ jsx(ContextDialog, {
|
|
7123
|
+
usage: state.contextUsage,
|
|
7124
|
+
open: panel === "context",
|
|
7125
|
+
onOpenChange: (next) => setPanel(next ? "context" : void 0)
|
|
7126
|
+
}),
|
|
7127
|
+
/* @__PURE__ */ jsx(UsageDialog, {
|
|
7128
|
+
rateLimits: windows,
|
|
7129
|
+
subscriptionType: state.subscriptionType,
|
|
7130
|
+
engine: state.engine ?? "claude",
|
|
7131
|
+
totalCostUsd: state.totalCostUsd,
|
|
7132
|
+
updatedAt: state.rateLimitsUpdatedAt,
|
|
7133
|
+
open: panel === "usage",
|
|
7134
|
+
onOpenChange: (next) => setPanel(next ? "usage" : void 0)
|
|
7135
|
+
}),
|
|
7136
|
+
/* @__PURE__ */ jsx(McpDialog, {
|
|
7137
|
+
client,
|
|
7138
|
+
sessionId,
|
|
7139
|
+
canManageServers: capabilities.mcpServerActions,
|
|
7140
|
+
open: panel === "mcp",
|
|
7141
|
+
onOpenChange: (next) => setPanel(next ? "mcp" : void 0)
|
|
7142
|
+
}),
|
|
7143
|
+
/* @__PURE__ */ jsx(SkillsDialog, {
|
|
7144
|
+
skills: state.skills,
|
|
7145
|
+
open: panel === "skills",
|
|
7146
|
+
onOpenChange: (next) => setPanel(next ? "skills" : void 0),
|
|
7147
|
+
onUse: (skill) => composerRef.current?.insertText(skillPrompt(skill))
|
|
7148
|
+
}),
|
|
7149
|
+
/* @__PURE__ */ jsx(HostFilesDialog, {
|
|
7150
|
+
client,
|
|
7151
|
+
cwd: state.cwd,
|
|
7152
|
+
open: panel === "files",
|
|
7153
|
+
onOpenChange: (next) => setPanel(next ? "files" : void 0)
|
|
5160
7154
|
})
|
|
5161
7155
|
]
|
|
5162
7156
|
});
|
|
5163
7157
|
}
|
|
7158
|
+
/**
|
|
7159
|
+
* Turns a host path a tool card is holding into something an `<img>` can show.
|
|
7160
|
+
*
|
|
7161
|
+
* Two sources, tried in that order and for a reason:
|
|
7162
|
+
*
|
|
7163
|
+
* 1. **The session's produced files.** If this session's own runner announced
|
|
7164
|
+
* writing that path (`file_produced`), the gateway will serve it from
|
|
7165
|
+
* `/sessions/:id/produced/:fileId` — no host-file roots to declare, no byte
|
|
7166
|
+
* cap to raise. This is the path codex's generated images take, and it is
|
|
7167
|
+
* why they now render out of the box.
|
|
7168
|
+
* 2. **`/fs/read`.** For a path nothing produced — a picture the model looked
|
|
7169
|
+
* at, an image already in the tree — where the operator's declared roots are
|
|
7170
|
+
* the right gate and the answer is legitimately "no" outside them.
|
|
7171
|
+
*
|
|
7172
|
+
* The cache is what makes this usable from a transcript row: rows re-render on
|
|
7173
|
+
* every streamed delta, and an uncached resolver would re-fetch the picture each
|
|
7174
|
+
* time. A refusal is cached too, so it costs one request rather than one per
|
|
7175
|
+
* render.
|
|
7176
|
+
*
|
|
7177
|
+
* Keyed by `fileId`-or-path so that a path which becomes produced *after* a
|
|
7178
|
+
* failed `/fs/read` is retried under a different key rather than staying cached
|
|
7179
|
+
* as a miss.
|
|
7180
|
+
*/
|
|
7181
|
+
function useHostImage(client, sessionId, producedFiles) {
|
|
7182
|
+
const cache = useRef(/* @__PURE__ */ new Map());
|
|
7183
|
+
const objectUrls = useRef([]);
|
|
7184
|
+
useEffect(() => () => {
|
|
7185
|
+
for (const url of objectUrls.current) URL.revokeObjectURL(url);
|
|
7186
|
+
objectUrls.current = [];
|
|
7187
|
+
}, []);
|
|
7188
|
+
return useCallback((path) => {
|
|
7189
|
+
const produced = producedFiles?.[path];
|
|
7190
|
+
const key = produced ? `produced:${produced.fileId}` : `fs:${path}`;
|
|
7191
|
+
const hit = cache.current.get(key);
|
|
7192
|
+
if (hit) return hit;
|
|
7193
|
+
const pending = produced && sessionId ? client.readProducedFile(sessionId, produced.fileId).then((blob) => {
|
|
7194
|
+
if (blob.size === 0) return void 0;
|
|
7195
|
+
const url = URL.createObjectURL(blob);
|
|
7196
|
+
objectUrls.current.push(url);
|
|
7197
|
+
return url;
|
|
7198
|
+
}).catch(() => void 0) : client.readHostFile(path).then((file) => {
|
|
7199
|
+
if (file.encoding !== "base64") return void 0;
|
|
7200
|
+
const mediaType = IMAGE_MEDIA_TYPES[path.slice(path.lastIndexOf(".") + 1).toLowerCase()];
|
|
7201
|
+
return mediaType ? `data:${mediaType};base64,${file.content}` : void 0;
|
|
7202
|
+
}).catch(() => void 0);
|
|
7203
|
+
cache.current.set(key, pending);
|
|
7204
|
+
return pending;
|
|
7205
|
+
}, [
|
|
7206
|
+
client,
|
|
7207
|
+
sessionId,
|
|
7208
|
+
producedFiles
|
|
7209
|
+
]);
|
|
7210
|
+
}
|
|
7211
|
+
/** Extensions worth rendering inline, and what to call them. Anything else is
|
|
7212
|
+
* left to the card's path text — guessing a media type is how an HTML file ends
|
|
7213
|
+
* up in an `<img>`. */
|
|
7214
|
+
const IMAGE_MEDIA_TYPES = {
|
|
7215
|
+
png: "image/png",
|
|
7216
|
+
jpg: "image/jpeg",
|
|
7217
|
+
jpeg: "image/jpeg",
|
|
7218
|
+
gif: "image/gif",
|
|
7219
|
+
webp: "image/webp"
|
|
7220
|
+
};
|
|
7221
|
+
/** A dismissible advisory strip above the transcript. */
|
|
7222
|
+
function Notice({ level, onDismiss, children }) {
|
|
7223
|
+
return /* @__PURE__ */ jsx("div", {
|
|
7224
|
+
className: "px-3 pt-2",
|
|
7225
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
7226
|
+
role: "alert",
|
|
7227
|
+
className: cn("mx-auto flex w-full max-w-3xl items-start gap-2 rounded-md border px-3 py-2 text-body-sm", level === "error" ? "border-danger/40 bg-danger-bg text-danger" : "border-warning/40 bg-warning-bg text-warning"),
|
|
7228
|
+
children: [
|
|
7229
|
+
/* @__PURE__ */ jsx(TriangleAlert, { className: "mt-0.5 size-3.5 shrink-0" }),
|
|
7230
|
+
/* @__PURE__ */ jsx("span", {
|
|
7231
|
+
className: "min-w-0 flex-1 break-words",
|
|
7232
|
+
children
|
|
7233
|
+
}),
|
|
7234
|
+
onDismiss ? /* @__PURE__ */ jsx("button", {
|
|
7235
|
+
type: "button",
|
|
7236
|
+
onClick: onDismiss,
|
|
7237
|
+
"aria-label": "Dismiss",
|
|
7238
|
+
className: "shrink-0 opacity-70 transition-opacity hover:opacity-100",
|
|
7239
|
+
children: /* @__PURE__ */ jsx(X, { className: "size-3.5" })
|
|
7240
|
+
}) : null
|
|
7241
|
+
]
|
|
7242
|
+
})
|
|
7243
|
+
});
|
|
7244
|
+
}
|
|
7245
|
+
//#endregion
|
|
7246
|
+
//#region src/components/agent/EditorTabs.tsx
|
|
7247
|
+
/**
|
|
7248
|
+
* The open-file tab strip.
|
|
7249
|
+
*
|
|
7250
|
+
* Hand-rolled rather than built on `@base-ui/react`'s `Tabs`: a VS Code tab
|
|
7251
|
+
* carries a close button, and a `<button>` inside a `<button>` is invalid HTML —
|
|
7252
|
+
* getting the primitive to render something else costs more than the roving
|
|
7253
|
+
* tabindex it would have provided. So that part is here, explicitly, along with
|
|
7254
|
+
* the two affordances that actually make a tab strip feel right: middle-click to
|
|
7255
|
+
* close, and the active tab scrolling itself into view.
|
|
7256
|
+
*
|
|
7257
|
+
* Deliberately state-free. Which files are open, which is focused and what
|
|
7258
|
+
* closing does are all decided by `useOpenFiles`.
|
|
7259
|
+
*/
|
|
7260
|
+
function EditorTabs({ files, activePath, onActivate, onClose, className }) {
|
|
7261
|
+
return /* @__PURE__ */ jsx(TooltipProvider, {
|
|
7262
|
+
delay: 500,
|
|
7263
|
+
closeDelay: 0,
|
|
7264
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
7265
|
+
"data-slot": "editor-tabs",
|
|
7266
|
+
role: "tablist",
|
|
7267
|
+
"aria-label": "Open files",
|
|
7268
|
+
className: cn("flex shrink-0 items-stretch overflow-x-auto border-b border-border bg-surface", className),
|
|
7269
|
+
children: files.map((file) => /* @__PURE__ */ jsx(Tab, {
|
|
7270
|
+
file,
|
|
7271
|
+
active: file.path === activePath,
|
|
7272
|
+
onActivate: () => onActivate(file.path),
|
|
7273
|
+
onClose: () => onClose(file.path),
|
|
7274
|
+
onArrow: (direction) => {
|
|
7275
|
+
const next = files[files.findIndex((f) => f.path === file.path) + direction];
|
|
7276
|
+
if (next) onActivate(next.path);
|
|
7277
|
+
}
|
|
7278
|
+
}, file.path))
|
|
7279
|
+
})
|
|
7280
|
+
});
|
|
7281
|
+
}
|
|
7282
|
+
function Tab({ file, active, onActivate, onClose, onArrow }) {
|
|
7283
|
+
const dirty = isDirty(file);
|
|
7284
|
+
const ref = useRef(null);
|
|
7285
|
+
useEffect(() => {
|
|
7286
|
+
if (active) ref.current?.scrollIntoView({
|
|
7287
|
+
block: "nearest",
|
|
7288
|
+
inline: "nearest"
|
|
7289
|
+
});
|
|
7290
|
+
}, [active]);
|
|
7291
|
+
const onAuxClick = (event) => {
|
|
7292
|
+
if (event.button !== 1) return;
|
|
7293
|
+
event.preventDefault();
|
|
7294
|
+
onClose();
|
|
7295
|
+
};
|
|
7296
|
+
return /* @__PURE__ */ jsxs(Tip, {
|
|
7297
|
+
side: "bottom",
|
|
7298
|
+
render: /* @__PURE__ */ jsx("div", {
|
|
7299
|
+
ref,
|
|
7300
|
+
role: "tab",
|
|
7301
|
+
tabIndex: active ? 0 : -1,
|
|
7302
|
+
"aria-selected": active,
|
|
7303
|
+
onClick: onActivate,
|
|
7304
|
+
onAuxClick,
|
|
7305
|
+
onKeyDown: (event) => {
|
|
7306
|
+
if (event.key === "ArrowRight") onArrow(1);
|
|
7307
|
+
else if (event.key === "ArrowLeft") onArrow(-1);
|
|
7308
|
+
else if (event.key === "Enter" || event.key === " ") onActivate();
|
|
7309
|
+
else return;
|
|
7310
|
+
event.preventDefault();
|
|
7311
|
+
},
|
|
7312
|
+
className: cn("group flex min-w-0 shrink-0 cursor-pointer items-center gap-1.5 border-r border-border px-3 py-1.5 transition-colors", "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent focus-visible:ring-inset", active ? "bg-bg text-fg-1" : "text-fg-3 hover:bg-surface-hover hover:text-fg-2")
|
|
7313
|
+
}),
|
|
7314
|
+
content: /* @__PURE__ */ jsxs("span", {
|
|
7315
|
+
className: "flex flex-col gap-0.5",
|
|
7316
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
7317
|
+
className: "font-mono break-all",
|
|
7318
|
+
children: file.path
|
|
7319
|
+
}), file.bytes !== void 0 ? /* @__PURE__ */ jsx("span", {
|
|
7320
|
+
className: "text-fg-4",
|
|
7321
|
+
children: formatBytes(file.bytes)
|
|
7322
|
+
}) : null]
|
|
7323
|
+
}),
|
|
7324
|
+
children: [
|
|
7325
|
+
/* @__PURE__ */ jsx("span", {
|
|
7326
|
+
className: cn("max-w-40 truncate font-mono text-label", dirty && "italic"),
|
|
7327
|
+
children: file.name
|
|
7328
|
+
}),
|
|
7329
|
+
file.status === "error" ? /* @__PURE__ */ jsx("span", {
|
|
7330
|
+
className: "shrink-0 text-danger",
|
|
7331
|
+
children: "!"
|
|
7332
|
+
}) : null,
|
|
7333
|
+
/* @__PURE__ */ jsxs("button", {
|
|
7334
|
+
type: "button",
|
|
7335
|
+
"aria-label": dirty ? `Close ${file.name} (unsaved changes)` : `Close ${file.name}`,
|
|
7336
|
+
onClick: (event) => {
|
|
7337
|
+
event.stopPropagation();
|
|
7338
|
+
onClose();
|
|
7339
|
+
},
|
|
7340
|
+
className: cn("shrink-0 rounded p-0.5 text-fg-4 transition-opacity hover:bg-surface-hover hover:text-fg-1", active || dirty ? "opacity-70" : "opacity-0 group-hover:opacity-70 focus-visible:opacity-100"),
|
|
7341
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
7342
|
+
className: cn("block", dirty && "group-hover:hidden"),
|
|
7343
|
+
children: dirty ? /* @__PURE__ */ jsx("span", { className: "block size-3 rounded-full bg-fg-2" }) : /* @__PURE__ */ jsx(X, { className: "size-3" })
|
|
7344
|
+
}), dirty ? /* @__PURE__ */ jsx("span", {
|
|
7345
|
+
className: "hidden group-hover:block",
|
|
7346
|
+
children: /* @__PURE__ */ jsx(X, { className: "size-3" })
|
|
7347
|
+
}) : null]
|
|
7348
|
+
})
|
|
7349
|
+
]
|
|
7350
|
+
});
|
|
7351
|
+
}
|
|
7352
|
+
//#endregion
|
|
7353
|
+
//#region src/components/agent/FileTree.tsx
|
|
7354
|
+
/** How far one level of nesting indents, in pixels. Inline rather than a Tailwind
|
|
7355
|
+
* class because depth is a number at runtime and `pl-${n}` is not a class. */
|
|
7356
|
+
const INDENT = 12;
|
|
7357
|
+
/**
|
|
7358
|
+
* The workspace's left rail: an expandable tree of the session's project,
|
|
7359
|
+
* with a search box over the same fuzzy route `@file` completion uses.
|
|
7360
|
+
*
|
|
7361
|
+
* Presentational by construction — every piece of state it renders comes from
|
|
7362
|
+
* the hooks in `@workerdeck/react`, and the only thing it owns is the search
|
|
7363
|
+
* query, which is the text in its own input.
|
|
7364
|
+
*
|
|
7365
|
+
* Searching replaces the tree with matches rather than filtering it: the route
|
|
7366
|
+
* answers with paths from all over the project, and threading those back into
|
|
7367
|
+
* tree positions would mean expanding a dozen directories to show six results.
|
|
7368
|
+
*/
|
|
7369
|
+
function FileTree({ tree, search, activePath, onOpenFile, onCollapse, style, className }) {
|
|
7370
|
+
const [query, setQuery] = useState("");
|
|
7371
|
+
const [matches, setMatches] = useState();
|
|
7372
|
+
const searching = query.trim().length > 0;
|
|
7373
|
+
useEffect(() => {
|
|
7374
|
+
if (!search?.available) return;
|
|
7375
|
+
const q = query.trim();
|
|
7376
|
+
if (!q) {
|
|
7377
|
+
setMatches(void 0);
|
|
7378
|
+
return;
|
|
7379
|
+
}
|
|
7380
|
+
const controller = new AbortController();
|
|
7381
|
+
const timer = setTimeout(() => {
|
|
7382
|
+
search.search(q, {
|
|
7383
|
+
limit: 60,
|
|
7384
|
+
signal: controller.signal
|
|
7385
|
+
}).then((found) => {
|
|
7386
|
+
if (!controller.signal.aborted) setMatches(found);
|
|
7387
|
+
});
|
|
7388
|
+
}, 150);
|
|
7389
|
+
return () => {
|
|
7390
|
+
controller.abort();
|
|
7391
|
+
clearTimeout(timer);
|
|
7392
|
+
};
|
|
7393
|
+
}, [search, query]);
|
|
7394
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
7395
|
+
"data-slot": "file-tree",
|
|
7396
|
+
style,
|
|
7397
|
+
className: cn("flex min-h-0 min-w-0 flex-col bg-surface", className),
|
|
7398
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
7399
|
+
className: "flex items-center gap-1 px-2 pt-2",
|
|
7400
|
+
children: [
|
|
7401
|
+
search?.available ? /* @__PURE__ */ jsxs("div", {
|
|
7402
|
+
className: "relative min-w-0 flex-1",
|
|
7403
|
+
children: [
|
|
7404
|
+
/* @__PURE__ */ jsx(Search, { className: "absolute top-1/2 left-2 size-3.5 -translate-y-1/2 text-fg-4" }),
|
|
7405
|
+
/* @__PURE__ */ jsx(Input, {
|
|
7406
|
+
value: query,
|
|
7407
|
+
onChange: (e) => setQuery(e.target.value),
|
|
7408
|
+
placeholder: "Search files…",
|
|
7409
|
+
className: "h-7 pr-7 pl-7 text-label",
|
|
7410
|
+
spellCheck: false
|
|
7411
|
+
}),
|
|
7412
|
+
searching ? /* @__PURE__ */ jsx("button", {
|
|
7413
|
+
type: "button",
|
|
7414
|
+
onClick: () => setQuery(""),
|
|
7415
|
+
"aria-label": "Clear search",
|
|
7416
|
+
className: "absolute top-1/2 right-1.5 -translate-y-1/2 text-fg-4 transition-colors hover:text-fg-2",
|
|
7417
|
+
children: /* @__PURE__ */ jsx(X, { className: "size-3.5" })
|
|
7418
|
+
}) : null
|
|
7419
|
+
]
|
|
7420
|
+
}) : /* @__PURE__ */ jsx("span", {
|
|
7421
|
+
className: "min-w-0 flex-1 truncate px-1 font-mono text-label text-fg-4",
|
|
7422
|
+
children: tree.root
|
|
7423
|
+
}),
|
|
7424
|
+
/* @__PURE__ */ jsx(Button, {
|
|
7425
|
+
variant: "ghost",
|
|
7426
|
+
size: "icon-sm",
|
|
7427
|
+
"aria-label": "Refresh the file tree",
|
|
7428
|
+
onClick: () => tree.refresh(),
|
|
7429
|
+
children: /* @__PURE__ */ jsx(RefreshCw, { className: "size-3.5 text-fg-3" })
|
|
7430
|
+
}),
|
|
7431
|
+
onCollapse ? /* @__PURE__ */ jsx(Button, {
|
|
7432
|
+
variant: "ghost",
|
|
7433
|
+
size: "icon-sm",
|
|
7434
|
+
"aria-label": "Hide project files",
|
|
7435
|
+
onClick: onCollapse,
|
|
7436
|
+
children: /* @__PURE__ */ jsx(PanelLeftClose, { className: "size-3.5 text-fg-3" })
|
|
7437
|
+
}) : null
|
|
7438
|
+
]
|
|
7439
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
7440
|
+
className: "min-h-0 flex-1 overflow-auto px-1 py-1",
|
|
7441
|
+
children: tree.error ? /* @__PURE__ */ jsx("p", {
|
|
7442
|
+
className: "px-2 py-3 text-body-sm text-danger",
|
|
7443
|
+
children: tree.error
|
|
7444
|
+
}) : searching ? matches === void 0 ? /* @__PURE__ */ jsx("div", {
|
|
7445
|
+
className: "py-6 text-center",
|
|
7446
|
+
children: /* @__PURE__ */ jsx(Spinner, { className: "size-4 text-fg-4" })
|
|
7447
|
+
}) : matches.length === 0 ? /* @__PURE__ */ jsx("p", {
|
|
7448
|
+
className: "px-2 py-6 text-center text-body-sm text-fg-4",
|
|
7449
|
+
children: "No matching files."
|
|
7450
|
+
}) : /* @__PURE__ */ jsx("ul", { children: matches.map((match) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(Row, {
|
|
7451
|
+
label: fileName(match.relative),
|
|
7452
|
+
detail: directoryOf(match.relative),
|
|
7453
|
+
title: match.relative,
|
|
7454
|
+
icon: /* @__PURE__ */ jsx(File, { className: "size-3.5 shrink-0 text-fg-4" }),
|
|
7455
|
+
active: match.path === activePath,
|
|
7456
|
+
onClick: () => onOpenFile(match.path)
|
|
7457
|
+
}) }, match.path)) }) : tree.loading ? /* @__PURE__ */ jsx("div", {
|
|
7458
|
+
className: "py-6 text-center",
|
|
7459
|
+
children: /* @__PURE__ */ jsx(Spinner, { className: "size-4 text-fg-4" })
|
|
7460
|
+
}) : tree.rows.length === 0 ? /* @__PURE__ */ jsx("p", {
|
|
7461
|
+
className: "px-2 py-6 text-center text-body-sm text-fg-4",
|
|
7462
|
+
children: "This project is empty."
|
|
7463
|
+
}) : /* @__PURE__ */ jsx("ul", {
|
|
7464
|
+
role: "tree",
|
|
7465
|
+
"aria-label": "Project files",
|
|
7466
|
+
children: tree.rows.map((row) => /* @__PURE__ */ jsxs("li", {
|
|
7467
|
+
role: "treeitem",
|
|
7468
|
+
"aria-expanded": row.expanded,
|
|
7469
|
+
"aria-level": row.depth + 1,
|
|
7470
|
+
children: [/* @__PURE__ */ jsx(Row, {
|
|
7471
|
+
label: row.entry.name,
|
|
7472
|
+
indent: row.depth,
|
|
7473
|
+
active: row.entry.path === activePath,
|
|
7474
|
+
icon: row.entry.type === "dir" ? row.loading ? /* @__PURE__ */ jsx(Spinner, { className: "size-3.5 shrink-0 text-fg-4" }) : row.expanded ? /* @__PURE__ */ jsx(FolderOpen, { className: "size-3.5 shrink-0 text-accent" }) : /* @__PURE__ */ jsx(Folder, { className: "size-3.5 shrink-0 text-accent" }) : /* @__PURE__ */ jsx(EntryIcon, { type: row.entry.type }),
|
|
7475
|
+
chevron: row.entry.type === "dir" ? /* @__PURE__ */ jsx(ChevronRight, { className: cn("size-3 shrink-0 text-fg-4 transition-transform", row.expanded && "rotate-90") }) : void 0,
|
|
7476
|
+
onClick: () => row.entry.type === "dir" ? tree.toggle(row.entry.path) : onOpenFile(row.entry.path)
|
|
7477
|
+
}), row.truncated ? /* @__PURE__ */ jsx("p", {
|
|
7478
|
+
className: "truncate py-0.5 text-label text-fg-4",
|
|
7479
|
+
style: { paddingLeft: (row.depth + 1) * INDENT + 22 },
|
|
7480
|
+
children: "More entries than the server will return — use search."
|
|
7481
|
+
}) : null]
|
|
7482
|
+
}, row.entry.path))
|
|
7483
|
+
})
|
|
7484
|
+
})]
|
|
7485
|
+
});
|
|
7486
|
+
}
|
|
7487
|
+
/** One clickable line. A directory and a file differ only in what the click does
|
|
7488
|
+
* and whether there is a chevron — visually they are the same row. */
|
|
7489
|
+
function Row({ label, detail, title, icon, chevron, indent = 0, active, onClick }) {
|
|
7490
|
+
const ref = useRef(null);
|
|
7491
|
+
useEffect(() => {
|
|
7492
|
+
if (active) ref.current?.scrollIntoView({ block: "nearest" });
|
|
7493
|
+
}, [active]);
|
|
7494
|
+
return /* @__PURE__ */ jsxs("button", {
|
|
7495
|
+
ref,
|
|
7496
|
+
type: "button",
|
|
7497
|
+
onClick,
|
|
7498
|
+
title: title ?? label,
|
|
7499
|
+
style: { paddingLeft: indent * INDENT + 4 },
|
|
7500
|
+
className: cn("flex w-full items-center gap-1 rounded py-1 pr-2 text-left transition-colors", active ? "bg-surface-hover text-fg-1" : "text-fg-2 hover:bg-surface-hover"),
|
|
7501
|
+
children: [
|
|
7502
|
+
/* @__PURE__ */ jsx("span", {
|
|
7503
|
+
className: "flex size-3 shrink-0 items-center justify-center",
|
|
7504
|
+
children: chevron
|
|
7505
|
+
}),
|
|
7506
|
+
icon,
|
|
7507
|
+
/* @__PURE__ */ jsx("span", {
|
|
7508
|
+
className: "shrink-0 truncate font-mono text-label",
|
|
7509
|
+
children: label
|
|
7510
|
+
}),
|
|
7511
|
+
detail ? /* @__PURE__ */ jsx("span", {
|
|
7512
|
+
className: "min-w-0 flex-1 truncate font-mono text-label text-fg-4",
|
|
7513
|
+
children: detail
|
|
7514
|
+
}) : null
|
|
7515
|
+
]
|
|
7516
|
+
});
|
|
7517
|
+
}
|
|
7518
|
+
/** Last segment of a relative match. */
|
|
7519
|
+
function fileName(relative) {
|
|
7520
|
+
return relative.slice(relative.lastIndexOf("/") + 1);
|
|
7521
|
+
}
|
|
7522
|
+
/** Everything before it, or `undefined` for a file at the search root. */
|
|
7523
|
+
function directoryOf(relative) {
|
|
7524
|
+
const cut = relative.lastIndexOf("/");
|
|
7525
|
+
return cut === -1 ? void 0 : relative.slice(0, cut);
|
|
7526
|
+
}
|
|
7527
|
+
/** A symlink is reported as itself and never silently resolved — following it is
|
|
7528
|
+
* the next request's problem, and that request is refused if it escapes the roots. */
|
|
7529
|
+
function EntryIcon({ type }) {
|
|
7530
|
+
if (type === "symlink") return /* @__PURE__ */ jsx(Link2, { className: "size-3.5 shrink-0 text-fg-4" });
|
|
7531
|
+
return /* @__PURE__ */ jsx(File, { className: "size-3.5 shrink-0 text-fg-4" });
|
|
7532
|
+
}
|
|
7533
|
+
//#endregion
|
|
7534
|
+
//#region src/components/agent/CodeEditor.tsx
|
|
7535
|
+
/**
|
|
7536
|
+
* Monaco — VS Code's own editor — behind a small React surface.
|
|
7537
|
+
*
|
|
7538
|
+
* Two deliberate choices, both about `@workerdeck/ui` being a **published
|
|
7539
|
+
* library** rather than an app:
|
|
7540
|
+
*
|
|
7541
|
+
* 1. **Loaded on demand.** The `import()` is inside an effect, so Monaco is a
|
|
7542
|
+
* separate chunk that arrives when someone first opens a file. A dashboard
|
|
7543
|
+
* that never opens one never pays for it, and Monaco's ~90 language grammars
|
|
7544
|
+
* are themselves lazy (each `registerLanguage` carries an `import()` loader),
|
|
7545
|
+
* so opening a `.ts` file fetches the TypeScript grammar and nothing else.
|
|
7546
|
+
* 2. **No `MonacoEnvironment` is configured here, and none is needed.** Workers
|
|
7547
|
+
* in a library become every embedder's bootstrapping problem, and
|
|
7548
|
+
* `packages/web` ships prebuilt static files at a domain root, which is
|
|
7549
|
+
* exactly where hardcoded worker URLs break. The editor is configured so it
|
|
7550
|
+
* never asks for one: `wordBasedSuggestions` and `quickSuggestions` off,
|
|
7551
|
+
* no diff editor. A host that wants the worker-backed language services
|
|
7552
|
+
* (TypeScript IntelliSense, JSON schema validation) sets `MonacoEnvironment`
|
|
7553
|
+
* itself before the first file is opened — Monaco is a singleton and nothing
|
|
7554
|
+
* here fights that.
|
|
7555
|
+
*
|
|
7556
|
+
* One model per path, kept across tab switches, so undo history and view state
|
|
7557
|
+
* survive clicking away and back — which is most of what makes tabs feel like
|
|
7558
|
+
* tabs rather than like re-opening a file.
|
|
7559
|
+
*/
|
|
7560
|
+
function CodeEditor({ path, value, onChange, onSave, readOnly, className }) {
|
|
7561
|
+
const host = useRef(null);
|
|
7562
|
+
const editor = useRef(null);
|
|
7563
|
+
const monaco = useRef(null);
|
|
7564
|
+
const [ready, setReady] = useState(false);
|
|
7565
|
+
const theme = useDocumentTheme();
|
|
7566
|
+
const handlers = useRef({
|
|
7567
|
+
onChange,
|
|
7568
|
+
onSave
|
|
7569
|
+
});
|
|
7570
|
+
handlers.current = {
|
|
7571
|
+
onChange,
|
|
7572
|
+
onSave
|
|
7573
|
+
};
|
|
7574
|
+
useEffect(() => {
|
|
7575
|
+
let disposed = false;
|
|
7576
|
+
loadMonaco().then((api) => {
|
|
7577
|
+
if (disposed || !host.current) return;
|
|
7578
|
+
monaco.current = api;
|
|
7579
|
+
const instance = api.editor.create(host.current, {
|
|
7580
|
+
value,
|
|
7581
|
+
language: languageOf(path),
|
|
7582
|
+
readOnly,
|
|
7583
|
+
automaticLayout: true,
|
|
7584
|
+
theme: monacoTheme(document.documentElement.getAttribute("data-theme")),
|
|
7585
|
+
minimap: { enabled: false },
|
|
7586
|
+
scrollBeyondLastLine: false,
|
|
7587
|
+
fontSize: 12,
|
|
7588
|
+
lineHeight: 20,
|
|
7589
|
+
fontFamily: "ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace",
|
|
7590
|
+
renderLineHighlight: "line",
|
|
7591
|
+
smoothScrolling: true,
|
|
7592
|
+
padding: {
|
|
7593
|
+
top: 8,
|
|
7594
|
+
bottom: 8
|
|
7595
|
+
},
|
|
7596
|
+
scrollbar: {
|
|
7597
|
+
verticalScrollbarSize: 10,
|
|
7598
|
+
horizontalScrollbarSize: 10
|
|
7599
|
+
},
|
|
7600
|
+
wordBasedSuggestions: "off",
|
|
7601
|
+
quickSuggestions: false
|
|
7602
|
+
});
|
|
7603
|
+
editor.current = instance;
|
|
7604
|
+
instance.onDidChangeModelContent(() => {
|
|
7605
|
+
handlers.current.onChange?.(instance.getValue());
|
|
7606
|
+
});
|
|
7607
|
+
instance.addCommand(api.KeyMod.CtrlCmd | api.KeyCode.KeyS, () => {
|
|
7608
|
+
handlers.current.onSave?.();
|
|
7609
|
+
});
|
|
7610
|
+
setReady(true);
|
|
7611
|
+
});
|
|
7612
|
+
return () => {
|
|
7613
|
+
disposed = true;
|
|
7614
|
+
editor.current?.dispose();
|
|
7615
|
+
editor.current = null;
|
|
7616
|
+
setReady(false);
|
|
7617
|
+
};
|
|
7618
|
+
}, []);
|
|
7619
|
+
useEffect(() => {
|
|
7620
|
+
const api = monaco.current;
|
|
7621
|
+
const instance = editor.current;
|
|
7622
|
+
if (!api || !instance || !ready) return;
|
|
7623
|
+
const uri = api.Uri.parse(`workerdeck://host${path}`);
|
|
7624
|
+
const model = api.editor.getModel(uri) ?? api.editor.createModel(value, languageOf(path), uri);
|
|
7625
|
+
if (instance.getModel() !== model) instance.setModel(model);
|
|
7626
|
+
}, [
|
|
7627
|
+
path,
|
|
7628
|
+
ready,
|
|
7629
|
+
value
|
|
7630
|
+
]);
|
|
7631
|
+
useEffect(() => {
|
|
7632
|
+
const instance = editor.current;
|
|
7633
|
+
if (!instance || !ready) return;
|
|
7634
|
+
const model = instance.getModel();
|
|
7635
|
+
if (!model || model.getValue() === value) return;
|
|
7636
|
+
model.pushEditOperations([], [{
|
|
7637
|
+
range: model.getFullModelRange(),
|
|
7638
|
+
text: value
|
|
7639
|
+
}], () => null);
|
|
7640
|
+
}, [value, ready]);
|
|
7641
|
+
useEffect(() => {
|
|
7642
|
+
if (ready) editor.current?.updateOptions({ readOnly });
|
|
7643
|
+
}, [readOnly, ready]);
|
|
7644
|
+
useEffect(() => {
|
|
7645
|
+
if (ready) monaco.current?.editor.setTheme(monacoTheme(theme));
|
|
7646
|
+
}, [theme, ready]);
|
|
7647
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
7648
|
+
className: cn("relative min-h-0 min-w-0 flex-1", className),
|
|
7649
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
7650
|
+
ref: host,
|
|
7651
|
+
className: "absolute inset-0"
|
|
7652
|
+
}), !ready ? /* @__PURE__ */ jsx("div", {
|
|
7653
|
+
className: "absolute inset-0 flex items-center justify-center bg-bg",
|
|
7654
|
+
children: /* @__PURE__ */ jsx(Spinner, { className: "size-4 text-fg-4" })
|
|
7655
|
+
}) : null]
|
|
7656
|
+
});
|
|
7657
|
+
}
|
|
7658
|
+
/**
|
|
7659
|
+
* Follow the `data-theme` the design tokens already swap on, so the editor is
|
|
7660
|
+
* never the one light rectangle in a dark app (or the reverse). An attribute
|
|
7661
|
+
* observer rather than a media query: the app has a manual toggle, and the
|
|
7662
|
+
* attribute is what that toggle writes.
|
|
7663
|
+
*/
|
|
7664
|
+
function useDocumentTheme() {
|
|
7665
|
+
const [theme, setTheme] = useState(() => typeof document === "undefined" ? null : document.documentElement.getAttribute("data-theme"));
|
|
7666
|
+
useEffect(() => {
|
|
7667
|
+
const root = document.documentElement;
|
|
7668
|
+
const sync = () => setTheme(root.getAttribute("data-theme"));
|
|
7669
|
+
sync();
|
|
7670
|
+
const observer = new MutationObserver(sync);
|
|
7671
|
+
observer.observe(root, {
|
|
7672
|
+
attributes: true,
|
|
7673
|
+
attributeFilter: ["data-theme"]
|
|
7674
|
+
});
|
|
7675
|
+
return () => observer.disconnect();
|
|
7676
|
+
}, []);
|
|
7677
|
+
return theme;
|
|
7678
|
+
}
|
|
7679
|
+
/** An unset attribute means the host never opted into the token themes, in which
|
|
7680
|
+
* case dark matches this package's default surface. */
|
|
7681
|
+
function monacoTheme(theme) {
|
|
7682
|
+
return theme === "light" ? "wd-light" : "wd-dark";
|
|
7683
|
+
}
|
|
7684
|
+
/**
|
|
7685
|
+
* Load Monaco once, register the themes, and hand back the API.
|
|
7686
|
+
*
|
|
7687
|
+
* Cached as a promise rather than a value so that several editors mounting in
|
|
7688
|
+
* the same frame share one load instead of racing three of them.
|
|
7689
|
+
*/
|
|
7690
|
+
let monacoPromise;
|
|
7691
|
+
function loadMonaco() {
|
|
7692
|
+
monacoPromise ??= (async () => {
|
|
7693
|
+
const api = await import("monaco-editor");
|
|
7694
|
+
api.editor.defineTheme("wd-dark", {
|
|
7695
|
+
base: "vs-dark",
|
|
7696
|
+
inherit: true,
|
|
7697
|
+
rules: [],
|
|
7698
|
+
colors: { "editor.background": "#00000000" }
|
|
7699
|
+
});
|
|
7700
|
+
api.editor.defineTheme("wd-light", {
|
|
7701
|
+
base: "vs",
|
|
7702
|
+
inherit: true,
|
|
7703
|
+
rules: [],
|
|
7704
|
+
colors: { "editor.background": "#00000000" }
|
|
7705
|
+
});
|
|
7706
|
+
return api;
|
|
7707
|
+
})();
|
|
7708
|
+
return monacoPromise;
|
|
7709
|
+
}
|
|
7710
|
+
/**
|
|
7711
|
+
* Monaco's language id for a path.
|
|
7712
|
+
*
|
|
7713
|
+
* By extension, with a short table for the files that have none — Monaco's own
|
|
7714
|
+
* registry is keyed on extensions it knows, and an unmatched file falls through
|
|
7715
|
+
* to `plaintext`, which is the honest answer rather than a guess.
|
|
7716
|
+
*/
|
|
7717
|
+
function languageOf(path) {
|
|
7718
|
+
const name = path.slice(path.lastIndexOf("/") + 1);
|
|
7719
|
+
const byName = FILENAME_LANGUAGES[name.toLowerCase()];
|
|
7720
|
+
if (byName) return byName;
|
|
7721
|
+
return EXTENSION_LANGUAGES[name.slice(name.lastIndexOf(".") + 1).toLowerCase()] ?? "plaintext";
|
|
7722
|
+
}
|
|
7723
|
+
/** Files whose type is their whole name. */
|
|
7724
|
+
const FILENAME_LANGUAGES = {
|
|
7725
|
+
dockerfile: "dockerfile",
|
|
7726
|
+
makefile: "plaintext",
|
|
7727
|
+
".gitignore": "plaintext",
|
|
7728
|
+
".env": "shell"
|
|
7729
|
+
};
|
|
7730
|
+
/** Extension → Monaco language id. Only where the id differs from the extension
|
|
7731
|
+
* or the extension is ambiguous; everything else Monaco resolves itself. */
|
|
7732
|
+
const EXTENSION_LANGUAGES = {
|
|
7733
|
+
ts: "typescript",
|
|
7734
|
+
tsx: "typescript",
|
|
7735
|
+
mts: "typescript",
|
|
7736
|
+
cts: "typescript",
|
|
7737
|
+
js: "javascript",
|
|
7738
|
+
jsx: "javascript",
|
|
7739
|
+
mjs: "javascript",
|
|
7740
|
+
cjs: "javascript",
|
|
7741
|
+
json: "json",
|
|
7742
|
+
jsonc: "json",
|
|
7743
|
+
md: "markdown",
|
|
7744
|
+
mdx: "markdown",
|
|
7745
|
+
css: "css",
|
|
7746
|
+
scss: "scss",
|
|
7747
|
+
less: "less",
|
|
7748
|
+
html: "html",
|
|
7749
|
+
htm: "html",
|
|
7750
|
+
xml: "xml",
|
|
7751
|
+
svg: "xml",
|
|
7752
|
+
yml: "yaml",
|
|
7753
|
+
yaml: "yaml",
|
|
7754
|
+
toml: "plaintext",
|
|
7755
|
+
sh: "shell",
|
|
7756
|
+
bash: "shell",
|
|
7757
|
+
zsh: "shell",
|
|
7758
|
+
fish: "shell",
|
|
7759
|
+
py: "python",
|
|
7760
|
+
rb: "ruby",
|
|
7761
|
+
rs: "rust",
|
|
7762
|
+
go: "go",
|
|
7763
|
+
swift: "swift",
|
|
7764
|
+
java: "java",
|
|
7765
|
+
kt: "kotlin",
|
|
7766
|
+
c: "c",
|
|
7767
|
+
h: "c",
|
|
7768
|
+
cpp: "cpp",
|
|
7769
|
+
cc: "cpp",
|
|
7770
|
+
hpp: "cpp",
|
|
7771
|
+
cs: "csharp",
|
|
7772
|
+
php: "php",
|
|
7773
|
+
sql: "sql",
|
|
7774
|
+
graphql: "graphql",
|
|
7775
|
+
gql: "graphql",
|
|
7776
|
+
lua: "lua",
|
|
7777
|
+
r: "r",
|
|
7778
|
+
pl: "perl",
|
|
7779
|
+
dart: "dart",
|
|
7780
|
+
scala: "scala",
|
|
7781
|
+
ini: "ini",
|
|
7782
|
+
cfg: "ini",
|
|
7783
|
+
conf: "ini",
|
|
7784
|
+
txt: "plaintext",
|
|
7785
|
+
log: "plaintext"
|
|
7786
|
+
};
|
|
7787
|
+
//#endregion
|
|
7788
|
+
//#region src/components/agent/FileViewer.tsx
|
|
7789
|
+
/**
|
|
7790
|
+
* The focused file: Monaco, plus the states a file can be in that are not
|
|
7791
|
+
* "here is some text".
|
|
7792
|
+
*
|
|
7793
|
+
* No path row — the tab's tooltip carries the path and the size, and a line of
|
|
7794
|
+
* monospace above every file is chrome that never earns its height.
|
|
7795
|
+
*/
|
|
7796
|
+
function FileViewer({ file, canWrite, onChange, onSave, onRevert, onReload, onOverwrite, onDismissConflict, className }) {
|
|
7797
|
+
if (!file) return null;
|
|
7798
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
7799
|
+
"data-slot": "file-viewer",
|
|
7800
|
+
className: cn("flex min-h-0 min-w-0 flex-1 flex-col bg-bg", className),
|
|
7801
|
+
children: [
|
|
7802
|
+
file.conflict ? /* @__PURE__ */ jsxs("div", {
|
|
7803
|
+
className: "flex shrink-0 flex-wrap items-center gap-2 border-b border-warning/40 bg-warning-bg px-3 py-2",
|
|
7804
|
+
children: [
|
|
7805
|
+
/* @__PURE__ */ jsx(TriangleAlert, { className: "size-3.5 shrink-0 text-warning" }),
|
|
7806
|
+
/* @__PURE__ */ jsx("span", {
|
|
7807
|
+
className: "min-w-0 flex-1 text-body-sm text-warning",
|
|
7808
|
+
children: "This file changed on disk since you opened it."
|
|
7809
|
+
}),
|
|
7810
|
+
/* @__PURE__ */ jsx(Button, {
|
|
7811
|
+
variant: "outline",
|
|
7812
|
+
size: "xs",
|
|
7813
|
+
onClick: () => onReload?.(file.path),
|
|
7814
|
+
children: "Use the version on disk"
|
|
7815
|
+
}),
|
|
7816
|
+
/* @__PURE__ */ jsx(Button, {
|
|
7817
|
+
variant: "outline",
|
|
7818
|
+
size: "xs",
|
|
7819
|
+
onClick: () => onOverwrite?.(file.path),
|
|
7820
|
+
children: "Keep mine"
|
|
7821
|
+
}),
|
|
7822
|
+
/* @__PURE__ */ jsx(Button, {
|
|
7823
|
+
variant: "ghost",
|
|
7824
|
+
size: "xs",
|
|
7825
|
+
onClick: () => onDismissConflict?.(file.path),
|
|
7826
|
+
children: "Dismiss"
|
|
7827
|
+
})
|
|
7828
|
+
]
|
|
7829
|
+
}) : file.saveError ? /* @__PURE__ */ jsxs("div", {
|
|
7830
|
+
className: "flex shrink-0 items-center gap-2 border-b border-danger/40 bg-danger-bg px-3 py-2",
|
|
7831
|
+
children: [
|
|
7832
|
+
/* @__PURE__ */ jsx(TriangleAlert, { className: "size-3.5 shrink-0 text-danger" }),
|
|
7833
|
+
/* @__PURE__ */ jsx("span", {
|
|
7834
|
+
className: "min-w-0 flex-1 text-body-sm text-danger",
|
|
7835
|
+
children: file.saveError
|
|
7836
|
+
}),
|
|
7837
|
+
/* @__PURE__ */ jsx(Button, {
|
|
7838
|
+
variant: "ghost",
|
|
7839
|
+
size: "xs",
|
|
7840
|
+
onClick: () => onDismissConflict?.(file.path),
|
|
7841
|
+
children: "Dismiss"
|
|
7842
|
+
})
|
|
7843
|
+
]
|
|
7844
|
+
}) : null,
|
|
7845
|
+
file.status === "loading" ? /* @__PURE__ */ jsx(Centred, { children: /* @__PURE__ */ jsx(Spinner, { className: "size-4 text-fg-4" }) }) : file.status === "error" ? /* @__PURE__ */ jsxs(Centred, { children: [/* @__PURE__ */ jsx(TriangleAlert, { className: "size-4 text-danger" }), /* @__PURE__ */ jsx("p", {
|
|
7846
|
+
className: "text-body-sm text-danger",
|
|
7847
|
+
children: file.error
|
|
7848
|
+
})] }) : file.status === "binary" ? /* @__PURE__ */ jsxs(Centred, { children: [
|
|
7849
|
+
/* @__PURE__ */ jsx(FileWarning, { className: "size-4 text-fg-4" }),
|
|
7850
|
+
/* @__PURE__ */ jsx("p", {
|
|
7851
|
+
className: "text-body-sm text-fg-4",
|
|
7852
|
+
children: "This file isn’t text."
|
|
7853
|
+
}),
|
|
7854
|
+
/* @__PURE__ */ jsx("p", {
|
|
7855
|
+
className: "text-label text-fg-4",
|
|
7856
|
+
children: "It can’t be edited here without corrupting it."
|
|
7857
|
+
})
|
|
7858
|
+
] }) : /* @__PURE__ */ jsx(CodeEditor, {
|
|
7859
|
+
path: file.path,
|
|
7860
|
+
value: currentText(file),
|
|
7861
|
+
readOnly: !canWrite,
|
|
7862
|
+
onChange: onChange ? (content) => onChange(file.path, content) : void 0,
|
|
7863
|
+
onSave: onSave ? () => onSave(file.path) : void 0
|
|
7864
|
+
}),
|
|
7865
|
+
file.status === "ready" && (file.saving || isDirty(file) || !canWrite) ? /* @__PURE__ */ jsx("div", {
|
|
7866
|
+
className: "flex shrink-0 items-center gap-2 border-t border-border px-3 py-1",
|
|
7867
|
+
children: file.saving ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Spinner, { className: "size-3 text-fg-4" }), /* @__PURE__ */ jsx("span", {
|
|
7868
|
+
className: "text-label text-fg-4",
|
|
7869
|
+
children: "Saving…"
|
|
7870
|
+
})] }) : !canWrite ? /* @__PURE__ */ jsx("span", {
|
|
7871
|
+
className: "text-label text-fg-4",
|
|
7872
|
+
children: "Read-only — this gateway doesn’t allow writes."
|
|
7873
|
+
}) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
7874
|
+
/* @__PURE__ */ jsx("span", {
|
|
7875
|
+
className: "text-label text-fg-3",
|
|
7876
|
+
children: "Unsaved changes"
|
|
7877
|
+
}),
|
|
7878
|
+
/* @__PURE__ */ jsx("span", { className: "flex-1" }),
|
|
7879
|
+
/* @__PURE__ */ jsx(Button, {
|
|
7880
|
+
variant: "ghost",
|
|
7881
|
+
size: "xs",
|
|
7882
|
+
onClick: () => onRevert?.(file.path),
|
|
7883
|
+
children: "Revert"
|
|
7884
|
+
}),
|
|
7885
|
+
/* @__PURE__ */ jsxs(Button, {
|
|
7886
|
+
variant: "outline",
|
|
7887
|
+
size: "xs",
|
|
7888
|
+
onClick: () => onSave?.(file.path),
|
|
7889
|
+
children: ["Save", /* @__PURE__ */ jsx("span", {
|
|
7890
|
+
className: "ml-1 text-fg-4",
|
|
7891
|
+
children: "⌘S"
|
|
7892
|
+
})]
|
|
7893
|
+
})
|
|
7894
|
+
] })
|
|
7895
|
+
}) : null
|
|
7896
|
+
]
|
|
7897
|
+
});
|
|
7898
|
+
}
|
|
7899
|
+
function Centred({ children }) {
|
|
7900
|
+
return /* @__PURE__ */ jsx("div", {
|
|
7901
|
+
className: "flex min-h-0 flex-1 flex-col items-center justify-center gap-2 p-6",
|
|
7902
|
+
children
|
|
7903
|
+
});
|
|
7904
|
+
}
|
|
7905
|
+
//#endregion
|
|
7906
|
+
//#region src/components/agent/SessionWorkspace.tsx
|
|
7907
|
+
const RAIL_MIN = 180;
|
|
7908
|
+
const RAIL_MAX = 520;
|
|
7909
|
+
/** How little of the agent column the editor may leave. Below this the composer
|
|
7910
|
+
* and a line of transcript stop fitting together, which is the point at which the
|
|
7911
|
+
* agent has stopped being usable rather than merely small. */
|
|
7912
|
+
const AGENT_MIN = 220;
|
|
7913
|
+
const EDITOR_MIN = 120;
|
|
7914
|
+
/**
|
|
7915
|
+
* A VS Code-shaped workspace around a live session: file tree on the left, open
|
|
7916
|
+
* files above, the agent below.
|
|
7917
|
+
*
|
|
7918
|
+
* **Strictly additive.** {@link SessionPanel} is untouched and still the whole
|
|
7919
|
+
* session surface on its own — an embedder picks one or the other, and one that
|
|
7920
|
+
* has its own file tree keeps using the panel.
|
|
7921
|
+
*
|
|
7922
|
+
* Two things here are load-bearing and easy to break:
|
|
7923
|
+
*
|
|
7924
|
+
* 1. **The editor region is absent from the layout when nothing is open**, not
|
|
7925
|
+
* collapsed to zero height. A zero-height pane leaves a draggable splitter and
|
|
7926
|
+
* parks the composer at an odd offset; absence is what makes the agent's
|
|
7927
|
+
* "claims the full column" state actually look like the panel alone.
|
|
7928
|
+
* 2. **`SessionPanel` keeps its position in the tree across that transition.** It
|
|
7929
|
+
* holds the WebSocket attach and the entire transcript, so moving it between
|
|
7930
|
+
* parents — or wrapping it conditionally — would remount it and drop the
|
|
7931
|
+
* session's rendered history on the floor the first time someone opens a file.
|
|
7932
|
+
* The conditional children before it are `? :` expressions that leave a null in
|
|
7933
|
+
* their slot, which is exactly what keeps its index stable.
|
|
7934
|
+
*/
|
|
7935
|
+
function SessionWorkspace({ client, sessionId, header, defaultRailWidth = 260, defaultRailCollapsed, className }) {
|
|
7936
|
+
const { info } = useSessionInfo(client, sessionId);
|
|
7937
|
+
const cwd = info?.cwd;
|
|
7938
|
+
const tree = useHostFileTree(client, cwd);
|
|
7939
|
+
const search = useHostFileSearch(client, cwd);
|
|
7940
|
+
const files = useOpenFiles(client);
|
|
7941
|
+
const { canWrite } = useHostFileRoots(client);
|
|
7942
|
+
useEffect(() => {
|
|
7943
|
+
if (!files.hasUnsaved) return;
|
|
7944
|
+
const warn = (event) => event.preventDefault();
|
|
7945
|
+
window.addEventListener("beforeunload", warn);
|
|
7946
|
+
return () => window.removeEventListener("beforeunload", warn);
|
|
7947
|
+
}, [files.hasUnsaved]);
|
|
7948
|
+
const wide = useIsWide();
|
|
7949
|
+
const [railCollapsed, setRailCollapsed] = useState(defaultRailCollapsed ?? false);
|
|
7950
|
+
const [railWidth, setRailWidth] = useState(defaultRailWidth);
|
|
7951
|
+
const [editorHeight, setEditorHeight] = useState(360);
|
|
7952
|
+
const hasFiles = files.files.length > 0;
|
|
7953
|
+
const overlayRail = !wide;
|
|
7954
|
+
const railOpen = tree.available && !railCollapsed;
|
|
7955
|
+
useEffect(() => {
|
|
7956
|
+
if (overlayRail) setRailCollapsed(true);
|
|
7957
|
+
}, [overlayRail]);
|
|
7958
|
+
const closeTab = useCallback((path) => {
|
|
7959
|
+
const file = files.files.find((f) => f.path === path);
|
|
7960
|
+
if (file && isDirty(file)) {
|
|
7961
|
+
const name = file.name;
|
|
7962
|
+
if (!window.confirm(`${name} has unsaved changes. Close it and lose them?`)) return;
|
|
7963
|
+
}
|
|
7964
|
+
files.close(path);
|
|
7965
|
+
}, [files]);
|
|
7966
|
+
const column = useRef(null);
|
|
7967
|
+
const columnHeight = useElementHeight(column);
|
|
7968
|
+
const editorMax = Math.max(EDITOR_MIN, columnHeight - AGENT_MIN);
|
|
7969
|
+
const [topBar, setTopBar] = useState(null);
|
|
7970
|
+
const hoisted = header === void 0 || topBar === null ? void 0 : typeof header === "function" ? (slots) => createPortal(header(slots), topBar) : createPortal(header, topBar);
|
|
7971
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
7972
|
+
"data-slot": "session-workspace",
|
|
7973
|
+
className: cn("flex h-full min-h-0 w-full flex-col overflow-hidden bg-bg", className),
|
|
7974
|
+
children: [header !== void 0 ? /* @__PURE__ */ jsx("div", {
|
|
7975
|
+
ref: setTopBar,
|
|
7976
|
+
className: "shrink-0"
|
|
7977
|
+
}) : null, /* @__PURE__ */ jsxs("div", {
|
|
7978
|
+
className: "relative flex min-h-0 flex-1",
|
|
7979
|
+
children: [
|
|
7980
|
+
railOpen ? /* @__PURE__ */ jsx(FileTree, {
|
|
7981
|
+
tree,
|
|
7982
|
+
search,
|
|
7983
|
+
activePath: files.activePath,
|
|
7984
|
+
onOpenFile: files.open,
|
|
7985
|
+
onCollapse: () => setRailCollapsed(true),
|
|
7986
|
+
style: { width: overlayRail ? Math.min(railWidth, 320) : railWidth },
|
|
7987
|
+
className: cn("shrink-0 border-r border-border", overlayRail && "absolute inset-y-0 left-0 z-20 shadow-lg")
|
|
7988
|
+
}) : tree.available ? /* @__PURE__ */ jsx("div", {
|
|
7989
|
+
className: "flex w-8 shrink-0 flex-col items-center border-r border-border bg-surface pt-1.5",
|
|
7990
|
+
children: /* @__PURE__ */ jsx(Button, {
|
|
7991
|
+
variant: "ghost",
|
|
7992
|
+
size: "icon-sm",
|
|
7993
|
+
"aria-label": "Show project files",
|
|
7994
|
+
onClick: () => setRailCollapsed(false),
|
|
7995
|
+
children: /* @__PURE__ */ jsx(PanelLeftOpen, { className: "size-4 text-fg-3" })
|
|
7996
|
+
})
|
|
7997
|
+
}) : null,
|
|
7998
|
+
railOpen && !overlayRail ? /* @__PURE__ */ jsx(Splitter, {
|
|
7999
|
+
orientation: "vertical",
|
|
8000
|
+
value: railWidth,
|
|
8001
|
+
onValueChange: setRailWidth,
|
|
8002
|
+
min: RAIL_MIN,
|
|
8003
|
+
max: RAIL_MAX,
|
|
8004
|
+
"aria-label": "Resize the file tree"
|
|
8005
|
+
}) : null,
|
|
8006
|
+
/* @__PURE__ */ jsxs("div", {
|
|
8007
|
+
ref: column,
|
|
8008
|
+
className: "flex min-h-0 min-w-0 flex-1 flex-col",
|
|
8009
|
+
children: [
|
|
8010
|
+
hasFiles ? /* @__PURE__ */ jsxs("div", {
|
|
8011
|
+
className: "flex min-h-0 shrink-0 flex-col overflow-hidden",
|
|
8012
|
+
style: { height: Math.min(editorHeight, editorMax || editorHeight) },
|
|
8013
|
+
children: [/* @__PURE__ */ jsx(EditorTabs, {
|
|
8014
|
+
files: files.files,
|
|
8015
|
+
activePath: files.activePath,
|
|
8016
|
+
onActivate: files.activate,
|
|
8017
|
+
onClose: closeTab
|
|
8018
|
+
}), /* @__PURE__ */ jsx(FileViewer, {
|
|
8019
|
+
file: files.active,
|
|
8020
|
+
canWrite,
|
|
8021
|
+
onChange: files.edit,
|
|
8022
|
+
onSave: (path) => void files.save(path),
|
|
8023
|
+
onRevert: files.revert,
|
|
8024
|
+
onReload: files.reload,
|
|
8025
|
+
onOverwrite: (path) => void files.overwrite(path),
|
|
8026
|
+
onDismissConflict: files.dismissConflict
|
|
8027
|
+
})]
|
|
8028
|
+
}) : null,
|
|
8029
|
+
hasFiles ? /* @__PURE__ */ jsx(Splitter, {
|
|
8030
|
+
orientation: "horizontal",
|
|
8031
|
+
value: Math.min(editorHeight, editorMax || editorHeight),
|
|
8032
|
+
onValueChange: setEditorHeight,
|
|
8033
|
+
min: EDITOR_MIN,
|
|
8034
|
+
max: editorMax,
|
|
8035
|
+
"aria-label": "Resize the open file"
|
|
8036
|
+
}) : null,
|
|
8037
|
+
/* @__PURE__ */ jsx(SessionPanel, {
|
|
8038
|
+
client,
|
|
8039
|
+
sessionId,
|
|
8040
|
+
header: hoisted,
|
|
8041
|
+
className: "min-h-0 flex-1"
|
|
8042
|
+
})
|
|
8043
|
+
]
|
|
8044
|
+
}),
|
|
8045
|
+
overlayRail && railOpen ? /* @__PURE__ */ jsx("button", {
|
|
8046
|
+
type: "button",
|
|
8047
|
+
"aria-label": "Close the file tree",
|
|
8048
|
+
onClick: () => setRailCollapsed(true),
|
|
8049
|
+
className: "absolute inset-0 z-10 bg-black/30"
|
|
8050
|
+
}) : null
|
|
8051
|
+
]
|
|
8052
|
+
})]
|
|
8053
|
+
});
|
|
8054
|
+
}
|
|
8055
|
+
/** Live height of an element, for a splitter that needs to know how much room it
|
|
8056
|
+
* is dividing. Zero until the first observation, which callers treat as
|
|
8057
|
+
* "unmeasured" rather than as a real bound. */
|
|
8058
|
+
function useElementHeight(ref) {
|
|
8059
|
+
const [height, setHeight] = useState(0);
|
|
8060
|
+
useLayoutEffect(() => {
|
|
8061
|
+
const element = ref.current;
|
|
8062
|
+
if (!element) return;
|
|
8063
|
+
const observer = new ResizeObserver(([entry]) => {
|
|
8064
|
+
if (entry) setHeight(entry.contentRect.height);
|
|
8065
|
+
});
|
|
8066
|
+
observer.observe(element);
|
|
8067
|
+
return () => observer.disconnect();
|
|
8068
|
+
}, [ref]);
|
|
8069
|
+
return height;
|
|
8070
|
+
}
|
|
8071
|
+
/** Whether there is room for a rail beside the content. Presentation only — the
|
|
8072
|
+
* workspace's actual state lives in the hooks from `@workerdeck/react`. */
|
|
8073
|
+
function useIsWide() {
|
|
8074
|
+
const [wide, setWide] = useState(true);
|
|
8075
|
+
useEffect(() => {
|
|
8076
|
+
const query = window.matchMedia("(min-width: 768px)");
|
|
8077
|
+
const sync = () => setWide(query.matches);
|
|
8078
|
+
sync();
|
|
8079
|
+
query.addEventListener("change", sync);
|
|
8080
|
+
return () => query.removeEventListener("change", sync);
|
|
8081
|
+
}, []);
|
|
8082
|
+
return wide;
|
|
8083
|
+
}
|
|
5164
8084
|
//#endregion
|
|
5165
8085
|
//#region src/components/agent/SessionList.tsx
|
|
5166
8086
|
function SessionListItem({ session, active, onSelect, onDelete }) {
|
|
@@ -5231,6 +8151,6 @@ function SessionList({ sessions, activeId, onSelect, onDelete, emptyText = "No s
|
|
|
5231
8151
|
});
|
|
5232
8152
|
}
|
|
5233
8153
|
//#endregion
|
|
5234
|
-
export { AlertDialog, AlertDialogClose, AlertDialogContent, AlertDialogDescription, AlertDialogTitle, AlertDialogTrigger, Badge, Button, Card, CardContent, CardHeader, CardTitle, CodeBlock, Composer, Conversation, ConversationContent, ConversationScrollButton, CopyButton, FileCard, Input, Loader, Message, MessageContent, ModelSelect, PERMISSION_MODES, PermissionModeSelect, PermissionPrompt, ProgressRing, PromptArea, QUESTION_BEHAVIORS, QuestionPrompt, Reasoning, Response, STATUS_META, Select, SelectContent, SelectItem, SelectItemText, SelectTrigger, SelectValue, SessionList, SessionListItem, SessionPanel, Spinner, StatusBar, Textarea, Tip, Toaster, ToolCallCard, TooltipContent, TooltipProvider, Transcript, badgeVariants, buttonVariants, cn, commandTrigger, formatBytes, formatCost, formatCountdown, formatDuration, formatRelativeTime, formatTokens, getChipsByTrigger, hashtagTrigger, isSegmentsEmpty, mentionTrigger, parseUserQuestions, plainTextToSegments, segmentsToPlainText, toast, toolInputPreview, usePromptAreaState };
|
|
8154
|
+
export { AlertDialog, AlertDialogClose, AlertDialogContent, AlertDialogDescription, AlertDialogTitle, AlertDialogTrigger, Badge, Button, Card, CardContent, CardHeader, CardTitle, CodeBlock, CodeEditor, Composer, ContextDialog, Conversation, ConversationContent, ConversationScrollButton, CopyButton, Dialog, DialogBody, DialogClose, DialogContent, DialogHeader, DialogRow, DialogTrigger, EditorTabs, FileCard, FileTree, FileViewer, HostFilesDialog, Input, Loader, McpDialog, Menu, MenuContent, MenuItem, MenuSeparator, MenuTrigger, Message, MessageContent, ModelSelect, PERMISSION_MODES, PermissionModeSelect, PermissionPrompt, ProgressRing, PromptArea, PromptTokenText, QUESTION_BEHAVIORS, QuestionPrompt, Reasoning, Response, STATUS_META, Select, SelectContent, SelectItem, SelectItemText, SelectTrigger, SelectValue, SessionEmptyState, SessionInfoDialog, SessionList, SessionListItem, SessionPanel, SessionWorkspace, SkillsDialog, Spinner, Splitter, StatusBar, Textarea, Tip, Toaster, ToolCallCard, TooltipContent, TooltipProvider, Transcript, UsageDialog, badgeVariants, buttonVariants, cn, commandTrigger, copyText, formatAgoPrecise, formatBytes, formatCost, formatCountdown, formatDuration, formatRateLimitWindow, formatRateLimitWindowLong, formatRelativeTime, formatTokens, getChipsByTrigger, hashtagTrigger, isSegmentsEmpty, mentionTrigger, parseUserQuestions, permissionModeMeta, plainTextToSegments, rateLimitWindowSeconds, segmentsToPlainText, skillPrompt, toast, toolIcon, toolInputPreview, usePromptAreaState };
|
|
5235
8155
|
|
|
5236
8156
|
//# sourceMappingURL=index.mjs.map
|