@streamoid/agent 0.6.52 → 0.6.54

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/README.md CHANGED
@@ -53,10 +53,137 @@ tank`); `title` keeps its original identity. Preserve proper names, acronyms and
53
53
 
54
54
  Embedded and standalone chat use `ScComposerSurface` and `ScSuggestionRow` from
55
55
  `@streamoid/ui` 0.6.86+. An empty composer starts with three 20px text lines and
56
- grows to 170px before scrolling. Suggestions are native buttons in flat rows
57
- with hairline separators, and fill the composer without navigating or sending.
56
+ grows to 170px before scrolling. Suggestions are native buttons in a stack of
57
+ cards (the DS row spaces and borders itself), and fill the composer without
58
+ navigating or sending. A panel scoped to an app shows exactly three when three
59
+ are available (see `AgentScopeAdapter.sections` in `index.d.ts`).
58
60
 
59
- The secondary line shows `meta.outcome_summary` (falling back to
60
- `meta.expected_output`), optionally followed by authored `meta.ceiling_minutes`
61
+ The secondary line shows `meta.summary` — one outcome-first sentence — falling
62
+ back to `meta.outcome_summary`, then `meta.expected_output` (which stays the
63
+ agent's spec), optionally followed by authored `meta.ceiling_minutes`
61
64
  and `meta.estimated_credits`. Unknown estimates are omitted. Scoped suggestions
62
65
  use `deliverable` and `ceilingMinutes` from the host's outcome catalogue.
66
+
67
+
68
+ ## The Ask CXO side pane — `@streamoid/agent/pane`
69
+
70
+ Every product app shows the chat the same way: rail | **pane** | page. The
71
+ shell for it ships here, so the apps stop hand-building it around
72
+ `AgentPanel` (they had drifted on width, unmount-on-close, workspace switch,
73
+ Escape, mobile and persistence).
74
+
75
+ `@streamoid/agent/pane` is a **light entry** (~16kB, React and
76
+ `@streamoid/ui` are yours). It never imports the 5MB chat statically — the
77
+ chat is `import()`ed when the pane first opens (or on `prefetch`), so import
78
+ the pane eagerly in your shell.
79
+
80
+ ```tsx
81
+ import { useMemo } from "react";
82
+ import { AgentSidePane, useAgentPaneController, type AgentSidePaneAdapters } from "@streamoid/agent/pane";
83
+
84
+ export function Shell() {
85
+ const pane = useAgentPaneController({
86
+ workspaceId, // persisted per workspace
87
+ legacyWidthKey: "artifax.assistant.width", // your old width key, migrated once
88
+ // persistKey: "artifax-assistant-session", // keep an existing session key
89
+ // bindShortcut: false, // if ⌘K is bound elsewhere
90
+ });
91
+
92
+ const adapters = useMemo<AgentSidePaneAdapters>(() => ({
93
+ auth, workspace: { current: { id: workspaceId, name } }, theme: { mode },
94
+ config: { apiBase, hide: { sidepane: true, newBrief: true, notifications: true }, widgetMode: "inline_ask" },
95
+ }), [/* … */]); // memoise it
96
+
97
+ return (
98
+ <div style={{ display: "flex", height: "100vh" }}>
99
+ <Rail
100
+ askActive={pane.open} // the trigger's `active`
101
+ onAsk={pane.askCxo} // open + focus the composer (never a toggle)
102
+ onAskIntent={pane.prefetch} // hover / focus
103
+ onHelp={() => pane.requestSupport({ currentPage })}
104
+ />
105
+ <AgentSidePane controller={pane} adapters={adapters}
106
+ hostContext={hostContext} scope={scope} support={{ appVersion }} canvas={canvas} />
107
+ {/* An artefact takes the page: hide main (display:none, not unmount). */}
108
+ <main style={{ flex: 1, minWidth: 0, display: pane.surface === "fullscreen" ? "none" : undefined }}>
109
+ <Outlet context={{ askAbout: pane.askAbout }} />
110
+ </main>
111
+ </div>
112
+ );
113
+ }
114
+ ```
115
+
116
+ What the pane does, identically in every app:
117
+
118
+ - **Geometry.** Docked at ≥768px: in flow beside the page, width
119
+ `SC_ASSISTANT_PANEL_WIDTH` (**510px**, from `@streamoid/ui`) within
120
+ `SC_ASSISTANT_PANEL_BOUNDS` (360–900), resizable with `ScPanelResizeHandle`,
121
+ persisted under `streamoid.askPane.width`. Below 768px: a full-screen sheet
122
+ (`role="dialog"`, focus trap, siblings `inert`, focus restored on close).
123
+ Geometry is inline style, so no utility class from either stylesheet can
124
+ move it.
125
+ - **Fullscreen.** `onSurfaceChange` reporting a non-text artefact widens the
126
+ pane over the page (`controller.surface === "fullscreen"` — hide your
127
+ `main`). Escape returns to the side pane and that artefact is not widened
128
+ again while it stays open (also across a close and reopen); a different one
129
+ still is. A pane closed over a live artefact — or one an artefact opened in
130
+ while closed — reopens fullscreen. No expand button.
131
+ - **Stays mounted** after the first open; closing hides it, so an answer still
132
+ streaming keeps going. Before the first open nothing loads.
133
+ - **Persistence.** Open state and the conversation, per workspace, survive a
134
+ reload (fullscreen comes back as the side pane). A workspace change closes
135
+ the pane, drops the old conversation, **remounts the chat** (no draft,
136
+ upload or selection chip carries over) and loads the new workspace's.
137
+ - **Escape** (bubble phase): docked, it closes only when focus is inside the
138
+ pane, no nested dialog holds the key, and nothing `preventDefault`ed it.
139
+ Fullscreen collapses to the side pane first. On the sheet, focus need not be
140
+ inside (the page is inert).
141
+ - **Back button** (phone): opening the sheet pushes one history entry (same
142
+ URL, your router's state kept); Back closes the sheet. Closing with × or
143
+ Escape removes the entry, so no stray Back is left behind.
144
+ - **Errors.** A pane-local error boundary: "Ask CXO couldn’t load" + Retry
145
+ (re-imports). Loading shows "Loading Ask CXO…".
146
+ - **Header** is the chat's own (title + context, New task, History, ×);
147
+ × calls `controller.close`.
148
+
149
+ Controller calls: `reveal()` / `close()` / `toggle()`, `askCxo()`,
150
+ `askAbout(prompt)` (new conversation + send, via `enqueueChatPrompt`),
151
+ `requestSupport({ currentPage }?)` (Get help home via `showAgentSupport`),
152
+ `prefetch()`, `width` / `setWidth` / `commitWidth`, and
153
+ `selectionSnapshot` — the selection the pane **holds** (its snapshot of
154
+ `scope.selection`: taken on open, on New task and on `askCxo` with no chat;
155
+ cleared when a chat opens or the chip is removed). Count or split suggestions
156
+ on that, with the pane's own rule: `paneSuggestionCount(sections,
157
+ pane.selectionSnapshot)` / `deriveScopeSuggestions(...)`, both exported here.
158
+ A host mounting `AgentPanel` directly gets the same value from
159
+ `adapters.onSelectionSnapshot`.
160
+
161
+ **Stylesheet.** Import the chat's CSS into its own cascade layer so its
162
+ compiled utilities cannot beat yours (the Catalogix / Photogenix pattern):
163
+
164
+ ```css
165
+ @import "@streamoid/agent/style.css" layer(streamoid-agent);
166
+ ```
167
+
168
+ ### Migrating each app (do it in the app's `@streamoid/agent` 0.6.54 bump PR)
169
+
170
+ **Artifax** (`apps/artifax/src/components/{AppShell,AssistantPanel}.tsx`)
171
+ - `useAgentPaneController({ workspaceId: workspaceID, persistKey: "artifax-assistant-session", legacyWidthKey: "artifax.assistant.width" })` in `AppShell`; render `<AgentSidePane>` in place of `AssistantPanel`, keeping its adapters/hostContext/scope.
172
+ - Delete `lib/assistantSurface.ts` (reducer + tests) and `lib/assistantSession.ts`, the shell's Escape effect, `prefetchAgentChunk`, `askCxo`, `requestSupport`, the `useStreamoidSearchShortcut` call. `main` hides on `pane.surface === "fullscreen"` (as now). `onAskCxoRequest(pane.askCxo)` stays.
173
+ - Delete `usePaneSelectionSnapshot` / `noteAssistantComposerFocus` (`lib/assistantScope.ts`) and the test copy `src/test/paneSuggestions.ts`; use `pane.selectionSnapshot` and `paneSuggestionCount` / `deriveScopeSuggestions`.
174
+ - Default width stays 510; a stored width migrates.
175
+
176
+ **Tactix** (`apps/tactix/src/components/{TactixShell,AssistantPanel}.tsx`)
177
+ - `useAgentPaneController({ workspaceId, legacyWidthKey: "tactix.assistant.width" })` in `TactixShell`; `<AgentSidePane>` replaces `AssistantPanel`. Outlet context `askCxoAbout` becomes `pane.askAbout`.
178
+ - Delete the shell's `askCxo` / `askCxoAbout` / `requestSupport` / `prefetchAgent` and the panel's unguarded Escape effect. Hide `main` on fullscreen (new: artefacts now widen). Gains: mobile sheet, persistence, workspace reset.
179
+ - Default width 480 → 510 (DS constant) for users who never resized.
180
+
181
+ **Catalogix** (`dashboard/app/containers/AskCxo/`)
182
+ - `useAgentPaneController({ workspaceId, legacyWidthKey: "catalogix.ask.width" })` where `useAppStore`'s ask slice is read; `<AgentSidePane>` replaces `AskCxo/index.jsx` + `AskCxoPanel.jsx`. The rail's trigger/⌘K/Help call the controller (pass `bindShortcut: false` if ⌘K stays bound in `LeftMenu`).
183
+ - Delete `askOpen` / `askChatId` from `useAppStore` (and the workspace-switch reset on its setter — the controller does it), `askCxo.js`'s `importAgent` / `prefetchAgent` / `askCxo`. Hide the page on fullscreen (new). Gains: persistence, Back button.
184
+ - Default width 480 → 510.
185
+
186
+ **Photogenix** (`dashboard/client/src/features/ask/`)
187
+ - `useAgentPaneController({ workspaceId, legacyWidthKey: "photogenix.ask.width" })` in the shell; `<AgentSidePane>` replaces `AskPane.tsx` + `AskCxoPanel.tsx`; `useAskConversation.ts` goes (the controller owns `chatId`).
188
+ - Delete the pane's own sheet/focus-trap/Escape code. Hide the page on fullscreen (new). Gains: stays mounted (no SSE abort on close), persistence, Back button.
189
+ - Default width 480 → 510.
@@ -0,0 +1,114 @@
1
+ import { resolveStreamoidProduct as b, streamoidProductLabel as k } from "@streamoid/ui";
2
+ function a(e) {
3
+ if (typeof e != "string" || !e.trim()) return null;
4
+ const n = b(e.trim());
5
+ return n && n !== "cxo" ? n : null;
6
+ }
7
+ function d(e) {
8
+ return e && k(e) || "All apps";
9
+ }
10
+ function $(e, n) {
11
+ if (e === null || e !== a(n?.app)) return null;
12
+ const i = (Array.isArray(n?.breadcrumbs) ? n.breadcrumbs.filter((t) => typeof t == "string" && !!t.trim()).map((t) => t.trim()) : []).join(" › ") || (typeof n?.page == "string" ? n.page.trim() : "");
13
+ return i ? `${d(e)} · ${i}` : d(e);
14
+ }
15
+ function p(e, n) {
16
+ return !e.kinds || e.kinds.length === 0 || n === void 0 ? !0 : e.kinds.map((r) => r.toLowerCase()).includes(n.toLowerCase());
17
+ }
18
+ function h(e) {
19
+ return e.charAt(0).toUpperCase() + e.slice(1);
20
+ }
21
+ function m(e, n) {
22
+ const r = e.instruction?.includes("{subject}") ? e.instruction : `${e.label}: {subject}`;
23
+ return h(r.replace(/\{subject\}/g, n).trim());
24
+ }
25
+ function w(e, n) {
26
+ const r = [];
27
+ for (const i of e ?? []) {
28
+ if (!i?.outcomes?.length) continue;
29
+ const t = (i.samples ?? []).find(
30
+ (u) => i.outcomes.some((f) => p(f, u.kind))
31
+ ), c = (u, f) => i.outcomes.find((l) => l.key === f && p(l, u)) ?? i.outcomes.find((l) => p(l, u)), s = n ? c(n.kind, t?.primaryOutcomeKey) : void 0;
32
+ if (n && s) {
33
+ r.push({
34
+ id: `${i.key}:${s.key}:selection`,
35
+ sectionKey: i.key,
36
+ outcomeKey: s.key,
37
+ subject: "selection",
38
+ text: m(s, n.subject || n.label),
39
+ ceilingMinutes: s.ceilingMinutes,
40
+ deliverable: s.deliverable
41
+ });
42
+ continue;
43
+ }
44
+ if (!t) continue;
45
+ const o = c(t.kind, t.primaryOutcomeKey);
46
+ o && r.push({
47
+ id: `${i.key}:${o.key}:${t.id}`,
48
+ sectionKey: i.key,
49
+ outcomeKey: o.key,
50
+ sampleId: t.id,
51
+ sampleTitle: t.subject || t.title,
52
+ subject: "sample",
53
+ text: m(o, t.subject || t.title),
54
+ ceilingMinutes: o.ceilingMinutes,
55
+ deliverable: o.deliverable,
56
+ imageUrl: t.imageUrl ?? null
57
+ });
58
+ }
59
+ return r;
60
+ }
61
+ function O(e, n) {
62
+ return n ? e.length === 0 ? !1 : e.every((r) => a(r) === n) : !0;
63
+ }
64
+ const v = 3;
65
+ function A(e, n, r = v) {
66
+ const i = (s) => (s.value ?? s.title).trim().toLowerCase(), t = e.slice(0, r), c = new Set(t.map(i));
67
+ for (const s of n) {
68
+ if (t.length >= r) break;
69
+ const o = i(s);
70
+ c.has(o) || (c.add(o), t.push(s));
71
+ }
72
+ return t;
73
+ }
74
+ function S(e) {
75
+ return ["headline", "subline", "placeholder"].filter(
76
+ (n) => !e?.[n]?.trim()
77
+ );
78
+ }
79
+ let g = !1;
80
+ function C() {
81
+ try {
82
+ return process.env.NODE_ENV === "development";
83
+ } catch {
84
+ return !1;
85
+ }
86
+ }
87
+ function L(e, n, r = C()) {
88
+ if (!r || g || !e) return !1;
89
+ const i = S(n);
90
+ return i.length === 0 ? !1 : (g = !0, console.warn(
91
+ `[@streamoid/agent] adapters.scope for "${e}" has no ${i.join(", ")}; the Ask CXO panel falls back to generic copy. Pass an app-specific question (headline), helper line (subline) and composer hint (placeholder).`
92
+ ), !0);
93
+ }
94
+ function y(e) {
95
+ return a(e.meta?.origin_app);
96
+ }
97
+ function K(e) {
98
+ const n = e.meta?.scope;
99
+ return n === "all" ? null : a(n) ?? y(e);
100
+ }
101
+ function M(e, n) {
102
+ return n ? e.filter((r) => y(r) === n) : [...e];
103
+ }
104
+ export {
105
+ M as a,
106
+ O as b,
107
+ K as c,
108
+ w as d,
109
+ $ as e,
110
+ A as f,
111
+ a as h,
112
+ d as s,
113
+ L as w
114
+ };
package/dist/index.d.ts CHANGED
@@ -50,10 +50,20 @@ export interface AgentHostContext {
50
50
  ids?: Record<string, string | number>;
51
51
  /** A short list of what is on screen — board names, selected items. */
52
52
  visible?: string[];
53
- /* The pane merges two keys of its own into what is sent, never replacing
54
- * the host's: `entry: "help_and_support"` on a conversation started from
55
- * the Get help home (`showAgentSupport`), and `app_version` from
56
- * `adapters.support.appVersion`. */
53
+ /* The pane merges keys of its own into what is sent, never replacing the
54
+ * host's: `app_version` from `adapters.support.appVersion` on every message;
55
+ * and on a conversation started from the Get help home (`showAgentSupport`)
56
+ * `entry: "help_and_support"`, `current_page` (from `showAgentSupport({
57
+ * currentPage })`), `browser` ("Chrome") and `details_shared: true`. If the
58
+ * user removes the home's "We'll include" chip, that conversation's
59
+ * messages carry only `entry`, `details_shared: false` and the routing keys
60
+ * `scope` and `app` (plus `selection` / `ids.selection` while the composer's
61
+ * selection chip is attached) — none of the host's other keys, nor the
62
+ * pane's. The backend latches the opt-out on the conversation and strips
63
+ * the details from every later turn itself, even after a remount. */
64
+ /** Pane-set on the Get help conversation: whether the user shared the
65
+ * "We'll include" details. Hosts do not set it. */
66
+ details_shared?: boolean;
57
67
  [key: string]: unknown;
58
68
  }
59
69
 
@@ -73,9 +83,9 @@ export interface AgentSupportAdapter {
73
83
  /**
74
84
  * The host app's own build version, e.g. `0.0.61`. Sent as
75
85
  * `hostContext.app_version` with EVERY message — support or not — so a
76
- * problem report names the build it came from. Also shown as a chip on the
77
- * Get help home's "We'll include" line. A `hostContext.app_version` the host
78
- * passes itself wins over this.
86
+ * problem report names the build it came from. Also shown in the Get help
87
+ * home's "We'll include" chip. A `hostContext.app_version` the host passes
88
+ * itself wins over this.
79
89
  */
80
90
  appVersion?: string;
81
91
  }
@@ -154,6 +164,19 @@ export interface AgentChatShellAdapters {
154
164
  * Fires only on change, and only for the conversation on screen.
155
165
  */
156
166
  onSurfaceChange?: (state: AgentSurfaceState) => void;
167
+ /**
168
+ * The selection the pane HOLDS — its snapshot of `scope.selection`, which its
169
+ * suggestions and composer chip are built from — reported whenever it
170
+ * changes, and once on mount.
171
+ *
172
+ * The pane does not follow `scope.selection` live: it snapshots it when it
173
+ * mounts, when the conversation goes back to none (New task), and on
174
+ * `focusAgentComposer()` while no conversation is open; it clears it when an
175
+ * existing conversation opens or the user removes the chip. A host that
176
+ * splits or counts its suggestions on the selection must use THIS value, not
177
+ * its live one. `AgentSidePane` wires it to `controller.selectionSnapshot`.
178
+ */
179
+ onSelectionSnapshot?: (selection: AgentScopeSelection | null) => void;
157
180
  /**
158
181
  * Supply the canvas as a component and the Dreamer panel drops its iframe:
159
182
  * no embedded page, so no cross-origin auth, no postMessage handshake, no
@@ -179,6 +202,16 @@ export interface AgentChatShellAdapters {
179
202
  * - `sections`: the app's outcome table. One suggestion per section, from
180
203
  * its first sample's primary outcome. Change the samples and the
181
204
  * suggestions follow. Omit it for the catalog briefs this app can run alone.
205
+ * The panel shows EXACTLY THREE suggested tasks when three exist: the
206
+ * sections' suggestions in table order (a fourth and later are left to
207
+ * "View all tasks"), topped up with the catalog briefs this app can run
208
+ * alone — those flagged for the empty chat first, by their authored rank,
209
+ * then the rest — never repeating one. Fewer only when fewer exist.
210
+ * - `headline`, `subline`, `placeholder`: the panel's app-specific question,
211
+ * helper line and composer hint. Pass all three: a scoped host that leaves
212
+ * any out gets one `console.warn` in a development build. Honoured at CXO
213
+ * home too (`hostContext.app` absent or `"cxo"`), where the defaults are
214
+ * the hub's own when they are absent.
182
215
  * - `selection`: what was selected when the pane opened — a removable chip
183
216
  * in the composer, and the subject suggestions name.
184
217
  * - `onSuggestion`: DEPRECATED AND NOT CALLED. Picking a suggestion always
@@ -254,13 +287,15 @@ export interface AgentScopeAdapter {
254
287
  sections?: AgentScopeSection[] | null;
255
288
  selection?: AgentScopeSelection | null;
256
289
  /** The line above the composer before the first turn, in the host's own
257
- * words. Absent or blank = "Describe what you want to create". */
290
+ * words — the app-specific question. Absent or blank = "Describe what you
291
+ * want to create" in an app, "Run your retail operations from here" at CXO
292
+ * home. */
258
293
  headline?: string | null;
259
- /** The quieter line under it. Absent or blank = "CXO will plan the steps
260
- * and pause for your approval." */
294
+ /** The quieter helper line under it. Absent or blank = "CXO will plan the
295
+ * steps and pause for your approval." in an app, none at CXO home. */
261
296
  subline?: string | null;
262
297
  /** The empty composer's hint before the first turn. Absent or blank =
263
- * "What needs to get done". */
298
+ * "What needs to get done". Honoured in an app and at CXO home alike. */
264
299
  placeholder?: string | null;
265
300
  /**
266
301
  * @deprecated NOT CALLED. A suggestion always fills the composer. Kept so
@@ -487,7 +522,16 @@ export declare function focusAgentComposer(): void;
487
522
  * Every message of the conversation that starts on the home carries
488
523
  * `hostContext.entry = "help_and_support"`, merged into the host's own
489
524
  * context, for as long as that conversation stays open — so follow-ups keep
490
- * routing to the support agent. Starting a new task, or opening another
525
+ * routing to the support agent.
526
+ *
527
+ * The home's "We'll include" line is ONE removable chip naming what the
528
+ * support conversation is sent: workspace, `options.currentPage`, version
529
+ * and browser, after the app (plain text, always shared — it routes the
530
+ * request). Removing it (✕) sends none of them — only `entry`,
531
+ * `details_shared: false` and the routing keys `scope` and `app` — until the
532
+ * user restores it or the home is opened again. The opt-out is persisted on
533
+ * the conversation server-side, which also leaves the workspace off the
534
+ * support email and the issue. Starting a new task, or opening another
491
535
  * conversation from history, leaves the mode.
492
536
  *
493
537
  * Queued like {@link enqueueChatPrompt}: the call may come before or after the
@@ -495,4 +539,142 @@ export declare function focusAgentComposer(): void;
495
539
  * empty one. Do not also set the panel's `chatId` to a new id — `null` (or
496
540
  * leaving it alone) is right; the pane navigates itself.
497
541
  */
498
- export declare function showAgentSupport(): void;
542
+ export declare function showAgentSupport(options?: ShowAgentSupportOptions | null): void;
543
+
544
+ /** Options for {@link showAgentSupport}. All optional; omit them entirely and
545
+ * the home behaves exactly as with no argument. */
546
+ export interface ShowAgentSupportOptions {
547
+ /**
548
+ * A short, human label for where the user was BEFORE opening help — the
549
+ * task or page they were on, e.g. `"SS26 range plan"` or `"Explore"`. Not a
550
+ * URL or a route (`hostContext.url`/`route` already carry those).
551
+ *
552
+ * Shown in the home's "We'll include" chip
553
+ * (`CXO · demo workspace · SS26 range plan · v0.0.61 · Chrome`) and sent as
554
+ * `hostContext.current_page` on every message of the support conversation,
555
+ * unless the user removes the chip. Cleared when the user leaves the home or
556
+ * its conversation (New task, another conversation); each call replaces it,
557
+ * and a call without it shows none.
558
+ */
559
+ currentPage?: string | null;
560
+ }
561
+
562
+ /* ── Ask CXO side pane (`@streamoid/agent/pane`) ───────────────────────────
563
+ *
564
+ * The TYPES of the shared side pane live here, beside the adapters they are
565
+ * built from; its VALUES (`useAgentPaneController`, `AgentSidePane`, …) are
566
+ * declared in `pane.d.ts` and exported only from the `@streamoid/agent/pane`
567
+ * subpath — the light entry that does not pull in this 5MB bundle. Keep both
568
+ * in step with `src/embed/pane/`. */
569
+
570
+ /** `closed` → `pane` (the side pane) → `fullscreen` (a non-text artefact took
571
+ * the page). Fullscreen is never restored on reload. */
572
+ export type AgentPaneSurface = 'closed' | 'pane' | 'fullscreen';
573
+
574
+ export interface AgentPaneControllerOptions {
575
+ /** The workspace the pane's conversation belongs to. The open state and the
576
+ * conversation are persisted per workspace; a change closes the pane, drops
577
+ * the old workspace's conversation and loads the new one's saved state. An
578
+ * empty value (not resolved yet) persists nothing. */
579
+ workspaceId: string | null | undefined;
580
+ /** localStorage prefix for the per-workspace session
581
+ * (`<persistKey>:<workspaceId>`). Default `streamoid.askPane.session`.
582
+ * Artifax passes `artifax-assistant-session` to keep its users' open chats
583
+ * across the migration — its stored `{ surface: 'rail', chatId }` is read. */
584
+ persistKey?: string;
585
+ /** The app's old width key(s), copied once into `streamoid.askPane.width`
586
+ * while that key is still empty: `artifax.assistant.width`,
587
+ * `tactix.assistant.width`, `catalogix.ask.width`, `photogenix.ask.width`. */
588
+ legacyWidthKey?: string | readonly string[];
589
+ /** Bind ⌘K / Ctrl+K to `askCxo` (with a prefetch). Default `true`; pass
590
+ * `false` where the host binds the chord to something else. */
591
+ bindShortcut?: boolean;
592
+ }
593
+
594
+ /** Options for {@link AgentPaneController.requestSupport} — exactly
595
+ * {@link showAgentSupport}'s, passed straight through. */
596
+ export type AgentPaneSupportOptions = ShowAgentSupportOptions;
597
+
598
+ export interface AgentPaneController {
599
+ /** The pane is showing (side pane or fullscreen). Hand it to the rail's Ask
600
+ * CXO trigger as `active`. */
601
+ open: boolean;
602
+ /** `fullscreen` while a non-text artefact holds the page. A host whose page
603
+ * sits beside the pane HIDES it (`display: none`, not unmount — the page
604
+ * keeps its scroll and form state) for exactly as long:
605
+ * `<main style={{ display: controller.surface === 'fullscreen' ? 'none' : undefined }}>`. */
606
+ surface: AgentPaneSurface;
607
+ /** The conversation on screen; `null` lets the chat create one. Persisted
608
+ * per workspace with the open state. */
609
+ chatId: string | null;
610
+ /** Show the pane. Open, never toggle. Applies the chat's latest surface
611
+ * report: an artefact still live behind a closed pane reopens fullscreen,
612
+ * unless the user declined it with Escape. */
613
+ reveal(): void;
614
+ /** Hide the pane. It stays mounted: an answer still streaming keeps going,
615
+ * and an artefact stays live (reports arriving while closed are kept). */
616
+ close(): void;
617
+ toggle(): void;
618
+ /** The rail trigger / ⌘K: reveal and put the caret in the composer. */
619
+ askCxo(): void;
620
+ /** Reveal on a NEW conversation and send `prompt` in it, without waiting for
621
+ * the network (`enqueueChatPrompt`). `false` when the prompt is blank. */
622
+ askAbout(prompt: string): boolean;
623
+ /** Help & Support: reveal on the Get help home ({@link showAgentSupport});
624
+ * sends nothing. */
625
+ requestSupport(options?: AgentPaneSupportOptions | null): void;
626
+ /** Warm the agent chunk — wire to the trigger's hover and focus. */
627
+ prefetch(): void;
628
+ /** Docked width in px, from `SC_ASSISTANT_PANEL_WIDTH` within
629
+ * `SC_ASSISTANT_PANEL_BOUNDS`, persisted under `streamoid.askPane.width`. */
630
+ width: number;
631
+ /** Live width during a drag; not persisted. */
632
+ setWidth(width: number): void;
633
+ /** Persist a width (drag end). */
634
+ commitWidth(width: number): void;
635
+ bounds: { min: number; max: number };
636
+ /** The selection the pane HOLDS (see `adapters.onSelectionSnapshot`) —
637
+ * `null` until the chat mounts. Split or count suggestions on this, e.g.
638
+ * `paneSuggestionCount(sections, controller.selectionSnapshot)`, not on the
639
+ * host's live selection. */
640
+ selectionSnapshot: AgentScopeSelection | null;
641
+ /** Changes on every real workspace switch (not when an unresolved id first
642
+ * resolves). `AgentSidePane` keys the chat on it, so a switch remounts the
643
+ * chat — workspace A's draft, uploads and selection chip never reach B —
644
+ * and `selectionSnapshot` resets in the same update. */
645
+ workspaceEpoch: number;
646
+ /** Wiring for `AgentSidePane`; hosts do not need these. */
647
+ setChatId(chatId: string | null): void;
648
+ reportSurface(state: AgentSurfaceState): void;
649
+ collapse(): void;
650
+ reportSelectionSnapshot(selection: AgentScopeSelection | null): void;
651
+ }
652
+
653
+ /** The adapters a host still supplies to {@link AgentSidePaneProps}. The pane
654
+ * fills `navigation`, `onClose`, `onSurfaceChange` and `onSelectionSnapshot`
655
+ * from the controller and takes `hostContext`, `scope`, `support` and
656
+ * `canvas` as props. */
657
+ export type AgentSidePaneAdapters = Omit<
658
+ AgentPanelAdapters,
659
+ | 'navigation'
660
+ | 'onClose'
661
+ | 'onSurfaceChange'
662
+ | 'onSelectionSnapshot'
663
+ | 'hostContext'
664
+ | 'scope'
665
+ | 'support'
666
+ | 'canvas'
667
+ >;
668
+
669
+ export interface AgentSidePaneProps {
670
+ controller: AgentPaneController;
671
+ /** Memoise it: a new object every render re-renders the whole chat. */
672
+ adapters: AgentSidePaneAdapters;
673
+ hostContext?: AgentHostContext | null;
674
+ scope?: AgentScopeAdapter | null;
675
+ support?: AgentSupportAdapter | null;
676
+ canvas?: HostCanvasContextValue;
677
+ className?: string;
678
+ /** Accessible name of the pane landmark. Default "Ask CXO assistant". */
679
+ ariaLabel?: string;
680
+ }