@genesislcap/ai-assistant 15.23.3 → 15.24.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.
Files changed (46) hide show
  1. package/dist/ai-assistant.api.json +419 -50
  2. package/dist/ai-assistant.d.ts +217 -683
  3. package/dist/chat-driver.cjs.map +1 -1
  4. package/dist/chat-driver.mjs.map +1 -1
  5. package/dist/custom-elements.json +310 -609
  6. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +16 -0
  7. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
  8. package/dist/dts/components/settings-modal/index.d.ts +1 -0
  9. package/dist/dts/components/settings-modal/index.d.ts.map +1 -1
  10. package/dist/dts/components/settings-modal/settings-modal.d.ts +215 -0
  11. package/dist/dts/components/settings-modal/settings-modal.d.ts.map +1 -0
  12. package/dist/dts/components/settings-modal/settings-modal.template.d.ts.map +1 -1
  13. package/dist/dts/main/attachment-controller.d.ts +66 -0
  14. package/dist/dts/main/attachment-controller.d.ts.map +1 -0
  15. package/dist/dts/main/budget-controller.d.ts +368 -0
  16. package/dist/dts/main/budget-controller.d.ts.map +1 -0
  17. package/dist/dts/main/composer-menu-controller.d.ts +113 -0
  18. package/dist/dts/main/composer-menu-controller.d.ts.map +1 -0
  19. package/dist/dts/main/composer-resize-controller.d.ts +80 -0
  20. package/dist/dts/main/composer-resize-controller.d.ts.map +1 -0
  21. package/dist/dts/main/context-gate-controller.d.ts +197 -0
  22. package/dist/dts/main/context-gate-controller.d.ts.map +1 -0
  23. package/dist/dts/main/cost-ledger-controller.d.ts +228 -0
  24. package/dist/dts/main/cost-ledger-controller.d.ts.map +1 -0
  25. package/dist/dts/main/main.d.ts +201 -722
  26. package/dist/dts/main/main.d.ts.map +1 -1
  27. package/dist/dts/main/main.types.d.ts +25 -0
  28. package/dist/dts/main/main.types.d.ts.map +1 -1
  29. package/dist/dts/main/meta-snapshot.d.ts +50 -0
  30. package/dist/dts/main/meta-snapshot.d.ts.map +1 -0
  31. package/dist/dts/state/persistence/session-persister.d.ts.map +1 -1
  32. package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +18 -0
  33. package/dist/esm/components/settings-modal/index.js +1 -0
  34. package/dist/esm/components/settings-modal/settings-modal.js +436 -0
  35. package/dist/esm/components/settings-modal/settings-modal.template.js +15 -4
  36. package/dist/esm/main/attachment-controller.js +254 -0
  37. package/dist/esm/main/budget-controller.js +745 -0
  38. package/dist/esm/main/composer-menu-controller.js +234 -0
  39. package/dist/esm/main/composer-resize-controller.js +119 -0
  40. package/dist/esm/main/context-gate-controller.js +289 -0
  41. package/dist/esm/main/cost-ledger-controller.js +498 -0
  42. package/dist/esm/main/main.js +501 -2268
  43. package/dist/esm/main/meta-snapshot.js +108 -0
  44. package/dist/esm/state/persistence/session-persister.js +5 -4
  45. package/docs/migration-GENC-1580.md +209 -0
  46. package/package.json +16 -16
@@ -0,0 +1,368 @@
1
+ import { type AIProviderRegistry, type AIProviderRegistryStatusEntry, type AIProviderType, type ChatConfig, type ChatDriverResult, type TurnFailureReason } from '@genesislcap/foundation-ai';
2
+ import type { VendorBudgetFigures } from '../state/ai-assistant-slice';
3
+ import type { SessionStoreReturn } from '../state/session-store';
4
+ import type { SettingsBudgetRow } from './main.types';
5
+ /**
6
+ * Banner copy built from the figures a 402 actually reported, or `undefined`
7
+ * when the proxy sent none.
8
+ *
9
+ * `undefined` is the meaningful return, not a fallback: paired with the
10
+ * slice's "a re-latch without a reason keeps the existing explanation" rule, it
11
+ * makes a figureless driver latch leave a host-supplied reason intact rather
12
+ * than blanking it back to the default at the exact moment the wall is hit.
13
+ *
14
+ * Exported for the unit test that pins the formatting; not part of the element
15
+ * API.
16
+ *
17
+ * @internal
18
+ */
19
+ export declare function formatBlockedReason(budget?: Partial<Extract<ChatDriverResult, {
20
+ reason: 'done';
21
+ }>['budget']>, vendor?: AIProviderType): string | undefined;
22
+ /**
23
+ * Whole-number percentage of a budget consumed, for the meter (GENC-1464).
24
+ *
25
+ * Deliberately unclamped — spend can genuinely overshoot the cap (the last
26
+ * allowed call may cross it), and the meter's TEXT shows the truth; only the
27
+ * bar clamps, at the render site. The one special case is a `$0` cap, which the
28
+ * platform treats as "no spend allowed" (not "unlimited"): it has no headroom
29
+ * by definition, so the meter reads full rather than dividing by zero.
30
+ *
31
+ * Exported for the unit test that pins the arithmetic; not element API.
32
+ *
33
+ * @internal
34
+ */
35
+ export declare function budgetPercentOf(figures: VendorBudgetFigures): number;
36
+ /**
37
+ * What the controller needs from its host.
38
+ *
39
+ * `sessionRef` is a deliberate departure from the other controllers, which take
40
+ * one value callback per field. The budget surface is a projection of the redux
41
+ * slice: nine of its members are thin reads of `store.aiAssistant.*` and four are
42
+ * action dispatches, so threading a callback per field would be ten pieces of
43
+ * plumbing that still hard-code the slice shape in their signatures — coupling
44
+ * without isolation. It is also required rather than merely tidier:
45
+ * {@link BudgetController.latchFrom} writes through a session ref *captured at
46
+ * call time*, so a teardown race cannot land a wall in a new session's store, and
47
+ * a value callback cannot express that.
48
+ */
49
+ export interface BudgetDeps {
50
+ /** The live session store. Observable on the element, so reads here stay tracked. */
51
+ sessionRef: () => SessionStoreReturn | undefined;
52
+ providerStatuses: () => AIProviderRegistryStatusEntry[];
53
+ /** Needed to tell a chat-capable provider from a status-only one — see `reachableVendors`. */
54
+ providerRegistry: () => AIProviderRegistry | undefined;
55
+ /** For `ui.showBudgetUsage` only. */
56
+ chatConfig: () => ChatConfig;
57
+ }
58
+ /**
59
+ * The AI-spend budget: which vendors are walled, whether that locks the composer,
60
+ * the banner copy that explains it, and the settings-modal meter (GENC-1464).
61
+ *
62
+ * Two invariants run through the whole file:
63
+ *
64
+ * - **Walls latch; locking derives.** A wall is one-way per vendor and nothing in
65
+ * here clears it, because nothing the user can do refills a budget. But
66
+ * {@link BudgetController.blocked} is *derived* from "is every reachable vendor
67
+ * walled", so a host swapping its registry to a vendor with headroom recovers
68
+ * the composer without un-latching anything.
69
+ * - **The banner may only name vendors the user could actually be routed to.**
70
+ * Hence {@link BudgetController.relevantBlockedVendors}, and hence the
71
+ * provenance mark the sweep writes.
72
+ *
73
+ * @internal
74
+ */
75
+ export declare class BudgetController {
76
+ private readonly deps;
77
+ /**
78
+ * When budget figures last arrived — from the host's
79
+ * {@link BudgetController.setVendorBudget} or the wall latch's own feed — as
80
+ * epoch ms; `null` until the first feed.
81
+ *
82
+ * ELEMENT state, not slice state, on purpose: the reducer is idempotent by
83
+ * value (an unchanged re-feed must not re-render the meter rows), but "when
84
+ * was this fetched" must move on every feed, changed figures or not — a
85
+ * refresh that confirms the same dollars is still a refresh. Only the
86
+ * toolbar label observes this, so a stamp re-renders one span, not the rows.
87
+ */
88
+ meterLastFedAt: number | null;
89
+ /**
90
+ * One warning per element for garbage fed to
91
+ * {@link BudgetController.setVendorBudget}. Once, not per call, because the
92
+ * documented feed re-runs after every turn — a host bug would otherwise repeat
93
+ * the same warning on every send for the life of the page.
94
+ *
95
+ * Not observable: nothing renders it.
96
+ */
97
+ private warningIssued;
98
+ constructor(deps: BudgetDeps);
99
+ private get slice();
100
+ /**
101
+ * Whether the assistant is blocked by a backend condition and cannot send.
102
+ *
103
+ * Derived, not stored — see the class note. Unknown reachability (statuses not
104
+ * loaded yet, or providers that report no status) fails SAFE: any wall blocks
105
+ * everything, and that needs no clause of its own, because `every` on the empty
106
+ * set is vacuously true.
107
+ */
108
+ get blocked(): boolean;
109
+ set blocked(value: boolean);
110
+ /**
111
+ * The distinct vendors the registry can currently reach, from the provider
112
+ * statuses the element loads on connect and refreshes on every
113
+ * observable-registry change.
114
+ *
115
+ * This is the answer to "which vendor would the next turn use" — a question
116
+ * that has **no** correct answer and deliberately gets no API. The provider is
117
+ * resolved per turn AND per agent: `activeProviderInput` may be an async
118
+ * function of the turn's context, an orchestrated turn picks its agent with an
119
+ * LLM `classify()` call, and sub-agents resolve their own providers. Any
120
+ * pre-turn "peek" would therefore be a guess that is wrong precisely on the
121
+ * multi-agent hosts per-vendor budgets exist for.
122
+ *
123
+ * Asking instead which vendors are REACHABLE is answerable, synchronous, and
124
+ * free — and it is enough: the composer must stay live while any reachable
125
+ * vendor has headroom, and must lock when none does. It also makes
126
+ * vendor-switch recovery a derivation rather than a mutation.
127
+ */
128
+ get reachableVendors(): readonly AIProviderType[];
129
+ /** Vendors currently walled, in the order they were walled. */
130
+ get blockedVendors(): readonly AIProviderType[];
131
+ /** Whether this specific vendor's budget is walled, regardless of the others. */
132
+ isVendorBlocked(vendor: AIProviderType): boolean;
133
+ /** Whether this vendor's wall came from the sweep alone — see the slice's `sweptVendors`. */
134
+ private isVendorSwept;
135
+ /**
136
+ * Wall (or release) one vendor's budget, optionally with banner copy for it.
137
+ *
138
+ * `'none'` is rejected with a warning rather than accepted: it is the "no
139
+ * provider configured" sentinel, not a vendor. Latching it would put the raw
140
+ * sentinel in the banner ("none's AI usage limit is reached") and could never be
141
+ * undone by derivation, because `'none'` never appears in
142
+ * {@link BudgetController.reachableVendors} — so `blocked` could never become
143
+ * derivable from it either.
144
+ */
145
+ setVendorBlocked(vendor: AIProviderType, blocked: boolean, reason?: string | null): void;
146
+ private warnOnce;
147
+ /**
148
+ * Feed (or clear) one vendor's AI-spend figures for the meter. Returns `false`
149
+ * — warning once, not per call — for a non-budgeted vendor or a figure that is
150
+ * not a finite number `>= 0`, and also for "no store yet", which is a lifecycle
151
+ * window rather than bad input and so is deliberately not warned about.
152
+ *
153
+ * The full host-facing contract, including the intended `GET /api/budget` feed,
154
+ * is on the element's `setVendorBudget`.
155
+ */
156
+ setVendorBudget(vendor: string, figures: {
157
+ budgetUsd: number;
158
+ spentUsd: number;
159
+ } | null): boolean;
160
+ /** "Updated HH:MM:SS" for the meter toolbar, or `undefined` before any feed. */
161
+ get updatedLabel(): string | undefined;
162
+ /**
163
+ * The walled vendors the user can still be routed to — i.e.
164
+ * {@link BudgetController.blockedVendors} narrowed to the reachable set, which
165
+ * is the only set the banner may name.
166
+ *
167
+ * A wall the registry can no longer reach is not news: it cannot be hit, and
168
+ * naming it puts a vendor in front of the user that is not theirs. The concrete
169
+ * failure this exists to stop: a host ships Anthropic-only, Anthropic walls, the
170
+ * host swaps its registry to Gemini, Gemini walls — and the banner reads "AI
171
+ * usage limits are reached for Anthropic and Gemini" to a user who has never
172
+ * had an Anthropic key. The spurious second name also flips the copy onto the
173
+ * plural branch, so even the closing sentence is wrong.
174
+ *
175
+ * When nothing is reachable YET (the statuses are still loading — the exact
176
+ * window the pre-flight and the cross-tab bus latch into), the fallback keeps
177
+ * every wall for LOCKING (`blocked` still derives true) but names only the
178
+ * vendors walled by a REFUSAL, dropping the ones the `otherVendorAvailable`
179
+ * sweep added. A refusal is a fact about a vendor this user just used; a sweep
180
+ * entry is the proxy's headroom verdict about a vendor the host may not even
181
+ * ship — naming it here reproduced the exact failure above from the other
182
+ * direction ("Gemini's AI usage limit is reached" to an Anthropic-only user),
183
+ * and flipped the copy onto the plural branch with it. Self-corrects when the
184
+ * statuses land: from then on reachability, not provenance, decides.
185
+ */
186
+ get relevantBlockedVendors(): readonly AIProviderType[];
187
+ /**
188
+ * Whether the blocked banner has anything to say — a vendor-agnostic block, or
189
+ * a partial wall that the NEXT send would actually hit.
190
+ *
191
+ * Partial exhaustion is announced only while the ACTIVE vendor (the registry
192
+ * default the next send resolves to) is among the walled ones. A user who has
193
+ * switched to a vendor with headroom has already acted on the advice —
194
+ * keeping the banner up past that point nagged about a vendor they just left
195
+ * (GENC-1464 tester feedback), while switching BACK to the walled vendor
196
+ * brings the banner straight back. Two conservative fallbacks keep it
197
+ * fail-informative: while the statuses are still loading the active vendor is
198
+ * unknowable, so any reachable-and-walled vendor still announces; and a
199
+ * vendor-agnostic block announces always, exactly as before.
200
+ */
201
+ get bannerVisible(): boolean;
202
+ /**
203
+ * Whether a suggestions fetch would hit a wall.
204
+ *
205
+ * Unlike a chat turn, this one CAN be resolved exactly: both suggestion paths
206
+ * go to the registry **default** and never to a per-agent override
207
+ * (`ChatDriver.getSuggestions` calls `providerRegistry.default()`, and
208
+ * `OrchestratingDriver.getSuggestions` just delegates to it). So the vendor a
209
+ * suggestions call would use is knowable, and this is the one place in the
210
+ * feature where that is true.
211
+ */
212
+ get suggestionsBlocked(): boolean;
213
+ /**
214
+ * The vendor the NEXT send resolves to — the registry default's provider, as
215
+ * reported through `providerStatuses`. `undefined` while the statuses are
216
+ * still loading (or when the default reports the `'none'` sentinel), which
217
+ * every consumer treats conservatively.
218
+ */
219
+ private get activeVendor();
220
+ /** Host override for the banner copy, or `null` for the element's default. */
221
+ get blockedReason(): string | null;
222
+ set blockedReason(value: string | null);
223
+ /**
224
+ * Block or unblock the assistant, optionally with custom banner copy.
225
+ *
226
+ * Blocking writes the **vendor-agnostic** block — what a host with one budget
227
+ * pot means — and does not populate {@link BudgetController.blockedVendors}.
228
+ * Unblocking clears the per-vendor walls as well, so a host asserting "the wall
229
+ * is gone" after its own pre-flight cannot be silently overruled by a driver
230
+ * latch it never saw.
231
+ */
232
+ setBlocked(blocked: boolean, reason?: string | null): void;
233
+ /**
234
+ * Latch the backend block off a turn outcome. The single decision point for
235
+ * both latch sites (the `tool-loop-end` bus subscription and `send()`'s
236
+ * return-value check), so the rule cannot diverge between them.
237
+ *
238
+ * Idempotent and one-way **per vendor**: it never unblocks, and it never
239
+ * re-writes an existing wall. That guard is load-bearing, not just an
240
+ * optimisation — the bus fires from the driver's `finally`, i.e. BEFORE
241
+ * `sendMessage()` resolves, so a host subscribed to `tool-loop-end` (what the
242
+ * migration guide's Option C recommends) sets its own detailed reason and the
243
+ * return-value latch would otherwise land a moment later and blank it.
244
+ *
245
+ * The guard being per-vendor rather than global is the whole difference. A
246
+ * single global "already blocked, do nothing" would swallow a second vendor's
247
+ * wall entirely.
248
+ *
249
+ * **Vendor attribution, in precedence order:**
250
+ *
251
+ * 1. `budget.vendorLabel` — stamped by the transport that was actually refused,
252
+ * so it is the only source that cannot be a guess. A label the element does
253
+ * not recognise leaves it unclaimed by any vendor.
254
+ * 2. `budget.vendor` — which the driver resolved from the label where it could,
255
+ * and otherwise from the proxy's own `vendor` field on the 402. It is
256
+ * therefore NOT simply `vendorLabel` normalised, and the two can disagree.
257
+ * 3. `vendorHint` — the bus detail's top-level vendor, i.e. the driver's
258
+ * best guess at the turn's vendor, which may be the previous turn's vendor
259
+ * or nothing at all.
260
+ *
261
+ * A wall that names no recognised vendor falls back to the vendor-agnostic
262
+ * block — fail-safe, and identical to the pre-per-vendor behaviour.
263
+ *
264
+ * **`otherVendorAvailable: false` is server-known truth and outranks every
265
+ * local guess.** Only the proxy knows whether anything else has headroom; this
266
+ * element can only observe which vendors the registry can reach. So when the
267
+ * proxy says no, every other *budgeted* vendor is walled in the same pass. That
268
+ * is what makes `blocked` derive true — locking the composer on the first wall
269
+ * rather than leaving it live against a vendor set the banner would otherwise
270
+ * have advised switching to.
271
+ *
272
+ * The sweep runs over `BUDGETED_VENDORS`, NOT
273
+ * {@link BudgetController.reachableVendors}: reachability is loaded
274
+ * asynchronously, so a reachability-scoped sweep dropped the verdict entirely
275
+ * when the wall beat the statuses home — making it effectively un-latch, since
276
+ * the single wall derived `blocked` only while nothing was reachable, and the
277
+ * composer came back to life against a budget the proxy had already said was
278
+ * gone. Sweeping the metered list instead makes the verdict independent of load
279
+ * order. Walling a vendor the registry cannot reach costs nothing: `blocked`
280
+ * only asks whether every REACHABLE vendor is walled, and the banner reads
281
+ * {@link BudgetController.relevantBlockedVendors}.
282
+ *
283
+ * One boundary remains on the sweep: it walls only vendors the proxy meters,
284
+ * because a vendor it does not meter has no budget there to exhaust, so "no
285
+ * other vendor has budget" is not a statement about it.
286
+ *
287
+ * The sweep runs last but sits OUTSIDE the per-vendor idempotence guard, and
288
+ * both halves of that matter. Last, so the vendor that actually refused this
289
+ * turn heads the wall order the multi-vendor banner reads. Outside the guard,
290
+ * because the verdict can arrive on a LATER 402 for a vendor that is already
291
+ * walled.
292
+ *
293
+ * @param reason - the turn's failure reason; anything but `'budget-exhausted'` is ignored.
294
+ * @param ref - the session store to write through, captured by the caller so a
295
+ * teardown race cannot land the wall in a new session's store.
296
+ * @param budget - what the 402 reported: figures when the proxy sent any, the
297
+ * refusing vendor, and its `otherVendorAvailable` verdict.
298
+ * @param vendorHint - the `tool-loop-end` detail's top-level vendor, used only
299
+ * when the budget payload names none.
300
+ */
301
+ latchFrom(reason?: TurnFailureReason, ref?: SessionStoreReturn | undefined, budget?: Extract<ChatDriverResult, {
302
+ reason: 'done';
303
+ }>['budget'], vendorHint?: AIProviderType): void;
304
+ /**
305
+ * Copy shown in the blocked banner, in ascending specificity:
306
+ *
307
+ * 1. the host's `blockedReason` — a whole-banner override, unchanged;
308
+ * 2. copy composed from the walled vendors (their per-vendor reasons when set,
309
+ * else their figures);
310
+ * 3. the default budget-exhaustion message.
311
+ *
312
+ * The action clause is composed HERE, not stored at latch time, because whether
313
+ * "switch vendor" or "contact your administrator" is honest depends on whether
314
+ * another reachable vendor still has headroom — which changes when the registry
315
+ * changes, long after the wall was latched. Telling a user to switch to a vendor
316
+ * that is also exhausted is worse than saying nothing.
317
+ *
318
+ * **Per-vendor copy is composed however many vendors are walled**, which is what
319
+ * `setVendorBlocked`'s `reason` parameter promises. It used to be honoured only
320
+ * on the single-vendor branch, so a host that had carefully set copy for each of
321
+ * its vendors watched all of it vanish the moment a second one walled.
322
+ */
323
+ get effectiveBlockedReason(): string;
324
+ /**
325
+ * Copy the driver writes into the TRANSCRIPT when a turn hits the budget wall,
326
+ * or `undefined` to let the driver compose it per wall.
327
+ *
328
+ * A set `blockedReason` still wins — a white-labelled host must not read its own
329
+ * explanation in the banner and shipped copy directly below it.
330
+ *
331
+ * `undefined` (the usual case) is NOT the old `DEFAULT_BUDGET_EXHAUSTED_MESSAGE`
332
+ * fallback: it hands composition to the driver, which builds the bubble from
333
+ * EACH wall's own 402 — naming the exhausted vendor and advising a switch only
334
+ * when that refusal's `otherVendorAvailable` positively vouched for headroom
335
+ * elsewhere. That is immune to the staleness this getter used to guard against
336
+ * by freezing the copy: construction-time advice could outlive its truth
337
+ * (`getOrCreateDriver` re-runs on an agents swap and would have baked the
338
+ * sentence into every later bubble), but per-wall advice is derived from the
339
+ * refusal it annotates and dies with the turn. Forcing the default here was also
340
+ * what made the driver's composed bubble unreachable in every real mount — the
341
+ * banner advised switching while the bubble under it never did (the GENC-1464
342
+ * tester finding, root cause).
343
+ */
344
+ get transcriptMessage(): string | undefined;
345
+ /**
346
+ * Built-in per-vendor budget meter (GENC-1464) — the money sibling of the
347
+ * settings modal's context-usage gate, with the same shape: not configured off,
348
+ * and there is data to show. "Data" here is "at least one vendor has figures",
349
+ * so a host that feeds nothing (or only unlimited vendors) gets no meter and no
350
+ * empty container.
351
+ *
352
+ * Answered from the RAW slice state rather than from
353
+ * {@link BudgetController.settingsRows}, whose `.length` would format every row
354
+ * (a `formatUsd` pair and a `budgetPercentOf` each) only to ask whether any
355
+ * exist. This is read several times per render pass — the getter, the section
356
+ * gate, the template's `when` — so it stays as cheap as the context sibling's
357
+ * `!= null`.
358
+ */
359
+ get settingsUsageVisible(): boolean;
360
+ /**
361
+ * The meter's rows — one per vendor that currently has figures, in
362
+ * `BUDGETED_VENDORS` order (the proxy's own metered list) so the rendering is
363
+ * stable however the host ordered its feed calls. Pre-formatted; see
364
+ * {@link SettingsBudgetRow} for why the text is unclamped while the bar is.
365
+ */
366
+ get settingsRows(): readonly SettingsBudgetRow[];
367
+ }
368
+ //# sourceMappingURL=budget-controller.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"budget-controller.d.ts","sourceRoot":"","sources":["../../../src/main/budget-controller.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,113 @@
1
+ import type { MetaEventType } from '../state/debug-event-log';
2
+ /** A read/write pair for one piece of state the controller does not own. */
3
+ export interface Accessor<T> {
4
+ get(): T;
5
+ set(value: T): void;
6
+ }
7
+ /** What the controller needs from its host beyond the shadow root. */
8
+ export interface ComposerMenuDeps {
9
+ agentPickerOpen: Accessor<boolean>;
10
+ sessionMenuOpen: Accessor<boolean>;
11
+ /** Return focus here after Escape, rather than to a button that just vanished. */
12
+ focusInput: () => void;
13
+ /**
14
+ * Clear the session menu's two-step confirm prompts. Called on EVERY session-menu
15
+ * toggle, not just a user click on the ⋮ button — the menu must always reopen
16
+ * showing the menu rather than a leftover Clear-confirm, and it can be closed
17
+ * from four paths (the button, Escape, an outside click, opening the agent
18
+ * picker). Keeping it in the controller is what makes that hold on all four.
19
+ */
20
+ resetSessionConfirmations: () => void;
21
+ logMeta: (type: MetaEventType, detail?: Record<string, unknown>) => void;
22
+ }
23
+ /**
24
+ * The two composer menus — the agent picker and the session (⋮) menu: their
25
+ * mutually-exclusive open/close animation, their measured placement above the
26
+ * button that opened them, and the document-level listeners that dismiss them.
27
+ *
28
+ * Owns no observable state: both open flags live on the session store so the
29
+ * bubble and popout instances stay in sync, and reach this controller as
30
+ * {@link Accessor}s.
31
+ *
32
+ * @internal
33
+ */
34
+ export declare class ComposerMenuController {
35
+ /**
36
+ * Typed `HTMLElement`, not the assistant: this controller needs the shadow root
37
+ * and nothing else, and the narrower type is what stops it reaching back into
38
+ * element API and recreating the god object with extra indirection.
39
+ */
40
+ private readonly host;
41
+ private readonly deps;
42
+ private readonly agentPicker;
43
+ private readonly sessionMenu;
44
+ /** Live only while a menu is open — see {@link ComposerMenuController.syncDismissal}. */
45
+ private dismissPointer?;
46
+ private dismissKey?;
47
+ private dismissResize?;
48
+ constructor(
49
+ /**
50
+ * Typed `HTMLElement`, not the assistant: this controller needs the shadow root
51
+ * and nothing else, and the narrower type is what stops it reaching back into
52
+ * element API and recreating the god object with extra indirection.
53
+ */
54
+ host: HTMLElement, deps: ComposerMenuDeps);
55
+ toggleAgentPicker(): void;
56
+ toggleSessionMenu(): void;
57
+ /** Close whichever menus are open, with their close animation. */
58
+ closeAll(): void;
59
+ /**
60
+ * Close both menus at once, with no close animation and no `panel.toggled`
61
+ * meta event — for a caller that is about to replace the view the menu sits in
62
+ * (Compact swaps the transcript for a progress indicator, Clear wipes it).
63
+ *
64
+ * Not {@link ComposerMenuController.closeAll}: animating a panel out over 200ms
65
+ * while the thing behind it is being replaced is the wrong effect, and routing
66
+ * through the toggles would log a spurious user-initiated panel toggle into the
67
+ * debug timeline. This matches what the direct flag assignment on the element did
68
+ * before — including leaving any in-flight close timer alone, which is safe
69
+ * because the toggle strips `closing` from the node it captured.
70
+ */
71
+ closeImmediate(): void;
72
+ /** Re-place whichever menu is open. Cheap enough to run from a resize. */
73
+ repositionOpen(): void;
74
+ /**
75
+ * Attach or drop the dismissal listeners to match whether a menu is open.
76
+ * Called from the open/close setters, so the listeners exist only while there is
77
+ * something to dismiss.
78
+ */
79
+ syncDismissal(): void;
80
+ /**
81
+ * Finalise in-flight close animations and drop every listener — call from
82
+ * `disconnectedCallback`.
83
+ *
84
+ * **Teardown runs LAST, and the order is load-bearing.** `finalize()` writes the
85
+ * flag through `setOpen`, which reaches the element setter and therefore
86
+ * {@link ComposerMenuController.syncDismissal} — so tearing down first let the
87
+ * sync re-arm the listeners on an element that is going away. Reachable, if
88
+ * narrowly: a panel's flag stays TRUE for the whole close animation, so
89
+ * switching from the session menu to the agent picker leaves both flags set for
90
+ * ~200ms, and a disconnect in that window finalised the closing menu while the
91
+ * picker was still open — `shouldListen` true, listeners re-attached, nothing
92
+ * left to remove them (PR #2503 review).
93
+ */
94
+ dispose(): void;
95
+ private teardownDismissal;
96
+ /**
97
+ * Places an open composer menu directly above the control that opened it.
98
+ *
99
+ * CSS alone cannot: the menu is a child of `.composer` — which is what lets its
100
+ * width be capped against the composer rather than against a button — while its
101
+ * anchor is a button several levels down the bar, and no selector can read a
102
+ * sibling's offset. So the left edge is measured on open and written as a custom
103
+ * property.
104
+ *
105
+ * Clamped to the composer, so a menu opened from a control near the right edge of
106
+ * a narrow panel slides back inside instead of overhanging. The animation origin
107
+ * is written separately and keeps pointing at the button's centre even after such
108
+ * a shift — the menu still grows out of the thing that opened it, which is the
109
+ * whole point of anchoring it there.
110
+ */
111
+ private position;
112
+ }
113
+ //# sourceMappingURL=composer-menu-controller.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"composer-menu-controller.d.ts","sourceRoot":"","sources":["../../../src/main/composer-menu-controller.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AA4B9D,4EAA4E;AAC5E,MAAM,WAAW,QAAQ,CAAC,CAAC;IACzB,GAAG,IAAI,CAAC,CAAC;IACT,GAAG,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC;CACrB;AAED,sEAAsE;AACtE,MAAM,WAAW,gBAAgB;IAC/B,eAAe,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;IACnC,eAAe,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;IACnC,kFAAkF;IAClF,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB;;;;;;OAMG;IACH,yBAAyB,EAAE,MAAM,IAAI,CAAC;IACtC,OAAO,EAAE,CAAC,IAAI,EAAE,aAAa,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;CAC1E;AAED;;;;;;;;;;GAUG;AACH,qBAAa,sBAAsB;IAU/B;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAfvB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAsB;IAClD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAsB;IAElD,yFAAyF;IACzF,OAAO,CAAC,cAAc,CAAC,CAAqB;IAC5C,OAAO,CAAC,UAAU,CAAC,CAA6B;IAChD,OAAO,CAAC,aAAa,CAAC,CAAa;;IAGjC;;;;OAIG;IACc,IAAI,EAAE,WAAW,EACjB,IAAI,EAAE,gBAAgB;IAkBzC,iBAAiB,IAAI,IAAI;IAkBzB,iBAAiB,IAAI,IAAI;IAmBzB,kEAAkE;IAClE,QAAQ,IAAI,IAAI;IAKhB;;;;;;;;;;;OAWG;IACH,cAAc,IAAI,IAAI;IAKtB,0EAA0E;IAC1E,cAAc,IAAI,IAAI;IAStB;;;;OAIG;IACH,aAAa,IAAI,IAAI;IA6BrB;;;;;;;;;;;;;OAaG;IACH,OAAO,IAAI,IAAI;IAMf,OAAO,CAAC,iBAAiB;IAezB;;;;;;;;;;;;;;OAcG;IACH,OAAO,CAAC,QAAQ;CAgDjB"}
@@ -0,0 +1,80 @@
1
+ /** What the controller needs from its host, as callbacks so it holds no element reference. */
2
+ export interface ComposerResizeDeps {
3
+ /** `chatConfig.ui.resizableInput !== false` — read live, since a host can flip it. */
4
+ enabled: () => boolean;
5
+ /** The composer box, for its measured natural height on the first drag. */
6
+ inputEl: () => HTMLElement | undefined;
7
+ /** The message list, whose height is the headroom the composer may claim. */
8
+ messagesEl: () => HTMLElement | undefined;
9
+ }
10
+ /**
11
+ * Drag-to-resize for the composer: owns the chosen height and the transient
12
+ * geometry of one drag gesture.
13
+ *
14
+ * @internal
15
+ */
16
+ export declare class ComposerResizeController {
17
+ private readonly deps;
18
+ /**
19
+ * Explicit composer height in px once the user has dragged; `null` keeps the
20
+ * default auto-grow behaviour.
21
+ *
22
+ * Observable because {@link ComposerResizeController.heightStyle} is bound in
23
+ * the template — see the sibling scratch fields for why the rest are not.
24
+ */
25
+ heightPx: number | null;
26
+ /**
27
+ * Pointer Y and height captured at the start of a drag.
28
+ *
29
+ * Deliberately NOT observable, and the same goes for the two bounds below.
30
+ * These are rewritten on every `pointermove`, and no binding reads them, so
31
+ * marking them would fire a notification per mouse-move for values nothing
32
+ * displays — while `heightPx`, which does drive the render, fires exactly one.
33
+ */
34
+ private startY;
35
+ private startHeight;
36
+ /** Natural (un-dragged) height, measured on the first drag — the floor for shrinking. */
37
+ private naturalHeight;
38
+ /** Upper bound for the current drag, capped to the headroom above the composer. */
39
+ private maxHeight;
40
+ constructor(deps: ComposerResizeDeps);
41
+ /** Whether the composer shows a drag handle. On by default; hosts opt out via config. */
42
+ get resizable(): boolean;
43
+ /**
44
+ * Inline style for the composer: writes the chosen height into the
45
+ * `--composer-height` custom property (read by `.chat-input` and its control in
46
+ * `main.styles.ts`). Empty until the user drags, or when resizing is disabled,
47
+ * so the CSS default applies.
48
+ *
49
+ * `heightPx` is read into a local BEFORE the branch, and that is load-bearing.
50
+ * Written as `this.resizable && this.heightPx != null ? …`, the `&&`
51
+ * short-circuits past `heightPx` whenever resizing is off — so on a host that
52
+ * mounts with `resizableInput: false`, FAST never records a dependency on it.
53
+ * The template's binding for this getter (`style=${(x) => x.composerHeightStyle}`)
54
+ * carries no `:`/`&&`/`||`, so FAST classifies it non-volatile and collects
55
+ * dependencies exactly once per bind; the missing dependency is therefore never
56
+ * picked up, and a later `resizableInput: true` gives a handle that drags —
57
+ * `heightPx` updates — while the composer never visibly moves.
58
+ *
59
+ * Reading it unconditionally makes the dependency honest and costs nothing.
60
+ * `@volatile` would also fix it, by re-collecting on every evaluation, but it pays
61
+ * a full unsubscribe/resubscribe cycle each time to express something that is
62
+ * simply true: this getter always depends on both.
63
+ */
64
+ get heightStyle(): string;
65
+ /**
66
+ * Begin a drag. Captures the pointer so move/up keep firing on the handle even
67
+ * when the cursor leaves its 7px band.
68
+ */
69
+ onStart(e: PointerEvent): void;
70
+ /**
71
+ * Resize while dragging. Dragging up (clientY decreases) grows the composer; the
72
+ * result is clamped between the natural height and the available headroom. No-op
73
+ * unless a drag is active (the handle holds pointer capture), so plain
74
+ * hover-moves are ignored.
75
+ */
76
+ onMove(e: PointerEvent): void;
77
+ /** End a drag and release the pointer capture. */
78
+ onEnd(e: PointerEvent): void;
79
+ }
80
+ //# sourceMappingURL=composer-resize-controller.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"composer-resize-controller.d.ts","sourceRoot":"","sources":["../../../src/main/composer-resize-controller.ts"],"names":[],"mappings":"AAaA,8FAA8F;AAC9F,MAAM,WAAW,kBAAkB;IACjC,sFAAsF;IACtF,OAAO,EAAE,MAAM,OAAO,CAAC;IACvB,2EAA2E;IAC3E,OAAO,EAAE,MAAM,WAAW,GAAG,SAAS,CAAC;IACvC,6EAA6E;IAC7E,UAAU,EAAE,MAAM,WAAW,GAAG,SAAS,CAAC;CAC3C;AAED;;;;;GAKG;AACH,qBAAa,wBAAwB;IAyBvB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAxBjC;;;;;;OAMG;IACS,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAQ;IAE3C;;;;;;;OAOG;IACH,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,WAAW,CAAK;IACxB,yFAAyF;IACzF,OAAO,CAAC,aAAa,CAAK;IAC1B,mFAAmF;IACnF,OAAO,CAAC,SAAS,CAA0B;gBAEd,IAAI,EAAE,kBAAkB;IAErD,yFAAyF;IACzF,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,IAAI,WAAW,IAAI,MAAM,CAGxB;IAED;;;OAGG;IACH,OAAO,CAAC,CAAC,EAAE,YAAY,GAAG,IAAI;IAoB9B;;;;;OAKG;IACH,MAAM,CAAC,CAAC,EAAE,YAAY,GAAG,IAAI;IAS7B,kDAAkD;IAClD,KAAK,CAAC,CAAC,EAAE,YAAY,GAAG,IAAI;CAI7B"}