@invergent/agent-chat-react 1.4.3 → 1.4.5

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/dist/index.js CHANGED
@@ -4,7 +4,7 @@ import { Collapsible as Collapsible$1, Slot, Dialog as Dialog$1, ScrollArea as S
4
4
  import { clsx } from 'clsx';
5
5
  import { twMerge } from 'tailwind-merge';
6
6
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
7
- import { ChevronDownIcon, AlertTriangle, ChevronDown, ChevronRight, RefreshCw, ChevronRightIcon, GitBranchIcon, XIcon, ThumbsUpIcon, ThumbsDownIcon, ActivityIcon, Loader2Icon, ListTodoIcon, Code, CheckIcon, CopyIcon, TerminalIcon, Trash2Icon, CircleDotIcon, CircleCheckIcon, CircleXIcon, SkullIcon, ClockIcon, CheckCircle2Icon, XCircleIcon, CheckCircleIcon, CircleIcon, MessageSquareIcon, ArrowDownIcon, DownloadIcon, Maximize2Icon, PlusIcon, ImageIcon, CornerDownLeftIcon, SquareIcon } from 'lucide-react';
7
+ import { ChevronDownIcon, AlertTriangle, ChevronDown, ChevronRight, RefreshCw, ChevronRightIcon, GitBranchIcon, XIcon, ThumbsUpIcon, ThumbsDownIcon, ActivityIcon, Loader2Icon, ListTodoIcon, Code, CheckIcon, CopyIcon, TerminalIcon, Trash2Icon, CircleDotIcon, CircleCheckIcon, CircleXIcon, SkullIcon, ClockIcon, CheckCircle2Icon, XCircleIcon, CheckCircleIcon, CircleIcon, MessageSquareIcon, FolderOpenIcon, UploadIcon, RefreshCwIcon, AlertCircleIcon, ArrowDownIcon, DownloadIcon, TrashIcon, ImageIcon, FileTextIcon, Maximize2Icon, FolderIcon, FileIcon, PlusIcon, CornerDownLeftIcon, SquareIcon } from 'lucide-react';
8
8
  import { StickToBottom, useStickToBottomContext } from 'use-stick-to-bottom';
9
9
  import { cjk } from '@streamdown/cjk';
10
10
  import { code } from '@streamdown/code';
@@ -1606,6 +1606,28 @@ function DialogHeader({ className, ...props }) {
1606
1606
  }
1607
1607
  );
1608
1608
  }
1609
+ function DialogFooter({
1610
+ className,
1611
+ showCloseButton = false,
1612
+ children,
1613
+ ...props
1614
+ }) {
1615
+ return /* @__PURE__ */ jsxs(
1616
+ "div",
1617
+ {
1618
+ "data-slot": "dialog-footer",
1619
+ className: cn(
1620
+ "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
1621
+ className
1622
+ ),
1623
+ ...props,
1624
+ children: [
1625
+ children,
1626
+ showCloseButton && /* @__PURE__ */ jsx(Dialog$1.Close, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "outline", children: "Close" }) })
1627
+ ]
1628
+ }
1629
+ );
1630
+ }
1609
1631
  function DialogTitle({
1610
1632
  className,
1611
1633
  ...props
@@ -5970,6 +5992,796 @@ function ChatThread({
5970
5992
  ] })
5971
5993
  ] });
5972
5994
  }
5995
+ var noop = () => {
5996
+ };
5997
+ var FileTreeContext = createContext({
5998
+ expandedPaths: /* @__PURE__ */ new Set(),
5999
+ togglePath: noop
6000
+ });
6001
+ function FileTree({
6002
+ expanded: controlledExpanded,
6003
+ defaultExpanded = /* @__PURE__ */ new Set(),
6004
+ selectedPath,
6005
+ onSelect,
6006
+ onExpandedChange,
6007
+ className,
6008
+ children,
6009
+ ...props
6010
+ }) {
6011
+ const [internalExpanded, setInternalExpanded] = useState(defaultExpanded);
6012
+ const expandedPaths = controlledExpanded ?? internalExpanded;
6013
+ const togglePath = useCallback(
6014
+ (path) => {
6015
+ const next = new Set(expandedPaths);
6016
+ if (next.has(path)) {
6017
+ next.delete(path);
6018
+ } else {
6019
+ next.add(path);
6020
+ }
6021
+ setInternalExpanded(next);
6022
+ onExpandedChange?.(next);
6023
+ },
6024
+ [expandedPaths, onExpandedChange]
6025
+ );
6026
+ const contextValue = useMemo(
6027
+ () => ({
6028
+ expandedPaths,
6029
+ onSelect,
6030
+ selectedPath: selectedPath ?? void 0,
6031
+ togglePath
6032
+ }),
6033
+ [expandedPaths, onSelect, selectedPath, togglePath]
6034
+ );
6035
+ return /* @__PURE__ */ jsx(FileTreeContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(
6036
+ "div",
6037
+ {
6038
+ className: cn("rounded-lg border bg-background text-sm", className),
6039
+ role: "tree",
6040
+ ...props,
6041
+ children: /* @__PURE__ */ jsx("div", { className: "p-2", children })
6042
+ }
6043
+ ) });
6044
+ }
6045
+ function FileTreeIcon({
6046
+ className,
6047
+ children,
6048
+ ...props
6049
+ }) {
6050
+ return /* @__PURE__ */ jsx("span", { className: cn("shrink-0", className), ...props, children });
6051
+ }
6052
+ function FileTreeName({
6053
+ className,
6054
+ children,
6055
+ ...props
6056
+ }) {
6057
+ return /* @__PURE__ */ jsx("span", { className: cn("truncate", className), ...props, children });
6058
+ }
6059
+ function FileTreeFolder({
6060
+ path,
6061
+ name,
6062
+ className,
6063
+ children,
6064
+ ...props
6065
+ }) {
6066
+ const { expandedPaths, togglePath, selectedPath, onSelect } = useContext(FileTreeContext);
6067
+ const isExpanded = expandedPaths.has(path);
6068
+ const isSelected = selectedPath === path;
6069
+ const handleOpenChange = useCallback(() => {
6070
+ togglePath(path);
6071
+ }, [togglePath, path]);
6072
+ const handleSelect = useCallback(() => {
6073
+ onSelect?.(path);
6074
+ }, [onSelect, path]);
6075
+ return /* @__PURE__ */ jsx(Collapsible, { onOpenChange: handleOpenChange, open: isExpanded, children: /* @__PURE__ */ jsxs("div", { className: cn("", className), role: "treeitem", tabIndex: 0, ...props, children: [
6076
+ /* @__PURE__ */ jsxs(
6077
+ "div",
6078
+ {
6079
+ className: cn(
6080
+ "flex w-full items-center gap-1 rounded px-2 py-1 text-left transition-colors hover:bg-muted/50",
6081
+ isSelected && "bg-muted"
6082
+ ),
6083
+ children: [
6084
+ /* @__PURE__ */ jsx(CollapsibleTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
6085
+ "button",
6086
+ {
6087
+ className: "flex shrink-0 cursor-pointer items-center border-none bg-transparent p-0",
6088
+ type: "button",
6089
+ "aria-label": isExpanded ? `Collapse ${name}` : `Expand ${name}`,
6090
+ children: /* @__PURE__ */ jsx(
6091
+ ChevronRightIcon,
6092
+ {
6093
+ className: cn(
6094
+ "size-4 shrink-0 text-muted-foreground transition-transform",
6095
+ isExpanded && "rotate-90"
6096
+ )
6097
+ }
6098
+ )
6099
+ }
6100
+ ) }),
6101
+ /* @__PURE__ */ jsxs(
6102
+ "button",
6103
+ {
6104
+ className: "flex min-w-0 flex-1 cursor-pointer items-center gap-1 border-none bg-transparent p-0 text-left",
6105
+ onClick: handleSelect,
6106
+ type: "button",
6107
+ children: [
6108
+ /* @__PURE__ */ jsx(FileTreeIcon, { children: isExpanded ? /* @__PURE__ */ jsx(FolderOpenIcon, { className: "size-4 text-blue-500" }) : /* @__PURE__ */ jsx(FolderIcon, { className: "size-4 text-blue-500" }) }),
6109
+ /* @__PURE__ */ jsx(FileTreeName, { children: name })
6110
+ ]
6111
+ }
6112
+ )
6113
+ ]
6114
+ }
6115
+ ),
6116
+ /* @__PURE__ */ jsx(CollapsibleContent, { children: /* @__PURE__ */ jsx("div", { className: "ml-4 border-l border-muted-foreground/50 pl-2", children }) })
6117
+ ] }) });
6118
+ }
6119
+ function FileTreeFile({
6120
+ path,
6121
+ name,
6122
+ icon,
6123
+ className,
6124
+ children,
6125
+ ...props
6126
+ }) {
6127
+ const { selectedPath, onSelect } = useContext(FileTreeContext);
6128
+ const isSelected = selectedPath === path;
6129
+ const handleClick = useCallback(() => {
6130
+ onSelect?.(path);
6131
+ }, [onSelect, path]);
6132
+ const handleKeyDown = useCallback(
6133
+ (event) => {
6134
+ if (event.key === "Enter" || event.key === " ") {
6135
+ onSelect?.(path);
6136
+ }
6137
+ },
6138
+ [onSelect, path]
6139
+ );
6140
+ return /* @__PURE__ */ jsx(
6141
+ "div",
6142
+ {
6143
+ className: cn(
6144
+ "group flex cursor-pointer items-center gap-1 rounded px-2 py-1 transition-colors hover:bg-muted/50",
6145
+ isSelected && "bg-muted",
6146
+ className
6147
+ ),
6148
+ onClick: handleClick,
6149
+ onKeyDown: handleKeyDown,
6150
+ role: "treeitem",
6151
+ tabIndex: 0,
6152
+ ...props,
6153
+ children: children ?? /* @__PURE__ */ jsxs(Fragment, { children: [
6154
+ /* @__PURE__ */ jsx("span", { className: "size-4 shrink-0" }),
6155
+ /* @__PURE__ */ jsx(FileTreeIcon, { children: icon ?? /* @__PURE__ */ jsx(FileIcon, { className: "size-4 text-muted-foreground" }) }),
6156
+ /* @__PURE__ */ jsx(FileTreeName, { children: name })
6157
+ ] })
6158
+ }
6159
+ );
6160
+ }
6161
+ function Skeleton({ className, ...props }) {
6162
+ return /* @__PURE__ */ jsx(
6163
+ "div",
6164
+ {
6165
+ className: cn("animate-pulse rounded-md bg-muted", className),
6166
+ ...props
6167
+ }
6168
+ );
6169
+ }
6170
+
6171
+ // src/lib/format.ts
6172
+ function formatFileSize(bytes) {
6173
+ if (bytes < 1024) return `${bytes} B`;
6174
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
6175
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
6176
+ }
6177
+ function getLanguageHint(path) {
6178
+ const name = path.split("/").pop() ?? path;
6179
+ const dot = name.lastIndexOf(".");
6180
+ if (dot < 0) return "plaintext";
6181
+ const ext = name.slice(dot).toLowerCase();
6182
+ const map = {
6183
+ ".py": "python",
6184
+ ".js": "javascript",
6185
+ ".ts": "typescript",
6186
+ ".tsx": "tsx",
6187
+ ".jsx": "jsx",
6188
+ ".json": "json",
6189
+ ".yaml": "yaml",
6190
+ ".yml": "yaml",
6191
+ ".toml": "toml",
6192
+ ".sql": "sql",
6193
+ ".sh": "bash",
6194
+ ".css": "css",
6195
+ ".html": "html",
6196
+ ".md": "markdown",
6197
+ ".rs": "rust",
6198
+ ".go": "go",
6199
+ ".java": "java",
6200
+ ".rb": "ruby",
6201
+ ".php": "php",
6202
+ ".c": "c",
6203
+ ".cpp": "cpp",
6204
+ ".h": "c",
6205
+ ".hpp": "cpp"
6206
+ };
6207
+ return map[ext] ?? "plaintext";
6208
+ }
6209
+ var SKELETON_WIDTHS = [70, 85, 55, 90, 60, 78, 45, 82, 65, 72];
6210
+ function FileViewer({
6211
+ file,
6212
+ loading,
6213
+ error,
6214
+ onClose
6215
+ }) {
6216
+ const visible = loading || file !== null || error !== null;
6217
+ if (!visible) return null;
6218
+ const fileName2 = file?.path.split("/").pop() ?? "File";
6219
+ const lang = file ? getLanguageHint(file.path) : "";
6220
+ const isImage = file?.encoding === "base64";
6221
+ const HeaderIcon = isImage ? ImageIcon : FileTextIcon;
6222
+ return /* @__PURE__ */ jsxs("div", { className: "flex min-h-0 flex-1 flex-col border-t border-line", children: [
6223
+ /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-2 border-b border-line px-3 py-2", children: [
6224
+ /* @__PURE__ */ jsx(HeaderIcon, { className: "size-3.5 shrink-0 text-muted-foreground" }),
6225
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
6226
+ /* @__PURE__ */ jsx("div", { className: "truncate text-xs font-medium", children: fileName2 }),
6227
+ /* @__PURE__ */ jsxs("div", { className: "truncate text-[10px] text-muted-foreground", children: [
6228
+ file?.path ?? "Loading...",
6229
+ file && /* @__PURE__ */ jsxs("span", { className: "ml-1.5", children: [
6230
+ formatFileSize(file.size),
6231
+ file.truncated && " (truncated)"
6232
+ ] })
6233
+ ] })
6234
+ ] }),
6235
+ /* @__PURE__ */ jsx(
6236
+ "button",
6237
+ {
6238
+ type: "button",
6239
+ onClick: onClose,
6240
+ className: "shrink-0 rounded p-0.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground",
6241
+ "aria-label": "Close file",
6242
+ children: /* @__PURE__ */ jsx(XIcon, { className: "size-3.5" })
6243
+ }
6244
+ )
6245
+ ] }),
6246
+ /* @__PURE__ */ jsxs(ScrollArea, { className: "min-h-0 flex-1", children: [
6247
+ loading && /* @__PURE__ */ jsx("div", { className: "space-y-1.5 p-3", children: Array.from({ length: 10 }).map((_, index) => /* @__PURE__ */ jsx(
6248
+ Skeleton,
6249
+ {
6250
+ className: "h-3.5 rounded",
6251
+ style: {
6252
+ width: `${SKELETON_WIDTHS[index % SKELETON_WIDTHS.length]}%`
6253
+ }
6254
+ },
6255
+ index
6256
+ )) }),
6257
+ error && /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2 p-3 text-xs text-destructive", children: [
6258
+ /* @__PURE__ */ jsx(AlertCircleIcon, { className: "mt-0.5 size-3.5 shrink-0" }),
6259
+ /* @__PURE__ */ jsx("span", { children: error })
6260
+ ] }),
6261
+ file && isImage && /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center p-4", children: /* @__PURE__ */ jsx(
6262
+ "img",
6263
+ {
6264
+ src: `data:${file.mime_type ?? "image/png"};base64,${file.content}`,
6265
+ alt: fileName2,
6266
+ className: "max-h-[60vh] max-w-full rounded object-contain"
6267
+ }
6268
+ ) }),
6269
+ file && !isImage && /* @__PURE__ */ jsx("pre", { className: "wrap-break-word whitespace-pre-wrap p-3 text-[11px] leading-relaxed text-foreground", children: /* @__PURE__ */ jsx("code", { "data-language": lang, children: file.content }) })
6270
+ ] })
6271
+ ] });
6272
+ }
6273
+ var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
6274
+ ".py",
6275
+ ".js",
6276
+ ".ts",
6277
+ ".tsx",
6278
+ ".jsx",
6279
+ ".json",
6280
+ ".md",
6281
+ ".yaml",
6282
+ ".yml",
6283
+ ".toml",
6284
+ ".sql",
6285
+ ".sh",
6286
+ ".css",
6287
+ ".html",
6288
+ ".txt",
6289
+ ".csv",
6290
+ ".rst",
6291
+ ".cfg",
6292
+ ".ini",
6293
+ ".env",
6294
+ ".rs",
6295
+ ".go",
6296
+ ".java",
6297
+ ".rb",
6298
+ ".php",
6299
+ ".c",
6300
+ ".cpp",
6301
+ ".h",
6302
+ ".lock",
6303
+ ".gitignore",
6304
+ ".dockerignore",
6305
+ ".editorconfig"
6306
+ ]);
6307
+ var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([
6308
+ ".png",
6309
+ ".jpg",
6310
+ ".jpeg",
6311
+ ".gif",
6312
+ ".webp",
6313
+ ".svg",
6314
+ ".bmp",
6315
+ ".ico",
6316
+ ".avif",
6317
+ ".tiff",
6318
+ ".tif"
6319
+ ]);
6320
+ var SKELETON_WIDTHS2 = [75, 60, 90, 65, 80, 70, 85, 55];
6321
+ var DEFAULT_WIDTH = 500;
6322
+ var MIN_WIDTH = 300;
6323
+ var MAX_WIDTH = 900;
6324
+ function isViewable(name) {
6325
+ const dot = name.lastIndexOf(".");
6326
+ const ext = dot >= 0 ? name.slice(dot).toLowerCase() : "";
6327
+ return TEXT_EXTENSIONS.has(ext) || IMAGE_EXTENSIONS.has(ext);
6328
+ }
6329
+ function collectExpandedPaths(entries, depth = 0) {
6330
+ const paths = [];
6331
+ for (const entry of entries) {
6332
+ if (entry.kind === "dir" && depth < 1) {
6333
+ paths.push(entry.path);
6334
+ if (entry.children) {
6335
+ paths.push(...collectExpandedPaths(entry.children, depth + 1));
6336
+ }
6337
+ }
6338
+ }
6339
+ return paths;
6340
+ }
6341
+ function findEntry(entries, path) {
6342
+ for (const entry of entries) {
6343
+ if (entry.path === path) return entry;
6344
+ if (entry.kind === "dir" && entry.children) {
6345
+ const found = findEntry(entry.children, path);
6346
+ if (found) return found;
6347
+ }
6348
+ }
6349
+ return null;
6350
+ }
6351
+ function RenderEntries({
6352
+ entries,
6353
+ onFileSelect,
6354
+ onDelete
6355
+ }) {
6356
+ return /* @__PURE__ */ jsx(Fragment, { children: entries.map((entry) => {
6357
+ if (entry.kind === "dir") {
6358
+ return /* @__PURE__ */ jsx(FileTreeFolder, { name: entry.name, path: entry.path, children: entry.children && entry.children.length > 0 && /* @__PURE__ */ jsx(
6359
+ RenderEntries,
6360
+ {
6361
+ entries: entry.children,
6362
+ onFileSelect,
6363
+ onDelete
6364
+ }
6365
+ ) }, entry.path);
6366
+ }
6367
+ const viewable = isViewable(entry.name);
6368
+ return /* @__PURE__ */ jsxs(FileTreeFile, { name: entry.name, path: entry.path, children: [
6369
+ /* @__PURE__ */ jsx("span", { className: "size-4 shrink-0" }),
6370
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: entry.name }),
6371
+ entry.size != null && /* @__PURE__ */ jsx("span", { className: "ml-1 shrink-0 text-xs text-muted-foreground/60", children: formatFileSize(entry.size) }),
6372
+ /* @__PURE__ */ jsxs("div", { className: "ml-1 flex shrink-0 items-center gap-0 opacity-0 transition-opacity group-hover:opacity-100", children: [
6373
+ viewable && /* @__PURE__ */ jsx(
6374
+ "button",
6375
+ {
6376
+ type: "button",
6377
+ className: "rounded p-0.5 text-muted-foreground hover:bg-muted hover:text-foreground",
6378
+ onClick: (event) => {
6379
+ event.stopPropagation();
6380
+ onFileSelect(entry.path);
6381
+ },
6382
+ title: "View",
6383
+ children: /* @__PURE__ */ jsx(DownloadIcon, { className: "size-3" })
6384
+ }
6385
+ ),
6386
+ /* @__PURE__ */ jsx(
6387
+ "button",
6388
+ {
6389
+ type: "button",
6390
+ className: "rounded p-0.5 text-muted-foreground hover:bg-destructive/10 hover:text-destructive",
6391
+ onClick: (event) => {
6392
+ event.stopPropagation();
6393
+ onDelete(entry.path);
6394
+ },
6395
+ title: "Delete",
6396
+ children: /* @__PURE__ */ jsx(TrashIcon, { className: "size-3" })
6397
+ }
6398
+ )
6399
+ ] })
6400
+ ] }, entry.path);
6401
+ }) });
6402
+ }
6403
+ function WorkspacePanel({
6404
+ adapter,
6405
+ sessionId,
6406
+ selectedPath,
6407
+ onSelectedPathChange
6408
+ }) {
6409
+ const fileInputRef = useRef(null);
6410
+ const [entries, setEntries] = useState([]);
6411
+ const [rootName, setRootName] = useState("workspace");
6412
+ const [treeLoading, setTreeLoading] = useState(false);
6413
+ const [treeError, setTreeError] = useState(null);
6414
+ const [file, setFile] = useState(null);
6415
+ const [fileLoading, setFileLoading] = useState(false);
6416
+ const [fileError, setFileError] = useState(null);
6417
+ const [notice, setNotice] = useState(null);
6418
+ const [uploading, setUploading] = useState(false);
6419
+ const [deleteTarget, setDeleteTarget] = useState(null);
6420
+ const [expandedPaths, setExpandedPaths] = useState(/* @__PURE__ */ new Set());
6421
+ const [width, setWidth] = useState(DEFAULT_WIDTH);
6422
+ const sessionIdRef = useRef(sessionId);
6423
+ const isResizing = useRef(false);
6424
+ const startX = useRef(0);
6425
+ const startWidth = useRef(DEFAULT_WIDTH);
6426
+ sessionIdRef.current = sessionId;
6427
+ const fetchTree = useCallback(async () => {
6428
+ if (!sessionId) {
6429
+ setEntries([]);
6430
+ setRootName("workspace");
6431
+ setExpandedPaths(/* @__PURE__ */ new Set());
6432
+ setTreeLoading(false);
6433
+ setTreeError(null);
6434
+ return;
6435
+ }
6436
+ const requestedSessionId = sessionId;
6437
+ setTreeLoading(true);
6438
+ setTreeError(null);
6439
+ try {
6440
+ const tree = await adapter.getWorkspaceTree({
6441
+ sessionId: requestedSessionId
6442
+ });
6443
+ if (sessionIdRef.current !== requestedSessionId) return;
6444
+ setEntries(tree.entries);
6445
+ setRootName(tree.root || "workspace");
6446
+ setExpandedPaths(new Set(collectExpandedPaths(tree.entries)));
6447
+ } catch (error) {
6448
+ if (sessionIdRef.current !== requestedSessionId) return;
6449
+ setEntries([]);
6450
+ setRootName("workspace");
6451
+ setTreeError(error.message);
6452
+ } finally {
6453
+ if (sessionIdRef.current === requestedSessionId) {
6454
+ setTreeLoading(false);
6455
+ }
6456
+ }
6457
+ }, [adapter, sessionId]);
6458
+ const fetchFile = useCallback(
6459
+ async (path) => {
6460
+ if (!sessionId) return;
6461
+ const requestedSessionId = sessionId;
6462
+ setFileLoading(true);
6463
+ setFileError(null);
6464
+ try {
6465
+ const nextFile = await adapter.getWorkspaceFile({
6466
+ sessionId: requestedSessionId,
6467
+ path
6468
+ });
6469
+ if (sessionIdRef.current !== requestedSessionId) return;
6470
+ setFile(nextFile);
6471
+ } catch (error) {
6472
+ if (sessionIdRef.current !== requestedSessionId) return;
6473
+ setFile(null);
6474
+ setFileError(error.message);
6475
+ } finally {
6476
+ if (sessionIdRef.current === requestedSessionId) {
6477
+ setFileLoading(false);
6478
+ }
6479
+ }
6480
+ },
6481
+ [adapter, sessionId]
6482
+ );
6483
+ useEffect(() => {
6484
+ void fetchTree();
6485
+ }, [fetchTree]);
6486
+ useEffect(() => {
6487
+ if (!sessionId) {
6488
+ setFile(null);
6489
+ setFileError(null);
6490
+ onSelectedPathChange(null);
6491
+ }
6492
+ }, [onSelectedPathChange, sessionId]);
6493
+ useEffect(() => {
6494
+ if (!selectedPath) return;
6495
+ const parts = selectedPath.split("/");
6496
+ if (parts.length <= 1) return;
6497
+ setExpandedPaths((prev) => {
6498
+ const next = new Set(prev);
6499
+ let path = "";
6500
+ for (let index = 0; index < parts.length - 1; index += 1) {
6501
+ path = path ? `${path}/${parts[index]}` : parts[index];
6502
+ next.add(path);
6503
+ }
6504
+ return next;
6505
+ });
6506
+ }, [selectedPath]);
6507
+ useEffect(() => {
6508
+ if (!selectedPath || !sessionId || entries.length === 0) return;
6509
+ const entry = findEntry(entries, selectedPath);
6510
+ if (entry?.kind === "file" && file?.path !== selectedPath) {
6511
+ void fetchFile(selectedPath);
6512
+ }
6513
+ }, [entries, fetchFile, file?.path, selectedPath, sessionId]);
6514
+ const handleSelect = useCallback(
6515
+ (path) => {
6516
+ onSelectedPathChange(path);
6517
+ const entry = findEntry(entries, path);
6518
+ if (entry?.kind === "file") {
6519
+ void fetchFile(path);
6520
+ }
6521
+ },
6522
+ [entries, fetchFile, onSelectedPathChange]
6523
+ );
6524
+ const handleUpload = useCallback(
6525
+ async (files) => {
6526
+ if (!sessionId || files.length === 0) return;
6527
+ setUploading(true);
6528
+ setNotice(null);
6529
+ try {
6530
+ for (const uploadedFile of Array.from(files)) {
6531
+ await adapter.uploadWorkspaceFile({
6532
+ sessionId,
6533
+ file: uploadedFile
6534
+ });
6535
+ }
6536
+ setNotice(
6537
+ files.length === 1 ? `Uploaded ${files[0]?.name ?? "file"}` : `Uploaded ${files.length} files`
6538
+ );
6539
+ await fetchTree();
6540
+ } catch (error) {
6541
+ setNotice(error.message);
6542
+ } finally {
6543
+ setUploading(false);
6544
+ if (fileInputRef.current) fileInputRef.current.value = "";
6545
+ }
6546
+ },
6547
+ [adapter, fetchTree, sessionId]
6548
+ );
6549
+ const handleDelete = useCallback(
6550
+ async (path) => {
6551
+ if (!sessionId) return;
6552
+ try {
6553
+ await adapter.deleteWorkspaceFile({ sessionId, path });
6554
+ if (selectedPath === path) {
6555
+ onSelectedPathChange(null);
6556
+ setFile(null);
6557
+ setFileError(null);
6558
+ }
6559
+ setNotice(`Deleted ${path.split("/").pop() ?? path}`);
6560
+ await fetchTree();
6561
+ } catch (error) {
6562
+ setNotice(error.message);
6563
+ } finally {
6564
+ setDeleteTarget(null);
6565
+ }
6566
+ },
6567
+ [adapter, fetchTree, onSelectedPathChange, selectedPath, sessionId]
6568
+ );
6569
+ const onResizeStart = useCallback(
6570
+ (event) => {
6571
+ event.preventDefault();
6572
+ isResizing.current = true;
6573
+ startX.current = event.clientX;
6574
+ startWidth.current = width;
6575
+ document.body.style.cursor = "col-resize";
6576
+ document.body.style.userSelect = "none";
6577
+ },
6578
+ [width]
6579
+ );
6580
+ useEffect(() => {
6581
+ const onMouseMove = (event) => {
6582
+ if (!isResizing.current) return;
6583
+ const delta = startX.current - event.clientX;
6584
+ setWidth(
6585
+ Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, startWidth.current + delta))
6586
+ );
6587
+ };
6588
+ const onMouseUp = () => {
6589
+ if (!isResizing.current) return;
6590
+ isResizing.current = false;
6591
+ document.body.style.cursor = "";
6592
+ document.body.style.userSelect = "";
6593
+ };
6594
+ window.addEventListener("mousemove", onMouseMove);
6595
+ window.addEventListener("mouseup", onMouseUp);
6596
+ return () => {
6597
+ window.removeEventListener("mousemove", onMouseMove);
6598
+ window.removeEventListener("mouseup", onMouseUp);
6599
+ };
6600
+ }, []);
6601
+ return /* @__PURE__ */ jsxs(
6602
+ "aside",
6603
+ {
6604
+ className: "relative z-10 flex min-h-0 flex-col overflow-hidden border-l border-muted-foreground/20 bg-card",
6605
+ style: { width, minWidth: MIN_WIDTH, maxWidth: MAX_WIDTH },
6606
+ children: [
6607
+ /* @__PURE__ */ jsx(
6608
+ "div",
6609
+ {
6610
+ className: "absolute inset-y-0 left-0 z-20 w-1.5 cursor-col-resize transition-colors hover:bg-primary/20 active:bg-primary/30",
6611
+ onMouseDown: onResizeStart
6612
+ }
6613
+ ),
6614
+ /* @__PURE__ */ jsx(
6615
+ "input",
6616
+ {
6617
+ ref: fileInputRef,
6618
+ type: "file",
6619
+ multiple: true,
6620
+ className: "hidden",
6621
+ onChange: (event) => {
6622
+ if (event.target.files) void handleUpload(event.target.files);
6623
+ }
6624
+ }
6625
+ ),
6626
+ /* @__PURE__ */ jsxs("div", { className: "flex min-h-14 items-center gap-2 border-b border-line px-3 py-3", children: [
6627
+ /* @__PURE__ */ jsx(FolderOpenIcon, { className: "size-4 shrink-0 text-amber-500" }),
6628
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
6629
+ /* @__PURE__ */ jsx("div", { className: "truncate text-sm font-medium text-foreground", children: rootName }),
6630
+ /* @__PURE__ */ jsx("div", { className: "truncate text-xs text-faint", children: "Workspace" })
6631
+ ] }),
6632
+ /* @__PURE__ */ jsxs(Tooltip, { children: [
6633
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
6634
+ Button,
6635
+ {
6636
+ variant: "ghost",
6637
+ size: "icon-sm",
6638
+ onClick: () => fileInputRef.current?.click(),
6639
+ disabled: !sessionId || uploading,
6640
+ "aria-label": "Upload files",
6641
+ children: uploading ? /* @__PURE__ */ jsx(Loader2Icon, { className: "size-4 animate-spin" }) : /* @__PURE__ */ jsx(UploadIcon, { className: "size-4" })
6642
+ }
6643
+ ) }),
6644
+ /* @__PURE__ */ jsx(TooltipContent, { side: "bottom", children: "Upload files" })
6645
+ ] }),
6646
+ /* @__PURE__ */ jsxs(Tooltip, { children: [
6647
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
6648
+ Button,
6649
+ {
6650
+ variant: "ghost",
6651
+ size: "icon-sm",
6652
+ onClick: () => void fetchTree(),
6653
+ disabled: !sessionId || treeLoading,
6654
+ "aria-label": "Refresh workspace",
6655
+ children: /* @__PURE__ */ jsx(
6656
+ RefreshCwIcon,
6657
+ {
6658
+ className: cn("size-4", treeLoading && "animate-spin")
6659
+ }
6660
+ )
6661
+ }
6662
+ ) }),
6663
+ /* @__PURE__ */ jsx(TooltipContent, { side: "bottom", children: "Refresh" })
6664
+ ] })
6665
+ ] }),
6666
+ notice && /* @__PURE__ */ jsx("div", { className: "border-b border-line px-3 py-2 text-xs text-muted-foreground", children: notice }),
6667
+ /* @__PURE__ */ jsx(ScrollArea, { className: "min-h-0 flex-1", children: /* @__PURE__ */ jsxs("div", { className: "px-1 py-1", children: [
6668
+ treeLoading && entries.length === 0 && /* @__PURE__ */ jsx("div", { className: "space-y-1 p-2", children: Array.from({ length: 8 }).map((_, index) => /* @__PURE__ */ jsx(
6669
+ Skeleton,
6670
+ {
6671
+ className: "h-5 rounded",
6672
+ style: {
6673
+ width: `${SKELETON_WIDTHS2[index % SKELETON_WIDTHS2.length]}%`
6674
+ }
6675
+ },
6676
+ index
6677
+ )) }),
6678
+ treeError && /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2 p-3 text-sm text-destructive", children: [
6679
+ /* @__PURE__ */ jsx(AlertCircleIcon, { className: "mt-0.5 size-4 shrink-0" }),
6680
+ /* @__PURE__ */ jsx("span", { children: treeError })
6681
+ ] }),
6682
+ !treeLoading && !treeError && entries.length === 0 && /* @__PURE__ */ jsxs("div", { className: "px-4 py-8 text-center text-sm text-faint", children: [
6683
+ /* @__PURE__ */ jsx("p", { children: "No workspace files" }),
6684
+ /* @__PURE__ */ jsxs(
6685
+ Button,
6686
+ {
6687
+ variant: "outline",
6688
+ size: "sm",
6689
+ className: "mt-3 gap-1.5",
6690
+ onClick: () => fileInputRef.current?.click(),
6691
+ disabled: !sessionId,
6692
+ children: [
6693
+ /* @__PURE__ */ jsx(UploadIcon, { className: "size-3.5" }),
6694
+ "Upload files"
6695
+ ]
6696
+ }
6697
+ )
6698
+ ] }),
6699
+ entries.length > 0 && /* @__PURE__ */ jsx(
6700
+ FileTree,
6701
+ {
6702
+ expanded: expandedPaths,
6703
+ onExpandedChange: setExpandedPaths,
6704
+ selectedPath,
6705
+ onSelect: handleSelect,
6706
+ className: "rounded-none border-0",
6707
+ children: /* @__PURE__ */ jsx(
6708
+ RenderEntries,
6709
+ {
6710
+ entries,
6711
+ onFileSelect: handleSelect,
6712
+ onDelete: setDeleteTarget
6713
+ }
6714
+ )
6715
+ }
6716
+ )
6717
+ ] }) }),
6718
+ /* @__PURE__ */ jsx(
6719
+ FileViewer,
6720
+ {
6721
+ file,
6722
+ loading: fileLoading,
6723
+ error: fileError,
6724
+ onClose: () => {
6725
+ setFile(null);
6726
+ setFileError(null);
6727
+ }
6728
+ }
6729
+ ),
6730
+ /* @__PURE__ */ jsx(
6731
+ ConfirmDialog,
6732
+ {
6733
+ open: deleteTarget !== null,
6734
+ title: "Delete file?",
6735
+ description: `This will permanently delete ${deleteTarget?.split("/").pop() ?? "this file"} from the workspace.`,
6736
+ confirmLabel: "Delete",
6737
+ onConfirm: () => deleteTarget ? handleDelete(deleteTarget) : Promise.resolve(),
6738
+ onCancel: () => setDeleteTarget(null)
6739
+ }
6740
+ )
6741
+ ]
6742
+ }
6743
+ );
6744
+ }
6745
+ function ConfirmDialog({
6746
+ open,
6747
+ title,
6748
+ description,
6749
+ confirmLabel,
6750
+ onConfirm,
6751
+ onCancel
6752
+ }) {
6753
+ const [loading, setLoading] = useState(false);
6754
+ const handleConfirm = async () => {
6755
+ setLoading(true);
6756
+ try {
6757
+ await onConfirm();
6758
+ } finally {
6759
+ setLoading(false);
6760
+ }
6761
+ };
6762
+ return /* @__PURE__ */ jsx(
6763
+ Dialog,
6764
+ {
6765
+ open,
6766
+ onOpenChange: (nextOpen) => {
6767
+ if (!nextOpen && !loading) onCancel();
6768
+ },
6769
+ children: /* @__PURE__ */ jsxs(DialogContent, { className: "sm:max-w-md", children: [
6770
+ /* @__PURE__ */ jsxs(DialogHeader, { children: [
6771
+ /* @__PURE__ */ jsx(DialogTitle, { children: title }),
6772
+ /* @__PURE__ */ jsx(DialogDescription, { children: description })
6773
+ ] }),
6774
+ /* @__PURE__ */ jsxs(DialogFooter, { children: [
6775
+ /* @__PURE__ */ jsx(Button, { variant: "outline", disabled: loading, onClick: onCancel, children: "Cancel" }),
6776
+ /* @__PURE__ */ jsxs(Button, { variant: "destructive", disabled: loading, onClick: handleConfirm, children: [
6777
+ loading && /* @__PURE__ */ jsx(Loader2Icon, { className: "mr-1.5 size-3.5 animate-spin" }),
6778
+ confirmLabel
6779
+ ] })
6780
+ ] })
6781
+ ] })
6782
+ }
6783
+ );
6784
+ }
5973
6785
 
5974
6786
  // src/runtime/events.ts
5975
6787
  var AGENT_CHAT_LISTENED_EVENTS = [
@@ -6780,6 +7592,7 @@ function AgentChat({
6780
7592
  onMessagesChange,
6781
7593
  disabled
6782
7594
  }) {
7595
+ const [workspacePath, setWorkspacePath] = useState(null);
6783
7596
  const runtime = useAgentChatRuntime({
6784
7597
  adapter,
6785
7598
  agentId,
@@ -6789,29 +7602,47 @@ function AgentChat({
6789
7602
  useEffect(() => {
6790
7603
  onMessagesChange?.(runtime.messages);
6791
7604
  }, [onMessagesChange, runtime.messages]);
7605
+ const handleFileSelect = useCallback(
7606
+ (path) => {
7607
+ setWorkspacePath(path);
7608
+ onFileSelect?.(path);
7609
+ },
7610
+ [onFileSelect]
7611
+ );
6792
7612
  return /* @__PURE__ */ jsx(
6793
7613
  AgentChatAdapterProvider,
6794
7614
  {
6795
7615
  value: {
6796
7616
  adapter,
6797
7617
  sessionId,
6798
- onFileSelect
7618
+ onFileSelect: handleFileSelect
6799
7619
  },
6800
- children: /* @__PURE__ */ jsx("section", { className: "flex h-full min-h-0 flex-col bg-card text-sm text-foreground", children: /* @__PURE__ */ jsx(
6801
- ChatThread,
6802
- {
6803
- sessionId,
6804
- messages: runtime.messages,
6805
- isRunning: runtime.isRunning,
6806
- onSend: (content) => void runtime.send(content),
6807
- onStop: () => void runtime.stop(),
6808
- onRetry: runtime.retry,
6809
- onFileSelect,
6810
- disabled,
6811
- tokenUsage: runtime.tokenUsage,
6812
- retryIndicator: runtime.retryIndicator
6813
- }
6814
- ) })
7620
+ children: /* @__PURE__ */ jsx(TooltipProvider, { children: /* @__PURE__ */ jsxs("section", { className: "flex h-full min-h-0 bg-card text-sm text-foreground", children: [
7621
+ /* @__PURE__ */ jsx("div", { className: "flex min-w-0 flex-1 flex-col", children: /* @__PURE__ */ jsx(
7622
+ ChatThread,
7623
+ {
7624
+ sessionId,
7625
+ messages: runtime.messages,
7626
+ isRunning: runtime.isRunning,
7627
+ onSend: (content) => void runtime.send(content),
7628
+ onStop: () => void runtime.stop(),
7629
+ onRetry: runtime.retry,
7630
+ onFileSelect: handleFileSelect,
7631
+ disabled,
7632
+ tokenUsage: runtime.tokenUsage,
7633
+ retryIndicator: runtime.retryIndicator
7634
+ }
7635
+ ) }),
7636
+ /* @__PURE__ */ jsx(
7637
+ WorkspacePanel,
7638
+ {
7639
+ adapter,
7640
+ sessionId,
7641
+ selectedPath: workspacePath,
7642
+ onSelectedPathChange: setWorkspacePath
7643
+ }
7644
+ )
7645
+ ] }) })
6815
7646
  }
6816
7647
  );
6817
7648
  }