@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,234 @@
1
+ import { DOM } from '@genesislcap/web-core';
2
+ import { AnimatedPanelToggle } from '../utils/animated-panel-toggle';
3
+ /**
4
+ * Duration of a composer menu's close animation. Must stay in sync with the
5
+ * `composer-menu-out` keyframe duration in `main.styles.ts` — the toggle strips
6
+ * the `closing` class on a timer, so a shorter value here unmounts the panel
7
+ * mid-animation and a longer one leaves it sitting invisible.
8
+ */
9
+ const AGENT_PICKER_CLOSE_MS = 200;
10
+ const SESSION_MENU_CLOSE_MS = 200;
11
+ /**
12
+ * Floor for an open menu's height, applied when the assistant is too short to
13
+ * give it the room it wants. Below this a menu is more frustrating than a
14
+ * scrollbar is, so it overhangs instead.
15
+ */
16
+ const COMPOSER_MENU_MIN_HEIGHT = 140;
17
+ /**
18
+ * Vertical space an open menu must leave free above the button, covering BOTH
19
+ * the gap the menu sits off the button by (8px, declared in design units in
20
+ * main.styles.ts) and the breathing room wanted above it. The two are lumped
21
+ * together because only the second half is a free choice: the first is fixed by
22
+ * the stylesheet and simply has to be subtracted from the headroom, and a
23
+ * constant named for only one of them invites someone to "fix" a gap that is
24
+ * doing exactly what it should.
25
+ */
26
+ const MENU_HEADROOM_RESERVE = 20;
27
+ /**
28
+ * The two composer menus — the agent picker and the session (⋮) menu: their
29
+ * mutually-exclusive open/close animation, their measured placement above the
30
+ * button that opened them, and the document-level listeners that dismiss them.
31
+ *
32
+ * Owns no observable state: both open flags live on the session store so the
33
+ * bubble and popout instances stay in sync, and reach this controller as
34
+ * {@link Accessor}s.
35
+ *
36
+ * @internal
37
+ */
38
+ export class ComposerMenuController {
39
+ constructor(
40
+ /**
41
+ * Typed `HTMLElement`, not the assistant: this controller needs the shadow root
42
+ * and nothing else, and the narrower type is what stops it reaching back into
43
+ * element API and recreating the god object with extra indirection.
44
+ */
45
+ host, deps) {
46
+ this.host = host;
47
+ this.deps = deps;
48
+ this.agentPicker = new AnimatedPanelToggle(host, '.agent-picker-panel', AGENT_PICKER_CLOSE_MS, () => deps.agentPickerOpen.get(), (v) => deps.agentPickerOpen.set(v));
49
+ this.sessionMenu = new AnimatedPanelToggle(host, '.session-menu-panel', SESSION_MENU_CLOSE_MS, () => deps.sessionMenuOpen.get(), (v) => deps.sessionMenuOpen.set(v));
50
+ }
51
+ toggleAgentPicker() {
52
+ // Mutually exclusive with the session menu — both anchor to the composer bar.
53
+ if (!this.deps.agentPickerOpen.get() && this.deps.sessionMenuOpen.get()) {
54
+ this.toggleSessionMenu();
55
+ }
56
+ this.deps.logMeta('panel.toggled', {
57
+ panel: 'agent-picker',
58
+ open: !this.deps.agentPickerOpen.get(),
59
+ });
60
+ this.agentPicker.toggle();
61
+ // After the render that mounts the panel — it has no box to measure before that.
62
+ if (this.deps.agentPickerOpen.get()) {
63
+ void DOM.nextUpdate().then(() => this.position('.agent-picker-panel', '.agent-toggle-button'));
64
+ }
65
+ }
66
+ toggleSessionMenu() {
67
+ // Mutually exclusive with the agent picker; always reopen showing the menu
68
+ // (not a leftover Clear-confirm prompt).
69
+ if (!this.deps.sessionMenuOpen.get() && this.deps.agentPickerOpen.get()) {
70
+ this.toggleAgentPicker();
71
+ }
72
+ this.deps.resetSessionConfirmations();
73
+ this.deps.logMeta('panel.toggled', {
74
+ panel: 'session-menu',
75
+ open: !this.deps.sessionMenuOpen.get(),
76
+ });
77
+ this.sessionMenu.toggle();
78
+ if (this.deps.sessionMenuOpen.get()) {
79
+ void DOM.nextUpdate().then(() => this.position('.session-menu-panel', '.session-menu-button'));
80
+ }
81
+ }
82
+ /** Close whichever menus are open, with their close animation. */
83
+ closeAll() {
84
+ if (this.deps.agentPickerOpen.get())
85
+ this.toggleAgentPicker();
86
+ if (this.deps.sessionMenuOpen.get())
87
+ this.toggleSessionMenu();
88
+ }
89
+ /**
90
+ * Close both menus at once, with no close animation and no `panel.toggled`
91
+ * meta event — for a caller that is about to replace the view the menu sits in
92
+ * (Compact swaps the transcript for a progress indicator, Clear wipes it).
93
+ *
94
+ * Not {@link ComposerMenuController.closeAll}: animating a panel out over 200ms
95
+ * while the thing behind it is being replaced is the wrong effect, and routing
96
+ * through the toggles would log a spurious user-initiated panel toggle into the
97
+ * debug timeline. This matches what the direct flag assignment on the element did
98
+ * before — including leaving any in-flight close timer alone, which is safe
99
+ * because the toggle strips `closing` from the node it captured.
100
+ */
101
+ closeImmediate() {
102
+ if (this.deps.agentPickerOpen.get())
103
+ this.deps.agentPickerOpen.set(false);
104
+ if (this.deps.sessionMenuOpen.get())
105
+ this.deps.sessionMenuOpen.set(false);
106
+ }
107
+ /** Re-place whichever menu is open. Cheap enough to run from a resize. */
108
+ repositionOpen() {
109
+ if (this.deps.agentPickerOpen.get()) {
110
+ this.position('.agent-picker-panel', '.agent-toggle-button');
111
+ }
112
+ if (this.deps.sessionMenuOpen.get()) {
113
+ this.position('.session-menu-panel', '.session-menu-button');
114
+ }
115
+ }
116
+ /**
117
+ * Attach or drop the dismissal listeners to match whether a menu is open.
118
+ * Called from the open/close setters, so the listeners exist only while there is
119
+ * something to dismiss.
120
+ */
121
+ syncDismissal() {
122
+ const shouldListen = this.deps.agentPickerOpen.get() || this.deps.sessionMenuOpen.get();
123
+ if (shouldListen === !!this.dismissPointer)
124
+ return;
125
+ if (!shouldListen) {
126
+ this.teardownDismissal();
127
+ return;
128
+ }
129
+ this.dismissPointer = (e) => {
130
+ var _a;
131
+ // The composer owns both the menus and the buttons that open them, so a path
132
+ // that reaches it is an inside click — including the toggle itself, whose own
133
+ // handler closes the menu. Closing here too would immediately
134
+ // reopen-then-close and the menu would never appear.
135
+ const composer = (_a = this.host.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('.composer');
136
+ if (composer && e.composedPath().includes(composer))
137
+ return;
138
+ this.closeAll();
139
+ };
140
+ this.dismissKey = (e) => {
141
+ if (e.key !== 'Escape')
142
+ return;
143
+ this.closeAll();
144
+ this.deps.focusInput();
145
+ };
146
+ this.dismissResize = () => this.repositionOpen();
147
+ document.addEventListener('pointerdown', this.dismissPointer, true);
148
+ document.addEventListener('keydown', this.dismissKey, true);
149
+ window.addEventListener('resize', this.dismissResize);
150
+ }
151
+ /**
152
+ * Finalise in-flight close animations and drop every listener — call from
153
+ * `disconnectedCallback`.
154
+ *
155
+ * **Teardown runs LAST, and the order is load-bearing.** `finalize()` writes the
156
+ * flag through `setOpen`, which reaches the element setter and therefore
157
+ * {@link ComposerMenuController.syncDismissal} — so tearing down first let the
158
+ * sync re-arm the listeners on an element that is going away. Reachable, if
159
+ * narrowly: a panel's flag stays TRUE for the whole close animation, so
160
+ * switching from the session menu to the agent picker leaves both flags set for
161
+ * ~200ms, and a disconnect in that window finalised the closing menu while the
162
+ * picker was still open — `shouldListen` true, listeners re-attached, nothing
163
+ * left to remove them (PR #2503 review).
164
+ */
165
+ dispose() {
166
+ this.agentPicker.finalize();
167
+ this.sessionMenu.finalize();
168
+ this.teardownDismissal();
169
+ }
170
+ teardownDismissal() {
171
+ if (this.dismissPointer) {
172
+ document.removeEventListener('pointerdown', this.dismissPointer, true);
173
+ this.dismissPointer = undefined;
174
+ }
175
+ if (this.dismissKey) {
176
+ document.removeEventListener('keydown', this.dismissKey, true);
177
+ this.dismissKey = undefined;
178
+ }
179
+ if (this.dismissResize) {
180
+ window.removeEventListener('resize', this.dismissResize);
181
+ this.dismissResize = undefined;
182
+ }
183
+ }
184
+ /**
185
+ * Places an open composer menu directly above the control that opened it.
186
+ *
187
+ * CSS alone cannot: the menu is a child of `.composer` — which is what lets its
188
+ * width be capped against the composer rather than against a button — while its
189
+ * anchor is a button several levels down the bar, and no selector can read a
190
+ * sibling's offset. So the left edge is measured on open and written as a custom
191
+ * property.
192
+ *
193
+ * Clamped to the composer, so a menu opened from a control near the right edge of
194
+ * a narrow panel slides back inside instead of overhanging. The animation origin
195
+ * is written separately and keeps pointing at the button's centre even after such
196
+ * a shift — the menu still grows out of the thing that opened it, which is the
197
+ * whole point of anchoring it there.
198
+ */
199
+ position(panelSelector, triggerSelector) {
200
+ const root = this.host.shadowRoot;
201
+ const composer = root === null || root === void 0 ? void 0 : root.querySelector('.composer');
202
+ const panel = root === null || root === void 0 ? void 0 : root.querySelector(panelSelector);
203
+ const trigger = root === null || root === void 0 ? void 0 : root.querySelector(triggerSelector);
204
+ if (!composer || !panel || !trigger)
205
+ return;
206
+ const box = composer.getBoundingClientRect();
207
+ const anchor = trigger.getBoundingClientRect();
208
+ // An absolutely-positioned child is placed against its containing block's
209
+ // PADDING box, while getBoundingClientRect reports the border box — so every
210
+ // offset below is taken from the padding edge. Skipping this is a 1px drift
211
+ // that reads as the menu being very slightly out of line with its button.
212
+ const style = getComputedStyle(composer);
213
+ const originLeft = box.left + (parseFloat(style.borderLeftWidth) || 0);
214
+ const originBottom = box.bottom - (parseFloat(style.borderBottomWidth) || 0);
215
+ // A menu wider than the composer clamps to 0 rather than a negative offset:
216
+ // Math.min picks the negative headroom, Math.max floors it at the left edge.
217
+ const left = Math.max(0, Math.min(anchor.left - originLeft, composer.clientWidth - panel.offsetWidth));
218
+ panel.style.setProperty('--composer-menu-left', `${left}px`);
219
+ panel.style.setProperty('--composer-menu-origin', `${anchor.left + anchor.width / 2 - originLeft - left}px`);
220
+ // Sit on the button rather than on top of the whole composer. The gap stays in
221
+ // CSS so it can be expressed in design units; this is the raw distance from the
222
+ // padding edge up to the button's top.
223
+ panel.style.setProperty('--composer-menu-bottom', `${originBottom - anchor.top}px`);
224
+ // Starting on the button rather than on the composer means less room above, and
225
+ // the assistant can be short enough that a full-height menu would be clipped by
226
+ // the wrapper's overflow. Cap it at what is actually available; the menu scrolls
227
+ // inside whatever it gets.
228
+ const wrapper = root === null || root === void 0 ? void 0 : root.querySelector('.chat-wrapper');
229
+ if (wrapper) {
230
+ const available = anchor.top - wrapper.getBoundingClientRect().top - MENU_HEADROOM_RESERVE;
231
+ panel.style.setProperty('--composer-menu-max-height', `${Math.max(COMPOSER_MENU_MIN_HEIGHT, available)}px`);
232
+ }
233
+ }
234
+ }
@@ -0,0 +1,119 @@
1
+ import { __decorate } from "tslib";
2
+ import { observable } from '@genesislcap/web-core';
3
+ /** Floor for a dragged composer height — below this the control bar clips. */
4
+ const COMPOSER_MIN_HEIGHT_PX = 48;
5
+ /** Ceiling for a dragged composer height, whatever headroom is available. */
6
+ const COMPOSER_MAX_HEIGHT_PX = 400;
7
+ /**
8
+ * Strip of the message list a resize must leave visible. The composer may claim
9
+ * the rest of the list's height, but not all of it — a composer filling the panel
10
+ * with no transcript behind it reads as a broken layout rather than a big input.
11
+ */
12
+ const COMPOSER_MIN_MESSAGES_PX = 80;
13
+ /**
14
+ * Drag-to-resize for the composer: owns the chosen height and the transient
15
+ * geometry of one drag gesture.
16
+ *
17
+ * @internal
18
+ */
19
+ export class ComposerResizeController {
20
+ constructor(deps) {
21
+ this.deps = deps;
22
+ /**
23
+ * Explicit composer height in px once the user has dragged; `null` keeps the
24
+ * default auto-grow behaviour.
25
+ *
26
+ * Observable because {@link ComposerResizeController.heightStyle} is bound in
27
+ * the template — see the sibling scratch fields for why the rest are not.
28
+ */
29
+ this.heightPx = null;
30
+ /**
31
+ * Pointer Y and height captured at the start of a drag.
32
+ *
33
+ * Deliberately NOT observable, and the same goes for the two bounds below.
34
+ * These are rewritten on every `pointermove`, and no binding reads them, so
35
+ * marking them would fire a notification per mouse-move for values nothing
36
+ * displays — while `heightPx`, which does drive the render, fires exactly one.
37
+ */
38
+ this.startY = 0;
39
+ this.startHeight = 0;
40
+ /** Natural (un-dragged) height, measured on the first drag — the floor for shrinking. */
41
+ this.naturalHeight = 0;
42
+ /** Upper bound for the current drag, capped to the headroom above the composer. */
43
+ this.maxHeight = COMPOSER_MAX_HEIGHT_PX;
44
+ }
45
+ /** Whether the composer shows a drag handle. On by default; hosts opt out via config. */
46
+ get resizable() {
47
+ return this.deps.enabled();
48
+ }
49
+ /**
50
+ * Inline style for the composer: writes the chosen height into the
51
+ * `--composer-height` custom property (read by `.chat-input` and its control in
52
+ * `main.styles.ts`). Empty until the user drags, or when resizing is disabled,
53
+ * so the CSS default applies.
54
+ *
55
+ * `heightPx` is read into a local BEFORE the branch, and that is load-bearing.
56
+ * Written as `this.resizable && this.heightPx != null ? …`, the `&&`
57
+ * short-circuits past `heightPx` whenever resizing is off — so on a host that
58
+ * mounts with `resizableInput: false`, FAST never records a dependency on it.
59
+ * The template's binding for this getter (`style=${(x) => x.composerHeightStyle}`)
60
+ * carries no `:`/`&&`/`||`, so FAST classifies it non-volatile and collects
61
+ * dependencies exactly once per bind; the missing dependency is therefore never
62
+ * picked up, and a later `resizableInput: true` gives a handle that drags —
63
+ * `heightPx` updates — while the composer never visibly moves.
64
+ *
65
+ * Reading it unconditionally makes the dependency honest and costs nothing.
66
+ * `@volatile` would also fix it, by re-collecting on every evaluation, but it pays
67
+ * a full unsubscribe/resubscribe cycle each time to express something that is
68
+ * simply true: this getter always depends on both.
69
+ */
70
+ get heightStyle() {
71
+ const px = this.heightPx;
72
+ return this.resizable && px != null ? `--composer-height: ${px}px` : '';
73
+ }
74
+ /**
75
+ * Begin a drag. Captures the pointer so move/up keep firing on the handle even
76
+ * when the cursor leaves its 7px band.
77
+ */
78
+ onStart(e) {
79
+ var _a, _b, _c, _d, _e;
80
+ const measured = (_b = (_a = this.deps.inputEl()) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect().height) !== null && _b !== void 0 ? _b : COMPOSER_MIN_HEIGHT_PX;
81
+ // First drag (no explicit height yet) measures the natural height — the floor
82
+ // for shrinking, so the box never goes below where it started.
83
+ if (this.heightPx == null)
84
+ this.naturalHeight = measured;
85
+ this.startY = e.clientY;
86
+ this.startHeight = (_c = this.heightPx) !== null && _c !== void 0 ? _c : measured;
87
+ // Cap growth to the headroom currently above the composer: the message list
88
+ // can yield all but a visible strip of its height. The pool (composer +
89
+ // messages) is fixed for the duration of one drag, so measuring once here is
90
+ // correct throughout the gesture.
91
+ const messagesHeight = (_e = (_d = this.deps.messagesEl()) === null || _d === void 0 ? void 0 : _d.getBoundingClientRect().height) !== null && _e !== void 0 ? _e : COMPOSER_MAX_HEIGHT_PX;
92
+ this.maxHeight = Math.min(COMPOSER_MAX_HEIGHT_PX, this.startHeight + Math.max(0, messagesHeight - COMPOSER_MIN_MESSAGES_PX));
93
+ e.currentTarget.setPointerCapture(e.pointerId);
94
+ }
95
+ /**
96
+ * Resize while dragging. Dragging up (clientY decreases) grows the composer; the
97
+ * result is clamped between the natural height and the available headroom. No-op
98
+ * unless a drag is active (the handle holds pointer capture), so plain
99
+ * hover-moves are ignored.
100
+ */
101
+ onMove(e) {
102
+ const handle = e.currentTarget;
103
+ if (!handle.hasPointerCapture(e.pointerId))
104
+ return;
105
+ const min = Math.max(COMPOSER_MIN_HEIGHT_PX, Math.round(this.naturalHeight));
106
+ const max = Math.max(min, Math.round(this.maxHeight));
107
+ const next = this.startHeight + (this.startY - e.clientY);
108
+ this.heightPx = Math.max(min, Math.min(max, Math.round(next)));
109
+ }
110
+ /** End a drag and release the pointer capture. */
111
+ onEnd(e) {
112
+ const handle = e.currentTarget;
113
+ if (handle.hasPointerCapture(e.pointerId))
114
+ handle.releasePointerCapture(e.pointerId);
115
+ }
116
+ }
117
+ __decorate([
118
+ observable
119
+ ], ComposerResizeController.prototype, "heightPx", void 0);
@@ -0,0 +1,289 @@
1
+ import { __decorate } from "tslib";
2
+ import { volatile } from '@genesislcap/web-core';
3
+ import { CONTEXT_ABORT_MARGIN_TOKENS, resolveContextBudget, } from '../utils/resolve-context-budget';
4
+ /**
5
+ * The context gate: how close the conversation is to the model's window, what
6
+ * compacting would reclaim, and the copy that states both (GENC-1567).
7
+ *
8
+ * Owns **no state at all** — every member derives from the callbacks above. That
9
+ * is what makes it the safest of these extractions: there is no field to forget
10
+ * to mark `@observable`, so the observability trap in `docs/code-health.md` §11.4
11
+ * cannot bite here.
12
+ *
13
+ * @internal
14
+ */
15
+ export class ContextGateController {
16
+ constructor(deps) {
17
+ this.deps = deps;
18
+ }
19
+ /** Window, warn/block thresholds and compaction sizing for the active model. */
20
+ get budget() {
21
+ // Resolved ONCE. The reserve re-reads live routing state on every access, so
22
+ // two reads can land on different winners and pair one agent's
23
+ // `warnAheadTokens` with another's `tokens` — the exact pairing
24
+ // `ContextReserveResolution` warns against (reviewer catch on #2504). It also
25
+ // stops the orchestrator walking every specialist plus the fallback twice per
26
+ // evaluation.
27
+ const reserve = this.deps.contextReserve();
28
+ return resolveContextBudget({
29
+ chatConfig: this.deps.chatConfig(),
30
+ contextLimit: this.deps.contextLimit(),
31
+ agentReserveTokens: reserve === null || reserve === void 0 ? void 0 : reserve.tokens,
32
+ agentWarnAheadTokens: reserve === null || reserve === void 0 ? void 0 : reserve.warnAheadTokens,
33
+ });
34
+ }
35
+ /**
36
+ * What compacting right now would reclaim, or `null` when it would not be worth
37
+ * it. Sized against {@link ContextGateController.budget}, so the plan aims at
38
+ * this model's window rather than a fixed default.
39
+ *
40
+ * Reads `messages` to stay reactive as the conversation grows: the driver's
41
+ * history is not a tracked observable, so every consumer keys its re-evaluation
42
+ * off the Redux mirror.
43
+ *
44
+ * Deliberately NOT memoized on `messages` identity: it runs against the driver's
45
+ * history, which can move without the mirror moving (a mid-turn `condenseWhen`
46
+ * rewrite), so an identity key would risk serving a plan for a transcript the
47
+ * driver no longer has.
48
+ */
49
+ get plan() {
50
+ if (this.deps.messages().length === 0)
51
+ return null;
52
+ const budget = this.budget;
53
+ return this.deps.compactionPlanFor({
54
+ preambleTokens: this.deps.preambleTokens(),
55
+ tailTokenBudget: budget.tailTokenBudget,
56
+ minReclaimTokens: budget.minReclaimTokens,
57
+ });
58
+ }
59
+ /**
60
+ * Guard policy pushed to the driver, or `undefined` when the host has switched
61
+ * the gate off — so `enabled: false` really does mean no local blocking of any
62
+ * kind, UI or mid-loop.
63
+ *
64
+ * A MARGIN, not a threshold: the driver resolves the window of the provider each
65
+ * call actually goes to. The fallback window rides along because the driver
66
+ * cannot learn it any other way — without it, a host that knows its window while
67
+ * its provider does not report one had `fallbackContextLimit` protecting the
68
+ * composer and nothing else.
69
+ */
70
+ get guardPolicy() {
71
+ var _a;
72
+ if (!this.budget.enabled)
73
+ return undefined;
74
+ return {
75
+ marginTokens: CONTEXT_ABORT_MARGIN_TOKENS,
76
+ fallbackLimit: (_a = this.deps.chatConfig().context) === null || _a === void 0 ? void 0 : _a.fallbackContextLimit,
77
+ };
78
+ }
79
+ /**
80
+ * How close this conversation is to the model's context window.
81
+ *
82
+ * **Derived, never latched** — the opposite of the budget wall. A wall stays set
83
+ * because nothing the user does fixes it; this clears itself the moment the
84
+ * conversation shrinks, which is precisely what the user is being asked to do.
85
+ *
86
+ * **A flow in progress is never blocked, only warned.** Machine state lives
87
+ * outside the transcript, so a stateful flow is not desynced by compaction — but
88
+ * the summary can still drop detail the flow's own prompt leans on, and blocking
89
+ * mid-journey would strand the user between a flow they cannot finish and a
90
+ * compaction that is a poor idea right there.
91
+ */
92
+ get gate() {
93
+ var _a;
94
+ const budget = this.budget;
95
+ if (!budget.enabled)
96
+ return 'ok';
97
+ // The figure from the last moment nothing was running, not the live one — see
98
+ // `gateContextTokens` for why the gauge and the gate differ here.
99
+ const tokens = (_a = this.deps.gateContextTokens()) !== null && _a !== void 0 ? _a : this.deps.contextTokens();
100
+ if (tokens == null || budget.blockAt == null || budget.warnAt == null)
101
+ return 'ok';
102
+ if (tokens >= budget.blockAt) {
103
+ return this.deps.flowOwnerAgentName() !== null ? 'warn' : 'blocked';
104
+ }
105
+ return tokens >= budget.warnAt ? 'warn' : 'ok';
106
+ }
107
+ /**
108
+ * WHY the composer is blocked, when it is (GENC-1582). `undefined` unless
109
+ * {@link ContextGateController.gate} is `'blocked'`.
110
+ *
111
+ * - `'transcript'` — the conversation has grown into the reserve.
112
+ * - `'model'` — the reserve meets or exceeds the whole window, so `blockAt` is 0
113
+ * and EVERY message is refused, including the first of an empty chat.
114
+ *
115
+ * Keyed off `blockAt === 0`, deliberately NOT off whether a compaction plan
116
+ * exists. A null plan has two unrelated causes — nothing left worth summarizing,
117
+ * versus a window that cannot host the agent at all — and only the second is a
118
+ * statement about the configuration rather than the transcript.
119
+ *
120
+ * **This drives behaviour, not copy.** It reads as though it should also pick the
121
+ * wording, and GENC-1582 had it do so: the `'model'` branch told the user to
122
+ * switch to a model with a larger context window, or pick a different agent.
123
+ * Removed, because the assistant offers no way to do either — the model follows
124
+ * from the host's provider registry and each agent's `provider` field, and the
125
+ * only user-facing lever is the agent picker, which is triple-gated
126
+ * (`agentPicker` enabled, two or more agents, at least one with
127
+ * `manualSelection.enabled`) and commonly absent. Copy that names a control the
128
+ * user does not have is worse than copy that names a remedy which merely turns
129
+ * out to be insufficient, and `contextGateReason` is not host-overridable, so
130
+ * nobody downstream could correct it either. Both causes now read the GENC-1567
131
+ * sentence.
132
+ *
133
+ * What it still decides is `sessionMenuForcedByContext`: the transcript cause
134
+ * forces the session menu open so a user told to compact can reach Compact, and
135
+ * the model cause deliberately does not, because there Compact and Clear are
136
+ * both futile and a menu of dead actions helps nobody.
137
+ *
138
+ * The block itself is correct in both cases and must stay: an agent that
139
+ * measurably cannot complete a turn in this window would otherwise start one and
140
+ * die mid-loop on a provider context error, with partial writes and no clean
141
+ * exit. Refusing up front is the better failure.
142
+ */
143
+ get blockCause() {
144
+ if (this.gate !== 'blocked')
145
+ return undefined;
146
+ return this.budget.blockAt === 0 ? 'model' : 'transcript';
147
+ }
148
+ /**
149
+ * What the composer notice says about the gate.
150
+ *
151
+ * The copy is a function of whether compaction can actually clear it, which is
152
+ * why it reads {@link ContextGateController.plan} rather than assuming. Telling
153
+ * someone to compact when the projection says it would not free enough — a tail
154
+ * that is one indivisible tool result, say — sends them into a summarizer call
155
+ * that leaves them exactly where they were.
156
+ */
157
+ get gateReason() {
158
+ const level = this.gate;
159
+ const canHelp = this.plan != null;
160
+ if (level === 'blocked') {
161
+ // Name the agent whose headroom is binding when routing is automatic
162
+ // (GENC-1582). The reserve is then the hungriest agent the turn could land
163
+ // on, so the composer can close while there is plenty of room for the short
164
+ // question the user was about to type — and a message that only said "not
165
+ // enough room for more work" would read as a bug rather than a reason.
166
+ const reserve = this.deps.contextReserve();
167
+ const cause = (reserve === null || reserve === void 0 ? void 0 : reserve.basis) === 'reachable-max'
168
+ ? `There is not enough room in the model context to start another task — ${reserve.agentName} needs the most of any I could pick up next.`
169
+ : 'There is not enough room in the model context for more work.';
170
+ const remedy = canHelp
171
+ ? 'Click the error pill and compact the context to continue working.'
172
+ : 'Compacting the context will not free enough room, click the error pill to clear and start a new conversation.';
173
+ return `${cause} ${remedy}`;
174
+ }
175
+ if (this.deps.flowOwnerAgentName() !== null) {
176
+ return 'This conversation is nearly full. The task in progress will finish, but compact before starting another.';
177
+ }
178
+ return canHelp
179
+ ? 'This conversation is getting long. Compact it to free up room.'
180
+ : 'This conversation is getting long and is close to the limit.';
181
+ }
182
+ /**
183
+ * Short label the session-menu pill wears while the gate is active — the primary
184
+ * visible signal, in place of a banner.
185
+ *
186
+ * It states the CONDITION only; the remedy lives in the placeholder once the
187
+ * composer is actually blocked, which is the moment the user needs to be told
188
+ * what to do rather than merely warned.
189
+ */
190
+ get gateLabel() {
191
+ switch (this.gate) {
192
+ case 'blocked':
193
+ return 'At context limit';
194
+ case 'warn':
195
+ return 'Approaching context limit';
196
+ default:
197
+ return '';
198
+ }
199
+ }
200
+ /** Tooltip and hint for the Compact row — why it is or is not available. */
201
+ get compactHint() {
202
+ if (this.plan)
203
+ return `Summarize older messages to free up context`;
204
+ return this.deps.messages().length === 0
205
+ ? 'Nothing to compact yet'
206
+ : 'There is nothing left to summarize here — the recent messages are too large to shrink further';
207
+ }
208
+ /**
209
+ * Whether the menu's "Compact" action would do anything. Delegates to the
210
+ * driver's plan so the gate uses the exact same history `compact()` acts on,
211
+ * judged against the same budget — one source of truth for both whether a cut is
212
+ * legal and whether it reclaims enough.
213
+ */
214
+ get compactable() {
215
+ return this.plan != null;
216
+ }
217
+ /**
218
+ * Whether the composer notice has anything to show — a budget wall or the gate.
219
+ * One region for both, so the live region stays single and registered.
220
+ */
221
+ get noticeVisible() {
222
+ return this.deps.bannerVisible() || this.gate !== 'ok';
223
+ }
224
+ /**
225
+ * Text for that region. A budget wall outranks the gate: it is the condition the
226
+ * user cannot clear, so it must not be hidden behind advice about compacting that
227
+ * would not restore sending anyway.
228
+ */
229
+ get noticeText() {
230
+ if (this.deps.bannerVisible())
231
+ return this.deps.effectiveBlockedReason();
232
+ return this.gate === 'ok' ? '' : this.gateReason;
233
+ }
234
+ /**
235
+ * Whether the notice region should be announced but not drawn.
236
+ *
237
+ * The gate shows itself visually through the pill's label, the composer outline
238
+ * and the placeholder — a strip of prose on top would be a fourth statement of
239
+ * the same thing. But dropping the region outright would be an accessibility
240
+ * regression: a `role="status"` region announces MUTATIONS to a region already
241
+ * being observed, and it is the only announcement available here, because the
242
+ * composer flips to `disabled` at the same moment and a disabled control is out
243
+ * of the tab order — so neither the placeholder nor an `aria-label` on it is ever
244
+ * voiced.
245
+ *
246
+ * So the region stays mounted, keeps its text and keeps announcing; only the
247
+ * visual treatment is withheld. A budget wall still draws its banner, because
248
+ * that condition has no other visible expression.
249
+ */
250
+ get noticeSrOnly() {
251
+ return !this.deps.bannerVisible() && this.gate !== 'ok';
252
+ }
253
+ }
254
+ __decorate([
255
+ volatile
256
+ ], ContextGateController.prototype, "budget", null);
257
+ __decorate([
258
+ volatile
259
+ ], ContextGateController.prototype, "plan", null);
260
+ __decorate([
261
+ volatile
262
+ ], ContextGateController.prototype, "guardPolicy", null);
263
+ __decorate([
264
+ volatile
265
+ ], ContextGateController.prototype, "gate", null);
266
+ __decorate([
267
+ volatile
268
+ ], ContextGateController.prototype, "blockCause", null);
269
+ __decorate([
270
+ volatile
271
+ ], ContextGateController.prototype, "gateReason", null);
272
+ __decorate([
273
+ volatile
274
+ ], ContextGateController.prototype, "gateLabel", null);
275
+ __decorate([
276
+ volatile
277
+ ], ContextGateController.prototype, "compactHint", null);
278
+ __decorate([
279
+ volatile
280
+ ], ContextGateController.prototype, "compactable", null);
281
+ __decorate([
282
+ volatile
283
+ ], ContextGateController.prototype, "noticeVisible", null);
284
+ __decorate([
285
+ volatile
286
+ ], ContextGateController.prototype, "noticeText", null);
287
+ __decorate([
288
+ volatile
289
+ ], ContextGateController.prototype, "noticeSrOnly", null);