@oxy-hq/sdk 2.3.0 → 2.5.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/shell.mjs ADDED
@@ -0,0 +1,2155 @@
1
+ // @oxy/sdk - TypeScript SDK for Oxy data platform
2
+ import { i as useAgentRun, l as useResolvedManifest, o as useOxyApp, t as OxyAnswer, v as getOxyAppLogger } from "./react-CVdWXcu8.mjs";
3
+ import * as React from "react";
4
+ import { Fragment, useEffect, useMemo, useState } from "react";
5
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
6
+ import * as TooltipPrimitive from "@radix-ui/react-tooltip";
7
+
8
+ //#region src/shell/cx.ts
9
+ /** Join truthy class names. Local minimal `clsx` — the shell uses fixed
10
+ * namespaced classes, so no Tailwind-style merge logic is needed. */
11
+ function cx(...parts) {
12
+ return parts.filter(Boolean).join(" ");
13
+ }
14
+
15
+ //#endregion
16
+ //#region src/shell/AnswerChart.tsx
17
+ const PALETTE = [
18
+ "#3b82f6",
19
+ "#22c55e",
20
+ "#eab308",
21
+ "#a855f7",
22
+ "#06b6d4",
23
+ "#f97316"
24
+ ];
25
+ const compact = new Intl.NumberFormat("en", {
26
+ notation: "compact",
27
+ maximumFractionDigits: 1
28
+ });
29
+ const toNum = (v) => {
30
+ const n = typeof v === "number" ? v : Number(v);
31
+ return Number.isFinite(n) ? n : 0;
32
+ };
33
+ const toStr = (v) => v === null || v === void 0 ? "" : String(v);
34
+ /** "sales_daily__total_net_sales" → "Total Net Sales" — a friendly series
35
+ * label instead of the raw semantic column id. */
36
+ function humanizeCol(name) {
37
+ return ((name ?? "").split("__").pop() ?? "").replace(/_/g, " ").trim().replace(/\b\w/g, (c) => c.toUpperCase());
38
+ }
39
+ /** Pivot rows into per-series value arrays aligned on x categories. */
40
+ function pivotChart(block) {
41
+ const { config, columns, rows } = block;
42
+ const xi = config.x ? columns.indexOf(config.x) : 0;
43
+ const yi = config.y ? columns.indexOf(config.y) : columns.length > 1 ? 1 : 0;
44
+ const si = config.series ? columns.indexOf(config.series) : -1;
45
+ const categories = [];
46
+ const catIndex = /* @__PURE__ */ new Map();
47
+ for (const row of rows) {
48
+ const cat = toStr(row[xi]);
49
+ if (!catIndex.has(cat)) {
50
+ catIndex.set(cat, categories.length);
51
+ categories.push(cat);
52
+ }
53
+ }
54
+ if (si === -1) {
55
+ const values = new Array(categories.length).fill(0);
56
+ for (const row of rows) {
57
+ const idx = catIndex.get(toStr(row[xi]));
58
+ if (idx !== void 0) values[idx] = toNum(row[yi]);
59
+ }
60
+ return {
61
+ categories,
62
+ series: [{
63
+ name: config.y ?? "value",
64
+ values
65
+ }]
66
+ };
67
+ }
68
+ const seriesIndex = /* @__PURE__ */ new Map();
69
+ for (const row of rows) {
70
+ const name = toStr(row[si]);
71
+ let values = seriesIndex.get(name);
72
+ if (!values) {
73
+ values = new Array(categories.length).fill(0);
74
+ seriesIndex.set(name, values);
75
+ }
76
+ const idx = catIndex.get(toStr(row[xi]));
77
+ if (idx !== void 0) values[idx] = toNum(row[yi]);
78
+ }
79
+ return {
80
+ categories,
81
+ series: [...seriesIndex.entries()].map(([name, values]) => ({
82
+ name,
83
+ values
84
+ }))
85
+ };
86
+ }
87
+ /** A "nice" axis ceiling so tick labels land on round numbers. */
88
+ function niceCeil(max) {
89
+ if (max <= 0) return 1;
90
+ const pow = 10 ** Math.floor(Math.log10(max));
91
+ const unit = max / pow;
92
+ return (unit <= 1 ? 1 : unit <= 2 ? 2 : unit <= 5 ? 5 : 10) * pow;
93
+ }
94
+ /** Axis domain `[lo, hi]` that always includes zero, with nice round
95
+ * bounds. Signed measures (profit, net change) must render below a zero
96
+ * baseline instead of being clamped to an invisible 0..max axis. */
97
+ function axisDomain(values) {
98
+ const rawMax = Math.max(...values, 0);
99
+ const rawMin = Math.min(...values, 0);
100
+ const hi = rawMax > 0 ? niceCeil(rawMax) : 0;
101
+ const lo = rawMin < 0 ? -niceCeil(-rawMin) : 0;
102
+ return hi === lo ? {
103
+ lo: 0,
104
+ hi: 1
105
+ } : {
106
+ lo,
107
+ hi
108
+ };
109
+ }
110
+ const W = 360;
111
+ const H = 190;
112
+ const M = {
113
+ top: 8,
114
+ right: 8,
115
+ bottom: 34,
116
+ left: 42
117
+ };
118
+ const IW = W - M.left - M.right;
119
+ const IH = H - M.top - M.bottom;
120
+ const TICKS = 4;
121
+ function truncate(label, max) {
122
+ return label.length > max ? `${label.slice(0, max - 1)}…` : label;
123
+ }
124
+ /** Map a value to its y pixel within the plot area for domain `[lo, hi]`. */
125
+ const yFor = (v, lo, hi) => M.top + IH * (1 - (v - lo) / (hi - lo || 1));
126
+ function Axes({ lo, hi }) {
127
+ const span = hi - lo || 1;
128
+ return /* @__PURE__ */ jsx("g", {
129
+ className: "oxy-chart__grid",
130
+ children: Array.from({ length: 5 }, (_, i) => {
131
+ const y = M.top + IH * i / TICKS;
132
+ const value = hi - span * i / TICKS;
133
+ return /* @__PURE__ */ jsxs("g", { children: [/* @__PURE__ */ jsx("line", {
134
+ x1: M.left,
135
+ x2: M.left + IW,
136
+ y1: y,
137
+ y2: y
138
+ }), /* @__PURE__ */ jsx("text", {
139
+ x: M.left - 5,
140
+ y: y + 3,
141
+ textAnchor: "end",
142
+ className: "oxy-chart__tick",
143
+ children: compact.format(value)
144
+ })] }, i);
145
+ })
146
+ });
147
+ }
148
+ function XLabels({ categories }) {
149
+ const slot = IW / categories.length;
150
+ const maxChars = Math.max(4, Math.floor(slot / 5.5));
151
+ return /* @__PURE__ */ jsx("g", { children: categories.map((cat, i) => /* @__PURE__ */ jsxs("text", {
152
+ x: M.left + slot * (i + .5),
153
+ y: H - M.bottom + 12,
154
+ textAnchor: "middle",
155
+ className: "oxy-chart__tick",
156
+ children: [/* @__PURE__ */ jsx("title", { children: cat }), truncate(cat, maxChars)]
157
+ }, cat)) });
158
+ }
159
+ function BarChart({ data }) {
160
+ const { lo, hi } = axisDomain(data.series.flatMap((s) => s.values));
161
+ const zeroY = yFor(0, lo, hi);
162
+ const slot = IW / data.categories.length;
163
+ const group = slot * .7;
164
+ const barW = group / data.series.length;
165
+ return /* @__PURE__ */ jsxs("svg", {
166
+ viewBox: `0 0 ${W} ${H}`,
167
+ className: "oxy-chart__svg",
168
+ role: "img",
169
+ children: [
170
+ /* @__PURE__ */ jsx(Axes, {
171
+ lo,
172
+ hi
173
+ }),
174
+ data.series.map((s, si) => s.values.map((v, ci) => {
175
+ const vy = yFor(v, lo, hi);
176
+ return /* @__PURE__ */ jsx("rect", {
177
+ x: M.left + slot * ci + (slot - group) / 2 + barW * si,
178
+ y: Math.min(zeroY, vy),
179
+ width: Math.max(1, barW - 1),
180
+ height: Math.max(0, Math.abs(vy - zeroY)),
181
+ rx: 1.5,
182
+ fill: PALETTE[si % PALETTE.length],
183
+ children: /* @__PURE__ */ jsx("title", { children: `${data.categories[ci]}${data.series.length > 1 ? ` · ${s.name}` : ""}: ${compact.format(v)}` })
184
+ }, `${s.name}-${data.categories[ci]}`);
185
+ })),
186
+ lo < 0 && /* @__PURE__ */ jsx("line", {
187
+ className: "oxy-chart__grid-zero",
188
+ x1: M.left,
189
+ x2: M.left + IW,
190
+ y1: zeroY,
191
+ y2: zeroY
192
+ }),
193
+ /* @__PURE__ */ jsx(XLabels, { categories: data.categories })
194
+ ]
195
+ });
196
+ }
197
+ function LineChart({ data }) {
198
+ const { lo, hi } = axisDomain(data.series.flatMap((s) => s.values));
199
+ const zeroY = yFor(0, lo, hi);
200
+ const slot = IW / Math.max(1, data.categories.length - 1 || 1);
201
+ const px = (ci) => data.categories.length === 1 ? M.left + IW / 2 : M.left + slot * ci;
202
+ const py = (v) => yFor(v, lo, hi);
203
+ return /* @__PURE__ */ jsxs("svg", {
204
+ viewBox: `0 0 ${W} ${H}`,
205
+ className: "oxy-chart__svg",
206
+ role: "img",
207
+ children: [
208
+ /* @__PURE__ */ jsx(Axes, {
209
+ lo,
210
+ hi
211
+ }),
212
+ lo < 0 && /* @__PURE__ */ jsx("line", {
213
+ className: "oxy-chart__grid-zero",
214
+ x1: M.left,
215
+ x2: M.left + IW,
216
+ y1: zeroY,
217
+ y2: zeroY
218
+ }),
219
+ data.series.map((s, si) => /* @__PURE__ */ jsxs("g", { children: [/* @__PURE__ */ jsx("polyline", {
220
+ points: s.values.map((v, ci) => `${px(ci)},${py(v)}`).join(" "),
221
+ fill: "none",
222
+ stroke: PALETTE[si % PALETTE.length],
223
+ strokeWidth: 1.8
224
+ }), s.values.map((v, ci) => /* @__PURE__ */ jsx("circle", {
225
+ cx: px(ci),
226
+ cy: py(v),
227
+ r: 2.2,
228
+ fill: PALETTE[si % PALETTE.length],
229
+ children: /* @__PURE__ */ jsx("title", { children: `${data.categories[ci]}${data.series.length > 1 ? ` · ${s.name}` : ""}: ${compact.format(v)}` })
230
+ }, data.categories[ci]))] }, s.name)),
231
+ /* @__PURE__ */ jsx(XLabels, { categories: data.categories })
232
+ ]
233
+ });
234
+ }
235
+ function PieChart({ block }) {
236
+ const { config, columns, rows } = block;
237
+ const ni = config.name ? columns.indexOf(config.name) : config.x ? columns.indexOf(config.x) : 0;
238
+ const vi = config.value ? columns.indexOf(config.value) : config.y ? columns.indexOf(config.y) : 1;
239
+ const slices = rows.map((r) => ({
240
+ name: toStr(r[ni]),
241
+ value: Math.max(0, toNum(r[vi]))
242
+ }));
243
+ const total = slices.reduce((sum, s) => sum + s.value, 0) || 1;
244
+ const cxp = W / 2;
245
+ const cyp = H / 2;
246
+ const r = Math.min(W, H) / 2 - 14;
247
+ let angle = -Math.PI / 2;
248
+ const paths = slices.map((s, i) => {
249
+ const sweep = s.value / total * Math.PI * 2;
250
+ const title = `${s.name}: ${compact.format(s.value)} (${Math.round(s.value / total * 100)}%)`;
251
+ const fill = PALETTE[i % PALETTE.length];
252
+ if (sweep >= Math.PI * 2 - 1e-6) {
253
+ angle += sweep;
254
+ return /* @__PURE__ */ jsx("circle", {
255
+ cx: cxp,
256
+ cy: cyp,
257
+ r,
258
+ fill,
259
+ stroke: "var(--oxy-shell-background, #fff)",
260
+ strokeWidth: 1,
261
+ children: /* @__PURE__ */ jsx("title", { children: title })
262
+ }, s.name);
263
+ }
264
+ const x1 = cxp + r * Math.cos(angle);
265
+ const y1 = cyp + r * Math.sin(angle);
266
+ angle += sweep;
267
+ const x2 = cxp + r * Math.cos(angle);
268
+ const y2 = cyp + r * Math.sin(angle);
269
+ return /* @__PURE__ */ jsx("path", {
270
+ d: `M ${cxp} ${cyp} L ${x1} ${y1} A ${r} ${r} 0 ${sweep > Math.PI ? 1 : 0} 1 ${x2} ${y2} Z`,
271
+ fill,
272
+ stroke: "var(--oxy-shell-background, #fff)",
273
+ strokeWidth: 1,
274
+ children: /* @__PURE__ */ jsx("title", { children: title })
275
+ }, s.name);
276
+ });
277
+ return /* @__PURE__ */ jsx("svg", {
278
+ viewBox: `0 0 ${W} ${H}`,
279
+ className: "oxy-chart__svg",
280
+ role: "img",
281
+ children: paths
282
+ });
283
+ }
284
+ const TABLE_MAX_ROWS = 10;
285
+ function TableChart({ block }) {
286
+ const visible = block.rows.slice(0, TABLE_MAX_ROWS);
287
+ return /* @__PURE__ */ jsxs("div", {
288
+ className: "oxy-chart__table-wrap",
289
+ children: [/* @__PURE__ */ jsxs("table", {
290
+ className: "oxy-chart__table",
291
+ children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { children: block.columns.map((c) => /* @__PURE__ */ jsx("th", { children: c }, c)) }) }), /* @__PURE__ */ jsx("tbody", { children: visible.map((row, i) => /* @__PURE__ */ jsx("tr", { children: block.columns.map((c, j) => /* @__PURE__ */ jsx("td", { children: toStr(row[j]) }, c)) }, row.map(toStr).join("|") || i)) })]
292
+ }), block.rows.length > TABLE_MAX_ROWS && /* @__PURE__ */ jsxs("div", {
293
+ className: "oxy-chart__note",
294
+ children: [
295
+ "+",
296
+ block.rows.length - TABLE_MAX_ROWS,
297
+ " more rows"
298
+ ]
299
+ })]
300
+ });
301
+ }
302
+ /** Dependency-free SVG rendering — the fallback when the host app has no
303
+ * `echarts`. */
304
+ function SvgChart({ block }) {
305
+ const data = block.config.chart_type === "bar_chart" || block.config.chart_type === "line_chart" ? pivotChart(block) : null;
306
+ let body;
307
+ switch (block.config.chart_type) {
308
+ case "bar_chart":
309
+ body = data && /* @__PURE__ */ jsx(BarChart, { data });
310
+ break;
311
+ case "line_chart":
312
+ body = data && /* @__PURE__ */ jsx(LineChart, { data });
313
+ break;
314
+ case "pie_chart":
315
+ body = /* @__PURE__ */ jsx(PieChart, { block });
316
+ break;
317
+ default: body = /* @__PURE__ */ jsx(TableChart, { block });
318
+ }
319
+ const multiSeries = data && data.series.length > 1;
320
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [body, multiSeries && /* @__PURE__ */ jsx("div", {
321
+ className: "oxy-chart__legend",
322
+ children: data.series.map((s, i) => /* @__PURE__ */ jsxs("span", {
323
+ className: "oxy-chart__legend-item",
324
+ children: [/* @__PURE__ */ jsx("span", {
325
+ className: "oxy-chart__legend-dot",
326
+ style: { background: PALETTE[i % PALETTE.length] }
327
+ }), s.name]
328
+ }, s.name))
329
+ })] });
330
+ }
331
+ /** Read the shell's design tokens off an element so the ECharts chart
332
+ * matches the surrounding light/dark theme. */
333
+ function tokenColors(el) {
334
+ const cs = getComputedStyle(el);
335
+ const pick = (name, fallback) => cs.getPropertyValue(name).trim() || fallback;
336
+ return {
337
+ fg: pick("--oxy-shell-foreground", "#0a0a0a"),
338
+ muted: pick("--oxy-shell-muted-fg", "#71717a"),
339
+ border: pick("--oxy-shell-border", "#d4d4d8"),
340
+ bg: pick("--oxy-shell-background", "#ffffff")
341
+ };
342
+ }
343
+ /** Build the ECharts option for a `chart_rendered` block — the analytics
344
+ * pipeline's `{chart_type, x, y, series, …}` config, the same shape the
345
+ * main web-app charts. Interactive: axis/item tooltips, legend, hover. */
346
+ function buildEchartsOption(block, el) {
347
+ const { fg, muted, border } = tokenColors(el);
348
+ const axisLabel = {
349
+ color: muted,
350
+ fontSize: 10
351
+ };
352
+ const ct = block.config.chart_type;
353
+ if (ct === "pie_chart") {
354
+ const { config, columns, rows } = block;
355
+ const ni = config.name ? columns.indexOf(config.name) : config.x ? columns.indexOf(config.x) : 0;
356
+ const vi = config.value ? columns.indexOf(config.value) : config.y ? columns.indexOf(config.y) : 1;
357
+ const data = rows.map((r) => ({
358
+ name: toStr(r[ni]),
359
+ value: Math.max(0, toNum(r[vi]))
360
+ }));
361
+ return {
362
+ color: PALETTE,
363
+ tooltip: {
364
+ trigger: "item",
365
+ formatter: "{b}: {c} ({d}%)"
366
+ },
367
+ legend: {
368
+ bottom: 0,
369
+ textStyle: {
370
+ color: muted,
371
+ fontSize: 10
372
+ },
373
+ type: "scroll"
374
+ },
375
+ series: [{
376
+ type: "pie",
377
+ radius: ["38%", "66%"],
378
+ center: ["50%", "44%"],
379
+ data,
380
+ label: {
381
+ color: fg,
382
+ fontSize: 10
383
+ },
384
+ labelLine: { lineStyle: { color: border } }
385
+ }]
386
+ };
387
+ }
388
+ const pivot = pivotChart(block);
389
+ const multi = pivot.series.length > 1;
390
+ const singleName = block.config.y_axis_label || humanizeCol(block.config.y) || "Value";
391
+ const longLabels = pivot.categories.length > 4 || pivot.categories.some((c) => c.length > 10);
392
+ return {
393
+ color: PALETTE,
394
+ tooltip: {
395
+ trigger: "axis",
396
+ valueFormatter: (v) => typeof v === "number" ? compact.format(v) : String(v)
397
+ },
398
+ grid: {
399
+ left: 6,
400
+ right: 12,
401
+ top: multi ? 30 : 12,
402
+ bottom: 4,
403
+ containLabel: true
404
+ },
405
+ legend: multi ? {
406
+ top: 0,
407
+ textStyle: {
408
+ color: muted,
409
+ fontSize: 10
410
+ },
411
+ type: "scroll"
412
+ } : void 0,
413
+ xAxis: {
414
+ type: "category",
415
+ data: pivot.categories,
416
+ name: block.config.x_axis_label,
417
+ nameLocation: "middle",
418
+ nameGap: longLabels ? 42 : 24,
419
+ nameTextStyle: {
420
+ color: muted,
421
+ fontSize: 10
422
+ },
423
+ axisLabel: {
424
+ ...axisLabel,
425
+ interval: 0,
426
+ rotate: longLabels ? 30 : 0,
427
+ formatter: (v) => v.length > 18 ? `${v.slice(0, 17)}…` : v
428
+ },
429
+ axisTick: { show: false },
430
+ axisLine: { lineStyle: { color: border } }
431
+ },
432
+ yAxis: {
433
+ type: "value",
434
+ name: block.config.y_axis_label,
435
+ nameTextStyle: {
436
+ color: muted,
437
+ fontSize: 10
438
+ },
439
+ axisLabel: {
440
+ ...axisLabel,
441
+ formatter: (v) => compact.format(v)
442
+ },
443
+ splitLine: { lineStyle: {
444
+ color: border,
445
+ opacity: .5
446
+ } }
447
+ },
448
+ series: pivot.series.map((s, i) => ({
449
+ name: multi ? s.name : singleName,
450
+ type: ct === "bar_chart" ? "bar" : "line",
451
+ data: s.values,
452
+ itemStyle: { color: PALETTE[i % PALETTE.length] },
453
+ ...ct === "line_chart" ? {
454
+ lineStyle: { width: 2 },
455
+ symbolSize: 5,
456
+ smooth: false
457
+ } : {},
458
+ barMaxWidth: 32
459
+ }))
460
+ };
461
+ }
462
+ /** Renders one `chart_rendered` block. Uses ECharts (interactive, matching
463
+ * the main web-app) when the host app has it installed; otherwise falls
464
+ * back to a dependency-free SVG render. Table type always uses SVG. */
465
+ function AnswerChart({ block, className }) {
466
+ const ref = React.useRef(null);
467
+ const [engine, setEngine] = React.useState(block.config.chart_type === "table" ? "svg" : "pending");
468
+ React.useEffect(() => {
469
+ if (engine === "svg") return;
470
+ let disposed = false;
471
+ let chart = null;
472
+ let ro = null;
473
+ import(
474
+ /* @vite-ignore */
475
+ "echarts"
476
+ ).then((echarts) => {
477
+ const el = ref.current;
478
+ if (disposed || !el) return;
479
+ chart = echarts.init(el, void 0, { renderer: "canvas" });
480
+ chart.setOption(buildEchartsOption(block, el));
481
+ ro = new ResizeObserver(() => chart?.resize());
482
+ ro.observe(el);
483
+ }).catch(() => {
484
+ if (!disposed) setEngine("svg");
485
+ });
486
+ return () => {
487
+ disposed = true;
488
+ ro?.disconnect();
489
+ chart?.dispose();
490
+ };
491
+ }, [block, engine]);
492
+ return /* @__PURE__ */ jsxs("figure", {
493
+ className: cx("oxy-chart", className),
494
+ "data-testid": "askdock-chart",
495
+ children: [block.config.title && /* @__PURE__ */ jsx("figcaption", {
496
+ className: "oxy-chart__title",
497
+ children: block.config.title
498
+ }), engine === "svg" ? /* @__PURE__ */ jsx(SvgChart, { block }) : /* @__PURE__ */ jsx("div", {
499
+ ref,
500
+ className: "oxy-chart__echarts",
501
+ style: {
502
+ width: "100%",
503
+ height: 240
504
+ }
505
+ })]
506
+ });
507
+ }
508
+
509
+ //#endregion
510
+ //#region src/shell/marks.tsx
511
+ /** The Oxygen "O" glyph path, in a 0 0 44 44 viewBox, used by `OxyMark`. */
512
+ const OXY_MARK_PATH = "M38.4 22C39.2837 22 40.0074 22.7178 39.9289 23.5979C39.6619 26.5942 38.6471 29.485 36.9665 32.0003C34.9886 34.9603 32.1774 37.2674 28.8883 38.6298C25.5992 39.9922 21.98 40.3487 18.4884 39.6541C14.9967 38.9596 11.7894 37.2453 9.27209 34.7279C6.75474 32.2106 5.04041 29.0033 4.34587 25.5116C3.65134 22.02 4.0078 18.4008 5.37018 15.1117C6.73255 11.8226 9.03966 9.0114 11.9997 7.03353C14.515 5.35287 17.4058 4.33807 20.4021 4.07104C21.2822 3.9926 22 4.71633 22 5.59998L22 11.2C22 12.0837 22.7163 12.8 23.6 12.8H29.2C30.0837 12.8 30.8 12.0837 30.8 11.2V5.6C30.8 4.71634 31.5163 4 32.4 4H38.4C39.2836 4 40 4.71634 40 5.6V11.6C40 12.4837 39.2836 13.2 38.4 13.2H32.8C31.9163 13.2 31.2 13.9163 31.2 14.8V20.4C31.2 21.2837 31.9163 22 32.8 22L38.4 22ZM23.6 22C22.7163 22 22 21.2837 22 20.4L22 14.8371C22 13.9534 21.2778 13.2223 20.4089 13.3827C19.2427 13.5981 18.1268 14.0489 17.1316 14.7139C15.6905 15.6768 14.5674 17.0454 13.9041 18.6466C13.2409 20.2478 13.0674 22.0097 13.4055 23.7095C13.7436 25.4094 14.5782 26.9708 15.8037 28.1963C17.0292 29.4218 18.5906 30.2564 20.2904 30.5945C21.9903 30.9326 23.7522 30.7591 25.3534 30.0959C26.9546 29.4326 28.3232 28.3095 29.2861 26.8684C29.9511 25.8732 30.4019 24.7573 30.6172 23.5911C30.7777 22.7222 30.0466 22 29.1629 22L23.6 22Z";
513
+ /** The Oxygen "O" mark as an inline SVG that inherits `currentColor`, so it
514
+ * reads correctly on any surface in light or dark mode. */
515
+ function OxyMark({ className }) {
516
+ return /* @__PURE__ */ jsx("svg", {
517
+ viewBox: "0 0 44 44",
518
+ fill: "currentColor",
519
+ xmlns: "http://www.w3.org/2000/svg",
520
+ className,
521
+ "aria-hidden": "true",
522
+ children: /* @__PURE__ */ jsx("path", {
523
+ fillRule: "evenodd",
524
+ clipRule: "evenodd",
525
+ d: OXY_MARK_PATH
526
+ })
527
+ });
528
+ }
529
+ const FACTORY_PATHS = [
530
+ "M64.7079 60.8981C66.2159 61.5769 66.42 63.424 64.8831 64.0349C53.9237 68.3911 22.4112 68.2339 12.6945 62.9613C11.4576 62.2901 11.6244 60.6958 12.7979 59.9191C17.3748 56.8895 25.3973 51.7874 31.0332 48.4864C31.9361 47.9575 33.0153 47.8364 34.0043 48.1784C41.0152 50.6027 55.7832 56.8809 64.7079 60.8981Z",
531
+ "M40.8612 14.4068C40.6005 13.1343 41.6723 12.0367 42.847 12.591C53.7523 17.7375 68.2377 45.3963 68.9964 57.4238C69.0787 58.7282 67.7002 59.4009 66.5675 58.7485C62.2989 56.2901 53.2249 51.0848 48.6689 48.6258C47.5323 48.0123 46.7649 46.8912 46.5938 45.611C45.4688 37.1953 42.1634 20.7629 40.8612 14.4068Z",
532
+ "M39.2805 32.7689C39.2947 33.8426 38.8352 34.8657 38.0314 35.5777C28.4104 44.1006 18.5237 51.9152 13.0125 56.3133C11.7919 57.2874 10.0954 56.4483 10.3571 54.9087C12.5527 41.9933 25.758 17.8554 35.9571 12.7314C36.9805 12.2172 38.0789 12.9771 38.1857 14.1175C38.5311 17.8049 39.186 25.6228 39.2805 32.7689Z"
533
+ ];
534
+ /** The Oxygen Factory mark (three-shard glyph) as an inline SVG in
535
+ * `currentColor`, so it follows the shell foreground in either theme.
536
+ * (The web-app's asset files are fixed-color; this inline variant frees
537
+ * external bundles from carrying those assets.) Decorative — the rail
538
+ * button supplies the "Oxygen Factory" aria-label. */
539
+ function OxygenFactoryMark({ className }) {
540
+ return /* @__PURE__ */ jsx("svg", {
541
+ viewBox: "0 0 80 80",
542
+ fill: "none",
543
+ xmlns: "http://www.w3.org/2000/svg",
544
+ className,
545
+ "aria-hidden": "true",
546
+ children: FACTORY_PATHS.map((d) => /* @__PURE__ */ jsx("path", {
547
+ d,
548
+ fill: "currentColor"
549
+ }, d.slice(0, 16)))
550
+ });
551
+ }
552
+
553
+ //#endregion
554
+ //#region src/shell/trace.ts
555
+ const str = (v) => typeof v === "string" ? v : void 0;
556
+ const num = (v) => typeof v === "number" ? v : void 0;
557
+ /** "search_catalog" -> "Search Catalog" — matches the web-app pill labels. */
558
+ function prettyToolName(name) {
559
+ return name.split(/[_-]+/).filter(Boolean).map((w) => w[0].toUpperCase() + w.slice(1)).join(" ");
560
+ }
561
+ /** Compact one-line preview of a tool input (string arg or first string
562
+ * field of an object), truncated for the trace row. */
563
+ function inputPreview(input, max = 60) {
564
+ if (typeof input === "string" && /^[[{]/.test(input.trim())) try {
565
+ return inputPreview(JSON.parse(input), max);
566
+ } catch {}
567
+ let text;
568
+ if (typeof input === "string") text = input;
569
+ else if (Array.isArray(input)) {
570
+ const first = input.find((v) => typeof v === "string" && v.trim());
571
+ text = typeof first === "string" ? first : JSON.stringify(input);
572
+ } else if (input && typeof input === "object") {
573
+ for (const v of Object.values(input)) if (typeof v === "string" && v.trim()) {
574
+ text = v;
575
+ break;
576
+ }
577
+ if (!text) {
578
+ const json = JSON.stringify(input);
579
+ text = json === "{}" ? void 0 : json;
580
+ }
581
+ }
582
+ if (!text) return void 0;
583
+ text = text.replace(/\s+/g, " ").trim();
584
+ return text.length > max ? `${text.slice(0, max - 1)}…` : text;
585
+ }
586
+ /** Full pretty-printed payload for the expandable row detail: unwraps
587
+ * pre-serialized JSON strings, indents objects, caps runaway sizes. */
588
+ function prettyPayload(v, max = 2e3) {
589
+ if (v === null || v === void 0) return void 0;
590
+ let val = v;
591
+ if (typeof val === "string") {
592
+ const t = val.trim();
593
+ if (!t) return void 0;
594
+ if (/^[[{]/.test(t)) try {
595
+ val = JSON.parse(t);
596
+ } catch {}
597
+ }
598
+ const text = typeof val === "string" ? val : JSON.stringify(val, null, 2);
599
+ if (!text || text === "{}" || text === "[]") return void 0;
600
+ return text.length > max ? `${text.slice(0, max)}\n…` : text;
601
+ }
602
+ function formatMs(ms) {
603
+ return ms >= 1e3 ? `${(ms / 1e3).toFixed(1)}s` : `${ms}ms`;
604
+ }
605
+ /** Header meta, same as the web-app trace: LLM call count + total LLM time
606
+ * folded from `llm_usage` events. */
607
+ function aggregateLlmStats(events) {
608
+ let calls = 0;
609
+ let totalMs = 0;
610
+ for (const ev of events) {
611
+ if (ev.type !== "llm_usage") continue;
612
+ calls++;
613
+ const d = ev.data ?? {};
614
+ totalMs += num(d.duration_ms) ?? 0;
615
+ }
616
+ return {
617
+ calls,
618
+ totalMs
619
+ };
620
+ }
621
+ /** Reconstruct the streamed answer markdown from persisted events —
622
+ * concatenate the `text_delta` tokens, the same accumulation the live
623
+ * `useAgentRun` does. Used to rebuild a turn's answer on history restore. */
624
+ function extractAnswer(events) {
625
+ let out = "";
626
+ for (const ev of events) {
627
+ if (ev.type !== "text_delta") continue;
628
+ const d = ev.data ?? {};
629
+ if (typeof d.token === "string") out += d.token;
630
+ }
631
+ return out;
632
+ }
633
+ /** Pull the clarifying-question prompt from a persisted `awaiting_input`
634
+ * event (`{ questions: [{ prompt, suggestions }] }`) so a turn that ended
635
+ * in a suspension restores with its question instead of a blank answer.
636
+ * Mirrors the live `useAgentRun` clarification parse. */
637
+ function extractClarification(events) {
638
+ for (const ev of events) {
639
+ if (ev.type !== "awaiting_input") continue;
640
+ const d = ev.data ?? {};
641
+ const first = (Array.isArray(d.questions) ? d.questions : [])[0];
642
+ if (first && typeof first.prompt === "string") return first.prompt;
643
+ if (typeof d.question === "string") return d.question;
644
+ }
645
+ return null;
646
+ }
647
+ function sqlDetail(d) {
648
+ const parts = [];
649
+ const rows = num(d.row_count) ?? (Array.isArray(d.rows) ? d.rows.length : void 0);
650
+ if (rows !== void 0) parts.push(`${rows} row${rows === 1 ? "" : "s"}`);
651
+ const ms = num(d.duration_ms);
652
+ if (ms !== void 0) parts.push(formatMs(ms));
653
+ return parts.length ? parts.join(" · ") : void 0;
654
+ }
655
+ /** Mark any still-running items in a step as settled — called when the step
656
+ * closes so a tool/SQL/semantic row whose matching result never arrived
657
+ * (cancel, error mid-flight, terminal event) doesn't spin forever on a
658
+ * finished turn. */
659
+ function settleStepItems(step, asError = false) {
660
+ for (const item of step.items) if (item.running) {
661
+ item.running = false;
662
+ if (asError) item.error = true;
663
+ }
664
+ }
665
+ /** Fold raw SSE events into display steps. Pure — safe to re-run on every
666
+ * render over the accumulated event array. */
667
+ function buildTraceSteps(events) {
668
+ const steps = [];
669
+ let open = null;
670
+ let n = 0;
671
+ const id = (prefix) => `${prefix}-${n++}`;
672
+ const ensureStep = () => {
673
+ if (open) return open;
674
+ open = {
675
+ id: id("step"),
676
+ label: "Working",
677
+ status: "running",
678
+ items: []
679
+ };
680
+ steps.push(open);
681
+ return open;
682
+ };
683
+ const lastRunning = (kind) => {
684
+ for (let i = steps.length - 1; i >= 0; i--) {
685
+ const items = steps[i].items;
686
+ for (let j = items.length - 1; j >= 0; j--) {
687
+ const item = items[j];
688
+ if (item.kind === kind && item.running) return item;
689
+ }
690
+ }
691
+ };
692
+ for (const ev of events) {
693
+ const d = ev.data ?? {};
694
+ switch (ev.type) {
695
+ case "step_start":
696
+ if (open) {
697
+ settleStepItems(open);
698
+ open.status = "done";
699
+ }
700
+ open = {
701
+ id: id("step"),
702
+ label: str(d.label) ?? "Step",
703
+ summary: str(d.summary),
704
+ status: "running",
705
+ items: []
706
+ };
707
+ steps.push(open);
708
+ break;
709
+ case "step_end": {
710
+ if (!open) break;
711
+ const outcome = str(d.outcome);
712
+ open.status = outcome === "failed" ? "failed" : outcome === "suspended" ? "suspended" : "done";
713
+ settleStepItems(open, open.status === "failed");
714
+ open = null;
715
+ break;
716
+ }
717
+ case "step_summary_update": {
718
+ const summary = str(d.summary);
719
+ if (open && summary) open.summary = summary;
720
+ break;
721
+ }
722
+ case "tool_call": {
723
+ const name = str(d.name) ?? "tool";
724
+ ensureStep().items.push({
725
+ id: id("tool"),
726
+ kind: "tool",
727
+ label: prettyToolName(name),
728
+ preview: inputPreview(d.input),
729
+ input: prettyPayload(d.input),
730
+ running: true
731
+ });
732
+ break;
733
+ }
734
+ case "tool_result": {
735
+ const item = lastRunning("tool");
736
+ if (item) {
737
+ item.running = false;
738
+ item.error = d.is_error === true;
739
+ item.output = prettyPayload(d.output);
740
+ const ms = num(d.duration_ms);
741
+ if (ms !== void 0) item.detail = formatMs(ms);
742
+ }
743
+ break;
744
+ }
745
+ case "thinking_start":
746
+ ensureStep().items.push({
747
+ id: id("think"),
748
+ kind: "thinking",
749
+ label: "Thinking",
750
+ text: "",
751
+ running: true
752
+ });
753
+ break;
754
+ case "thinking_token": {
755
+ const item = lastRunning("thinking");
756
+ if (item) item.text = (item.text ?? "") + (str(d.token) ?? "");
757
+ break;
758
+ }
759
+ case "thinking_end": {
760
+ const item = lastRunning("thinking");
761
+ if (item) item.running = false;
762
+ break;
763
+ }
764
+ case "semantic_shortcut_attempted":
765
+ ensureStep().items.push({
766
+ id: id("sem"),
767
+ kind: "tool",
768
+ label: "Compile semantic query",
769
+ input: prettyPayload(d),
770
+ running: true
771
+ });
772
+ break;
773
+ case "semantic_shortcut_resolved": {
774
+ const item = lastRunning("tool");
775
+ if (item) {
776
+ item.running = false;
777
+ item.output = prettyPayload(d.sql ?? d);
778
+ }
779
+ break;
780
+ }
781
+ case "query_executed": {
782
+ const failed = d.success === false;
783
+ ensureStep().items.push({
784
+ id: id("sql"),
785
+ kind: "sql",
786
+ label: failed ? "Query failed" : "Query",
787
+ detail: failed ? str(d.error) ?? void 0 : sqlDetail(d),
788
+ input: prettyPayload(d.query ?? d.sql),
789
+ output: failed ? prettyPayload(d.error) : void 0,
790
+ error: failed
791
+ });
792
+ break;
793
+ }
794
+ case "verified_sql":
795
+ ensureStep().items.push({
796
+ id: id("sql"),
797
+ kind: "sql",
798
+ label: "Verified query",
799
+ detail: sqlDetail(d),
800
+ input: prettyPayload(d.query ?? d.sql)
801
+ });
802
+ break;
803
+ case "schema_resolved": {
804
+ const tables = Array.isArray(d.tables) ? d.tables.length : void 0;
805
+ ensureStep().items.push({
806
+ id: id("schema"),
807
+ kind: "note",
808
+ label: "Schema resolved",
809
+ detail: tables ? `${tables} table${tables === 1 ? "" : "s"}` : void 0
810
+ });
811
+ break;
812
+ }
813
+ case "recovery_resumed":
814
+ if (open) {
815
+ settleStepItems(open);
816
+ open.status = "done";
817
+ open = null;
818
+ }
819
+ steps.push({
820
+ id: id("step"),
821
+ label: "Resuming",
822
+ summary: str(d.message) ?? "Resuming from server restart",
823
+ status: "done",
824
+ items: []
825
+ });
826
+ break;
827
+ case "awaiting_input":
828
+ if (open) {
829
+ settleStepItems(open);
830
+ open.status = "suspended";
831
+ open = null;
832
+ }
833
+ break;
834
+ case "error":
835
+ case "failed":
836
+ if (open) {
837
+ settleStepItems(open, true);
838
+ open.status = "failed";
839
+ open = null;
840
+ }
841
+ break;
842
+ case "done":
843
+ case "cancelled":
844
+ if (open) {
845
+ settleStepItems(open);
846
+ open.status = "done";
847
+ open = null;
848
+ }
849
+ break;
850
+ default: break;
851
+ }
852
+ }
853
+ return steps;
854
+ }
855
+
856
+ //#endregion
857
+ //#region src/shell/ReasoningTrace.tsx
858
+ function StatusIcon({ status }) {
859
+ if (status === "running") return /* @__PURE__ */ jsx("span", {
860
+ role: "status",
861
+ className: "oxy-trace__spin",
862
+ "aria-label": "running"
863
+ });
864
+ if (status === "failed") return /* @__PURE__ */ jsx("span", {
865
+ className: "oxy-trace__icon oxy-trace__icon--fail",
866
+ children: "✕"
867
+ });
868
+ if (status === "suspended") return /* @__PURE__ */ jsx("span", {
869
+ className: "oxy-trace__icon oxy-trace__icon--wait",
870
+ children: "…"
871
+ });
872
+ return /* @__PURE__ */ jsx("span", {
873
+ className: "oxy-trace__icon oxy-trace__icon--ok",
874
+ children: "✓"
875
+ });
876
+ }
877
+ function ChevronIcon({ open }) {
878
+ return /* @__PURE__ */ jsx("svg", {
879
+ width: "12",
880
+ height: "12",
881
+ viewBox: "0 0 24 24",
882
+ fill: "none",
883
+ stroke: "currentColor",
884
+ strokeWidth: "2",
885
+ strokeLinecap: "round",
886
+ strokeLinejoin: "round",
887
+ "aria-hidden": "true",
888
+ style: {
889
+ transform: open ? "rotate(90deg)" : void 0,
890
+ transition: "transform 0.15s"
891
+ },
892
+ children: /* @__PURE__ */ jsx("path", { d: "m9 18 6-6-6-6" })
893
+ });
894
+ }
895
+ function TraceItemRows({ step, itemToggles, onToggleItem }) {
896
+ return /* @__PURE__ */ jsx("div", {
897
+ className: "oxy-trace__items",
898
+ children: step.items.map((item) => {
899
+ if (item.kind === "thinking") return (item.text ?? "").trim() ? /* @__PURE__ */ jsx("p", {
900
+ className: "oxy-trace__think",
901
+ children: item.text
902
+ }, item.id) : null;
903
+ const hasDetail = Boolean(item.input || item.output);
904
+ const itemOpen = hasDetail && (itemToggles[item.id] ?? item.running === true);
905
+ const row = /* @__PURE__ */ jsxs(Fragment$1, { children: [
906
+ item.running && /* @__PURE__ */ jsx("span", {
907
+ role: "status",
908
+ className: "oxy-trace__spin oxy-trace__spin--sm",
909
+ "aria-label": "running"
910
+ }),
911
+ /* @__PURE__ */ jsx("span", {
912
+ className: "oxy-trace__row-label",
913
+ children: item.label
914
+ }),
915
+ item.preview && /* @__PURE__ */ jsx("span", {
916
+ className: "oxy-trace__row-preview",
917
+ children: item.preview
918
+ }),
919
+ item.detail && /* @__PURE__ */ jsx("span", {
920
+ className: "oxy-trace__row-detail",
921
+ children: item.detail
922
+ })
923
+ ] });
924
+ return /* @__PURE__ */ jsxs("div", {
925
+ className: "oxy-trace__item",
926
+ children: [hasDetail ? /* @__PURE__ */ jsxs("button", {
927
+ type: "button",
928
+ className: cx("oxy-trace__row", "oxy-trace__row--btn", item.error && "oxy-trace__row--error"),
929
+ onClick: () => onToggleItem(item.id, !itemOpen),
930
+ "aria-expanded": itemOpen,
931
+ "data-testid": `reasoning-pill-${item.label.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`,
932
+ children: [row, /* @__PURE__ */ jsx("span", {
933
+ className: "oxy-trace__step-chevron",
934
+ children: /* @__PURE__ */ jsx(ChevronIcon, { open: itemOpen })
935
+ })]
936
+ }) : /* @__PURE__ */ jsx("div", {
937
+ className: cx("oxy-trace__row", item.error && "oxy-trace__row--error"),
938
+ "data-testid": `reasoning-pill-${item.label.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`,
939
+ children: row
940
+ }), itemOpen && /* @__PURE__ */ jsxs("div", {
941
+ className: "oxy-trace__payloads",
942
+ children: [item.input && /* @__PURE__ */ jsx("pre", {
943
+ className: "oxy-trace__payload",
944
+ children: item.input
945
+ }), item.output && /* @__PURE__ */ jsx("pre", {
946
+ className: "oxy-trace__payload oxy-trace__payload--out",
947
+ children: item.output
948
+ })]
949
+ })]
950
+ }, item.id);
951
+ })
952
+ });
953
+ }
954
+ /** Renders nothing when there are no steps (runs whose stream carries no
955
+ * step events degrade to the plain answer, same as before). */
956
+ function ReasoningTrace({ steps, streaming = false, llm, className }) {
957
+ const [userToggled, setUserToggled] = useState(null);
958
+ const [stepToggles, setStepToggles] = useState({});
959
+ const [itemToggles, setItemToggles] = useState({});
960
+ const runKey = steps[0]?.id ?? "";
961
+ const [prevRunKey, setPrevRunKey] = useState(runKey);
962
+ if (prevRunKey !== runKey) {
963
+ setPrevRunKey(runKey);
964
+ setUserToggled(null);
965
+ setStepToggles({});
966
+ setItemToggles({});
967
+ }
968
+ const open = userToggled ?? streaming;
969
+ const meta = useMemo(() => {
970
+ const parts = [];
971
+ if (llm && llm.calls > 0) {
972
+ parts.push(`${llm.calls} LLM ${llm.calls === 1 ? "call" : "calls"}`);
973
+ if (llm.totalMs > 0) parts.push(formatMs(llm.totalMs));
974
+ }
975
+ parts.push(`${steps.length} step${steps.length === 1 ? "" : "s"}`);
976
+ if (steps.some((s) => s.status === "failed")) parts.push("failed");
977
+ return parts.join(" · ");
978
+ }, [llm, steps]);
979
+ if (steps.length === 0) return null;
980
+ return /* @__PURE__ */ jsxs("div", {
981
+ className: cx("oxy-trace", className),
982
+ "data-run": runKey,
983
+ children: [/* @__PURE__ */ jsxs("button", {
984
+ type: "button",
985
+ className: "oxy-trace__head",
986
+ onClick: () => setUserToggled(!open),
987
+ "aria-expanded": open,
988
+ children: [
989
+ /* @__PURE__ */ jsx(ChevronIcon, { open }),
990
+ /* @__PURE__ */ jsx("span", {
991
+ className: "oxy-trace__title",
992
+ children: "Reasoning trace"
993
+ }),
994
+ /* @__PURE__ */ jsx("span", {
995
+ className: "oxy-trace__meta",
996
+ children: meta
997
+ }),
998
+ streaming && /* @__PURE__ */ jsx("span", {
999
+ role: "status",
1000
+ className: "oxy-trace__spin",
1001
+ "aria-label": "running"
1002
+ })
1003
+ ]
1004
+ }), open && /* @__PURE__ */ jsx("div", {
1005
+ className: "oxy-trace__body",
1006
+ children: steps.map((step) => {
1007
+ const stepOpen = stepToggles[step.id] ?? step.status === "running";
1008
+ if (step.items.length === 0) return /* @__PURE__ */ jsx("div", {
1009
+ className: "oxy-trace__step",
1010
+ children: /* @__PURE__ */ jsxs("div", {
1011
+ className: "oxy-trace__step-row",
1012
+ children: [
1013
+ /* @__PURE__ */ jsx(StatusIcon, { status: step.status }),
1014
+ /* @__PURE__ */ jsx("span", {
1015
+ className: "oxy-trace__label",
1016
+ children: step.label
1017
+ }),
1018
+ step.summary && /* @__PURE__ */ jsx("span", {
1019
+ className: "oxy-trace__summary",
1020
+ children: step.summary
1021
+ })
1022
+ ]
1023
+ })
1024
+ }, step.id);
1025
+ return /* @__PURE__ */ jsxs("div", {
1026
+ className: "oxy-trace__step",
1027
+ children: [/* @__PURE__ */ jsxs("button", {
1028
+ type: "button",
1029
+ className: "oxy-trace__step-row oxy-trace__step-row--btn",
1030
+ onClick: () => setStepToggles((t) => ({
1031
+ ...t,
1032
+ [step.id]: !stepOpen
1033
+ })),
1034
+ "aria-expanded": stepOpen,
1035
+ children: [
1036
+ /* @__PURE__ */ jsx(StatusIcon, { status: step.status }),
1037
+ /* @__PURE__ */ jsx("span", {
1038
+ className: "oxy-trace__label",
1039
+ children: step.label
1040
+ }),
1041
+ step.summary && /* @__PURE__ */ jsx("span", {
1042
+ className: "oxy-trace__summary",
1043
+ children: step.summary
1044
+ }),
1045
+ !stepOpen && /* @__PURE__ */ jsxs("span", {
1046
+ className: "oxy-trace__count",
1047
+ children: [
1048
+ step.items.length,
1049
+ " action",
1050
+ step.items.length === 1 ? "" : "s"
1051
+ ]
1052
+ }),
1053
+ /* @__PURE__ */ jsx("span", {
1054
+ className: "oxy-trace__step-chevron",
1055
+ children: /* @__PURE__ */ jsx(ChevronIcon, { open: stepOpen })
1056
+ })
1057
+ ]
1058
+ }), stepOpen && /* @__PURE__ */ jsx(TraceItemRows, {
1059
+ step,
1060
+ itemToggles,
1061
+ onToggleItem: (id, next) => setItemToggles((t) => ({
1062
+ ...t,
1063
+ [id]: next
1064
+ }))
1065
+ })]
1066
+ }, step.id);
1067
+ })
1068
+ })]
1069
+ });
1070
+ }
1071
+
1072
+ //#endregion
1073
+ //#region src/shell/threadHistory.ts
1074
+ /**
1075
+ * List the viewer's persistent chat threads for the current project.
1076
+ * Non-fatal on failure (older server / no session): returns an empty
1077
+ * list so the dock falls back to session-only history.
1078
+ */
1079
+ function useThreadHistory() {
1080
+ const { projectId, fetcher } = useOxyApp();
1081
+ const [state, setState] = React.useState({
1082
+ threads: [],
1083
+ loading: false,
1084
+ error: null
1085
+ });
1086
+ const [nonce, setNonce] = React.useState(0);
1087
+ const refetch = React.useCallback(() => setNonce((n) => n + 1), []);
1088
+ React.useEffect(() => {
1089
+ if (!projectId) {
1090
+ setState({
1091
+ threads: [],
1092
+ loading: false,
1093
+ error: null
1094
+ });
1095
+ return;
1096
+ }
1097
+ let cancelled = false;
1098
+ setState((s) => ({
1099
+ ...s,
1100
+ loading: true
1101
+ }));
1102
+ fetcher(`/api/projects/${projectId}/threads`, { method: "GET" }).then(async (resp) => {
1103
+ if (!resp.ok) throw new Error(`threads list failed: HTTP ${resp.status}`);
1104
+ const threads = await resp.json();
1105
+ if (!cancelled) setState({
1106
+ threads,
1107
+ loading: false,
1108
+ error: null
1109
+ });
1110
+ }).catch((e) => {
1111
+ const error = e instanceof Error ? e : new Error(String(e));
1112
+ getOxyAppLogger().log("warn", "thread history unavailable", { error: error.message });
1113
+ if (!cancelled) setState({
1114
+ threads: [],
1115
+ loading: false,
1116
+ error
1117
+ });
1118
+ });
1119
+ return () => {
1120
+ cancelled = true;
1121
+ };
1122
+ }, [
1123
+ projectId,
1124
+ fetcher,
1125
+ nonce
1126
+ ]);
1127
+ return {
1128
+ ...state,
1129
+ refetch
1130
+ };
1131
+ }
1132
+ /**
1133
+ * Fetch a thread's transcript for restore. Throws on failure so the
1134
+ * caller can surface a toast / fall back.
1135
+ */
1136
+ async function fetchThreadTranscript(fetcher, projectId, threadId) {
1137
+ const resp = await fetcher(`/api/projects/${projectId}/threads/${threadId}`, { method: "GET" });
1138
+ if (!resp.ok) throw new Error(`thread transcript failed: HTTP ${resp.status}`);
1139
+ return await resp.json();
1140
+ }
1141
+
1142
+ //#endregion
1143
+ //#region src/shell/AskDock.tsx
1144
+ function CloseIcon() {
1145
+ return /* @__PURE__ */ jsxs("svg", {
1146
+ width: "14",
1147
+ height: "14",
1148
+ viewBox: "0 0 24 24",
1149
+ fill: "none",
1150
+ stroke: "currentColor",
1151
+ strokeWidth: "2",
1152
+ strokeLinecap: "round",
1153
+ strokeLinejoin: "round",
1154
+ "aria-hidden": "true",
1155
+ children: [/* @__PURE__ */ jsx("path", { d: "M18 6 6 18" }), /* @__PURE__ */ jsx("path", { d: "m6 6 12 12" })]
1156
+ });
1157
+ }
1158
+ function PlusIcon() {
1159
+ return /* @__PURE__ */ jsxs("svg", {
1160
+ width: "15",
1161
+ height: "15",
1162
+ viewBox: "0 0 24 24",
1163
+ fill: "none",
1164
+ stroke: "currentColor",
1165
+ strokeWidth: "2",
1166
+ strokeLinecap: "round",
1167
+ strokeLinejoin: "round",
1168
+ "aria-hidden": "true",
1169
+ children: [/* @__PURE__ */ jsx("path", { d: "M5 12h14" }), /* @__PURE__ */ jsx("path", { d: "M12 5v14" })]
1170
+ });
1171
+ }
1172
+ function HistoryIcon() {
1173
+ return /* @__PURE__ */ jsxs("svg", {
1174
+ width: "14",
1175
+ height: "14",
1176
+ viewBox: "0 0 24 24",
1177
+ fill: "none",
1178
+ stroke: "currentColor",
1179
+ strokeWidth: "2",
1180
+ strokeLinecap: "round",
1181
+ strokeLinejoin: "round",
1182
+ "aria-hidden": "true",
1183
+ children: [
1184
+ /* @__PURE__ */ jsx("path", { d: "M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" }),
1185
+ /* @__PURE__ */ jsx("path", { d: "M3 3v5h5" }),
1186
+ /* @__PURE__ */ jsx("path", { d: "M12 7v5l4 2" })
1187
+ ]
1188
+ });
1189
+ }
1190
+ function SearchIcon() {
1191
+ return /* @__PURE__ */ jsxs("svg", {
1192
+ width: "14",
1193
+ height: "14",
1194
+ viewBox: "0 0 24 24",
1195
+ fill: "none",
1196
+ stroke: "currentColor",
1197
+ strokeWidth: "2",
1198
+ strokeLinecap: "round",
1199
+ strokeLinejoin: "round",
1200
+ "aria-hidden": "true",
1201
+ children: [/* @__PURE__ */ jsx("circle", {
1202
+ cx: "11",
1203
+ cy: "11",
1204
+ r: "8"
1205
+ }), /* @__PURE__ */ jsx("path", { d: "m21 21-4.3-4.3" })]
1206
+ });
1207
+ }
1208
+ function ThreadIcon() {
1209
+ return /* @__PURE__ */ jsxs("svg", {
1210
+ width: "15",
1211
+ height: "15",
1212
+ viewBox: "0 0 24 24",
1213
+ fill: "none",
1214
+ stroke: "currentColor",
1215
+ strokeWidth: "2",
1216
+ strokeLinecap: "round",
1217
+ strokeLinejoin: "round",
1218
+ "aria-hidden": "true",
1219
+ children: [/* @__PURE__ */ jsx("path", { d: "M14 9a2 2 0 0 1-2 2H6l-4 4V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2z" }), /* @__PURE__ */ jsx("path", { d: "M18 9h2a2 2 0 0 1 2 2v11l-4-4h-6a2 2 0 0 1-2-2v-1" })]
1220
+ });
1221
+ }
1222
+ function ExternalIcon() {
1223
+ return /* @__PURE__ */ jsxs("svg", {
1224
+ width: "14",
1225
+ height: "14",
1226
+ viewBox: "0 0 24 24",
1227
+ fill: "none",
1228
+ stroke: "currentColor",
1229
+ strokeWidth: "2",
1230
+ strokeLinecap: "round",
1231
+ strokeLinejoin: "round",
1232
+ "aria-hidden": "true",
1233
+ children: [
1234
+ /* @__PURE__ */ jsx("path", { d: "M15 3h6v6" }),
1235
+ /* @__PURE__ */ jsx("path", { d: "M10 14 21 3" }),
1236
+ /* @__PURE__ */ jsx("path", { d: "M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" })
1237
+ ]
1238
+ });
1239
+ }
1240
+ function ArrowUpIcon() {
1241
+ return /* @__PURE__ */ jsxs("svg", {
1242
+ width: "14",
1243
+ height: "14",
1244
+ viewBox: "0 0 24 24",
1245
+ fill: "none",
1246
+ stroke: "currentColor",
1247
+ strokeWidth: "2",
1248
+ strokeLinecap: "round",
1249
+ strokeLinejoin: "round",
1250
+ "aria-hidden": "true",
1251
+ children: [/* @__PURE__ */ jsx("path", { d: "m5 12 7-7 7 7" }), /* @__PURE__ */ jsx("path", { d: "M12 19V5" })]
1252
+ });
1253
+ }
1254
+ function StopIcon() {
1255
+ return /* @__PURE__ */ jsx("svg", {
1256
+ width: "12",
1257
+ height: "12",
1258
+ viewBox: "0 0 24 24",
1259
+ fill: "currentColor",
1260
+ "aria-hidden": "true",
1261
+ children: /* @__PURE__ */ jsx("rect", {
1262
+ x: "6",
1263
+ y: "6",
1264
+ width: "12",
1265
+ height: "12",
1266
+ rx: "2"
1267
+ })
1268
+ });
1269
+ }
1270
+ function chartsFrom(events) {
1271
+ return events.filter((ev) => ev.type === "chart_rendered").map((ev) => ev.data).filter((b) => b?.config && Array.isArray(b.columns) && Array.isArray(b.rows));
1272
+ }
1273
+ /** History label: a relative "N minutes/hours/days ago" for when the
1274
+ * conversation was started. */
1275
+ function formatHistoryTime(iso) {
1276
+ const d = new Date(iso);
1277
+ if (Number.isNaN(d.getTime())) return "";
1278
+ const s = Math.max(0, Math.round((Date.now() - d.getTime()) / 1e3));
1279
+ if (s < 45) return "just now";
1280
+ const min = Math.round(s / 60);
1281
+ if (min < 60) return `${min} minute${min === 1 ? "" : "s"} ago`;
1282
+ const hr = Math.round(s / 3600);
1283
+ if (hr < 24) return `${hr} hour${hr === 1 ? "" : "s"} ago`;
1284
+ const day = Math.round(s / 86400);
1285
+ if (day < 30) return `${day} day${day === 1 ? "" : "s"} ago`;
1286
+ const mo = Math.round(day / 30);
1287
+ if (mo < 12) return `${mo} month${mo === 1 ? "" : "s"} ago`;
1288
+ const yr = Math.round(day / 365);
1289
+ return `${yr} year${yr === 1 ? "" : "s"} ago`;
1290
+ }
1291
+ /** How many history rows to show before the "Show more" button. */
1292
+ const HISTORY_PAGE_SIZE = 12;
1293
+ /** Rebuild a completed turn from a transcript turn's processed events —
1294
+ * identical to what the live dock accumulates during a run. */
1295
+ function turnFromEvents(question, events) {
1296
+ return {
1297
+ question,
1298
+ answer: extractAnswer(events) || extractClarification(events) || null,
1299
+ charts: chartsFrom(events),
1300
+ steps: buildTraceSteps(events),
1301
+ llm: aggregateLlmStats(events)
1302
+ };
1303
+ }
1304
+ /**
1305
+ * The Ask Oxygen drawer. Keep it mounted and toggle `open` — the
1306
+ * transcript survives close/reopen, matching the web-app dock.
1307
+ */
1308
+ function AskDock({ agentId, workspaceName, suggestions = [], threadsHref, open, onClose, className }) {
1309
+ const run = useAgentRun({ agentId });
1310
+ const { projectId, fetcher } = useOxyApp();
1311
+ const { threads: serverThreads, refetch: refetchHistory } = useThreadHistory();
1312
+ const [turns, setTurns] = React.useState([]);
1313
+ const [pending, setPending] = React.useState(null);
1314
+ const [draft, setDraft] = React.useState("");
1315
+ const [threadId, setThreadId] = React.useState(null);
1316
+ const [history, setHistory] = React.useState([]);
1317
+ const [historyOpen, setHistoryOpen] = React.useState(false);
1318
+ const [historyQuery, setHistoryQuery] = React.useState("");
1319
+ const [historyShown, setHistoryShown] = React.useState(HISTORY_PAGE_SIZE);
1320
+ const [restoring, setRestoring] = React.useState(false);
1321
+ const scrollRef = React.useRef(null);
1322
+ const restoreGenRef = React.useRef(0);
1323
+ const startedAtRef = React.useRef(null);
1324
+ const busy = run.state === "running";
1325
+ const liveSteps = React.useMemo(() => buildTraceSteps(run.events), [run.events]);
1326
+ const liveLlm = React.useMemo(() => aggregateLlmStats(run.events), [run.events]);
1327
+ const liveCharts = React.useMemo(() => chartsFrom(run.events), [run.events]);
1328
+ React.useEffect(() => {
1329
+ if (run.threadId) setThreadId(run.threadId);
1330
+ }, [run.threadId]);
1331
+ const finalizedPendingTurn = () => pending === null ? null : {
1332
+ question: pending,
1333
+ answer: run.answer ?? run.clarification,
1334
+ charts: liveCharts,
1335
+ steps: liveSteps,
1336
+ llm: liveLlm
1337
+ };
1338
+ /** The current conversation as a snapshot, or null when it's empty. */
1339
+ const snapshotCurrent = () => {
1340
+ const finalized = finalizedPendingTurn();
1341
+ const all = finalized ? [...turns, finalized] : turns;
1342
+ if (all.length === 0) return null;
1343
+ return {
1344
+ id: threadId ?? all[0].question,
1345
+ title: all[0].question,
1346
+ turns: all,
1347
+ threadId,
1348
+ createdAt: startedAtRef.current ?? (/* @__PURE__ */ new Date()).toISOString()
1349
+ };
1350
+ };
1351
+ const resetToEmpty = () => {
1352
+ restoreGenRef.current += 1;
1353
+ run.cancel();
1354
+ setTurns([]);
1355
+ setPending(null);
1356
+ setDraft("");
1357
+ setThreadId(null);
1358
+ startedAtRef.current = null;
1359
+ };
1360
+ const newChat = () => {
1361
+ const snap = snapshotCurrent();
1362
+ if (snap) setHistory((h) => [snap, ...h.filter((c) => c.id !== snap.id)]);
1363
+ resetToEmpty();
1364
+ setHistoryOpen(false);
1365
+ };
1366
+ const openConversation = (conv) => {
1367
+ restoreGenRef.current += 1;
1368
+ const snap = snapshotCurrent();
1369
+ setHistory((h) => {
1370
+ const rest = h.filter((c) => c.id !== conv.id && (!snap || c.id !== snap.id));
1371
+ return snap && snap.id !== conv.id ? [snap, ...rest] : rest;
1372
+ });
1373
+ run.cancel();
1374
+ setTurns(conv.turns);
1375
+ setPending(null);
1376
+ setDraft("");
1377
+ setThreadId(conv.threadId);
1378
+ setHistoryOpen(false);
1379
+ };
1380
+ /** Restore a persistent (server) thread: archive the current chat, then
1381
+ * fetch + rebuild the transcript. Follow-ups resume the same thread. */
1382
+ const openServerThread = async (id) => {
1383
+ if (!projectId) return;
1384
+ const gen = restoreGenRef.current += 1;
1385
+ const snap = snapshotCurrent();
1386
+ if (snap && snap.id !== id) setHistory((h) => [snap, ...h.filter((c) => c.id !== snap.id)]);
1387
+ run.cancel();
1388
+ setTurns([]);
1389
+ setPending(null);
1390
+ setDraft("");
1391
+ setThreadId(id);
1392
+ startedAtRef.current = serverThreads.find((t) => t.id === id)?.created_at ?? (/* @__PURE__ */ new Date()).toISOString();
1393
+ setHistoryOpen(false);
1394
+ setRestoring(true);
1395
+ try {
1396
+ const transcript = await fetchThreadTranscript(fetcher, projectId, id);
1397
+ if (restoreGenRef.current !== gen) return;
1398
+ setTurns(transcript.turns.map((t) => turnFromEvents(t.question, t.events)));
1399
+ } catch {} finally {
1400
+ if (restoreGenRef.current === gen) setRestoring(false);
1401
+ }
1402
+ };
1403
+ const send = (raw) => {
1404
+ const question = raw.trim();
1405
+ if (!question || busy) return;
1406
+ restoreGenRef.current += 1;
1407
+ if (startedAtRef.current === null) startedAtRef.current = (/* @__PURE__ */ new Date()).toISOString();
1408
+ if (pending !== null) {
1409
+ const finalized = finalizedPendingTurn();
1410
+ if (finalized) setTurns((t) => [...t, finalized]);
1411
+ }
1412
+ setPending(question);
1413
+ setDraft("");
1414
+ run.ask(question, threadId ? { threadId } : void 0);
1415
+ };
1416
+ React.useEffect(() => {
1417
+ const el = scrollRef.current;
1418
+ if (!el) return;
1419
+ if (el.scrollHeight - el.scrollTop - el.clientHeight < 120) el.scrollTo({ top: el.scrollHeight });
1420
+ });
1421
+ const empty = pending === null && turns.length === 0;
1422
+ const canNewChat = !empty;
1423
+ const currentId = threadId ?? turns[0]?.question ?? pending ?? null;
1424
+ const historyEntries = [...history.map((c) => ({
1425
+ id: c.id,
1426
+ title: c.title,
1427
+ createdAt: c.createdAt,
1428
+ active: c.id === currentId,
1429
+ onOpen: c.id === currentId ? () => setHistoryOpen(false) : () => openConversation(c)
1430
+ })), ...serverThreads.filter((t) => !history.some((c) => c.id === t.id)).map((t) => ({
1431
+ id: t.id,
1432
+ title: t.title,
1433
+ createdAt: t.created_at,
1434
+ active: t.id === currentId,
1435
+ onOpen: t.id === currentId ? () => setHistoryOpen(false) : () => void openServerThread(t.id)
1436
+ }))];
1437
+ if (!empty && currentId != null && !historyEntries.some((e) => e.id === currentId)) historyEntries.push({
1438
+ id: currentId,
1439
+ title: turns[0]?.question ?? pending ?? currentId,
1440
+ createdAt: startedAtRef.current ?? (/* @__PURE__ */ new Date()).toISOString(),
1441
+ active: true,
1442
+ onOpen: () => setHistoryOpen(false)
1443
+ });
1444
+ historyEntries.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
1445
+ const q = historyQuery.trim().toLowerCase();
1446
+ const filteredEntries = q ? historyEntries.filter((e) => e.title.toLowerCase().includes(q)) : historyEntries;
1447
+ const visibleEntries = filteredEntries.slice(0, historyShown);
1448
+ const placeholder = run.state === "needs_clarification" ? "Reply…" : pending !== null ? "Ask a follow-up…" : `Ask Oxygen anything about ${workspaceName ?? "your workspace"}…`;
1449
+ return /* @__PURE__ */ jsxs("aside", {
1450
+ className: cx("oxy-shell-scope oxy-askdock", !open && "oxy-askdock--closed", className),
1451
+ "aria-label": "Ask Oxygen",
1452
+ "aria-hidden": !open,
1453
+ children: [/* @__PURE__ */ jsxs("header", {
1454
+ className: "oxy-askdock__head",
1455
+ children: [
1456
+ /* @__PURE__ */ jsx(OxyMark, { className: "oxy-askdock__mark" }),
1457
+ /* @__PURE__ */ jsx("span", {
1458
+ className: "oxy-askdock__title",
1459
+ children: "Ask Oxygen"
1460
+ }),
1461
+ /* @__PURE__ */ jsxs("span", {
1462
+ className: "oxy-askdock__actions",
1463
+ children: [
1464
+ /* @__PURE__ */ jsx("button", {
1465
+ type: "button",
1466
+ className: "oxy-askdock__iconbtn",
1467
+ onClick: newChat,
1468
+ disabled: !canNewChat,
1469
+ "data-testid": "askdock-new-chat",
1470
+ "aria-label": "New chat",
1471
+ title: "New chat",
1472
+ children: /* @__PURE__ */ jsx(PlusIcon, {})
1473
+ }),
1474
+ /* @__PURE__ */ jsx("button", {
1475
+ type: "button",
1476
+ className: cx("oxy-askdock__iconbtn", historyOpen && "oxy-askdock__iconbtn--active"),
1477
+ onClick: () => {
1478
+ const next = !historyOpen;
1479
+ setHistoryOpen(next);
1480
+ if (next) {
1481
+ refetchHistory();
1482
+ setHistoryQuery("");
1483
+ setHistoryShown(HISTORY_PAGE_SIZE);
1484
+ }
1485
+ },
1486
+ "data-testid": "askdock-history",
1487
+ "aria-label": "Chat history",
1488
+ "aria-expanded": historyOpen,
1489
+ title: "Chat history",
1490
+ children: /* @__PURE__ */ jsx(HistoryIcon, {})
1491
+ }),
1492
+ threadsHref && /* @__PURE__ */ jsx("a", {
1493
+ className: "oxy-askdock__iconbtn",
1494
+ href: threadsHref,
1495
+ "aria-label": "Open chat in Oxygen",
1496
+ title: "Open chat in Oxygen",
1497
+ children: /* @__PURE__ */ jsx(ExternalIcon, {})
1498
+ }),
1499
+ /* @__PURE__ */ jsx("button", {
1500
+ type: "button",
1501
+ className: "oxy-askdock__iconbtn",
1502
+ onClick: onClose,
1503
+ "aria-label": "Close Ask Oxygen",
1504
+ children: /* @__PURE__ */ jsx(CloseIcon, {})
1505
+ })
1506
+ ]
1507
+ })
1508
+ ]
1509
+ }), historyOpen ? /* @__PURE__ */ jsxs("div", {
1510
+ className: "oxy-askdock__history",
1511
+ "data-testid": "askdock-history-panel",
1512
+ children: [/* @__PURE__ */ jsxs("div", {
1513
+ className: "oxy-askdock__history-search",
1514
+ children: [/* @__PURE__ */ jsx(SearchIcon, {}), /* @__PURE__ */ jsx("input", {
1515
+ type: "text",
1516
+ className: "oxy-askdock__history-searchinput",
1517
+ placeholder: "Search threads…",
1518
+ value: historyQuery,
1519
+ onChange: (e) => {
1520
+ setHistoryQuery(e.target.value);
1521
+ setHistoryShown(HISTORY_PAGE_SIZE);
1522
+ },
1523
+ "aria-label": "Search threads"
1524
+ })]
1525
+ }), visibleEntries.length === 0 ? /* @__PURE__ */ jsx("p", {
1526
+ className: "oxy-askdock__history-empty",
1527
+ children: historyQuery ? "No matching chats." : "No previous chats yet."
1528
+ }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [visibleEntries.map((entry) => /* @__PURE__ */ jsxs("button", {
1529
+ type: "button",
1530
+ className: cx("oxy-askdock__history-item", entry.active && "oxy-askdock__history-item--active"),
1531
+ onClick: entry.onOpen,
1532
+ title: entry.title,
1533
+ "aria-current": entry.active ? "true" : void 0,
1534
+ children: [
1535
+ /* @__PURE__ */ jsx("span", {
1536
+ className: "oxy-askdock__history-icon",
1537
+ children: /* @__PURE__ */ jsx(ThreadIcon, {})
1538
+ }),
1539
+ /* @__PURE__ */ jsx("span", {
1540
+ className: "oxy-askdock__history-title",
1541
+ children: entry.title
1542
+ }),
1543
+ /* @__PURE__ */ jsx("span", {
1544
+ className: "oxy-askdock__history-time",
1545
+ children: formatHistoryTime(entry.createdAt)
1546
+ })
1547
+ ]
1548
+ }, entry.id)), filteredEntries.length > visibleEntries.length && /* @__PURE__ */ jsx("button", {
1549
+ type: "button",
1550
+ className: "oxy-askdock__history-more",
1551
+ onClick: () => setHistoryShown((n) => n + HISTORY_PAGE_SIZE),
1552
+ children: "Show more"
1553
+ })] })]
1554
+ }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("div", {
1555
+ className: "oxy-askdock__scroll",
1556
+ ref: scrollRef,
1557
+ children: [
1558
+ restoring && /* @__PURE__ */ jsx("p", {
1559
+ className: "oxy-askdock__history-empty",
1560
+ children: "Loading conversation…"
1561
+ }),
1562
+ empty && !restoring && suggestions.length > 0 && /* @__PURE__ */ jsx("div", {
1563
+ className: "oxy-askdock__chips",
1564
+ children: suggestions.map((s) => /* @__PURE__ */ jsx("button", {
1565
+ type: "button",
1566
+ className: "oxy-askdock__chip",
1567
+ onClick: () => send(s),
1568
+ children: s
1569
+ }, s))
1570
+ }),
1571
+ turns.map((t, i) => /* @__PURE__ */ jsxs("div", {
1572
+ className: "oxy-askdock__turn",
1573
+ children: [
1574
+ /* @__PURE__ */ jsx("div", {
1575
+ className: "oxy-askdock__q",
1576
+ children: t.question
1577
+ }),
1578
+ /* @__PURE__ */ jsx(ReasoningTrace, {
1579
+ steps: t.steps,
1580
+ llm: t.llm
1581
+ }),
1582
+ t.charts.map((c, ci) => /* @__PURE__ */ jsx(AnswerChart, { block: c }, `${ci}-${c.config.title ?? c.config.chart_type}`)),
1583
+ /* @__PURE__ */ jsx(OxyAnswer, {
1584
+ answer: t.answer,
1585
+ state: "done",
1586
+ threadUrl: null
1587
+ })
1588
+ ]
1589
+ }, `${i}-${t.question}`)),
1590
+ pending !== null && /* @__PURE__ */ jsxs("div", {
1591
+ className: "oxy-askdock__turn",
1592
+ children: [
1593
+ /* @__PURE__ */ jsx("div", {
1594
+ className: "oxy-askdock__q",
1595
+ children: pending
1596
+ }),
1597
+ /* @__PURE__ */ jsx(ReasoningTrace, {
1598
+ steps: liveSteps,
1599
+ streaming: busy,
1600
+ llm: liveLlm
1601
+ }),
1602
+ liveCharts.map((c, ci) => /* @__PURE__ */ jsx(AnswerChart, { block: c }, `${ci}-${c.config.title ?? c.config.chart_type}`)),
1603
+ /* @__PURE__ */ jsx(OxyAnswer, {
1604
+ answer: run.answer,
1605
+ state: run.state,
1606
+ clarification: run.clarification,
1607
+ error: run.error,
1608
+ threadUrl: null
1609
+ })
1610
+ ]
1611
+ })
1612
+ ]
1613
+ }), /* @__PURE__ */ jsxs("form", {
1614
+ className: "oxy-askdock__composer",
1615
+ onSubmit: (e) => {
1616
+ e.preventDefault();
1617
+ send(draft);
1618
+ },
1619
+ children: [/* @__PURE__ */ jsx("textarea", {
1620
+ className: "oxy-askdock__input",
1621
+ rows: 3,
1622
+ placeholder,
1623
+ value: draft,
1624
+ onChange: (e) => setDraft(e.target.value),
1625
+ onKeyDown: (e) => {
1626
+ if (e.key === "Enter" && !e.shiftKey) {
1627
+ e.preventDefault();
1628
+ send(draft);
1629
+ }
1630
+ }
1631
+ }), /* @__PURE__ */ jsx("div", {
1632
+ className: "oxy-askdock__composer-row",
1633
+ children: busy ? /* @__PURE__ */ jsx("button", {
1634
+ type: "button",
1635
+ className: "oxy-askdock__send",
1636
+ onClick: run.cancel,
1637
+ "aria-label": "Stop generating",
1638
+ children: /* @__PURE__ */ jsx(StopIcon, {})
1639
+ }) : /* @__PURE__ */ jsx("button", {
1640
+ type: "submit",
1641
+ className: "oxy-askdock__send",
1642
+ disabled: !draft.trim(),
1643
+ "aria-label": "Send",
1644
+ children: /* @__PURE__ */ jsx(ArrowUpIcon, {})
1645
+ })
1646
+ })]
1647
+ })] })]
1648
+ });
1649
+ }
1650
+
1651
+ //#endregion
1652
+ //#region src/shell/logoUrl.ts
1653
+ /** URL of the workspace logo endpoint (org-uploaded logo, falling back to
1654
+ * the code-first file). Consumers render an `<img>` and fall back on error.
1655
+ * `apiBaseUrl` is the API root (e.g. `"/api"` same-origin, or an absolute
1656
+ * base in dev); pass `version` (the org's `updated_at`) to bust the cache
1657
+ * after an upload/remove. */
1658
+ const workspaceLogoUrl = (apiBaseUrl, workspaceId, version) => {
1659
+ const base = `${apiBaseUrl.replace(/\/$/, "")}/${workspaceId}/logo`;
1660
+ return version ? `${base}?v=${encodeURIComponent(version)}` : base;
1661
+ };
1662
+
1663
+ //#endregion
1664
+ //#region src/shell/portal.tsx
1665
+ /**
1666
+ * Floating shell content (tooltips) portals into the nearest shell scope
1667
+ * element instead of `document.body`, so the `oxy-shell-scope` tokens and a
1668
+ * host-applied `.dark` ancestor keep applying to it. Falls back to the
1669
+ * default body portal when no provider is mounted (standalone `ShellRail`
1670
+ * under a host that themes `<body>` itself).
1671
+ */
1672
+ const ShellPortalContext = React.createContext(null);
1673
+ /** Track the portal container element for descendants of a shell root. */
1674
+ function useShellPortalContainer() {
1675
+ const [container, setContainer] = React.useState(null);
1676
+ return {
1677
+ container,
1678
+ setContainer
1679
+ };
1680
+ }
1681
+
1682
+ //#endregion
1683
+ //#region src/shell/Tooltip.tsx
1684
+ /**
1685
+ * Hover tooltip for shell controls. Mirrors the web-app's Radix tooltip
1686
+ * (zero delay, side arrow, `--zinc-200`/muted surface) but portals into the
1687
+ * shell scope so the namespaced tokens and dark-mode class reach it.
1688
+ */
1689
+ function ShellTooltip({ content, side = "right", children }) {
1690
+ const container = React.useContext(ShellPortalContext);
1691
+ return /* @__PURE__ */ jsx(TooltipPrimitive.Provider, {
1692
+ delayDuration: 0,
1693
+ children: /* @__PURE__ */ jsxs(TooltipPrimitive.Root, { children: [/* @__PURE__ */ jsx(TooltipPrimitive.Trigger, {
1694
+ asChild: true,
1695
+ children
1696
+ }), /* @__PURE__ */ jsx(TooltipPrimitive.Portal, {
1697
+ container: container ?? void 0,
1698
+ children: /* @__PURE__ */ jsxs(TooltipPrimitive.Content, {
1699
+ side,
1700
+ sideOffset: 0,
1701
+ className: "oxy-shell-scope oxy-shell-tooltip",
1702
+ children: [content, /* @__PURE__ */ jsx(TooltipPrimitive.Arrow, { className: "oxy-shell-tooltip__arrow" })]
1703
+ })
1704
+ })] })
1705
+ });
1706
+ }
1707
+
1708
+ //#endregion
1709
+ //#region src/shell/ShellRail.tsx
1710
+ function RailEntry({ item }) {
1711
+ const [failedUrl, setFailedUrl] = useState(null);
1712
+ const content = item.imageUrl && failedUrl !== item.imageUrl ? /* @__PURE__ */ jsx("img", {
1713
+ src: item.imageUrl,
1714
+ alt: "",
1715
+ onError: () => setFailedUrl(item.imageUrl ?? null),
1716
+ className: "oxy-rail__item-img"
1717
+ }) : item.icon ?? /* @__PURE__ */ jsx("span", {
1718
+ className: "oxy-rail__item-letter",
1719
+ children: item.letter
1720
+ });
1721
+ const className = cx("oxy-rail__item", item.active && "oxy-rail__item--active");
1722
+ const ariaCurrent = item.active ? "page" : void 0;
1723
+ const entry = item.href ? /* @__PURE__ */ jsx("a", {
1724
+ href: item.href,
1725
+ "data-testid": item.testId,
1726
+ "aria-label": item.label,
1727
+ "aria-current": ariaCurrent,
1728
+ className,
1729
+ children: content
1730
+ }) : /* @__PURE__ */ jsx("button", {
1731
+ type: "button",
1732
+ onClick: item.onSelect,
1733
+ "data-testid": item.testId,
1734
+ "aria-label": item.label,
1735
+ "aria-current": ariaCurrent,
1736
+ className,
1737
+ children: content
1738
+ });
1739
+ return /* @__PURE__ */ jsx(ShellTooltip, {
1740
+ content: item.tooltip ?? item.label,
1741
+ children: entry
1742
+ });
1743
+ }
1744
+ /** A hairline divider between conceptual rail groups. */
1745
+ function RailDivider() {
1746
+ return /* @__PURE__ */ jsx("div", { className: "oxy-rail__divider" });
1747
+ }
1748
+ /** The icon rail. `groups` are the conceptual sections (HQ · Apps ·
1749
+ * Intelligence), rendered top-down with a hairline divider between them.
1750
+ * `footerItems` pin to the bottom of the scroll area as a distinct
1751
+ * "system" zone (Oxygen Factory), above the `bottom` account slot
1752
+ * (workspace switch · user menu — injected by the host app). */
1753
+ function ShellRail({ top, groups, footerItems, bottom, className }) {
1754
+ const { container, setContainer } = useShellPortalContainer();
1755
+ return /* @__PURE__ */ jsx(ShellPortalContext.Provider, {
1756
+ value: container,
1757
+ children: /* @__PURE__ */ jsxs("div", {
1758
+ ref: setContainer,
1759
+ "data-testid": "shell-rail",
1760
+ className: cx("oxy-shell-scope oxy-rail", className),
1761
+ children: [
1762
+ top && /* @__PURE__ */ jsx("div", {
1763
+ className: "oxy-rail__top",
1764
+ children: top
1765
+ }),
1766
+ /* @__PURE__ */ jsxs("div", {
1767
+ className: "oxy-rail__nav",
1768
+ children: [groups.map((group, i) => /* @__PURE__ */ jsxs(Fragment, { children: [i > 0 && /* @__PURE__ */ jsx(RailDivider, {}), group.map((item) => /* @__PURE__ */ jsx(RailEntry, { item }, item.key))] }, group[0]?.key ?? i)), footerItems && footerItems.length > 0 && /* @__PURE__ */ jsxs("div", {
1769
+ className: "oxy-rail__footer",
1770
+ children: [/* @__PURE__ */ jsx(RailDivider, {}), footerItems.map((item) => /* @__PURE__ */ jsx(RailEntry, { item }, item.key))]
1771
+ })]
1772
+ }),
1773
+ bottom && /* @__PURE__ */ jsx("div", {
1774
+ className: "oxy-rail__bottom",
1775
+ children: bottom
1776
+ })
1777
+ ]
1778
+ })
1779
+ });
1780
+ }
1781
+
1782
+ //#endregion
1783
+ //#region src/shell/shellContext.ts
1784
+ /**
1785
+ * Fetch the shell bootstrap payload for the current customer app. Must be
1786
+ * called inside `<OxyAppProvider>`. Failure is non-fatal by design: the
1787
+ * shell degrades to chrome-less rendering (older servers don't have the
1788
+ * endpoint), so errors are surfaced on the result, never thrown.
1789
+ */
1790
+ function useShellContext() {
1791
+ const { projectId, fetcher } = useOxyApp();
1792
+ const [state, setState] = React.useState({
1793
+ data: null,
1794
+ loading: true,
1795
+ error: null
1796
+ });
1797
+ React.useEffect(() => {
1798
+ if (!projectId) {
1799
+ setState({
1800
+ data: null,
1801
+ loading: false,
1802
+ error: /* @__PURE__ */ new Error("no projectId resolved")
1803
+ });
1804
+ return;
1805
+ }
1806
+ let cancelled = false;
1807
+ fetcher(`/api/projects/${projectId}/shell-context`, { method: "GET" }).then(async (resp) => {
1808
+ if (!resp.ok) throw new Error(`shell-context failed: HTTP ${resp.status}`);
1809
+ const data = await resp.json();
1810
+ if (!cancelled) setState({
1811
+ data,
1812
+ loading: false,
1813
+ error: null
1814
+ });
1815
+ }).catch((e) => {
1816
+ const error = e instanceof Error ? e : new Error(String(e));
1817
+ getOxyAppLogger().log("warn", "shell-context unavailable, rendering without chrome", { error: error.message });
1818
+ if (!cancelled) setState({
1819
+ data: null,
1820
+ loading: false,
1821
+ error
1822
+ });
1823
+ });
1824
+ return () => {
1825
+ cancelled = true;
1826
+ };
1827
+ }, [projectId, fetcher]);
1828
+ return state;
1829
+ }
1830
+
1831
+ //#endregion
1832
+ //#region src/shell/TopBar.tsx
1833
+ /** Frame only: left content + right cluster. Mirrors the rail's
1834
+ * `--sidebar-background` so the two read as one 48px-tall frame. */
1835
+ function TopBar({ left, right, className }) {
1836
+ const { container, setContainer } = useShellPortalContainer();
1837
+ return /* @__PURE__ */ jsx(ShellPortalContext.Provider, {
1838
+ value: container,
1839
+ children: /* @__PURE__ */ jsxs("header", {
1840
+ ref: setContainer,
1841
+ "data-testid": "workspace-topbar",
1842
+ className: cx("oxy-shell-scope oxy-topbar", className),
1843
+ children: [left, /* @__PURE__ */ jsx("div", {
1844
+ className: "oxy-topbar__right",
1845
+ children: right
1846
+ })]
1847
+ })
1848
+ });
1849
+ }
1850
+ /**
1851
+ * "<Workspace> / <Page>" — e.g. "Poke House / HQ". The workspace name links
1852
+ * back to the HQ home; pass `onHomeNavigate` to intercept for SPA routing
1853
+ * (the anchor still carries `homeHref` for middle-click/new-tab).
1854
+ */
1855
+ function Breadcrumb({ workspaceLabel, pageLabel, homeHref, onHomeNavigate }) {
1856
+ return /* @__PURE__ */ jsxs("div", {
1857
+ className: "oxy-breadcrumb",
1858
+ children: [
1859
+ /* @__PURE__ */ jsx("a", {
1860
+ href: homeHref,
1861
+ "data-testid": "topbar-workspace-link",
1862
+ className: "oxy-breadcrumb__workspace",
1863
+ onClick: onHomeNavigate ? (e) => {
1864
+ if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0) return;
1865
+ e.preventDefault();
1866
+ onHomeNavigate();
1867
+ } : void 0,
1868
+ children: workspaceLabel
1869
+ }),
1870
+ /* @__PURE__ */ jsx("span", {
1871
+ className: "oxy-breadcrumb__sep",
1872
+ children: "/"
1873
+ }),
1874
+ /* @__PURE__ */ jsx("span", {
1875
+ className: "oxy-breadcrumb__page",
1876
+ children: pageLabel
1877
+ })
1878
+ ]
1879
+ });
1880
+ }
1881
+ /** `navigator.onLine` + online/offline events. */
1882
+ function useOnlineStatus() {
1883
+ const [online, setOnline] = useState(() => typeof navigator === "undefined" ? true : navigator.onLine);
1884
+ useEffect(() => {
1885
+ const up = () => setOnline(true);
1886
+ const down = () => setOnline(false);
1887
+ window.addEventListener("online", up);
1888
+ window.addEventListener("offline", down);
1889
+ return () => {
1890
+ window.removeEventListener("online", up);
1891
+ window.removeEventListener("offline", down);
1892
+ };
1893
+ }, []);
1894
+ return online;
1895
+ }
1896
+ /**
1897
+ * Universal "Sys: Connected" indicator — a pulsing green dot when the browser
1898
+ * is online, red when offline. Presentational connectivity signal; a real
1899
+ * backend health ping can replace `useOnlineStatus` later without touching
1900
+ * consumers.
1901
+ */
1902
+ function SystemIndicator() {
1903
+ const online = useOnlineStatus();
1904
+ return /* @__PURE__ */ jsxs("span", {
1905
+ "data-testid": "topbar-system",
1906
+ className: "oxy-shell-scope oxy-sysind",
1907
+ children: [/* @__PURE__ */ jsx("span", { children: "Sys:" }), /* @__PURE__ */ jsxs("span", {
1908
+ className: cx("oxy-sysind__state", online ? "oxy-sysind__state--online" : "oxy-sysind__state--offline"),
1909
+ children: [/* @__PURE__ */ jsxs("span", {
1910
+ className: "oxy-sysind__dot-wrap",
1911
+ children: [online && /* @__PURE__ */ jsx("span", { className: "oxy-sysind__ping" }), /* @__PURE__ */ jsx("span", { className: cx("oxy-sysind__dot", online ? "oxy-sysind__dot--online" : "oxy-sysind__dot--offline") })]
1912
+ }), online ? "Connected" : "Offline"]
1913
+ })]
1914
+ });
1915
+ }
1916
+ /**
1917
+ * Live clock in the given IANA timezone (viewer-local when omitted or
1918
+ * invalid). Updates each half-minute (minute precision), pausing while the
1919
+ * tab is hidden.
1920
+ */
1921
+ function WorkspaceClock({ timezone }) {
1922
+ const [now, setNow] = useState(() => /* @__PURE__ */ new Date());
1923
+ useEffect(() => {
1924
+ const update = () => setNow(/* @__PURE__ */ new Date());
1925
+ const id = setInterval(() => {
1926
+ if (!document.hidden) update();
1927
+ }, 3e4);
1928
+ const onVisible = () => {
1929
+ if (!document.hidden) update();
1930
+ };
1931
+ document.addEventListener("visibilitychange", onVisible);
1932
+ return () => {
1933
+ clearInterval(id);
1934
+ document.removeEventListener("visibilitychange", onVisible);
1935
+ };
1936
+ }, []);
1937
+ return /* @__PURE__ */ jsx("span", {
1938
+ "data-testid": "topbar-clock",
1939
+ className: "oxy-shell-scope oxy-clock",
1940
+ children: useMemo(() => {
1941
+ try {
1942
+ return new Intl.DateTimeFormat(void 0, {
1943
+ timeZone: timezone,
1944
+ hour: "numeric",
1945
+ minute: "2-digit",
1946
+ timeZoneName: timezone ? "short" : void 0
1947
+ }).format(now);
1948
+ } catch {
1949
+ return new Intl.DateTimeFormat(void 0, {
1950
+ hour: "numeric",
1951
+ minute: "2-digit"
1952
+ }).format(now);
1953
+ }
1954
+ }, [now, timezone])
1955
+ });
1956
+ }
1957
+
1958
+ //#endregion
1959
+ //#region src/shell/WorkspaceTile.tsx
1960
+ /** Rail-top workspace identity: the org/code-first logo when present, the
1961
+ * name initial otherwise. Pure branding — switching workspaces is the host
1962
+ * app's concern (the rail `bottom` slot). */
1963
+ function WorkspaceTile({ name, logoUrl }) {
1964
+ const [failedUrl, setFailedUrl] = useState(null);
1965
+ return /* @__PURE__ */ jsx("span", {
1966
+ "data-testid": "rail-workspace",
1967
+ title: name,
1968
+ className: "oxy-shell-scope oxy-workspace-tile",
1969
+ children: !!logoUrl && failedUrl !== logoUrl ? /* @__PURE__ */ jsx("img", {
1970
+ src: logoUrl,
1971
+ alt: name,
1972
+ onError: () => setFailedUrl(logoUrl ?? null),
1973
+ className: "oxy-workspace-tile__img"
1974
+ }) : /* @__PURE__ */ jsx("span", {
1975
+ className: "oxy-workspace-tile__letter",
1976
+ children: name.slice(0, 1).toUpperCase()
1977
+ })
1978
+ });
1979
+ }
1980
+
1981
+ //#endregion
1982
+ //#region src/shell/OxyShell.tsx
1983
+ /** Home/HQ glyph (lucide "house" outline, inlined). */
1984
+ function HouseIcon() {
1985
+ return /* @__PURE__ */ jsxs("svg", {
1986
+ width: "16",
1987
+ height: "16",
1988
+ viewBox: "0 0 24 24",
1989
+ fill: "none",
1990
+ stroke: "currentColor",
1991
+ strokeWidth: "2",
1992
+ strokeLinecap: "round",
1993
+ strokeLinejoin: "round",
1994
+ "aria-hidden": "true",
1995
+ children: [/* @__PURE__ */ jsx("path", { d: "M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8" }), /* @__PURE__ */ jsx("path", { d: "M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" })]
1996
+ });
1997
+ }
1998
+ /** Chat glyph (lucide "messages-square" outline, inlined). */
1999
+ function MessagesIcon() {
2000
+ return /* @__PURE__ */ jsxs("svg", {
2001
+ width: "16",
2002
+ height: "16",
2003
+ viewBox: "0 0 24 24",
2004
+ fill: "none",
2005
+ stroke: "currentColor",
2006
+ strokeWidth: "2",
2007
+ strokeLinecap: "round",
2008
+ strokeLinejoin: "round",
2009
+ "aria-hidden": "true",
2010
+ children: [/* @__PURE__ */ jsx("path", { d: "M16 10a2 2 0 0 1-2 2H6l-4 4V4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" }), /* @__PURE__ */ jsx("path", { d: "M20 9a2 2 0 0 1 2 2v11l-4-4h-6a2 2 0 0 1-2-2v-1" })]
2011
+ });
2012
+ }
2013
+ /** Rewrite a shell-context relative product path to an absolute URL against
2014
+ * the oxy origin, when one is configured. `/api/*` paths are left relative
2015
+ * — they ride the host's same-origin fetch / dev proxy (which attaches
2016
+ * auth), so absolutizing them would drop the credentials the proxy adds. */
2017
+ function makeAbs(base) {
2018
+ const origin = base?.replace(/\/$/, "");
2019
+ return (url) => {
2020
+ if (!url) return void 0;
2021
+ if (!origin || !url.startsWith("/") || url.startsWith("/api/")) return url;
2022
+ return `${origin}${url}`;
2023
+ };
2024
+ }
2025
+ function buildRailGroups(data, currentAppSlug, abs) {
2026
+ const hq = {
2027
+ key: "hq",
2028
+ label: "HQ",
2029
+ testId: "rail-hq",
2030
+ icon: /* @__PURE__ */ jsx(HouseIcon, {}),
2031
+ href: abs(data.links.home)
2032
+ };
2033
+ const chat = {
2034
+ key: "chat",
2035
+ label: "Chat",
2036
+ testId: "rail-chat",
2037
+ icon: /* @__PURE__ */ jsx(MessagesIcon, {}),
2038
+ href: abs(data.links.threads)
2039
+ };
2040
+ const appItems = data.apps.map((app) => ({
2041
+ key: app.id,
2042
+ label: app.name,
2043
+ testId: `rail-app-${app.slug}`,
2044
+ letter: app.name.slice(0, 1).toUpperCase(),
2045
+ imageUrl: abs(app.icon_url),
2046
+ active: app.slug === currentAppSlug,
2047
+ href: abs(app.url)
2048
+ }));
2049
+ return appItems.length ? [[hq, chat], appItems] : [[hq, chat]];
2050
+ }
2051
+ /**
2052
+ * The workspace chrome around a customer app: icon rail + universal top bar
2053
+ * + content column — visually identical to the main web-app shell.
2054
+ */
2055
+ function OxyShell({ children, pageLabel, topBarLeft, topBarExtra, railBottom, hideTopBar, askHotkey = true, productBaseUrl, className, chromeBackground, chromeForeground }) {
2056
+ const abs = makeAbs(productBaseUrl);
2057
+ const chromeStyle = {
2058
+ ...chromeBackground ? { "--sidebar-background": chromeBackground } : {},
2059
+ ...chromeForeground ? {
2060
+ "--foreground": chromeForeground,
2061
+ "--muted-foreground": `color-mix(in srgb, ${chromeForeground} 65%, transparent)`
2062
+ } : {}
2063
+ };
2064
+ const { appSlug } = useOxyApp();
2065
+ const { manifest } = useResolvedManifest();
2066
+ const { data, loading } = useShellContext();
2067
+ const { container, setContainer } = useShellPortalContainer();
2068
+ const degraded = !loading && !data;
2069
+ const effectiveSlug = appSlug || manifest.slug;
2070
+ const currentApp = data?.apps.find((app) => app.slug === effectiveSlug);
2071
+ const workspaceLabel = data ? data.org.name || data.workspace.name : "";
2072
+ const page = pageLabel ?? currentApp?.name ?? manifest.name ?? effectiveSlug ?? "";
2073
+ const askAgent = currentApp?.default_agent ?? manifest.ask?.agent ?? void 0;
2074
+ const askSuggestions = currentApp?.suggested_questions?.length ? currentApp.suggested_questions : manifest.ask?.suggestedQuestions ?? [];
2075
+ const [askOpen, setAskOpen] = useState(false);
2076
+ useEffect(() => {
2077
+ if (!askAgent || !askHotkey) return;
2078
+ const onKey = (e) => {
2079
+ if (e.key.toLowerCase() !== "k" || !(e.metaKey || e.ctrlKey)) return;
2080
+ e.preventDefault();
2081
+ setAskOpen((o) => !o);
2082
+ };
2083
+ window.addEventListener("keydown", onKey);
2084
+ return () => window.removeEventListener("keydown", onKey);
2085
+ }, [askAgent, askHotkey]);
2086
+ const bottom = railBottom;
2087
+ return /* @__PURE__ */ jsx(ShellPortalContext.Provider, {
2088
+ value: container,
2089
+ children: /* @__PURE__ */ jsxs("div", {
2090
+ ref: setContainer,
2091
+ className: cx("oxy-shell-scope oxy-shell", degraded && "oxy-shell--degraded", className),
2092
+ style: chromeStyle,
2093
+ children: [
2094
+ !degraded && /* @__PURE__ */ jsx(ShellRail, {
2095
+ top: data ? /* @__PURE__ */ jsx(WorkspaceTile, {
2096
+ name: data.workspace.name,
2097
+ logoUrl: data.logo_url ?? void 0
2098
+ }) : /* @__PURE__ */ jsx("span", {
2099
+ className: "oxy-workspace-tile",
2100
+ children: /* @__PURE__ */ jsx(OxyMark, { className: "oxy-rail__item-img" })
2101
+ }),
2102
+ groups: data ? buildRailGroups(data, effectiveSlug, abs) : [[]],
2103
+ bottom
2104
+ }),
2105
+ /* @__PURE__ */ jsxs("div", {
2106
+ className: "oxy-shell__content-col",
2107
+ children: [!hideTopBar && (!degraded || topBarLeft || topBarExtra) && /* @__PURE__ */ jsx(TopBar, {
2108
+ left: topBarLeft ?? (data ? /* @__PURE__ */ jsx(Breadcrumb, {
2109
+ workspaceLabel,
2110
+ pageLabel: page,
2111
+ homeHref: abs(data.links.home) ?? data.links.home
2112
+ }) : void 0),
2113
+ right: /* @__PURE__ */ jsxs(Fragment$1, { children: [
2114
+ /* @__PURE__ */ jsx(SystemIndicator, {}),
2115
+ /* @__PURE__ */ jsx(WorkspaceClock, {}),
2116
+ topBarExtra,
2117
+ askAgent && !degraded && /* @__PURE__ */ jsxs("button", {
2118
+ type: "button",
2119
+ "data-testid": "ask-oxygen-button",
2120
+ onClick: () => setAskOpen((o) => !o),
2121
+ className: cx("oxy-askbtn", askOpen && "oxy-askbtn--open"),
2122
+ children: [
2123
+ /* @__PURE__ */ jsx(OxyMark, { className: "oxy-askbtn__mark" }),
2124
+ /* @__PURE__ */ jsx("span", {
2125
+ className: "oxy-askbtn__label",
2126
+ children: "Ask Oxygen"
2127
+ }),
2128
+ /* @__PURE__ */ jsx("kbd", {
2129
+ className: "oxy-askbtn__kbd",
2130
+ children: "⌘K"
2131
+ })
2132
+ ]
2133
+ })
2134
+ ] })
2135
+ }), /* @__PURE__ */ jsx("div", {
2136
+ className: "oxy-shell__main",
2137
+ children
2138
+ })]
2139
+ }),
2140
+ askAgent && !degraded && /* @__PURE__ */ jsx(AskDock, {
2141
+ agentId: askAgent,
2142
+ open: askOpen,
2143
+ onClose: () => setAskOpen(false),
2144
+ workspaceName: data?.workspace.name,
2145
+ suggestions: askSuggestions,
2146
+ threadsHref: abs(data?.links.threads)
2147
+ })
2148
+ ]
2149
+ })
2150
+ });
2151
+ }
2152
+
2153
+ //#endregion
2154
+ export { AnswerChart, AskDock, Breadcrumb, OXY_MARK_PATH, OxyMark, OxyShell, OxygenFactoryMark, ReasoningTrace, ShellRail, ShellTooltip, SystemIndicator, TopBar, WorkspaceClock, WorkspaceTile, buildTraceSteps, fetchThreadTranscript, useShellContext, useThreadHistory, workspaceLogoUrl };
2155
+ //# sourceMappingURL=shell.mjs.map