@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.
- package/dist/ai-assistant.api.json +419 -50
- package/dist/ai-assistant.d.ts +217 -683
- package/dist/chat-driver.cjs.map +1 -1
- package/dist/chat-driver.mjs.map +1 -1
- package/dist/custom-elements.json +310 -609
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +16 -0
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
- package/dist/dts/components/settings-modal/index.d.ts +1 -0
- package/dist/dts/components/settings-modal/index.d.ts.map +1 -1
- package/dist/dts/components/settings-modal/settings-modal.d.ts +215 -0
- package/dist/dts/components/settings-modal/settings-modal.d.ts.map +1 -0
- package/dist/dts/components/settings-modal/settings-modal.template.d.ts.map +1 -1
- package/dist/dts/main/attachment-controller.d.ts +66 -0
- package/dist/dts/main/attachment-controller.d.ts.map +1 -0
- package/dist/dts/main/budget-controller.d.ts +368 -0
- package/dist/dts/main/budget-controller.d.ts.map +1 -0
- package/dist/dts/main/composer-menu-controller.d.ts +113 -0
- package/dist/dts/main/composer-menu-controller.d.ts.map +1 -0
- package/dist/dts/main/composer-resize-controller.d.ts +80 -0
- package/dist/dts/main/composer-resize-controller.d.ts.map +1 -0
- package/dist/dts/main/context-gate-controller.d.ts +197 -0
- package/dist/dts/main/context-gate-controller.d.ts.map +1 -0
- package/dist/dts/main/cost-ledger-controller.d.ts +228 -0
- package/dist/dts/main/cost-ledger-controller.d.ts.map +1 -0
- package/dist/dts/main/main.d.ts +201 -722
- package/dist/dts/main/main.d.ts.map +1 -1
- package/dist/dts/main/main.types.d.ts +25 -0
- package/dist/dts/main/main.types.d.ts.map +1 -1
- package/dist/dts/main/meta-snapshot.d.ts +50 -0
- package/dist/dts/main/meta-snapshot.d.ts.map +1 -0
- package/dist/dts/state/persistence/session-persister.d.ts.map +1 -1
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +18 -0
- package/dist/esm/components/settings-modal/index.js +1 -0
- package/dist/esm/components/settings-modal/settings-modal.js +436 -0
- package/dist/esm/components/settings-modal/settings-modal.template.js +15 -4
- package/dist/esm/main/attachment-controller.js +254 -0
- package/dist/esm/main/budget-controller.js +745 -0
- package/dist/esm/main/composer-menu-controller.js +234 -0
- package/dist/esm/main/composer-resize-controller.js +119 -0
- package/dist/esm/main/context-gate-controller.js +289 -0
- package/dist/esm/main/cost-ledger-controller.js +498 -0
- package/dist/esm/main/main.js +501 -2268
- package/dist/esm/main/meta-snapshot.js +108 -0
- package/dist/esm/state/persistence/session-persister.js +5 -4
- package/docs/migration-GENC-1580.md +209 -0
- 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);
|