@invergent/agent-chat-react 1.4.3 → 1.4.4

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,777 @@ 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 isResizing = useRef(false);
6423
+ const startX = useRef(0);
6424
+ const startWidth = useRef(DEFAULT_WIDTH);
6425
+ const fetchTree = useCallback(async () => {
6426
+ if (!sessionId) {
6427
+ setEntries([]);
6428
+ setRootName("workspace");
6429
+ setTreeError(null);
6430
+ return;
6431
+ }
6432
+ setTreeLoading(true);
6433
+ setTreeError(null);
6434
+ try {
6435
+ const tree = await adapter.getWorkspaceTree({ sessionId });
6436
+ setEntries(tree.entries);
6437
+ setRootName(tree.root || "workspace");
6438
+ setExpandedPaths(new Set(collectExpandedPaths(tree.entries)));
6439
+ } catch (error) {
6440
+ setEntries([]);
6441
+ setRootName("workspace");
6442
+ setTreeError(error.message);
6443
+ } finally {
6444
+ setTreeLoading(false);
6445
+ }
6446
+ }, [adapter, sessionId]);
6447
+ const fetchFile = useCallback(
6448
+ async (path) => {
6449
+ if (!sessionId) return;
6450
+ setFileLoading(true);
6451
+ setFileError(null);
6452
+ try {
6453
+ const nextFile = await adapter.getWorkspaceFile({ sessionId, path });
6454
+ setFile(nextFile);
6455
+ } catch (error) {
6456
+ setFile(null);
6457
+ setFileError(error.message);
6458
+ } finally {
6459
+ setFileLoading(false);
6460
+ }
6461
+ },
6462
+ [adapter, sessionId]
6463
+ );
6464
+ useEffect(() => {
6465
+ void fetchTree();
6466
+ }, [fetchTree]);
6467
+ useEffect(() => {
6468
+ if (!sessionId) {
6469
+ setFile(null);
6470
+ setFileError(null);
6471
+ onSelectedPathChange(null);
6472
+ }
6473
+ }, [onSelectedPathChange, sessionId]);
6474
+ useEffect(() => {
6475
+ if (!selectedPath) return;
6476
+ const parts = selectedPath.split("/");
6477
+ if (parts.length <= 1) return;
6478
+ setExpandedPaths((prev) => {
6479
+ const next = new Set(prev);
6480
+ let path = "";
6481
+ for (let index = 0; index < parts.length - 1; index += 1) {
6482
+ path = path ? `${path}/${parts[index]}` : parts[index];
6483
+ next.add(path);
6484
+ }
6485
+ return next;
6486
+ });
6487
+ }, [selectedPath]);
6488
+ useEffect(() => {
6489
+ if (!selectedPath || !sessionId || entries.length === 0) return;
6490
+ const entry = findEntry(entries, selectedPath);
6491
+ if (entry?.kind === "file" && file?.path !== selectedPath) {
6492
+ void fetchFile(selectedPath);
6493
+ }
6494
+ }, [entries, fetchFile, file?.path, selectedPath, sessionId]);
6495
+ const handleSelect = useCallback(
6496
+ (path) => {
6497
+ onSelectedPathChange(path);
6498
+ const entry = findEntry(entries, path);
6499
+ if (entry?.kind === "file") {
6500
+ void fetchFile(path);
6501
+ }
6502
+ },
6503
+ [entries, fetchFile, onSelectedPathChange]
6504
+ );
6505
+ const handleUpload = useCallback(
6506
+ async (files) => {
6507
+ if (!sessionId || files.length === 0) return;
6508
+ setUploading(true);
6509
+ setNotice(null);
6510
+ try {
6511
+ for (const uploadedFile of Array.from(files)) {
6512
+ await adapter.uploadWorkspaceFile({
6513
+ sessionId,
6514
+ file: uploadedFile
6515
+ });
6516
+ }
6517
+ setNotice(
6518
+ files.length === 1 ? `Uploaded ${files[0]?.name ?? "file"}` : `Uploaded ${files.length} files`
6519
+ );
6520
+ await fetchTree();
6521
+ } catch (error) {
6522
+ setNotice(error.message);
6523
+ } finally {
6524
+ setUploading(false);
6525
+ if (fileInputRef.current) fileInputRef.current.value = "";
6526
+ }
6527
+ },
6528
+ [adapter, fetchTree, sessionId]
6529
+ );
6530
+ const handleDelete = useCallback(
6531
+ async (path) => {
6532
+ if (!sessionId) return;
6533
+ try {
6534
+ await adapter.deleteWorkspaceFile({ sessionId, path });
6535
+ if (selectedPath === path) {
6536
+ onSelectedPathChange(null);
6537
+ setFile(null);
6538
+ setFileError(null);
6539
+ }
6540
+ setNotice(`Deleted ${path.split("/").pop() ?? path}`);
6541
+ await fetchTree();
6542
+ } catch (error) {
6543
+ setNotice(error.message);
6544
+ } finally {
6545
+ setDeleteTarget(null);
6546
+ }
6547
+ },
6548
+ [adapter, fetchTree, onSelectedPathChange, selectedPath, sessionId]
6549
+ );
6550
+ const onResizeStart = useCallback(
6551
+ (event) => {
6552
+ event.preventDefault();
6553
+ isResizing.current = true;
6554
+ startX.current = event.clientX;
6555
+ startWidth.current = width;
6556
+ document.body.style.cursor = "col-resize";
6557
+ document.body.style.userSelect = "none";
6558
+ },
6559
+ [width]
6560
+ );
6561
+ useEffect(() => {
6562
+ const onMouseMove = (event) => {
6563
+ if (!isResizing.current) return;
6564
+ const delta = startX.current - event.clientX;
6565
+ setWidth(
6566
+ Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, startWidth.current + delta))
6567
+ );
6568
+ };
6569
+ const onMouseUp = () => {
6570
+ if (!isResizing.current) return;
6571
+ isResizing.current = false;
6572
+ document.body.style.cursor = "";
6573
+ document.body.style.userSelect = "";
6574
+ };
6575
+ window.addEventListener("mousemove", onMouseMove);
6576
+ window.addEventListener("mouseup", onMouseUp);
6577
+ return () => {
6578
+ window.removeEventListener("mousemove", onMouseMove);
6579
+ window.removeEventListener("mouseup", onMouseUp);
6580
+ };
6581
+ }, []);
6582
+ return /* @__PURE__ */ jsxs(
6583
+ "aside",
6584
+ {
6585
+ className: "relative z-10 flex min-h-0 flex-col overflow-hidden border-l border-muted-foreground/20 bg-card",
6586
+ style: { width, minWidth: MIN_WIDTH, maxWidth: MAX_WIDTH },
6587
+ children: [
6588
+ /* @__PURE__ */ jsx(
6589
+ "div",
6590
+ {
6591
+ 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",
6592
+ onMouseDown: onResizeStart
6593
+ }
6594
+ ),
6595
+ /* @__PURE__ */ jsx(
6596
+ "input",
6597
+ {
6598
+ ref: fileInputRef,
6599
+ type: "file",
6600
+ multiple: true,
6601
+ className: "hidden",
6602
+ onChange: (event) => {
6603
+ if (event.target.files) void handleUpload(event.target.files);
6604
+ }
6605
+ }
6606
+ ),
6607
+ /* @__PURE__ */ jsxs("div", { className: "flex min-h-14 items-center gap-2 border-b border-line px-3 py-3", children: [
6608
+ /* @__PURE__ */ jsx(FolderOpenIcon, { className: "size-4 shrink-0 text-amber-500" }),
6609
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
6610
+ /* @__PURE__ */ jsx("div", { className: "truncate text-sm font-medium text-foreground", children: rootName }),
6611
+ /* @__PURE__ */ jsx("div", { className: "truncate text-xs text-faint", children: "Workspace" })
6612
+ ] }),
6613
+ /* @__PURE__ */ jsxs(Tooltip, { children: [
6614
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
6615
+ Button,
6616
+ {
6617
+ variant: "ghost",
6618
+ size: "icon-sm",
6619
+ onClick: () => fileInputRef.current?.click(),
6620
+ disabled: !sessionId || uploading,
6621
+ "aria-label": "Upload files",
6622
+ children: uploading ? /* @__PURE__ */ jsx(Loader2Icon, { className: "size-4 animate-spin" }) : /* @__PURE__ */ jsx(UploadIcon, { className: "size-4" })
6623
+ }
6624
+ ) }),
6625
+ /* @__PURE__ */ jsx(TooltipContent, { side: "bottom", children: "Upload files" })
6626
+ ] }),
6627
+ /* @__PURE__ */ jsxs(Tooltip, { children: [
6628
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
6629
+ Button,
6630
+ {
6631
+ variant: "ghost",
6632
+ size: "icon-sm",
6633
+ onClick: () => void fetchTree(),
6634
+ disabled: !sessionId || treeLoading,
6635
+ "aria-label": "Refresh workspace",
6636
+ children: /* @__PURE__ */ jsx(
6637
+ RefreshCwIcon,
6638
+ {
6639
+ className: cn("size-4", treeLoading && "animate-spin")
6640
+ }
6641
+ )
6642
+ }
6643
+ ) }),
6644
+ /* @__PURE__ */ jsx(TooltipContent, { side: "bottom", children: "Refresh" })
6645
+ ] })
6646
+ ] }),
6647
+ notice && /* @__PURE__ */ jsx("div", { className: "border-b border-line px-3 py-2 text-xs text-muted-foreground", children: notice }),
6648
+ /* @__PURE__ */ jsx(ScrollArea, { className: "min-h-0 flex-1", children: /* @__PURE__ */ jsxs("div", { className: "px-1 py-1", children: [
6649
+ treeLoading && entries.length === 0 && /* @__PURE__ */ jsx("div", { className: "space-y-1 p-2", children: Array.from({ length: 8 }).map((_, index) => /* @__PURE__ */ jsx(
6650
+ Skeleton,
6651
+ {
6652
+ className: "h-5 rounded",
6653
+ style: {
6654
+ width: `${SKELETON_WIDTHS2[index % SKELETON_WIDTHS2.length]}%`
6655
+ }
6656
+ },
6657
+ index
6658
+ )) }),
6659
+ treeError && /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2 p-3 text-sm text-destructive", children: [
6660
+ /* @__PURE__ */ jsx(AlertCircleIcon, { className: "mt-0.5 size-4 shrink-0" }),
6661
+ /* @__PURE__ */ jsx("span", { children: treeError })
6662
+ ] }),
6663
+ !treeLoading && !treeError && entries.length === 0 && /* @__PURE__ */ jsxs("div", { className: "px-4 py-8 text-center text-sm text-faint", children: [
6664
+ /* @__PURE__ */ jsx("p", { children: "No workspace files" }),
6665
+ /* @__PURE__ */ jsxs(
6666
+ Button,
6667
+ {
6668
+ variant: "outline",
6669
+ size: "sm",
6670
+ className: "mt-3 gap-1.5",
6671
+ onClick: () => fileInputRef.current?.click(),
6672
+ disabled: !sessionId,
6673
+ children: [
6674
+ /* @__PURE__ */ jsx(UploadIcon, { className: "size-3.5" }),
6675
+ "Upload files"
6676
+ ]
6677
+ }
6678
+ )
6679
+ ] }),
6680
+ entries.length > 0 && /* @__PURE__ */ jsx(
6681
+ FileTree,
6682
+ {
6683
+ expanded: expandedPaths,
6684
+ onExpandedChange: setExpandedPaths,
6685
+ selectedPath,
6686
+ onSelect: handleSelect,
6687
+ className: "rounded-none border-0",
6688
+ children: /* @__PURE__ */ jsx(
6689
+ RenderEntries,
6690
+ {
6691
+ entries,
6692
+ onFileSelect: handleSelect,
6693
+ onDelete: setDeleteTarget
6694
+ }
6695
+ )
6696
+ }
6697
+ )
6698
+ ] }) }),
6699
+ /* @__PURE__ */ jsx(
6700
+ FileViewer,
6701
+ {
6702
+ file,
6703
+ loading: fileLoading,
6704
+ error: fileError,
6705
+ onClose: () => {
6706
+ setFile(null);
6707
+ setFileError(null);
6708
+ }
6709
+ }
6710
+ ),
6711
+ /* @__PURE__ */ jsx(
6712
+ ConfirmDialog,
6713
+ {
6714
+ open: deleteTarget !== null,
6715
+ title: "Delete file?",
6716
+ description: `This will permanently delete ${deleteTarget?.split("/").pop() ?? "this file"} from the workspace.`,
6717
+ confirmLabel: "Delete",
6718
+ onConfirm: () => deleteTarget ? handleDelete(deleteTarget) : Promise.resolve(),
6719
+ onCancel: () => setDeleteTarget(null)
6720
+ }
6721
+ )
6722
+ ]
6723
+ }
6724
+ );
6725
+ }
6726
+ function ConfirmDialog({
6727
+ open,
6728
+ title,
6729
+ description,
6730
+ confirmLabel,
6731
+ onConfirm,
6732
+ onCancel
6733
+ }) {
6734
+ const [loading, setLoading] = useState(false);
6735
+ const handleConfirm = async () => {
6736
+ setLoading(true);
6737
+ try {
6738
+ await onConfirm();
6739
+ } finally {
6740
+ setLoading(false);
6741
+ }
6742
+ };
6743
+ return /* @__PURE__ */ jsx(
6744
+ Dialog,
6745
+ {
6746
+ open,
6747
+ onOpenChange: (nextOpen) => {
6748
+ if (!nextOpen && !loading) onCancel();
6749
+ },
6750
+ children: /* @__PURE__ */ jsxs(DialogContent, { className: "sm:max-w-md", children: [
6751
+ /* @__PURE__ */ jsxs(DialogHeader, { children: [
6752
+ /* @__PURE__ */ jsx(DialogTitle, { children: title }),
6753
+ /* @__PURE__ */ jsx(DialogDescription, { children: description })
6754
+ ] }),
6755
+ /* @__PURE__ */ jsxs(DialogFooter, { children: [
6756
+ /* @__PURE__ */ jsx(Button, { variant: "outline", disabled: loading, onClick: onCancel, children: "Cancel" }),
6757
+ /* @__PURE__ */ jsxs(Button, { variant: "destructive", disabled: loading, onClick: handleConfirm, children: [
6758
+ loading && /* @__PURE__ */ jsx(Loader2Icon, { className: "mr-1.5 size-3.5 animate-spin" }),
6759
+ confirmLabel
6760
+ ] })
6761
+ ] })
6762
+ ] })
6763
+ }
6764
+ );
6765
+ }
5973
6766
 
5974
6767
  // src/runtime/events.ts
5975
6768
  var AGENT_CHAT_LISTENED_EVENTS = [
@@ -6780,6 +7573,7 @@ function AgentChat({
6780
7573
  onMessagesChange,
6781
7574
  disabled
6782
7575
  }) {
7576
+ const [workspacePath, setWorkspacePath] = useState(null);
6783
7577
  const runtime = useAgentChatRuntime({
6784
7578
  adapter,
6785
7579
  agentId,
@@ -6789,29 +7583,47 @@ function AgentChat({
6789
7583
  useEffect(() => {
6790
7584
  onMessagesChange?.(runtime.messages);
6791
7585
  }, [onMessagesChange, runtime.messages]);
7586
+ const handleFileSelect = useCallback(
7587
+ (path) => {
7588
+ setWorkspacePath(path);
7589
+ onFileSelect?.(path);
7590
+ },
7591
+ [onFileSelect]
7592
+ );
6792
7593
  return /* @__PURE__ */ jsx(
6793
7594
  AgentChatAdapterProvider,
6794
7595
  {
6795
7596
  value: {
6796
7597
  adapter,
6797
7598
  sessionId,
6798
- onFileSelect
7599
+ onFileSelect: handleFileSelect
6799
7600
  },
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
- ) })
7601
+ children: /* @__PURE__ */ jsx(TooltipProvider, { children: /* @__PURE__ */ jsxs("section", { className: "flex h-full min-h-0 bg-card text-sm text-foreground", children: [
7602
+ /* @__PURE__ */ jsx("div", { className: "flex min-w-0 flex-1 flex-col", children: /* @__PURE__ */ jsx(
7603
+ ChatThread,
7604
+ {
7605
+ sessionId,
7606
+ messages: runtime.messages,
7607
+ isRunning: runtime.isRunning,
7608
+ onSend: (content) => void runtime.send(content),
7609
+ onStop: () => void runtime.stop(),
7610
+ onRetry: runtime.retry,
7611
+ onFileSelect: handleFileSelect,
7612
+ disabled,
7613
+ tokenUsage: runtime.tokenUsage,
7614
+ retryIndicator: runtime.retryIndicator
7615
+ }
7616
+ ) }),
7617
+ /* @__PURE__ */ jsx(
7618
+ WorkspacePanel,
7619
+ {
7620
+ adapter,
7621
+ sessionId,
7622
+ selectedPath: workspacePath,
7623
+ onSelectedPathChange: setWorkspacePath
7624
+ }
7625
+ )
7626
+ ] }) })
6815
7627
  }
6816
7628
  );
6817
7629
  }