@tangle-network/sandbox-ui 0.137.2 → 0.138.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chat-Cw13rqn9.js → chat-BYTVLZQW.js} +2 -2
- package/dist/chat.d.ts +1 -1
- package/dist/chat.js +1 -1
- package/dist/diff-view-A0O0Z_SR.js +287 -0
- package/dist/diff-view-DDHWtR6X.d.ts +21 -0
- package/dist/globals.css +43 -0
- package/dist/index-C5h5T0nb.d.ts +91 -0
- package/dist/{index-T4-5mWTq.d.ts → index-CvtEhIpk.d.ts} +23 -28
- package/dist/{index-B1oEiBks.d.ts → index-F42BOxUM.d.ts} +15 -1
- package/dist/{index-4KFqFK1f.d.ts → index-ucJHuuVZ.d.ts} +2 -2
- package/dist/index.d.ts +6 -5
- package/dist/index.js +12 -7
- package/dist/run-CliTxkfw.js +397 -0
- package/dist/run.d.ts +2 -2
- package/dist/run.js +1 -1
- package/dist/styles.css +43 -0
- package/dist/workbench/diff.d.ts +2 -0
- package/dist/workbench/diff.js +1 -0
- package/dist/workbench-B3JL1qbt.js +892 -0
- package/dist/workbench.d.ts +3 -2
- package/dist/workbench.js +3 -2
- package/dist/{workspace-Cpgyw5N7.js → workspace-DRhi6S3o.js} +304 -103
- package/dist/workspace.d.ts +1 -1
- package/dist/workspace.js +1 -1
- package/package.json +10 -2
- package/dist/workbench-GDuy3rB5.js +0 -8053
|
@@ -0,0 +1,892 @@
|
|
|
1
|
+
import { t as cn$1 } from "./utils-BER_bdgE.js";
|
|
2
|
+
import { a as PortsList } from "./dashboard-ClKA3opr.js";
|
|
3
|
+
import { n as TerminalView } from "./terminal-nUCznZRj.js";
|
|
4
|
+
import { RichFileTree } from "./files.js";
|
|
5
|
+
import { a as CodeSurface, n as buildUnifiedPatch, r as computeDiffStats, t as DiffView } from "./diff-view-A0O0Z_SR.js";
|
|
6
|
+
import { n as PreviewView } from "./embedded-app-view-DXsoPale.js";
|
|
7
|
+
import { Button, Skeleton, Textarea } from "@tangle-network/ui/primitives";
|
|
8
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
import * as React$1 from "react";
|
|
10
|
+
import { Check, ChevronRight, Code2, Copy, FileCode2, GitBranch, GitCompare, Globe, Network, RefreshCw, SquareTerminal, WrapText } from "lucide-react";
|
|
11
|
+
import { focusRing, focusRingInset } from "@tangle-network/ui/utils";
|
|
12
|
+
// src/workbench/pill-tabs.tsx
|
|
13
|
+
/**
|
|
14
|
+
* Sliding-pill segmented control. Reimplemented from blueprint-agent's
|
|
15
|
+
* framer-motion `PillTabs` with a measured CSS transform instead — no motion
|
|
16
|
+
* dependency. A `ResizeObserver` re-measures on container resize and font
|
|
17
|
+
* load so the indicator tracks the active tab exactly. Token-swapped onto the
|
|
18
|
+
* sandbox-ui surface ladder.
|
|
19
|
+
*/
|
|
20
|
+
function PillTabs({ items, value, onChange, className, "aria-label": ariaLabel, idPrefix, panelId }) {
|
|
21
|
+
const containerRef = React$1.useRef(null);
|
|
22
|
+
const tabRefs = React$1.useRef(/* @__PURE__ */ new Map());
|
|
23
|
+
const [indicator, setIndicator] = React$1.useState(null);
|
|
24
|
+
const measure = React$1.useCallback(() => {
|
|
25
|
+
const container = containerRef.current;
|
|
26
|
+
const active = tabRefs.current.get(value);
|
|
27
|
+
if (!container || !active) return;
|
|
28
|
+
const c = container.getBoundingClientRect();
|
|
29
|
+
const a = active.getBoundingClientRect();
|
|
30
|
+
setIndicator({
|
|
31
|
+
left: a.left - c.left,
|
|
32
|
+
width: a.width
|
|
33
|
+
});
|
|
34
|
+
}, [value]);
|
|
35
|
+
React$1.useLayoutEffect(() => {
|
|
36
|
+
measure();
|
|
37
|
+
}, [measure, items]);
|
|
38
|
+
React$1.useEffect(() => {
|
|
39
|
+
const container = containerRef.current;
|
|
40
|
+
if (!container) return;
|
|
41
|
+
const ro = new ResizeObserver(() => measure());
|
|
42
|
+
ro.observe(container);
|
|
43
|
+
const raf = requestAnimationFrame(measure);
|
|
44
|
+
return () => {
|
|
45
|
+
ro.disconnect();
|
|
46
|
+
cancelAnimationFrame(raf);
|
|
47
|
+
};
|
|
48
|
+
}, [measure]);
|
|
49
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
50
|
+
ref: containerRef,
|
|
51
|
+
role: "tablist",
|
|
52
|
+
"aria-label": ariaLabel,
|
|
53
|
+
className: cn$1("relative inline-flex items-center gap-0.5 rounded-full border border-[var(--md3-outline-variant)] bg-surface-container-low p-1", className),
|
|
54
|
+
children: [indicator && /* @__PURE__ */ jsx("span", {
|
|
55
|
+
"aria-hidden": true,
|
|
56
|
+
className: "absolute top-1 bottom-1 rounded-full bg-surface-container-high shadow-[var(--shadow-card)] ring-1 ring-[var(--md3-outline-variant)] transition-[transform,width] duration-200 ease-out",
|
|
57
|
+
style: {
|
|
58
|
+
transform: `translateX(${indicator.left}px)`,
|
|
59
|
+
width: indicator.width,
|
|
60
|
+
left: 0
|
|
61
|
+
}
|
|
62
|
+
}), items.map((item) => {
|
|
63
|
+
const selected = item.value === value;
|
|
64
|
+
return /* @__PURE__ */ jsxs("button", {
|
|
65
|
+
ref: (el) => {
|
|
66
|
+
if (el) tabRefs.current.set(item.value, el);
|
|
67
|
+
else tabRefs.current.delete(item.value);
|
|
68
|
+
},
|
|
69
|
+
type: "button",
|
|
70
|
+
role: "tab",
|
|
71
|
+
id: idPrefix ? `${idPrefix}-${item.value}` : void 0,
|
|
72
|
+
"aria-controls": panelId,
|
|
73
|
+
"aria-selected": selected,
|
|
74
|
+
tabIndex: selected ? 0 : -1,
|
|
75
|
+
onClick: () => onChange(item.value),
|
|
76
|
+
onKeyDown: (event) => {
|
|
77
|
+
const index = items.findIndex((candidate) => candidate.value === item.value);
|
|
78
|
+
let nextIndex;
|
|
79
|
+
switch (event.key) {
|
|
80
|
+
case "ArrowRight":
|
|
81
|
+
nextIndex = (index + 1) % items.length;
|
|
82
|
+
break;
|
|
83
|
+
case "ArrowLeft":
|
|
84
|
+
nextIndex = (index - 1 + items.length) % items.length;
|
|
85
|
+
break;
|
|
86
|
+
case "Home":
|
|
87
|
+
nextIndex = 0;
|
|
88
|
+
break;
|
|
89
|
+
case "End":
|
|
90
|
+
nextIndex = items.length - 1;
|
|
91
|
+
break;
|
|
92
|
+
default: return;
|
|
93
|
+
}
|
|
94
|
+
event.preventDefault();
|
|
95
|
+
const next = items[nextIndex];
|
|
96
|
+
onChange(next.value);
|
|
97
|
+
const nextTab = tabRefs.current.get(next.value);
|
|
98
|
+
const container = containerRef.current;
|
|
99
|
+
if (!nextTab || !container) return;
|
|
100
|
+
nextTab.focus({ preventScroll: true });
|
|
101
|
+
const padding = getComputedStyle(container);
|
|
102
|
+
const target = nextTab.getBoundingClientRect();
|
|
103
|
+
const targetLeft = target.left - (Number.parseFloat(padding.paddingLeft) || 0);
|
|
104
|
+
const targetRight = target.right + (Number.parseFloat(padding.paddingRight) || 0);
|
|
105
|
+
for (let scrollport = container; scrollport; scrollport = scrollport.parentElement) {
|
|
106
|
+
if (![
|
|
107
|
+
"auto",
|
|
108
|
+
"scroll",
|
|
109
|
+
"hidden"
|
|
110
|
+
].includes(getComputedStyle(scrollport).overflowX)) continue;
|
|
111
|
+
const left = scrollport.getBoundingClientRect().left + scrollport.clientLeft;
|
|
112
|
+
const right = left + scrollport.clientWidth;
|
|
113
|
+
scrollport.scrollLeft += targetLeft < left ? targetLeft - left : targetRight > right ? targetRight - right : 0;
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
className: cn$1("relative z-10 inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-medium whitespace-nowrap transition-colors", focusRing, selected ? "text-foreground" : "text-muted-foreground hover:text-foreground"),
|
|
118
|
+
children: [item.icon && /* @__PURE__ */ jsx("span", {
|
|
119
|
+
className: "flex h-3.5 w-3.5 items-center justify-center",
|
|
120
|
+
children: item.icon
|
|
121
|
+
}), item.label]
|
|
122
|
+
}, item.value);
|
|
123
|
+
})]
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
// src/workbench/panel-header.tsx
|
|
127
|
+
const GIT_STATUS_META = {
|
|
128
|
+
added: {
|
|
129
|
+
label: "Added",
|
|
130
|
+
className: "bg-[var(--surface-success-bg)] text-[var(--surface-success-text)]"
|
|
131
|
+
},
|
|
132
|
+
modified: {
|
|
133
|
+
label: "Modified",
|
|
134
|
+
className: "bg-[var(--surface-warning-bg)] text-[var(--surface-warning-text)]"
|
|
135
|
+
},
|
|
136
|
+
deleted: {
|
|
137
|
+
label: "Deleted",
|
|
138
|
+
className: "bg-[var(--surface-danger-bg)] text-[var(--surface-danger-text)]"
|
|
139
|
+
},
|
|
140
|
+
renamed: {
|
|
141
|
+
label: "Renamed",
|
|
142
|
+
className: "bg-[var(--surface-info-bg)] text-[var(--surface-info-text)]"
|
|
143
|
+
},
|
|
144
|
+
untracked: {
|
|
145
|
+
label: "Untracked",
|
|
146
|
+
className: "bg-surface-container-high text-muted-foreground"
|
|
147
|
+
},
|
|
148
|
+
ignored: {
|
|
149
|
+
label: "Ignored",
|
|
150
|
+
className: "bg-surface-container-high text-muted-foreground"
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
function GitStatusBadge({ status }) {
|
|
154
|
+
const meta = GIT_STATUS_META[status];
|
|
155
|
+
return /* @__PURE__ */ jsx("span", {
|
|
156
|
+
className: cn$1("inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wider", meta.className),
|
|
157
|
+
children: meta.label
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
/** `+added −removed` line counts in mono; the same pair the Changes list prints per file. */
|
|
161
|
+
function DiffStatsBadge({ stats, className }) {
|
|
162
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
163
|
+
className: cn$1("inline-flex items-center gap-1.5 font-mono text-xs", className),
|
|
164
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
165
|
+
className: "text-[var(--code-success,#10B981)]",
|
|
166
|
+
children: ["+", stats.added]
|
|
167
|
+
}), /* @__PURE__ */ jsxs("span", {
|
|
168
|
+
className: "text-[var(--code-error,#FF4D6D)]",
|
|
169
|
+
children: ["-", stats.removed]
|
|
170
|
+
})]
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Thin bordered chrome bar above the artifact body — filename, git-status
|
|
175
|
+
* badge, optional diff counts, and a copy action. Ported in spirit from
|
|
176
|
+
* blueprint-agent's `PanelHeader` (min-h, hairline bottom border, dense
|
|
177
|
+
* horizontal rhythm), retoned to the sandbox-ui surface ladder.
|
|
178
|
+
*/
|
|
179
|
+
function PanelHeader({ filename, gitStatus, stats, copyText, children, className }) {
|
|
180
|
+
const [copied, setCopied] = React$1.useState(false);
|
|
181
|
+
const timer = React$1.useRef(null);
|
|
182
|
+
React$1.useEffect(() => () => {
|
|
183
|
+
if (timer.current) clearTimeout(timer.current);
|
|
184
|
+
}, []);
|
|
185
|
+
const handleCopy = React$1.useCallback(async () => {
|
|
186
|
+
if (copyText == null) return;
|
|
187
|
+
try {
|
|
188
|
+
await navigator.clipboard.writeText(copyText);
|
|
189
|
+
setCopied(true);
|
|
190
|
+
if (timer.current) clearTimeout(timer.current);
|
|
191
|
+
timer.current = setTimeout(() => setCopied(false), 2e3);
|
|
192
|
+
} catch (err) {
|
|
193
|
+
console.warn("Clipboard write failed:", err);
|
|
194
|
+
}
|
|
195
|
+
}, [copyText]);
|
|
196
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
197
|
+
className: cn$1("flex min-h-[38px] items-center gap-2 border-b border-[var(--md3-outline-variant)] bg-surface-container-low px-3 py-1", className),
|
|
198
|
+
children: [
|
|
199
|
+
/* @__PURE__ */ jsx("span", {
|
|
200
|
+
className: "truncate font-mono text-xs text-foreground",
|
|
201
|
+
children: filename
|
|
202
|
+
}),
|
|
203
|
+
gitStatus && /* @__PURE__ */ jsx(GitStatusBadge, { status: gitStatus }),
|
|
204
|
+
/* @__PURE__ */ jsxs("div", {
|
|
205
|
+
className: "ml-auto flex items-center gap-3",
|
|
206
|
+
children: [
|
|
207
|
+
stats && /* @__PURE__ */ jsx(DiffStatsBadge, { stats }),
|
|
208
|
+
children,
|
|
209
|
+
copyText != null && /* @__PURE__ */ jsxs("button", {
|
|
210
|
+
type: "button",
|
|
211
|
+
onClick: handleCopy,
|
|
212
|
+
"aria-label": "Copy to clipboard",
|
|
213
|
+
className: "inline-flex items-center gap-1.5 rounded-md border border-[var(--md3-outline-variant)] bg-surface-container px-2 py-1 text-xs text-muted-foreground transition-colors hover:bg-surface-container-high hover:text-foreground",
|
|
214
|
+
children: [copied ? /* @__PURE__ */ jsx(Check, { className: "h-3.5 w-3.5 text-[var(--surface-success-text)]" }) : /* @__PURE__ */ jsx(Copy, { className: "h-3.5 w-3.5" }), copied ? "Copied" : "Copy"]
|
|
215
|
+
})
|
|
216
|
+
]
|
|
217
|
+
})
|
|
218
|
+
]
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
// src/workbench/file-breadcrumb.tsx
|
|
222
|
+
/**
|
|
223
|
+
* Slim path breadcrumb for the code panel. Reimplemented (not ported) from
|
|
224
|
+
* blueprint-agent's `FileBreadcrumb` — that one nests a `FileTree` dropdown per
|
|
225
|
+
* segment and couples to the workbench store + framer-motion. Here the rail
|
|
226
|
+
* already owns navigation, so this is a read-only trail: caret-separated parent
|
|
227
|
+
* segments with a file-icon leaf.
|
|
228
|
+
*/
|
|
229
|
+
function FileBreadcrumb({ path, className }) {
|
|
230
|
+
const segments = path.split("/").filter(Boolean);
|
|
231
|
+
const leaf = segments[segments.length - 1] ?? path;
|
|
232
|
+
const parents = segments.slice(0, -1);
|
|
233
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
234
|
+
className: cn$1("flex min-h-[30px] items-center gap-1 overflow-hidden border-b border-[var(--md3-outline-variant)] bg-surface-container-lowest px-3 text-xs text-muted-foreground", className),
|
|
235
|
+
children: [
|
|
236
|
+
/* @__PURE__ */ jsx(FileCode2, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground" }),
|
|
237
|
+
parents.map((seg, i) => /* @__PURE__ */ jsxs(React$1.Fragment, { children: [/* @__PURE__ */ jsx("span", {
|
|
238
|
+
className: "truncate font-mono",
|
|
239
|
+
children: seg
|
|
240
|
+
}), /* @__PURE__ */ jsx(ChevronRight, { className: "h-3 w-3 shrink-0 opacity-50" })] }, `${seg}-${i}`)),
|
|
241
|
+
/* @__PURE__ */ jsx("span", {
|
|
242
|
+
className: "truncate font-mono text-foreground",
|
|
243
|
+
children: leaf
|
|
244
|
+
})
|
|
245
|
+
]
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
// src/workbench/code-view.tsx
|
|
249
|
+
/**
|
|
250
|
+
* Code surface: a fixed-width `RichFileTree` rail (with git-status
|
|
251
|
+
* decorations) beside a breadcrumb-topped, syntax-highlighted read-only file
|
|
252
|
+
* view. The rail owns navigation; selecting a path fans out to `onFileSelect`
|
|
253
|
+
* so the host can swap the active artifact.
|
|
254
|
+
*/
|
|
255
|
+
function CodeView({ path, filename, content, language, paths, gitStatus, onFileSelect, showTree = true, defaultWrap = false, themeType, className }) {
|
|
256
|
+
const [narrowPane, setNarrowPane] = React$1.useState("code");
|
|
257
|
+
const [wrap, setWrap] = React$1.useState(defaultWrap);
|
|
258
|
+
const [copied, setCopied] = React$1.useState(false);
|
|
259
|
+
const [line, setLine] = React$1.useState();
|
|
260
|
+
const lineInput = React$1.useRef(null);
|
|
261
|
+
const lineCount = React$1.useMemo(() => content.split("\n").length, [content]);
|
|
262
|
+
React$1.useEffect(() => {
|
|
263
|
+
setLine(void 0);
|
|
264
|
+
}, [path]);
|
|
265
|
+
React$1.useEffect(() => {
|
|
266
|
+
if (!copied) return;
|
|
267
|
+
const timer = setTimeout(() => setCopied(false), 1500);
|
|
268
|
+
return () => clearTimeout(timer);
|
|
269
|
+
}, [copied]);
|
|
270
|
+
const copy = () => {
|
|
271
|
+
navigator.clipboard?.writeText(content).then(() => setCopied(true));
|
|
272
|
+
};
|
|
273
|
+
const goToLine = (event) => {
|
|
274
|
+
event.preventDefault();
|
|
275
|
+
const value = Math.round(Number(lineInput.current?.value));
|
|
276
|
+
if (Number.isFinite(value) && value >= 1) setLine(Math.min(value, lineCount));
|
|
277
|
+
};
|
|
278
|
+
const treePaths = React$1.useMemo(() => {
|
|
279
|
+
const list = paths && paths.length > 0 ? paths : [path];
|
|
280
|
+
return list.includes(path) ? list : [...list, path];
|
|
281
|
+
}, [paths, path]);
|
|
282
|
+
return /* @__PURE__ */ jsx("div", {
|
|
283
|
+
className: cn$1("sandbox-code-view h-full min-h-0 w-full", className),
|
|
284
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
285
|
+
className: "sandbox-code-view-layout flex h-full min-h-0 w-full",
|
|
286
|
+
children: [
|
|
287
|
+
showTree && /* @__PURE__ */ jsxs("div", {
|
|
288
|
+
className: "sandbox-code-view-mobile-toolbar",
|
|
289
|
+
role: "group",
|
|
290
|
+
"aria-label": "Code view pane",
|
|
291
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
292
|
+
type: "button",
|
|
293
|
+
"aria-pressed": narrowPane === "files",
|
|
294
|
+
onClick: () => setNarrowPane("files"),
|
|
295
|
+
className: focusRing,
|
|
296
|
+
children: "Files"
|
|
297
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
298
|
+
type: "button",
|
|
299
|
+
"aria-pressed": narrowPane === "code",
|
|
300
|
+
onClick: () => setNarrowPane("code"),
|
|
301
|
+
className: focusRing,
|
|
302
|
+
children: "Code"
|
|
303
|
+
})]
|
|
304
|
+
}),
|
|
305
|
+
showTree && /* @__PURE__ */ jsx("div", {
|
|
306
|
+
className: "sandbox-code-view-tree sandbox-tree-surface flex w-60 shrink-0 flex-col border-r border-[var(--md3-outline-variant)] bg-surface-container-lowest",
|
|
307
|
+
"data-mobile-open": narrowPane === "files",
|
|
308
|
+
children: /* @__PURE__ */ jsx(RichFileTree, {
|
|
309
|
+
paths: treePaths,
|
|
310
|
+
selectedPath: path,
|
|
311
|
+
onSelect: (next) => {
|
|
312
|
+
onFileSelect?.(next);
|
|
313
|
+
setNarrowPane("code");
|
|
314
|
+
},
|
|
315
|
+
gitStatus,
|
|
316
|
+
search: treePaths.length > 12,
|
|
317
|
+
height: "100%"
|
|
318
|
+
})
|
|
319
|
+
}),
|
|
320
|
+
/* @__PURE__ */ jsxs("div", {
|
|
321
|
+
className: "sandbox-code-view-editor flex min-w-0 flex-1 flex-col bg-surface-container",
|
|
322
|
+
"data-mobile-hidden": showTree && narrowPane === "files",
|
|
323
|
+
onKeyDown: (event) => {
|
|
324
|
+
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "g") {
|
|
325
|
+
event.preventDefault();
|
|
326
|
+
lineInput.current?.focus();
|
|
327
|
+
lineInput.current?.select();
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
331
|
+
className: "flex shrink-0 items-center border-b border-[var(--md3-outline-variant)] bg-surface-container-lowest",
|
|
332
|
+
children: [/* @__PURE__ */ jsx(FileBreadcrumb, {
|
|
333
|
+
path,
|
|
334
|
+
className: "min-w-0 flex-1 border-b-0"
|
|
335
|
+
}), /* @__PURE__ */ jsxs("form", {
|
|
336
|
+
onSubmit: goToLine,
|
|
337
|
+
className: "flex shrink-0 items-center gap-0.5 pr-1",
|
|
338
|
+
children: [
|
|
339
|
+
/* @__PURE__ */ jsx("input", {
|
|
340
|
+
ref: lineInput,
|
|
341
|
+
type: "number",
|
|
342
|
+
min: 1,
|
|
343
|
+
max: lineCount,
|
|
344
|
+
inputMode: "numeric",
|
|
345
|
+
placeholder: "Line",
|
|
346
|
+
"aria-label": "Go to line",
|
|
347
|
+
"aria-keyshortcuts": "Control+g Meta+g",
|
|
348
|
+
className: cn$1("h-6 w-14 rounded border border-transparent bg-transparent px-1.5 font-mono text-xs text-foreground placeholder:text-muted-foreground hover:border-border", focusRing)
|
|
349
|
+
}),
|
|
350
|
+
/* @__PURE__ */ jsx(Button, {
|
|
351
|
+
type: "button",
|
|
352
|
+
variant: "ghost",
|
|
353
|
+
size: "sm",
|
|
354
|
+
"aria-pressed": wrap,
|
|
355
|
+
"aria-label": "Wrap lines",
|
|
356
|
+
title: "Wrap lines",
|
|
357
|
+
onClick: () => setWrap((value) => !value),
|
|
358
|
+
className: cn$1("h-6 w-6 p-0", wrap && "text-foreground bg-surface-container-high"),
|
|
359
|
+
children: /* @__PURE__ */ jsx(WrapText, { className: "h-3.5 w-3.5" })
|
|
360
|
+
}),
|
|
361
|
+
/* @__PURE__ */ jsx(Button, {
|
|
362
|
+
type: "button",
|
|
363
|
+
variant: "ghost",
|
|
364
|
+
size: "sm",
|
|
365
|
+
"aria-label": copied ? "Copied" : "Copy file",
|
|
366
|
+
title: copied ? "Copied" : "Copy file",
|
|
367
|
+
onClick: copy,
|
|
368
|
+
className: "h-6 w-6 p-0",
|
|
369
|
+
children: copied ? /* @__PURE__ */ jsx(Check, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx(Copy, { className: "h-3.5 w-3.5" })
|
|
370
|
+
})
|
|
371
|
+
]
|
|
372
|
+
})]
|
|
373
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
374
|
+
className: "min-h-0 flex-1",
|
|
375
|
+
children: /* @__PURE__ */ jsx(CodeSurface, {
|
|
376
|
+
code: content,
|
|
377
|
+
filename,
|
|
378
|
+
language,
|
|
379
|
+
wrap,
|
|
380
|
+
line,
|
|
381
|
+
themeType
|
|
382
|
+
})
|
|
383
|
+
})]
|
|
384
|
+
})
|
|
385
|
+
]
|
|
386
|
+
})
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
// src/workbench/sandbox-artifact-pane.tsx
|
|
390
|
+
const VIEW_ORDER = [
|
|
391
|
+
"code",
|
|
392
|
+
"diff",
|
|
393
|
+
"preview",
|
|
394
|
+
"ports",
|
|
395
|
+
"terminal"
|
|
396
|
+
];
|
|
397
|
+
const VIEW_META = {
|
|
398
|
+
code: {
|
|
399
|
+
label: "Code",
|
|
400
|
+
icon: /* @__PURE__ */ jsx(Code2, { className: "h-3.5 w-3.5" })
|
|
401
|
+
},
|
|
402
|
+
diff: {
|
|
403
|
+
label: "Diff",
|
|
404
|
+
icon: /* @__PURE__ */ jsx(GitCompare, { className: "h-3.5 w-3.5" })
|
|
405
|
+
},
|
|
406
|
+
preview: {
|
|
407
|
+
label: "Preview",
|
|
408
|
+
icon: /* @__PURE__ */ jsx(Globe, { className: "h-3.5 w-3.5" })
|
|
409
|
+
},
|
|
410
|
+
ports: {
|
|
411
|
+
label: "Ports",
|
|
412
|
+
icon: /* @__PURE__ */ jsx(Network, { className: "h-3.5 w-3.5" })
|
|
413
|
+
},
|
|
414
|
+
terminal: {
|
|
415
|
+
label: "Terminal",
|
|
416
|
+
icon: /* @__PURE__ */ jsx(SquareTerminal, { className: "h-3.5 w-3.5" })
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
const noop = () => {};
|
|
420
|
+
/**
|
|
421
|
+
* Blueprint-grade artifact workbench. Composes the sandbox-ui substrate
|
|
422
|
+
* (`RichFileTree`, `PortsList`, `TerminalView`, `@pierre/diffs`) behind a
|
|
423
|
+
* single pill-tab view switcher: Code · Diff · Preview · Ports · Terminal.
|
|
424
|
+
* Tabs appear only when their data is present (Diff requires a differing
|
|
425
|
+
* baseline; Preview a url; Ports a port list; Terminal a live binding), so the
|
|
426
|
+
* chrome never shows a dead surface.
|
|
427
|
+
*/
|
|
428
|
+
function SandboxArtifactPane({ artifact, paths, gitStatus, showTree, ports, previewUrl, terminal, activeView, defaultView, onViewChange, onFileSelect, onExposePort, onRemovePort, className, style }) {
|
|
429
|
+
const hasDiff = artifact.baselineContent != null && artifact.baselineContent !== artifact.content;
|
|
430
|
+
const hasPreview = previewUrl != null && previewUrl !== "";
|
|
431
|
+
const hasPorts = ports != null;
|
|
432
|
+
const hasTerminal = terminal != null;
|
|
433
|
+
const available = React$1.useMemo(() => {
|
|
434
|
+
return VIEW_ORDER.filter((v) => {
|
|
435
|
+
if (v === "diff") return hasDiff;
|
|
436
|
+
if (v === "preview") return hasPreview;
|
|
437
|
+
if (v === "ports") return hasPorts;
|
|
438
|
+
if (v === "terminal") return hasTerminal;
|
|
439
|
+
return true;
|
|
440
|
+
});
|
|
441
|
+
}, [
|
|
442
|
+
hasDiff,
|
|
443
|
+
hasPreview,
|
|
444
|
+
hasPorts,
|
|
445
|
+
hasTerminal
|
|
446
|
+
]);
|
|
447
|
+
const [internalView, setInternalView] = React$1.useState(() => defaultView && (defaultView !== "diff" || hasDiff) ? defaultView : "code");
|
|
448
|
+
const tabIdPrefix = React$1.useId();
|
|
449
|
+
const panelId = `${tabIdPrefix}-panel`;
|
|
450
|
+
const controlled = activeView != null;
|
|
451
|
+
const requestedView = controlled ? activeView : internalView;
|
|
452
|
+
const view = available.includes(requestedView) ? requestedView : available[0];
|
|
453
|
+
const selectView = React$1.useCallback((next) => {
|
|
454
|
+
if (!controlled) setInternalView(next);
|
|
455
|
+
onViewChange?.(next);
|
|
456
|
+
}, [controlled, onViewChange]);
|
|
457
|
+
const tabs = React$1.useMemo(() => available.map((v) => ({
|
|
458
|
+
value: v,
|
|
459
|
+
label: VIEW_META[v].label,
|
|
460
|
+
icon: VIEW_META[v].icon
|
|
461
|
+
})), [available]);
|
|
462
|
+
const activeGitStatus = React$1.useMemo(() => gitStatus?.find((e) => e.path === artifact.path)?.status, [gitStatus, artifact.path]);
|
|
463
|
+
const stats = React$1.useMemo(() => hasDiff ? computeDiffStats(artifact.baselineContent ?? "", artifact.content) : void 0, [
|
|
464
|
+
hasDiff,
|
|
465
|
+
artifact.baselineContent,
|
|
466
|
+
artifact.content
|
|
467
|
+
]);
|
|
468
|
+
const copyText = React$1.useMemo(() => {
|
|
469
|
+
if (view === "diff" && hasDiff) return buildUnifiedPatch(artifact.filename, artifact.baselineContent ?? "", artifact.content);
|
|
470
|
+
if (view === "code" || view === "diff") return artifact.content;
|
|
471
|
+
}, [
|
|
472
|
+
view,
|
|
473
|
+
hasDiff,
|
|
474
|
+
artifact.filename,
|
|
475
|
+
artifact.baselineContent,
|
|
476
|
+
artifact.content
|
|
477
|
+
]);
|
|
478
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
479
|
+
className: cn$1("flex h-full min-h-0 flex-col overflow-hidden rounded-xl border border-[var(--md3-outline-variant)] bg-surface-container shadow-[var(--shadow-card)]", className),
|
|
480
|
+
style,
|
|
481
|
+
children: [
|
|
482
|
+
/* @__PURE__ */ jsx(PanelHeader, {
|
|
483
|
+
filename: artifact.filename,
|
|
484
|
+
gitStatus: activeGitStatus,
|
|
485
|
+
stats: view === "diff" ? stats : void 0,
|
|
486
|
+
copyText
|
|
487
|
+
}),
|
|
488
|
+
/* @__PURE__ */ jsx("div", {
|
|
489
|
+
className: "flex min-w-0 items-center overflow-x-auto border-b border-[var(--md3-outline-variant)] bg-surface-container-lowest px-3 py-1.5",
|
|
490
|
+
children: /* @__PURE__ */ jsx(PillTabs, {
|
|
491
|
+
items: tabs,
|
|
492
|
+
value: view,
|
|
493
|
+
onChange: selectView,
|
|
494
|
+
"aria-label": "Artifact view",
|
|
495
|
+
idPrefix: tabIdPrefix,
|
|
496
|
+
panelId
|
|
497
|
+
})
|
|
498
|
+
}),
|
|
499
|
+
/* @__PURE__ */ jsxs("div", {
|
|
500
|
+
id: panelId,
|
|
501
|
+
role: "tabpanel",
|
|
502
|
+
"aria-labelledby": `${tabIdPrefix}-${view}`,
|
|
503
|
+
tabIndex: 0,
|
|
504
|
+
className: `min-h-0 flex-1 ${focusRingInset}`,
|
|
505
|
+
children: [
|
|
506
|
+
view === "code" && /* @__PURE__ */ jsx(CodeView, {
|
|
507
|
+
path: artifact.path,
|
|
508
|
+
filename: artifact.filename,
|
|
509
|
+
content: artifact.content,
|
|
510
|
+
language: artifact.language,
|
|
511
|
+
paths,
|
|
512
|
+
gitStatus,
|
|
513
|
+
showTree,
|
|
514
|
+
onFileSelect
|
|
515
|
+
}),
|
|
516
|
+
view === "diff" && hasDiff && /* @__PURE__ */ jsx(DiffView, {
|
|
517
|
+
filename: artifact.filename,
|
|
518
|
+
baseline: artifact.baselineContent ?? "",
|
|
519
|
+
current: artifact.content
|
|
520
|
+
}),
|
|
521
|
+
view === "preview" && hasPreview && /* @__PURE__ */ jsx(PreviewView, { url: previewUrl }),
|
|
522
|
+
view === "ports" && hasPorts && /* @__PURE__ */ jsx("div", {
|
|
523
|
+
className: "h-full overflow-auto bg-surface-container p-4",
|
|
524
|
+
children: /* @__PURE__ */ jsx(PortsList, {
|
|
525
|
+
ports,
|
|
526
|
+
onExposePort: onExposePort ?? noop,
|
|
527
|
+
onRemovePort
|
|
528
|
+
})
|
|
529
|
+
}),
|
|
530
|
+
view === "terminal" && hasTerminal && /* @__PURE__ */ jsx("div", {
|
|
531
|
+
className: "h-full bg-surface-container-lowest",
|
|
532
|
+
children: /* @__PURE__ */ jsx(TerminalView, {
|
|
533
|
+
apiUrl: terminal.apiUrl,
|
|
534
|
+
token: terminal.token,
|
|
535
|
+
connectionId: terminal.connectionId,
|
|
536
|
+
title: terminal.title,
|
|
537
|
+
subtitle: terminal.subtitle
|
|
538
|
+
})
|
|
539
|
+
})
|
|
540
|
+
]
|
|
541
|
+
})
|
|
542
|
+
]
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
// src/workbench/changes-pane.tsx
|
|
546
|
+
const BORDER = "border-[var(--md3-outline-variant)]";
|
|
547
|
+
function splitPath(path) {
|
|
548
|
+
const at = path.lastIndexOf("/");
|
|
549
|
+
if (at < 0) return {
|
|
550
|
+
dir: "",
|
|
551
|
+
base: path
|
|
552
|
+
};
|
|
553
|
+
return {
|
|
554
|
+
dir: path.slice(0, at + 1),
|
|
555
|
+
base: path.slice(at + 1)
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* The contents a diff needs, or `null` while the consumer is still resolving
|
|
560
|
+
* them. An added or untracked file has no baseline, a deleted file has no
|
|
561
|
+
* working copy; each diffs against the empty string once its one side is
|
|
562
|
+
* present. Every other status waits for both sides so a half-resolved
|
|
563
|
+
* modification is never drawn as a wholesale rewrite.
|
|
564
|
+
*/
|
|
565
|
+
function resolveContents(file) {
|
|
566
|
+
const { status, baseline, current } = file;
|
|
567
|
+
if (status === "added" || status === "untracked") return current == null ? null : {
|
|
568
|
+
baseline: baseline ?? "",
|
|
569
|
+
current
|
|
570
|
+
};
|
|
571
|
+
if (status === "deleted") return baseline == null ? null : {
|
|
572
|
+
baseline,
|
|
573
|
+
current: current ?? ""
|
|
574
|
+
};
|
|
575
|
+
return baseline == null || current == null ? null : {
|
|
576
|
+
baseline,
|
|
577
|
+
current
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
function errorText(err) {
|
|
581
|
+
if (err instanceof Error && err.message) return err.message;
|
|
582
|
+
return String(err);
|
|
583
|
+
}
|
|
584
|
+
const SKELETON_ROWS = [
|
|
585
|
+
0,
|
|
586
|
+
1,
|
|
587
|
+
2,
|
|
588
|
+
3
|
|
589
|
+
];
|
|
590
|
+
/**
|
|
591
|
+
* The sandbox's git working tree as a review surface: every changed file with
|
|
592
|
+
* its line counts, the selected file's word-level diff beneath, and a commit
|
|
593
|
+
* box in the footer. Data in, callbacks out — the pane never fetches; the
|
|
594
|
+
* consumer feeds it the sidecar's `/git/status` + `/git/diff` and resolves
|
|
595
|
+
* `baseline` / `current` for the file it is told was selected.
|
|
596
|
+
*
|
|
597
|
+
* The list is capped at 40% of the pane and scrolls, so the diff keeps its
|
|
598
|
+
* room however many files changed. The row's own chrome is compact: the
|
|
599
|
+
* workspace shell already gives its right pane the 56px `WorkspacePaneHeader`,
|
|
600
|
+
* so this header nests beneath it at `PanelHeader` rhythm rather than adding a
|
|
601
|
+
* second tall bar.
|
|
602
|
+
*/
|
|
603
|
+
function ChangesPane({ branch, ahead = 0, behind = 0, files, selectedPath, onSelectFile, loading = false, error, onRefresh, onCommit, onPush, busy, onOpenFile, className }) {
|
|
604
|
+
const idPrefix = React$1.useId();
|
|
605
|
+
const listRef = React$1.useRef(null);
|
|
606
|
+
const rowRefs = React$1.useRef(/* @__PURE__ */ new Map());
|
|
607
|
+
const [message, setMessage] = React$1.useState("");
|
|
608
|
+
const [actionError, setActionError] = React$1.useState(null);
|
|
609
|
+
const [pending, setPending] = React$1.useState(null);
|
|
610
|
+
const inFlight = busy ?? pending;
|
|
611
|
+
const totals = React$1.useMemo(() => {
|
|
612
|
+
let added = 0;
|
|
613
|
+
let removed = 0;
|
|
614
|
+
for (const f of files) {
|
|
615
|
+
added += f.additions;
|
|
616
|
+
removed += f.deletions;
|
|
617
|
+
}
|
|
618
|
+
return {
|
|
619
|
+
added,
|
|
620
|
+
removed
|
|
621
|
+
};
|
|
622
|
+
}, [files]);
|
|
623
|
+
const selectedIndex = selectedPath == null ? -1 : files.findIndex((f) => f.path === selectedPath);
|
|
624
|
+
const selected = selectedIndex >= 0 ? files[selectedIndex] : void 0;
|
|
625
|
+
React$1.useEffect(() => {
|
|
626
|
+
const list = listRef.current;
|
|
627
|
+
if (!list || selected == null) return;
|
|
628
|
+
if (!list.contains(document.activeElement)) return;
|
|
629
|
+
rowRefs.current.get(selected.path)?.focus();
|
|
630
|
+
}, [selected]);
|
|
631
|
+
const handleListKeyDown = React$1.useCallback((event) => {
|
|
632
|
+
if (files.length === 0 || !onSelectFile) return;
|
|
633
|
+
const last = files.length - 1;
|
|
634
|
+
let next = null;
|
|
635
|
+
switch (event.key) {
|
|
636
|
+
case "ArrowDown":
|
|
637
|
+
next = selectedIndex < 0 ? 0 : Math.min(selectedIndex + 1, last);
|
|
638
|
+
break;
|
|
639
|
+
case "ArrowUp":
|
|
640
|
+
next = selectedIndex < 0 ? last : Math.max(selectedIndex - 1, 0);
|
|
641
|
+
break;
|
|
642
|
+
case "Home":
|
|
643
|
+
next = 0;
|
|
644
|
+
break;
|
|
645
|
+
case "End":
|
|
646
|
+
next = last;
|
|
647
|
+
break;
|
|
648
|
+
case "Enter":
|
|
649
|
+
case " ":
|
|
650
|
+
next = selectedIndex < 0 ? 0 : selectedIndex;
|
|
651
|
+
break;
|
|
652
|
+
default: return;
|
|
653
|
+
}
|
|
654
|
+
event.preventDefault();
|
|
655
|
+
const file = files[next];
|
|
656
|
+
if (file) onSelectFile(file.path);
|
|
657
|
+
}, [
|
|
658
|
+
files,
|
|
659
|
+
onSelectFile,
|
|
660
|
+
selectedIndex
|
|
661
|
+
]);
|
|
662
|
+
const runCommit = React$1.useCallback(async () => {
|
|
663
|
+
const text = message.trim();
|
|
664
|
+
if (!onCommit || !text || inFlight) return;
|
|
665
|
+
setActionError(null);
|
|
666
|
+
setPending("commit");
|
|
667
|
+
try {
|
|
668
|
+
await onCommit(text);
|
|
669
|
+
setMessage("");
|
|
670
|
+
} catch (err) {
|
|
671
|
+
setActionError(errorText(err));
|
|
672
|
+
} finally {
|
|
673
|
+
setPending(null);
|
|
674
|
+
}
|
|
675
|
+
}, [
|
|
676
|
+
message,
|
|
677
|
+
onCommit,
|
|
678
|
+
inFlight
|
|
679
|
+
]);
|
|
680
|
+
const runPush = React$1.useCallback(async () => {
|
|
681
|
+
if (!onPush || inFlight) return;
|
|
682
|
+
setActionError(null);
|
|
683
|
+
setPending("push");
|
|
684
|
+
try {
|
|
685
|
+
await onPush();
|
|
686
|
+
} catch (err) {
|
|
687
|
+
setActionError(errorText(err));
|
|
688
|
+
} finally {
|
|
689
|
+
setPending(null);
|
|
690
|
+
}
|
|
691
|
+
}, [onPush, inFlight]);
|
|
692
|
+
const handleMessageKeyDown = React$1.useCallback((event) => {
|
|
693
|
+
if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) {
|
|
694
|
+
event.preventDefault();
|
|
695
|
+
runCommit();
|
|
696
|
+
}
|
|
697
|
+
}, [runCommit]);
|
|
698
|
+
const canCommit = files.length > 0 && message.trim().length > 0 && inFlight == null;
|
|
699
|
+
const canPush = ahead > 0 && inFlight == null;
|
|
700
|
+
const contents = React$1.useMemo(() => selected ? resolveContents(selected) : null, [selected]);
|
|
701
|
+
const stats = React$1.useMemo(() => contents ? computeDiffStats(contents.baseline, contents.current) : void 0, [contents]);
|
|
702
|
+
const patch = React$1.useMemo(() => selected && contents ? buildUnifiedPatch(selected.path, contents.baseline, contents.current) : void 0, [selected, contents]);
|
|
703
|
+
const showList = !loading && files.length > 0;
|
|
704
|
+
const showEmpty = !loading && files.length === 0 && !error;
|
|
705
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
706
|
+
className: cn$1("flex h-full min-h-0 flex-col text-foreground", className),
|
|
707
|
+
children: [
|
|
708
|
+
/* @__PURE__ */ jsxs("div", {
|
|
709
|
+
className: cn$1("flex min-h-[38px] shrink-0 items-center gap-2 border-b bg-surface-container-low px-3 py-1", BORDER),
|
|
710
|
+
children: [
|
|
711
|
+
/* @__PURE__ */ jsx(GitBranch, {
|
|
712
|
+
className: "h-3.5 w-3.5 shrink-0 text-muted-foreground",
|
|
713
|
+
"aria-hidden": true
|
|
714
|
+
}),
|
|
715
|
+
loading && branch == null ? /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-24 rounded-full" }) : branch == null ? /* @__PURE__ */ jsx("span", {
|
|
716
|
+
className: "text-xs text-muted-foreground",
|
|
717
|
+
children: "no commits yet"
|
|
718
|
+
}) : /* @__PURE__ */ jsx("span", {
|
|
719
|
+
className: "truncate rounded-full bg-surface-container-high px-2 py-0.5 font-mono text-xs text-foreground",
|
|
720
|
+
children: branch
|
|
721
|
+
}),
|
|
722
|
+
(ahead > 0 || behind > 0) && /* @__PURE__ */ jsxs("span", {
|
|
723
|
+
className: "flex items-center gap-1.5 font-mono text-xs text-muted-foreground",
|
|
724
|
+
children: [ahead > 0 && /* @__PURE__ */ jsxs("span", {
|
|
725
|
+
"aria-label": `${ahead} ahead`,
|
|
726
|
+
children: ["↑", ahead]
|
|
727
|
+
}), behind > 0 && /* @__PURE__ */ jsxs("span", {
|
|
728
|
+
"aria-label": `${behind} behind`,
|
|
729
|
+
children: ["↓", behind]
|
|
730
|
+
})]
|
|
731
|
+
}),
|
|
732
|
+
/* @__PURE__ */ jsxs("div", {
|
|
733
|
+
className: "ml-auto flex items-center gap-3",
|
|
734
|
+
children: [files.length > 0 && /* @__PURE__ */ jsx(DiffStatsBadge, { stats: totals }), onRefresh && /* @__PURE__ */ jsx("button", {
|
|
735
|
+
type: "button",
|
|
736
|
+
onClick: onRefresh,
|
|
737
|
+
"aria-label": "Refresh",
|
|
738
|
+
className: `rounded-md p-1 text-muted-foreground transition-colors hover:bg-surface-container-high hover:text-foreground ${focusRing}`,
|
|
739
|
+
children: /* @__PURE__ */ jsx(RefreshCw, { className: cn$1("h-3.5 w-3.5", loading && "animate-spin") })
|
|
740
|
+
})]
|
|
741
|
+
})
|
|
742
|
+
]
|
|
743
|
+
}),
|
|
744
|
+
/* @__PURE__ */ jsxs("div", {
|
|
745
|
+
className: cn$1("flex max-h-[40%] shrink-0 flex-col border-b", BORDER),
|
|
746
|
+
"aria-busy": loading || void 0,
|
|
747
|
+
children: [
|
|
748
|
+
error && /* @__PURE__ */ jsx("p", {
|
|
749
|
+
role: "alert",
|
|
750
|
+
className: "px-3 py-2 text-xs text-[var(--surface-danger-text)]",
|
|
751
|
+
children: error
|
|
752
|
+
}),
|
|
753
|
+
loading && /* @__PURE__ */ jsxs("div", {
|
|
754
|
+
className: "space-y-1 px-3 py-2",
|
|
755
|
+
"data-testid": "changes-skeleton",
|
|
756
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
757
|
+
className: "sr-only",
|
|
758
|
+
children: "Loading changes…"
|
|
759
|
+
}), SKELETON_ROWS.map((row) => /* @__PURE__ */ jsx(Skeleton, { className: "h-6 w-full" }, row))]
|
|
760
|
+
}),
|
|
761
|
+
showEmpty && /* @__PURE__ */ jsx("p", {
|
|
762
|
+
className: "px-3 py-6 text-center text-xs text-muted-foreground",
|
|
763
|
+
children: "No changes in the working tree."
|
|
764
|
+
}),
|
|
765
|
+
showList && /* @__PURE__ */ jsx("ul", {
|
|
766
|
+
ref: listRef,
|
|
767
|
+
role: "listbox",
|
|
768
|
+
"aria-label": "Changed files",
|
|
769
|
+
"aria-activedescendant": selected ? `${idPrefix}-${selectedIndex}` : void 0,
|
|
770
|
+
onKeyDown: handleListKeyDown,
|
|
771
|
+
className: "min-h-0 overflow-y-auto py-1 focus:outline-none",
|
|
772
|
+
children: files.map((file, index) => {
|
|
773
|
+
const isSelected = index === selectedIndex;
|
|
774
|
+
const { dir, base } = splitPath(file.path);
|
|
775
|
+
const tabbable = isSelected || selectedIndex < 0 && index === 0;
|
|
776
|
+
return /* @__PURE__ */ jsxs("li", {
|
|
777
|
+
id: `${idPrefix}-${index}`,
|
|
778
|
+
ref: (el) => {
|
|
779
|
+
if (el) rowRefs.current.set(file.path, el);
|
|
780
|
+
else rowRefs.current.delete(file.path);
|
|
781
|
+
},
|
|
782
|
+
role: "option",
|
|
783
|
+
"aria-selected": isSelected,
|
|
784
|
+
tabIndex: tabbable ? 0 : -1,
|
|
785
|
+
onClick: () => onSelectFile?.(file.path),
|
|
786
|
+
className: cn$1("flex cursor-pointer items-center gap-2 px-3 py-1.5 text-xs transition-colors", focusRingInset, isSelected ? "bg-surface-container-high" : "hover:bg-surface-container"),
|
|
787
|
+
children: [
|
|
788
|
+
/* @__PURE__ */ jsxs("span", {
|
|
789
|
+
className: "min-w-0 flex-1 truncate font-mono",
|
|
790
|
+
title: file.path,
|
|
791
|
+
children: [dir && /* @__PURE__ */ jsx("span", {
|
|
792
|
+
className: "text-muted-foreground",
|
|
793
|
+
children: dir
|
|
794
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
795
|
+
className: "text-foreground",
|
|
796
|
+
children: base
|
|
797
|
+
})]
|
|
798
|
+
}),
|
|
799
|
+
/* @__PURE__ */ jsx(GitStatusBadge, { status: file.status }),
|
|
800
|
+
/* @__PURE__ */ jsx(DiffStatsBadge, {
|
|
801
|
+
stats: {
|
|
802
|
+
added: file.additions,
|
|
803
|
+
removed: file.deletions
|
|
804
|
+
},
|
|
805
|
+
className: "w-[5.5rem] justify-end tabular-nums"
|
|
806
|
+
})
|
|
807
|
+
]
|
|
808
|
+
}, file.path);
|
|
809
|
+
})
|
|
810
|
+
})
|
|
811
|
+
]
|
|
812
|
+
}),
|
|
813
|
+
/* @__PURE__ */ jsx("div", {
|
|
814
|
+
className: "flex min-h-0 flex-1 flex-col",
|
|
815
|
+
children: selected ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(PanelHeader, {
|
|
816
|
+
filename: selected.path,
|
|
817
|
+
gitStatus: selected.status,
|
|
818
|
+
stats,
|
|
819
|
+
copyText: patch,
|
|
820
|
+
children: onOpenFile && /* @__PURE__ */ jsx(Button, {
|
|
821
|
+
type: "button",
|
|
822
|
+
variant: "outline",
|
|
823
|
+
size: "sm",
|
|
824
|
+
className: "h-7 px-2",
|
|
825
|
+
onClick: () => onOpenFile(selected.path),
|
|
826
|
+
children: "Open"
|
|
827
|
+
})
|
|
828
|
+
}), contents ? /* @__PURE__ */ jsx(DiffView, {
|
|
829
|
+
filename: selected.path,
|
|
830
|
+
baseline: contents.baseline,
|
|
831
|
+
current: contents.current,
|
|
832
|
+
showFileHeader: false,
|
|
833
|
+
className: "flex-1"
|
|
834
|
+
}) : selected.loadError ? /* @__PURE__ */ jsx("p", {
|
|
835
|
+
className: "px-3 py-3 text-xs text-destructive",
|
|
836
|
+
role: "alert",
|
|
837
|
+
children: selected.loadError
|
|
838
|
+
}) : /* @__PURE__ */ jsx("p", {
|
|
839
|
+
className: "px-3 py-3 text-xs text-muted-foreground",
|
|
840
|
+
role: "status",
|
|
841
|
+
children: "Loading diff…"
|
|
842
|
+
})] }) : showList && /* @__PURE__ */ jsx("p", {
|
|
843
|
+
className: "px-3 py-3 text-xs text-muted-foreground",
|
|
844
|
+
children: "Select a file to read its diff."
|
|
845
|
+
})
|
|
846
|
+
}),
|
|
847
|
+
(onCommit || onPush) && /* @__PURE__ */ jsxs("div", {
|
|
848
|
+
className: cn$1("shrink-0 space-y-2 border-t bg-surface-container-low p-3", BORDER),
|
|
849
|
+
children: [
|
|
850
|
+
onCommit && /* @__PURE__ */ jsx(Textarea, {
|
|
851
|
+
"aria-label": "Commit message",
|
|
852
|
+
placeholder: "Commit message",
|
|
853
|
+
rows: 2,
|
|
854
|
+
value: message,
|
|
855
|
+
onChange: (event) => setMessage(event.target.value),
|
|
856
|
+
onKeyDown: handleMessageKeyDown,
|
|
857
|
+
disabled: inFlight != null,
|
|
858
|
+
className: "min-h-[3.5rem] resize-none px-3 py-2 text-xs"
|
|
859
|
+
}),
|
|
860
|
+
/* @__PURE__ */ jsxs("div", {
|
|
861
|
+
className: "flex items-center gap-2",
|
|
862
|
+
children: [onCommit && /* @__PURE__ */ jsx(Button, {
|
|
863
|
+
type: "button",
|
|
864
|
+
size: "sm",
|
|
865
|
+
disabled: !canCommit,
|
|
866
|
+
loading: inFlight === "commit",
|
|
867
|
+
onClick: () => void runCommit(),
|
|
868
|
+
children: "Commit"
|
|
869
|
+
}), onPush && /* @__PURE__ */ jsxs(Button, {
|
|
870
|
+
type: "button",
|
|
871
|
+
size: "sm",
|
|
872
|
+
variant: "secondary",
|
|
873
|
+
disabled: !canPush,
|
|
874
|
+
loading: inFlight === "push",
|
|
875
|
+
onClick: () => void runPush(),
|
|
876
|
+
children: ["Push", ahead > 0 && /* @__PURE__ */ jsxs("span", {
|
|
877
|
+
className: "font-mono text-muted-foreground",
|
|
878
|
+
children: ["↑", ahead]
|
|
879
|
+
})]
|
|
880
|
+
})]
|
|
881
|
+
}),
|
|
882
|
+
actionError && /* @__PURE__ */ jsx("p", {
|
|
883
|
+
role: "alert",
|
|
884
|
+
className: "text-xs text-[var(--surface-danger-text)]",
|
|
885
|
+
children: actionError
|
|
886
|
+
})
|
|
887
|
+
]
|
|
888
|
+
})
|
|
889
|
+
]
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
export { DiffStatsBadge as a, PillTabs as c, FileBreadcrumb as i, SandboxArtifactPane as n, GitStatusBadge as o, CodeView as r, PanelHeader as s, ChangesPane as t };
|