@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.cjs CHANGED
@@ -1711,6 +1711,28 @@ function DialogHeader({ className, ...props }) {
1711
1711
  }
1712
1712
  );
1713
1713
  }
1714
+ function DialogFooter({
1715
+ className,
1716
+ showCloseButton = false,
1717
+ children,
1718
+ ...props
1719
+ }) {
1720
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1721
+ "div",
1722
+ {
1723
+ "data-slot": "dialog-footer",
1724
+ className: cn(
1725
+ "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
1726
+ className
1727
+ ),
1728
+ ...props,
1729
+ children: [
1730
+ children,
1731
+ showCloseButton && /* @__PURE__ */ jsxRuntime.jsx(radixUi.Dialog.Close, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "outline", children: "Close" }) })
1732
+ ]
1733
+ }
1734
+ );
1735
+ }
1714
1736
  function DialogTitle({
1715
1737
  className,
1716
1738
  ...props
@@ -6075,6 +6097,796 @@ function ChatThread({
6075
6097
  ] })
6076
6098
  ] });
6077
6099
  }
6100
+ var noop = () => {
6101
+ };
6102
+ var FileTreeContext = react.createContext({
6103
+ expandedPaths: /* @__PURE__ */ new Set(),
6104
+ togglePath: noop
6105
+ });
6106
+ function FileTree({
6107
+ expanded: controlledExpanded,
6108
+ defaultExpanded = /* @__PURE__ */ new Set(),
6109
+ selectedPath,
6110
+ onSelect,
6111
+ onExpandedChange,
6112
+ className,
6113
+ children,
6114
+ ...props
6115
+ }) {
6116
+ const [internalExpanded, setInternalExpanded] = react.useState(defaultExpanded);
6117
+ const expandedPaths = controlledExpanded ?? internalExpanded;
6118
+ const togglePath = react.useCallback(
6119
+ (path) => {
6120
+ const next = new Set(expandedPaths);
6121
+ if (next.has(path)) {
6122
+ next.delete(path);
6123
+ } else {
6124
+ next.add(path);
6125
+ }
6126
+ setInternalExpanded(next);
6127
+ onExpandedChange?.(next);
6128
+ },
6129
+ [expandedPaths, onExpandedChange]
6130
+ );
6131
+ const contextValue = react.useMemo(
6132
+ () => ({
6133
+ expandedPaths,
6134
+ onSelect,
6135
+ selectedPath: selectedPath ?? void 0,
6136
+ togglePath
6137
+ }),
6138
+ [expandedPaths, onSelect, selectedPath, togglePath]
6139
+ );
6140
+ return /* @__PURE__ */ jsxRuntime.jsx(FileTreeContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsxRuntime.jsx(
6141
+ "div",
6142
+ {
6143
+ className: cn("rounded-lg border bg-background text-sm", className),
6144
+ role: "tree",
6145
+ ...props,
6146
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-2", children })
6147
+ }
6148
+ ) });
6149
+ }
6150
+ function FileTreeIcon({
6151
+ className,
6152
+ children,
6153
+ ...props
6154
+ }) {
6155
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { className: cn("shrink-0", className), ...props, children });
6156
+ }
6157
+ function FileTreeName({
6158
+ className,
6159
+ children,
6160
+ ...props
6161
+ }) {
6162
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { className: cn("truncate", className), ...props, children });
6163
+ }
6164
+ function FileTreeFolder({
6165
+ path,
6166
+ name,
6167
+ className,
6168
+ children,
6169
+ ...props
6170
+ }) {
6171
+ const { expandedPaths, togglePath, selectedPath, onSelect } = react.useContext(FileTreeContext);
6172
+ const isExpanded = expandedPaths.has(path);
6173
+ const isSelected = selectedPath === path;
6174
+ const handleOpenChange = react.useCallback(() => {
6175
+ togglePath(path);
6176
+ }, [togglePath, path]);
6177
+ const handleSelect = react.useCallback(() => {
6178
+ onSelect?.(path);
6179
+ }, [onSelect, path]);
6180
+ return /* @__PURE__ */ jsxRuntime.jsx(Collapsible, { onOpenChange: handleOpenChange, open: isExpanded, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("", className), role: "treeitem", tabIndex: 0, ...props, children: [
6181
+ /* @__PURE__ */ jsxRuntime.jsxs(
6182
+ "div",
6183
+ {
6184
+ className: cn(
6185
+ "flex w-full items-center gap-1 rounded px-2 py-1 text-left transition-colors hover:bg-muted/50",
6186
+ isSelected && "bg-muted"
6187
+ ),
6188
+ children: [
6189
+ /* @__PURE__ */ jsxRuntime.jsx(CollapsibleTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx(
6190
+ "button",
6191
+ {
6192
+ className: "flex shrink-0 cursor-pointer items-center border-none bg-transparent p-0",
6193
+ type: "button",
6194
+ "aria-label": isExpanded ? `Collapse ${name}` : `Expand ${name}`,
6195
+ children: /* @__PURE__ */ jsxRuntime.jsx(
6196
+ lucideReact.ChevronRightIcon,
6197
+ {
6198
+ className: cn(
6199
+ "size-4 shrink-0 text-muted-foreground transition-transform",
6200
+ isExpanded && "rotate-90"
6201
+ )
6202
+ }
6203
+ )
6204
+ }
6205
+ ) }),
6206
+ /* @__PURE__ */ jsxRuntime.jsxs(
6207
+ "button",
6208
+ {
6209
+ className: "flex min-w-0 flex-1 cursor-pointer items-center gap-1 border-none bg-transparent p-0 text-left",
6210
+ onClick: handleSelect,
6211
+ type: "button",
6212
+ children: [
6213
+ /* @__PURE__ */ jsxRuntime.jsx(FileTreeIcon, { children: isExpanded ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.FolderOpenIcon, { className: "size-4 text-blue-500" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.FolderIcon, { className: "size-4 text-blue-500" }) }),
6214
+ /* @__PURE__ */ jsxRuntime.jsx(FileTreeName, { children: name })
6215
+ ]
6216
+ }
6217
+ )
6218
+ ]
6219
+ }
6220
+ ),
6221
+ /* @__PURE__ */ jsxRuntime.jsx(CollapsibleContent, { children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ml-4 border-l border-muted-foreground/50 pl-2", children }) })
6222
+ ] }) });
6223
+ }
6224
+ function FileTreeFile({
6225
+ path,
6226
+ name,
6227
+ icon,
6228
+ className,
6229
+ children,
6230
+ ...props
6231
+ }) {
6232
+ const { selectedPath, onSelect } = react.useContext(FileTreeContext);
6233
+ const isSelected = selectedPath === path;
6234
+ const handleClick = react.useCallback(() => {
6235
+ onSelect?.(path);
6236
+ }, [onSelect, path]);
6237
+ const handleKeyDown = react.useCallback(
6238
+ (event) => {
6239
+ if (event.key === "Enter" || event.key === " ") {
6240
+ onSelect?.(path);
6241
+ }
6242
+ },
6243
+ [onSelect, path]
6244
+ );
6245
+ return /* @__PURE__ */ jsxRuntime.jsx(
6246
+ "div",
6247
+ {
6248
+ className: cn(
6249
+ "group flex cursor-pointer items-center gap-1 rounded px-2 py-1 transition-colors hover:bg-muted/50",
6250
+ isSelected && "bg-muted",
6251
+ className
6252
+ ),
6253
+ onClick: handleClick,
6254
+ onKeyDown: handleKeyDown,
6255
+ role: "treeitem",
6256
+ tabIndex: 0,
6257
+ ...props,
6258
+ children: children ?? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
6259
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "size-4 shrink-0" }),
6260
+ /* @__PURE__ */ jsxRuntime.jsx(FileTreeIcon, { children: icon ?? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.FileIcon, { className: "size-4 text-muted-foreground" }) }),
6261
+ /* @__PURE__ */ jsxRuntime.jsx(FileTreeName, { children: name })
6262
+ ] })
6263
+ }
6264
+ );
6265
+ }
6266
+ function Skeleton({ className, ...props }) {
6267
+ return /* @__PURE__ */ jsxRuntime.jsx(
6268
+ "div",
6269
+ {
6270
+ className: cn("animate-pulse rounded-md bg-muted", className),
6271
+ ...props
6272
+ }
6273
+ );
6274
+ }
6275
+
6276
+ // src/lib/format.ts
6277
+ function formatFileSize(bytes) {
6278
+ if (bytes < 1024) return `${bytes} B`;
6279
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
6280
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
6281
+ }
6282
+ function getLanguageHint(path) {
6283
+ const name = path.split("/").pop() ?? path;
6284
+ const dot = name.lastIndexOf(".");
6285
+ if (dot < 0) return "plaintext";
6286
+ const ext = name.slice(dot).toLowerCase();
6287
+ const map = {
6288
+ ".py": "python",
6289
+ ".js": "javascript",
6290
+ ".ts": "typescript",
6291
+ ".tsx": "tsx",
6292
+ ".jsx": "jsx",
6293
+ ".json": "json",
6294
+ ".yaml": "yaml",
6295
+ ".yml": "yaml",
6296
+ ".toml": "toml",
6297
+ ".sql": "sql",
6298
+ ".sh": "bash",
6299
+ ".css": "css",
6300
+ ".html": "html",
6301
+ ".md": "markdown",
6302
+ ".rs": "rust",
6303
+ ".go": "go",
6304
+ ".java": "java",
6305
+ ".rb": "ruby",
6306
+ ".php": "php",
6307
+ ".c": "c",
6308
+ ".cpp": "cpp",
6309
+ ".h": "c",
6310
+ ".hpp": "cpp"
6311
+ };
6312
+ return map[ext] ?? "plaintext";
6313
+ }
6314
+ var SKELETON_WIDTHS = [70, 85, 55, 90, 60, 78, 45, 82, 65, 72];
6315
+ function FileViewer({
6316
+ file,
6317
+ loading,
6318
+ error,
6319
+ onClose
6320
+ }) {
6321
+ const visible = loading || file !== null || error !== null;
6322
+ if (!visible) return null;
6323
+ const fileName2 = file?.path.split("/").pop() ?? "File";
6324
+ const lang = file ? getLanguageHint(file.path) : "";
6325
+ const isImage = file?.encoding === "base64";
6326
+ const HeaderIcon = isImage ? lucideReact.ImageIcon : lucideReact.FileTextIcon;
6327
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-h-0 flex-1 flex-col border-t border-line", children: [
6328
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex shrink-0 items-center gap-2 border-b border-line px-3 py-2", children: [
6329
+ /* @__PURE__ */ jsxRuntime.jsx(HeaderIcon, { className: "size-3.5 shrink-0 text-muted-foreground" }),
6330
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [
6331
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "truncate text-xs font-medium", children: fileName2 }),
6332
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "truncate text-[10px] text-muted-foreground", children: [
6333
+ file?.path ?? "Loading...",
6334
+ file && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "ml-1.5", children: [
6335
+ formatFileSize(file.size),
6336
+ file.truncated && " (truncated)"
6337
+ ] })
6338
+ ] })
6339
+ ] }),
6340
+ /* @__PURE__ */ jsxRuntime.jsx(
6341
+ "button",
6342
+ {
6343
+ type: "button",
6344
+ onClick: onClose,
6345
+ className: "shrink-0 rounded p-0.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground",
6346
+ "aria-label": "Close file",
6347
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.XIcon, { className: "size-3.5" })
6348
+ }
6349
+ )
6350
+ ] }),
6351
+ /* @__PURE__ */ jsxRuntime.jsxs(ScrollArea, { className: "min-h-0 flex-1", children: [
6352
+ loading && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-1.5 p-3", children: Array.from({ length: 10 }).map((_, index) => /* @__PURE__ */ jsxRuntime.jsx(
6353
+ Skeleton,
6354
+ {
6355
+ className: "h-3.5 rounded",
6356
+ style: {
6357
+ width: `${SKELETON_WIDTHS[index % SKELETON_WIDTHS.length]}%`
6358
+ }
6359
+ },
6360
+ index
6361
+ )) }),
6362
+ error && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-2 p-3 text-xs text-destructive", children: [
6363
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertCircleIcon, { className: "mt-0.5 size-3.5 shrink-0" }),
6364
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: error })
6365
+ ] }),
6366
+ file && isImage && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center p-4", children: /* @__PURE__ */ jsxRuntime.jsx(
6367
+ "img",
6368
+ {
6369
+ src: `data:${file.mime_type ?? "image/png"};base64,${file.content}`,
6370
+ alt: fileName2,
6371
+ className: "max-h-[60vh] max-w-full rounded object-contain"
6372
+ }
6373
+ ) }),
6374
+ file && !isImage && /* @__PURE__ */ jsxRuntime.jsx("pre", { className: "wrap-break-word whitespace-pre-wrap p-3 text-[11px] leading-relaxed text-foreground", children: /* @__PURE__ */ jsxRuntime.jsx("code", { "data-language": lang, children: file.content }) })
6375
+ ] })
6376
+ ] });
6377
+ }
6378
+ var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
6379
+ ".py",
6380
+ ".js",
6381
+ ".ts",
6382
+ ".tsx",
6383
+ ".jsx",
6384
+ ".json",
6385
+ ".md",
6386
+ ".yaml",
6387
+ ".yml",
6388
+ ".toml",
6389
+ ".sql",
6390
+ ".sh",
6391
+ ".css",
6392
+ ".html",
6393
+ ".txt",
6394
+ ".csv",
6395
+ ".rst",
6396
+ ".cfg",
6397
+ ".ini",
6398
+ ".env",
6399
+ ".rs",
6400
+ ".go",
6401
+ ".java",
6402
+ ".rb",
6403
+ ".php",
6404
+ ".c",
6405
+ ".cpp",
6406
+ ".h",
6407
+ ".lock",
6408
+ ".gitignore",
6409
+ ".dockerignore",
6410
+ ".editorconfig"
6411
+ ]);
6412
+ var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([
6413
+ ".png",
6414
+ ".jpg",
6415
+ ".jpeg",
6416
+ ".gif",
6417
+ ".webp",
6418
+ ".svg",
6419
+ ".bmp",
6420
+ ".ico",
6421
+ ".avif",
6422
+ ".tiff",
6423
+ ".tif"
6424
+ ]);
6425
+ var SKELETON_WIDTHS2 = [75, 60, 90, 65, 80, 70, 85, 55];
6426
+ var DEFAULT_WIDTH = 500;
6427
+ var MIN_WIDTH = 300;
6428
+ var MAX_WIDTH = 900;
6429
+ function isViewable(name) {
6430
+ const dot = name.lastIndexOf(".");
6431
+ const ext = dot >= 0 ? name.slice(dot).toLowerCase() : "";
6432
+ return TEXT_EXTENSIONS.has(ext) || IMAGE_EXTENSIONS.has(ext);
6433
+ }
6434
+ function collectExpandedPaths(entries, depth = 0) {
6435
+ const paths = [];
6436
+ for (const entry of entries) {
6437
+ if (entry.kind === "dir" && depth < 1) {
6438
+ paths.push(entry.path);
6439
+ if (entry.children) {
6440
+ paths.push(...collectExpandedPaths(entry.children, depth + 1));
6441
+ }
6442
+ }
6443
+ }
6444
+ return paths;
6445
+ }
6446
+ function findEntry(entries, path) {
6447
+ for (const entry of entries) {
6448
+ if (entry.path === path) return entry;
6449
+ if (entry.kind === "dir" && entry.children) {
6450
+ const found = findEntry(entry.children, path);
6451
+ if (found) return found;
6452
+ }
6453
+ }
6454
+ return null;
6455
+ }
6456
+ function RenderEntries({
6457
+ entries,
6458
+ onFileSelect,
6459
+ onDelete
6460
+ }) {
6461
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: entries.map((entry) => {
6462
+ if (entry.kind === "dir") {
6463
+ return /* @__PURE__ */ jsxRuntime.jsx(FileTreeFolder, { name: entry.name, path: entry.path, children: entry.children && entry.children.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
6464
+ RenderEntries,
6465
+ {
6466
+ entries: entry.children,
6467
+ onFileSelect,
6468
+ onDelete
6469
+ }
6470
+ ) }, entry.path);
6471
+ }
6472
+ const viewable = isViewable(entry.name);
6473
+ return /* @__PURE__ */ jsxRuntime.jsxs(FileTreeFile, { name: entry.name, path: entry.path, children: [
6474
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "size-4 shrink-0" }),
6475
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 flex-1 truncate", children: entry.name }),
6476
+ entry.size != null && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ml-1 shrink-0 text-xs text-muted-foreground/60", children: formatFileSize(entry.size) }),
6477
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ml-1 flex shrink-0 items-center gap-0 opacity-0 transition-opacity group-hover:opacity-100", children: [
6478
+ viewable && /* @__PURE__ */ jsxRuntime.jsx(
6479
+ "button",
6480
+ {
6481
+ type: "button",
6482
+ className: "rounded p-0.5 text-muted-foreground hover:bg-muted hover:text-foreground",
6483
+ onClick: (event) => {
6484
+ event.stopPropagation();
6485
+ onFileSelect(entry.path);
6486
+ },
6487
+ title: "View",
6488
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.DownloadIcon, { className: "size-3" })
6489
+ }
6490
+ ),
6491
+ /* @__PURE__ */ jsxRuntime.jsx(
6492
+ "button",
6493
+ {
6494
+ type: "button",
6495
+ className: "rounded p-0.5 text-muted-foreground hover:bg-destructive/10 hover:text-destructive",
6496
+ onClick: (event) => {
6497
+ event.stopPropagation();
6498
+ onDelete(entry.path);
6499
+ },
6500
+ title: "Delete",
6501
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.TrashIcon, { className: "size-3" })
6502
+ }
6503
+ )
6504
+ ] })
6505
+ ] }, entry.path);
6506
+ }) });
6507
+ }
6508
+ function WorkspacePanel({
6509
+ adapter,
6510
+ sessionId,
6511
+ selectedPath,
6512
+ onSelectedPathChange
6513
+ }) {
6514
+ const fileInputRef = react.useRef(null);
6515
+ const [entries, setEntries] = react.useState([]);
6516
+ const [rootName, setRootName] = react.useState("workspace");
6517
+ const [treeLoading, setTreeLoading] = react.useState(false);
6518
+ const [treeError, setTreeError] = react.useState(null);
6519
+ const [file, setFile] = react.useState(null);
6520
+ const [fileLoading, setFileLoading] = react.useState(false);
6521
+ const [fileError, setFileError] = react.useState(null);
6522
+ const [notice, setNotice] = react.useState(null);
6523
+ const [uploading, setUploading] = react.useState(false);
6524
+ const [deleteTarget, setDeleteTarget] = react.useState(null);
6525
+ const [expandedPaths, setExpandedPaths] = react.useState(/* @__PURE__ */ new Set());
6526
+ const [width, setWidth] = react.useState(DEFAULT_WIDTH);
6527
+ const sessionIdRef = react.useRef(sessionId);
6528
+ const isResizing = react.useRef(false);
6529
+ const startX = react.useRef(0);
6530
+ const startWidth = react.useRef(DEFAULT_WIDTH);
6531
+ sessionIdRef.current = sessionId;
6532
+ const fetchTree = react.useCallback(async () => {
6533
+ if (!sessionId) {
6534
+ setEntries([]);
6535
+ setRootName("workspace");
6536
+ setExpandedPaths(/* @__PURE__ */ new Set());
6537
+ setTreeLoading(false);
6538
+ setTreeError(null);
6539
+ return;
6540
+ }
6541
+ const requestedSessionId = sessionId;
6542
+ setTreeLoading(true);
6543
+ setTreeError(null);
6544
+ try {
6545
+ const tree = await adapter.getWorkspaceTree({
6546
+ sessionId: requestedSessionId
6547
+ });
6548
+ if (sessionIdRef.current !== requestedSessionId) return;
6549
+ setEntries(tree.entries);
6550
+ setRootName(tree.root || "workspace");
6551
+ setExpandedPaths(new Set(collectExpandedPaths(tree.entries)));
6552
+ } catch (error) {
6553
+ if (sessionIdRef.current !== requestedSessionId) return;
6554
+ setEntries([]);
6555
+ setRootName("workspace");
6556
+ setTreeError(error.message);
6557
+ } finally {
6558
+ if (sessionIdRef.current === requestedSessionId) {
6559
+ setTreeLoading(false);
6560
+ }
6561
+ }
6562
+ }, [adapter, sessionId]);
6563
+ const fetchFile = react.useCallback(
6564
+ async (path) => {
6565
+ if (!sessionId) return;
6566
+ const requestedSessionId = sessionId;
6567
+ setFileLoading(true);
6568
+ setFileError(null);
6569
+ try {
6570
+ const nextFile = await adapter.getWorkspaceFile({
6571
+ sessionId: requestedSessionId,
6572
+ path
6573
+ });
6574
+ if (sessionIdRef.current !== requestedSessionId) return;
6575
+ setFile(nextFile);
6576
+ } catch (error) {
6577
+ if (sessionIdRef.current !== requestedSessionId) return;
6578
+ setFile(null);
6579
+ setFileError(error.message);
6580
+ } finally {
6581
+ if (sessionIdRef.current === requestedSessionId) {
6582
+ setFileLoading(false);
6583
+ }
6584
+ }
6585
+ },
6586
+ [adapter, sessionId]
6587
+ );
6588
+ react.useEffect(() => {
6589
+ void fetchTree();
6590
+ }, [fetchTree]);
6591
+ react.useEffect(() => {
6592
+ if (!sessionId) {
6593
+ setFile(null);
6594
+ setFileError(null);
6595
+ onSelectedPathChange(null);
6596
+ }
6597
+ }, [onSelectedPathChange, sessionId]);
6598
+ react.useEffect(() => {
6599
+ if (!selectedPath) return;
6600
+ const parts = selectedPath.split("/");
6601
+ if (parts.length <= 1) return;
6602
+ setExpandedPaths((prev) => {
6603
+ const next = new Set(prev);
6604
+ let path = "";
6605
+ for (let index = 0; index < parts.length - 1; index += 1) {
6606
+ path = path ? `${path}/${parts[index]}` : parts[index];
6607
+ next.add(path);
6608
+ }
6609
+ return next;
6610
+ });
6611
+ }, [selectedPath]);
6612
+ react.useEffect(() => {
6613
+ if (!selectedPath || !sessionId || entries.length === 0) return;
6614
+ const entry = findEntry(entries, selectedPath);
6615
+ if (entry?.kind === "file" && file?.path !== selectedPath) {
6616
+ void fetchFile(selectedPath);
6617
+ }
6618
+ }, [entries, fetchFile, file?.path, selectedPath, sessionId]);
6619
+ const handleSelect = react.useCallback(
6620
+ (path) => {
6621
+ onSelectedPathChange(path);
6622
+ const entry = findEntry(entries, path);
6623
+ if (entry?.kind === "file") {
6624
+ void fetchFile(path);
6625
+ }
6626
+ },
6627
+ [entries, fetchFile, onSelectedPathChange]
6628
+ );
6629
+ const handleUpload = react.useCallback(
6630
+ async (files) => {
6631
+ if (!sessionId || files.length === 0) return;
6632
+ setUploading(true);
6633
+ setNotice(null);
6634
+ try {
6635
+ for (const uploadedFile of Array.from(files)) {
6636
+ await adapter.uploadWorkspaceFile({
6637
+ sessionId,
6638
+ file: uploadedFile
6639
+ });
6640
+ }
6641
+ setNotice(
6642
+ files.length === 1 ? `Uploaded ${files[0]?.name ?? "file"}` : `Uploaded ${files.length} files`
6643
+ );
6644
+ await fetchTree();
6645
+ } catch (error) {
6646
+ setNotice(error.message);
6647
+ } finally {
6648
+ setUploading(false);
6649
+ if (fileInputRef.current) fileInputRef.current.value = "";
6650
+ }
6651
+ },
6652
+ [adapter, fetchTree, sessionId]
6653
+ );
6654
+ const handleDelete = react.useCallback(
6655
+ async (path) => {
6656
+ if (!sessionId) return;
6657
+ try {
6658
+ await adapter.deleteWorkspaceFile({ sessionId, path });
6659
+ if (selectedPath === path) {
6660
+ onSelectedPathChange(null);
6661
+ setFile(null);
6662
+ setFileError(null);
6663
+ }
6664
+ setNotice(`Deleted ${path.split("/").pop() ?? path}`);
6665
+ await fetchTree();
6666
+ } catch (error) {
6667
+ setNotice(error.message);
6668
+ } finally {
6669
+ setDeleteTarget(null);
6670
+ }
6671
+ },
6672
+ [adapter, fetchTree, onSelectedPathChange, selectedPath, sessionId]
6673
+ );
6674
+ const onResizeStart = react.useCallback(
6675
+ (event) => {
6676
+ event.preventDefault();
6677
+ isResizing.current = true;
6678
+ startX.current = event.clientX;
6679
+ startWidth.current = width;
6680
+ document.body.style.cursor = "col-resize";
6681
+ document.body.style.userSelect = "none";
6682
+ },
6683
+ [width]
6684
+ );
6685
+ react.useEffect(() => {
6686
+ const onMouseMove = (event) => {
6687
+ if (!isResizing.current) return;
6688
+ const delta = startX.current - event.clientX;
6689
+ setWidth(
6690
+ Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, startWidth.current + delta))
6691
+ );
6692
+ };
6693
+ const onMouseUp = () => {
6694
+ if (!isResizing.current) return;
6695
+ isResizing.current = false;
6696
+ document.body.style.cursor = "";
6697
+ document.body.style.userSelect = "";
6698
+ };
6699
+ window.addEventListener("mousemove", onMouseMove);
6700
+ window.addEventListener("mouseup", onMouseUp);
6701
+ return () => {
6702
+ window.removeEventListener("mousemove", onMouseMove);
6703
+ window.removeEventListener("mouseup", onMouseUp);
6704
+ };
6705
+ }, []);
6706
+ return /* @__PURE__ */ jsxRuntime.jsxs(
6707
+ "aside",
6708
+ {
6709
+ className: "relative z-10 flex min-h-0 flex-col overflow-hidden border-l border-muted-foreground/20 bg-card",
6710
+ style: { width, minWidth: MIN_WIDTH, maxWidth: MAX_WIDTH },
6711
+ children: [
6712
+ /* @__PURE__ */ jsxRuntime.jsx(
6713
+ "div",
6714
+ {
6715
+ 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",
6716
+ onMouseDown: onResizeStart
6717
+ }
6718
+ ),
6719
+ /* @__PURE__ */ jsxRuntime.jsx(
6720
+ "input",
6721
+ {
6722
+ ref: fileInputRef,
6723
+ type: "file",
6724
+ multiple: true,
6725
+ className: "hidden",
6726
+ onChange: (event) => {
6727
+ if (event.target.files) void handleUpload(event.target.files);
6728
+ }
6729
+ }
6730
+ ),
6731
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-h-14 items-center gap-2 border-b border-line px-3 py-3", children: [
6732
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.FolderOpenIcon, { className: "size-4 shrink-0 text-amber-500" }),
6733
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [
6734
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "truncate text-sm font-medium text-foreground", children: rootName }),
6735
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "truncate text-xs text-faint", children: "Workspace" })
6736
+ ] }),
6737
+ /* @__PURE__ */ jsxRuntime.jsxs(Tooltip, { children: [
6738
+ /* @__PURE__ */ jsxRuntime.jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx(
6739
+ Button,
6740
+ {
6741
+ variant: "ghost",
6742
+ size: "icon-sm",
6743
+ onClick: () => fileInputRef.current?.click(),
6744
+ disabled: !sessionId || uploading,
6745
+ "aria-label": "Upload files",
6746
+ children: uploading ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2Icon, { className: "size-4 animate-spin" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.UploadIcon, { className: "size-4" })
6747
+ }
6748
+ ) }),
6749
+ /* @__PURE__ */ jsxRuntime.jsx(TooltipContent, { side: "bottom", children: "Upload files" })
6750
+ ] }),
6751
+ /* @__PURE__ */ jsxRuntime.jsxs(Tooltip, { children: [
6752
+ /* @__PURE__ */ jsxRuntime.jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx(
6753
+ Button,
6754
+ {
6755
+ variant: "ghost",
6756
+ size: "icon-sm",
6757
+ onClick: () => void fetchTree(),
6758
+ disabled: !sessionId || treeLoading,
6759
+ "aria-label": "Refresh workspace",
6760
+ children: /* @__PURE__ */ jsxRuntime.jsx(
6761
+ lucideReact.RefreshCwIcon,
6762
+ {
6763
+ className: cn("size-4", treeLoading && "animate-spin")
6764
+ }
6765
+ )
6766
+ }
6767
+ ) }),
6768
+ /* @__PURE__ */ jsxRuntime.jsx(TooltipContent, { side: "bottom", children: "Refresh" })
6769
+ ] })
6770
+ ] }),
6771
+ notice && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "border-b border-line px-3 py-2 text-xs text-muted-foreground", children: notice }),
6772
+ /* @__PURE__ */ jsxRuntime.jsx(ScrollArea, { className: "min-h-0 flex-1", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-1 py-1", children: [
6773
+ treeLoading && entries.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-1 p-2", children: Array.from({ length: 8 }).map((_, index) => /* @__PURE__ */ jsxRuntime.jsx(
6774
+ Skeleton,
6775
+ {
6776
+ className: "h-5 rounded",
6777
+ style: {
6778
+ width: `${SKELETON_WIDTHS2[index % SKELETON_WIDTHS2.length]}%`
6779
+ }
6780
+ },
6781
+ index
6782
+ )) }),
6783
+ treeError && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-2 p-3 text-sm text-destructive", children: [
6784
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertCircleIcon, { className: "mt-0.5 size-4 shrink-0" }),
6785
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: treeError })
6786
+ ] }),
6787
+ !treeLoading && !treeError && entries.length === 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-4 py-8 text-center text-sm text-faint", children: [
6788
+ /* @__PURE__ */ jsxRuntime.jsx("p", { children: "No workspace files" }),
6789
+ /* @__PURE__ */ jsxRuntime.jsxs(
6790
+ Button,
6791
+ {
6792
+ variant: "outline",
6793
+ size: "sm",
6794
+ className: "mt-3 gap-1.5",
6795
+ onClick: () => fileInputRef.current?.click(),
6796
+ disabled: !sessionId,
6797
+ children: [
6798
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.UploadIcon, { className: "size-3.5" }),
6799
+ "Upload files"
6800
+ ]
6801
+ }
6802
+ )
6803
+ ] }),
6804
+ entries.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
6805
+ FileTree,
6806
+ {
6807
+ expanded: expandedPaths,
6808
+ onExpandedChange: setExpandedPaths,
6809
+ selectedPath,
6810
+ onSelect: handleSelect,
6811
+ className: "rounded-none border-0",
6812
+ children: /* @__PURE__ */ jsxRuntime.jsx(
6813
+ RenderEntries,
6814
+ {
6815
+ entries,
6816
+ onFileSelect: handleSelect,
6817
+ onDelete: setDeleteTarget
6818
+ }
6819
+ )
6820
+ }
6821
+ )
6822
+ ] }) }),
6823
+ /* @__PURE__ */ jsxRuntime.jsx(
6824
+ FileViewer,
6825
+ {
6826
+ file,
6827
+ loading: fileLoading,
6828
+ error: fileError,
6829
+ onClose: () => {
6830
+ setFile(null);
6831
+ setFileError(null);
6832
+ }
6833
+ }
6834
+ ),
6835
+ /* @__PURE__ */ jsxRuntime.jsx(
6836
+ ConfirmDialog,
6837
+ {
6838
+ open: deleteTarget !== null,
6839
+ title: "Delete file?",
6840
+ description: `This will permanently delete ${deleteTarget?.split("/").pop() ?? "this file"} from the workspace.`,
6841
+ confirmLabel: "Delete",
6842
+ onConfirm: () => deleteTarget ? handleDelete(deleteTarget) : Promise.resolve(),
6843
+ onCancel: () => setDeleteTarget(null)
6844
+ }
6845
+ )
6846
+ ]
6847
+ }
6848
+ );
6849
+ }
6850
+ function ConfirmDialog({
6851
+ open,
6852
+ title,
6853
+ description,
6854
+ confirmLabel,
6855
+ onConfirm,
6856
+ onCancel
6857
+ }) {
6858
+ const [loading, setLoading] = react.useState(false);
6859
+ const handleConfirm = async () => {
6860
+ setLoading(true);
6861
+ try {
6862
+ await onConfirm();
6863
+ } finally {
6864
+ setLoading(false);
6865
+ }
6866
+ };
6867
+ return /* @__PURE__ */ jsxRuntime.jsx(
6868
+ Dialog,
6869
+ {
6870
+ open,
6871
+ onOpenChange: (nextOpen) => {
6872
+ if (!nextOpen && !loading) onCancel();
6873
+ },
6874
+ children: /* @__PURE__ */ jsxRuntime.jsxs(DialogContent, { className: "sm:max-w-md", children: [
6875
+ /* @__PURE__ */ jsxRuntime.jsxs(DialogHeader, { children: [
6876
+ /* @__PURE__ */ jsxRuntime.jsx(DialogTitle, { children: title }),
6877
+ /* @__PURE__ */ jsxRuntime.jsx(DialogDescription, { children: description })
6878
+ ] }),
6879
+ /* @__PURE__ */ jsxRuntime.jsxs(DialogFooter, { children: [
6880
+ /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "outline", disabled: loading, onClick: onCancel, children: "Cancel" }),
6881
+ /* @__PURE__ */ jsxRuntime.jsxs(Button, { variant: "destructive", disabled: loading, onClick: handleConfirm, children: [
6882
+ loading && /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2Icon, { className: "mr-1.5 size-3.5 animate-spin" }),
6883
+ confirmLabel
6884
+ ] })
6885
+ ] })
6886
+ ] })
6887
+ }
6888
+ );
6889
+ }
6078
6890
 
6079
6891
  // src/runtime/events.ts
6080
6892
  var AGENT_CHAT_LISTENED_EVENTS = [
@@ -6885,6 +7697,7 @@ function AgentChat({
6885
7697
  onMessagesChange,
6886
7698
  disabled
6887
7699
  }) {
7700
+ const [workspacePath, setWorkspacePath] = react.useState(null);
6888
7701
  const runtime = useAgentChatRuntime({
6889
7702
  adapter,
6890
7703
  agentId,
@@ -6894,29 +7707,47 @@ function AgentChat({
6894
7707
  react.useEffect(() => {
6895
7708
  onMessagesChange?.(runtime.messages);
6896
7709
  }, [onMessagesChange, runtime.messages]);
7710
+ const handleFileSelect = react.useCallback(
7711
+ (path) => {
7712
+ setWorkspacePath(path);
7713
+ onFileSelect?.(path);
7714
+ },
7715
+ [onFileSelect]
7716
+ );
6897
7717
  return /* @__PURE__ */ jsxRuntime.jsx(
6898
7718
  AgentChatAdapterProvider,
6899
7719
  {
6900
7720
  value: {
6901
7721
  adapter,
6902
7722
  sessionId,
6903
- onFileSelect
7723
+ onFileSelect: handleFileSelect
6904
7724
  },
6905
- children: /* @__PURE__ */ jsxRuntime.jsx("section", { className: "flex h-full min-h-0 flex-col bg-card text-sm text-foreground", children: /* @__PURE__ */ jsxRuntime.jsx(
6906
- ChatThread,
6907
- {
6908
- sessionId,
6909
- messages: runtime.messages,
6910
- isRunning: runtime.isRunning,
6911
- onSend: (content) => void runtime.send(content),
6912
- onStop: () => void runtime.stop(),
6913
- onRetry: runtime.retry,
6914
- onFileSelect,
6915
- disabled,
6916
- tokenUsage: runtime.tokenUsage,
6917
- retryIndicator: runtime.retryIndicator
6918
- }
6919
- ) })
7725
+ children: /* @__PURE__ */ jsxRuntime.jsx(TooltipProvider, { children: /* @__PURE__ */ jsxRuntime.jsxs("section", { className: "flex h-full min-h-0 bg-card text-sm text-foreground", children: [
7726
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex min-w-0 flex-1 flex-col", children: /* @__PURE__ */ jsxRuntime.jsx(
7727
+ ChatThread,
7728
+ {
7729
+ sessionId,
7730
+ messages: runtime.messages,
7731
+ isRunning: runtime.isRunning,
7732
+ onSend: (content) => void runtime.send(content),
7733
+ onStop: () => void runtime.stop(),
7734
+ onRetry: runtime.retry,
7735
+ onFileSelect: handleFileSelect,
7736
+ disabled,
7737
+ tokenUsage: runtime.tokenUsage,
7738
+ retryIndicator: runtime.retryIndicator
7739
+ }
7740
+ ) }),
7741
+ /* @__PURE__ */ jsxRuntime.jsx(
7742
+ WorkspacePanel,
7743
+ {
7744
+ adapter,
7745
+ sessionId,
7746
+ selectedPath: workspacePath,
7747
+ onSelectedPathChange: setWorkspacePath
7748
+ }
7749
+ )
7750
+ ] }) })
6920
7751
  }
6921
7752
  );
6922
7753
  }