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