@genesislcap/ai-assistant 15.19.6 → 15.20.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 +605 -72
- package/dist/ai-assistant.d.ts +404 -25
- package/dist/chat-driver.cjs +341 -28
- package/dist/chat-driver.cjs.map +4 -4
- package/dist/chat-driver.mjs +341 -28
- package/dist/chat-driver.mjs.map +4 -4
- package/dist/custom-elements.json +630 -20
- package/dist/dts/components/ai-driver/ai-driver.d.ts +33 -7
- package/dist/dts/components/ai-driver/ai-driver.d.ts.map +1 -1
- package/dist/dts/components/chat-driver/chat-driver.d.ts +63 -2
- package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +9 -3
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
- package/dist/dts/config/config.d.ts +44 -0
- package/dist/dts/config/config.d.ts.map +1 -1
- package/dist/dts/main/main.d.ts +187 -5
- package/dist/dts/main/main.d.ts.map +1 -1
- package/dist/dts/main/main.styles.d.ts.map +1 -1
- package/dist/dts/main/main.template.d.ts.map +1 -1
- package/dist/dts/utils/condense-history.d.ts.map +1 -1
- package/dist/dts/utils/context-tokens.d.ts +156 -0
- package/dist/dts/utils/context-tokens.d.ts.map +1 -0
- package/dist/dts/utils/history-transform.d.ts +76 -14
- package/dist/dts/utils/history-transform.d.ts.map +1 -1
- package/dist/dts/utils/resolve-context-budget.d.ts +98 -0
- package/dist/dts/utils/resolve-context-budget.d.ts.map +1 -0
- package/dist/esm/components/chat-driver/chat-driver.js +179 -34
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +12 -4
- package/dist/esm/main/main.js +391 -21
- package/dist/esm/main/main.styles.js +128 -0
- package/dist/esm/main/main.template.js +64 -29
- package/dist/esm/state/debug-event-log.js +1 -1
- package/dist/esm/utils/condense-history.js +1 -5
- package/dist/esm/utils/context-tokens.js +339 -0
- package/dist/esm/utils/history-transform.js +101 -19
- package/dist/esm/utils/resolve-context-budget.js +84 -0
- package/package.json +16 -16
- package/sandbox/README.md +93 -4
- package/sandbox/controls.ts +77 -10
- package/sandbox/fixtures.ts +163 -6
- package/sandbox/sandbox.css +54 -1
- package/sandbox/sandbox.ts +384 -7
package/sandbox/sandbox.ts
CHANGED
|
@@ -13,6 +13,7 @@ import '../src/index';
|
|
|
13
13
|
import type { FoundationAiAssistant } from '../src/main/main';
|
|
14
14
|
import type { PopoutMode } from '../src/main/main.types';
|
|
15
15
|
import { WebStorageSessionProvider } from '../src/state/persistence';
|
|
16
|
+
import { estimateContextTokens } from '../src/utils/context-tokens';
|
|
16
17
|
import { renderControls, type ControlGroup } from './controls';
|
|
17
18
|
import {
|
|
18
19
|
buildAgents,
|
|
@@ -35,12 +36,77 @@ registerRapidDesignSystem();
|
|
|
35
36
|
// resolves to the built-in empty registry and every send is a no-op; with it,
|
|
36
37
|
// Send returns a canned reply so the busy states are real. Registered before the
|
|
37
38
|
// element connects — `createDriver()` reads the registry there.
|
|
38
|
-
|
|
39
|
+
// Held rather than inlined: the sandbox writes its `contextLimit` on every apply
|
|
40
|
+
// so the element's own re-resolve agrees with what the panel is showing.
|
|
41
|
+
const aiProvider = new SandboxAIProvider();
|
|
42
|
+
registerAIProviders(DI.getOrCreateDOMContainer(), { sandbox: aiProvider });
|
|
39
43
|
|
|
40
44
|
const sessionProvider = new WebStorageSessionProvider(window.sessionStorage, {
|
|
41
45
|
keyPrefix: 'ai-assistant-sandbox',
|
|
42
46
|
});
|
|
43
47
|
|
|
48
|
+
// ─── Context-gate presets ────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* One-click jumps to each state the gate can be in (GENC-1567).
|
|
52
|
+
*
|
|
53
|
+
* Reaching these by hand means getting a transcript, a window and a reserve to
|
|
54
|
+
* agree, and the two blocked states differ by something not obvious from the
|
|
55
|
+
* panel at all — whether a compaction would reclaim enough to be worth offering.
|
|
56
|
+
* Each preset drives the ordinary controls rather than forcing the element, so
|
|
57
|
+
* what you land on is the component deciding for itself; the readouts underneath
|
|
58
|
+
* show it doing so.
|
|
59
|
+
*
|
|
60
|
+
* All four use the measured fill, so the gate and the compaction planner are
|
|
61
|
+
* always looking at the same conversation.
|
|
62
|
+
*/
|
|
63
|
+
const CONTEXT_PRESETS = {
|
|
64
|
+
normal: {
|
|
65
|
+
label: 'Normal (plenty of room)',
|
|
66
|
+
transcript: 'rich',
|
|
67
|
+
},
|
|
68
|
+
warning: {
|
|
69
|
+
label: 'Warning (approaching the limit)',
|
|
70
|
+
transcript: 'huge',
|
|
71
|
+
},
|
|
72
|
+
'blocked-compactable': {
|
|
73
|
+
label: 'Blocked — compacting will clear it',
|
|
74
|
+
// A long tool loop: over the block, and with cuttable boundaries inside it.
|
|
75
|
+
transcript: 'loop',
|
|
76
|
+
},
|
|
77
|
+
'blocked-stuck': {
|
|
78
|
+
label: 'Blocked — compacting will NOT clear it',
|
|
79
|
+
// One indivisible tool result. No cut can shrink it, so the copy has to offer
|
|
80
|
+
// a new chat instead of Compact.
|
|
81
|
+
transcript: 'wall',
|
|
82
|
+
},
|
|
83
|
+
} as const satisfies Record<string, { label: string; transcript: TranscriptName }>;
|
|
84
|
+
|
|
85
|
+
type ContextPresetName = keyof typeof CONTEXT_PRESETS;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Shown once any control in the group has been moved by hand, so the dropdown
|
|
89
|
+
* never claims a state the panel has since been steered away from.
|
|
90
|
+
*
|
|
91
|
+
* It also makes re-selecting a preset work at all: a `<select>` fires no
|
|
92
|
+
* `change` event when you pick the option already showing, so without something
|
|
93
|
+
* to fall back to, "jump to Normal" would be a no-op for anyone already sitting
|
|
94
|
+
* on Normal — which is the default, and so the first thing most people try.
|
|
95
|
+
*/
|
|
96
|
+
const CUSTOM_PRESET = 'custom';
|
|
97
|
+
|
|
98
|
+
/** Keys the preset owns. Touching any of them by hand drops the panel to custom. */
|
|
99
|
+
const CONTEXT_PRESET_KEYS = [
|
|
100
|
+
'transcript',
|
|
101
|
+
'contextSource',
|
|
102
|
+
'contextWindow',
|
|
103
|
+
'contextFill',
|
|
104
|
+
'reserveTokens',
|
|
105
|
+
'warnMultiplier',
|
|
106
|
+
'contextGateEnabled',
|
|
107
|
+
'flowInProgress',
|
|
108
|
+
] as const;
|
|
109
|
+
|
|
44
110
|
// ─── State ───────────────────────────────────────────────────────────────────
|
|
45
111
|
|
|
46
112
|
interface SandboxState {
|
|
@@ -70,6 +136,15 @@ interface SandboxState {
|
|
|
70
136
|
showThinkingSteps: boolean;
|
|
71
137
|
showNarration: boolean;
|
|
72
138
|
animations: boolean;
|
|
139
|
+
// Context window (GENC-1567)
|
|
140
|
+
contextPreset: ContextPresetName | typeof CUSTOM_PRESET;
|
|
141
|
+
contextWindow: number;
|
|
142
|
+
contextSource: 'auto' | 'manual' | 'none';
|
|
143
|
+
contextFill: number;
|
|
144
|
+
contextGateEnabled: boolean;
|
|
145
|
+
reserveTokens: number;
|
|
146
|
+
warnMultiplier: number;
|
|
147
|
+
flowInProgress: boolean;
|
|
73
148
|
// Simulated runtime
|
|
74
149
|
runtime: 'idle' | 'loading' | 'cancelling' | 'blocked' | 'compacting' | 'restoring';
|
|
75
150
|
transcript: TranscriptName;
|
|
@@ -102,6 +177,18 @@ const DEFAULT_STATE: SandboxState = {
|
|
|
102
177
|
showThinkingSteps: true,
|
|
103
178
|
showNarration: true,
|
|
104
179
|
animations: true,
|
|
180
|
+
// Chosen so the `huge` (~20k) and `loop` (~22k) seeds actually clear the tail
|
|
181
|
+
// budget these produce (~12.8k) and so have something to compact. Verified, not
|
|
182
|
+
// guessed — a slightly roomier pairing leaves the tail larger than either seed,
|
|
183
|
+
// and Compact then greys out for a reason that looks like a bug.
|
|
184
|
+
contextPreset: 'normal',
|
|
185
|
+
contextWindow: 30_000,
|
|
186
|
+
contextSource: 'auto',
|
|
187
|
+
contextFill: 60,
|
|
188
|
+
contextGateEnabled: true,
|
|
189
|
+
reserveTokens: 8_000,
|
|
190
|
+
warnMultiplier: 2,
|
|
191
|
+
flowInProgress: false,
|
|
105
192
|
runtime: 'idle',
|
|
106
193
|
transcript: 'rich',
|
|
107
194
|
width: 480,
|
|
@@ -109,12 +196,25 @@ const DEFAULT_STATE: SandboxState = {
|
|
|
109
196
|
theme: 'dark',
|
|
110
197
|
};
|
|
111
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Narrowest stage the sandbox offers.
|
|
201
|
+
*
|
|
202
|
+
* The assistant's blocked-state copy is sized against this — see the
|
|
203
|
+
* `.composer.context-blocked` floor in `main.styles.ts` — so letting the stage go
|
|
204
|
+
* narrower would show a clipped message that no supported width produces.
|
|
205
|
+
*/
|
|
206
|
+
const MIN_STAGE_WIDTH = 400;
|
|
207
|
+
|
|
112
208
|
const STORAGE_KEY = 'ai-assistant-sandbox:state';
|
|
113
209
|
|
|
114
210
|
const loadState = (): SandboxState => {
|
|
115
211
|
try {
|
|
116
212
|
const raw = localStorage.getItem(STORAGE_KEY);
|
|
117
|
-
|
|
213
|
+
const loaded = raw ? { ...DEFAULT_STATE, ...JSON.parse(raw) } : { ...DEFAULT_STATE };
|
|
214
|
+
// A panel saved before the floor existed can carry a narrower width, and it
|
|
215
|
+
// would survive every reload while the slider refused to reproduce it.
|
|
216
|
+
loaded.width = Math.max(MIN_STAGE_WIDTH, loaded.width);
|
|
217
|
+
return loaded;
|
|
118
218
|
} catch {
|
|
119
219
|
return { ...DEFAULT_STATE };
|
|
120
220
|
}
|
|
@@ -163,6 +263,11 @@ const seedTranscript = (s: SandboxState): void => {
|
|
|
163
263
|
const seed = TRANSCRIPTS[s.transcript].map((m) => ({ ...m }));
|
|
164
264
|
(assistant as any).driver?.loadHistory?.(seed.map((m) => ({ ...m })));
|
|
165
265
|
assistant.messages = seed;
|
|
266
|
+
// `auto` measures the transcript, which only exists as of this tick — so the
|
|
267
|
+
// fill and every readout have to be recomputed now rather than in the
|
|
268
|
+
// `apply()` that scheduled this.
|
|
269
|
+
applyContext(s);
|
|
270
|
+
refreshReadouts();
|
|
166
271
|
}, 0);
|
|
167
272
|
};
|
|
168
273
|
|
|
@@ -172,6 +277,12 @@ const seedTranscript = (s: SandboxState): void => {
|
|
|
172
277
|
* cog-menu choices — which is what you want when a template edit hot-reloads.
|
|
173
278
|
*/
|
|
174
279
|
const mountAssistant = (): void => {
|
|
280
|
+
// BEFORE the element connects. `connectedCallback` resolves the context limit
|
|
281
|
+
// from the provider straight away, and `getStatus()` reads this field
|
|
282
|
+
// synchronously — so setting it in `apply()` (which runs after `appendChild`)
|
|
283
|
+
// loses the race and the element keeps the stub's default window for the rest
|
|
284
|
+
// of the session.
|
|
285
|
+
aiProvider.contextLimit = state.contextWindow;
|
|
175
286
|
provider.textContent = '';
|
|
176
287
|
assistant = document.createElement('foundation-ai-assistant') as FoundationAiAssistant;
|
|
177
288
|
assistant.setAttribute('session-key', 'sandbox');
|
|
@@ -202,6 +313,11 @@ const buildChatConfig = (s: SandboxState): ChatConfig => ({
|
|
|
202
313
|
// `userConfigurable` turns the defaults on and adds the multiselect to the cog.
|
|
203
314
|
animations: s.animations ? { userConfigurable: true } : undefined,
|
|
204
315
|
},
|
|
316
|
+
context: {
|
|
317
|
+
enabled: s.contextGateEnabled,
|
|
318
|
+
reserveTokens: s.reserveTokens,
|
|
319
|
+
warnMultiplier: s.warnMultiplier,
|
|
320
|
+
},
|
|
205
321
|
picker: {
|
|
206
322
|
mode: s.pickerMode,
|
|
207
323
|
disablePinColours: s.disablePinColours,
|
|
@@ -250,6 +366,92 @@ const applyDivider = (on: boolean): void => {
|
|
|
250
366
|
);
|
|
251
367
|
};
|
|
252
368
|
|
|
369
|
+
/**
|
|
370
|
+
* Drives the context gate (GENC-1567).
|
|
371
|
+
*
|
|
372
|
+
* The window goes onto the PROVIDER as well as the element. The element
|
|
373
|
+
* re-resolves its limit from the active provider, so a value poked only onto the
|
|
374
|
+
* element is live until that happens and then silently reverts — writing both
|
|
375
|
+
* means the panel and the component cannot disagree.
|
|
376
|
+
*
|
|
377
|
+
* **`contextTokens` has to be simulated** — the stub reports no usage, so nothing
|
|
378
|
+
* would ever set it and the gate would sit inert forever. How it is simulated
|
|
379
|
+
* matters more than it looks:
|
|
380
|
+
*
|
|
381
|
+
* - `auto` measures the seeded transcript with the component's own estimator, so
|
|
382
|
+
* the gate and the compaction planner are looking at the same conversation.
|
|
383
|
+
* This is the honest mode and the default.
|
|
384
|
+
* - `manual` forces a percentage. Useful for parking on a threshold without
|
|
385
|
+
* seeding anything, but it can DISAGREE with the transcript — claim 85% of the
|
|
386
|
+
* window while the transcript holds 250 tokens and the gate says "full" while
|
|
387
|
+
* the planner correctly says there is nothing here worth compacting, so the
|
|
388
|
+
* blocked copy offers a new chat rather than Compact. That is the component
|
|
389
|
+
* being right about an impossible situation, not a bug; the readouts show both
|
|
390
|
+
* figures so the mismatch is visible rather than mystifying.
|
|
391
|
+
* - `none` leaves it unset — the real state on a provider that reports no usage,
|
|
392
|
+
* where the gate must stay quiet rather than guess.
|
|
393
|
+
*/
|
|
394
|
+
/**
|
|
395
|
+
* Expand a preset into the ordinary controls.
|
|
396
|
+
*
|
|
397
|
+
* Writes the whole context group, not just the transcript — including the pieces
|
|
398
|
+
* a previous experiment may have left set (a manual fill, a flow in progress, the
|
|
399
|
+
* gate switched off). A preset that only moved some of them would land somewhere
|
|
400
|
+
* different depending on what you had been doing, which is exactly what a "show
|
|
401
|
+
* me this state" shortcut must not do.
|
|
402
|
+
*/
|
|
403
|
+
const applyContextPreset = (s: SandboxState): void => {
|
|
404
|
+
if (s.contextPreset === CUSTOM_PRESET) return;
|
|
405
|
+
const preset = CONTEXT_PRESETS[s.contextPreset];
|
|
406
|
+
s.transcript = preset.transcript;
|
|
407
|
+
s.contextSource = 'auto';
|
|
408
|
+
s.contextWindow = 30_000;
|
|
409
|
+
s.reserveTokens = 8_000;
|
|
410
|
+
s.warnMultiplier = 2;
|
|
411
|
+
s.contextGateEnabled = true;
|
|
412
|
+
s.flowInProgress = false;
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Keep the measured fill live.
|
|
417
|
+
*
|
|
418
|
+
* Without this, `auto` measures only when a panel control moves — so a
|
|
419
|
+
* compaction (which the component performs on its own) would shrink the
|
|
420
|
+
* transcript while the readouts, and the gate, went on quoting the old size. The
|
|
421
|
+
* panel would then show a block that compacting had already cleared, which is
|
|
422
|
+
* precisely the bug the component was fixed for and exactly the wrong thing for a
|
|
423
|
+
* harness to fake.
|
|
424
|
+
*
|
|
425
|
+
* Idempotent and re-checked on every apply: the driver is rebuilt whenever the
|
|
426
|
+
* agents change, and the listener has to follow it.
|
|
427
|
+
*/
|
|
428
|
+
let boundDriver: unknown;
|
|
429
|
+
|
|
430
|
+
const bindHistoryListener = (): void => {
|
|
431
|
+
const driver = (assistant as any).driver;
|
|
432
|
+
if (!driver || driver === boundDriver) return;
|
|
433
|
+
boundDriver = driver;
|
|
434
|
+
driver.addEventListener('history-updated', () => {
|
|
435
|
+
if (state.contextSource !== 'auto') return;
|
|
436
|
+
applyContext(state);
|
|
437
|
+
refreshReadouts();
|
|
438
|
+
});
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
const applyContext = (s: SandboxState): void => {
|
|
442
|
+
aiProvider.contextLimit = s.contextWindow;
|
|
443
|
+
assistant.contextLimit = s.contextWindow;
|
|
444
|
+
assistant.contextTokens =
|
|
445
|
+
s.contextSource === 'none'
|
|
446
|
+
? undefined
|
|
447
|
+
: s.contextSource === 'manual'
|
|
448
|
+
? Math.round((s.contextWindow * s.contextFill) / 100)
|
|
449
|
+
: estimateContextTokens(assistant.messages);
|
|
450
|
+
// Standing in for a stateful agent holding the flow. The gate downgrades a
|
|
451
|
+
// block to a warning while this is set, so a flow can always finish.
|
|
452
|
+
assistant.flowOwnerAgentName = s.flowInProgress ? 'Reconciliation' : null;
|
|
453
|
+
};
|
|
454
|
+
|
|
253
455
|
const applyRuntime = (s: SandboxState): void => {
|
|
254
456
|
assistant.compacting = s.runtime === 'compacting';
|
|
255
457
|
assistant.restoring = s.runtime === 'restoring';
|
|
@@ -280,6 +482,8 @@ function apply(): void {
|
|
|
280
482
|
assistant.persistence = state.persistenceProvider
|
|
281
483
|
? { enabled: false, provider: sessionProvider }
|
|
282
484
|
: {};
|
|
485
|
+
bindHistoryListener();
|
|
486
|
+
applyContext(state);
|
|
283
487
|
applyRuntime(state);
|
|
284
488
|
applyGradientSend(state.gradientSend);
|
|
285
489
|
applyDivider(state.showDivider);
|
|
@@ -288,8 +492,55 @@ function apply(): void {
|
|
|
288
492
|
seedTranscript(state);
|
|
289
493
|
applyStage(state);
|
|
290
494
|
saveState(state);
|
|
495
|
+
refreshReadouts();
|
|
291
496
|
}
|
|
292
497
|
|
|
498
|
+
// ─── Readouts ────────────────────────────────────────────────────────────────
|
|
499
|
+
//
|
|
500
|
+
// Every figure below is read back off the ELEMENT rather than recomputed from
|
|
501
|
+
// panel state. Recomputing here would only ever confirm the panel agrees with
|
|
502
|
+
// itself; the point is to see what the component derived.
|
|
503
|
+
|
|
504
|
+
const n = (v: number | undefined): string => (v == null ? '—' : Math.round(v).toLocaleString());
|
|
505
|
+
|
|
506
|
+
const describeGate = (): string => {
|
|
507
|
+
const tokens = assistant.contextTokens;
|
|
508
|
+
const limit = assistant.contextBudget.limit;
|
|
509
|
+
const pct = tokens != null && limit ? ` (${Math.round((tokens / limit) * 100)}%)` : '';
|
|
510
|
+
return `${assistant.contextGate} — ${n(tokens)} / ${n(limit)}${pct}`;
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
const describeThresholds = (): string => {
|
|
514
|
+
const b = assistant.contextBudget;
|
|
515
|
+
if (!b.enabled) return 'gate off';
|
|
516
|
+
return `warn ${n(b.warnAt)} · block ${n(b.blockAt)} · abort ${n(b.abortAt)} · tail ${n(b.tailTokenBudget)}`;
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* How far a manual fill may drift from the measured transcript before the
|
|
521
|
+
* readout calls it out. Generous — the two are never expected to match exactly,
|
|
522
|
+
* and only an order-of-magnitude disagreement misleads.
|
|
523
|
+
*/
|
|
524
|
+
const FILL_DRIFT_WARN_TOKENS = 2_000;
|
|
525
|
+
|
|
526
|
+
const describeTranscript = (): string => {
|
|
527
|
+
const measured = estimateContextTokens(assistant.messages);
|
|
528
|
+
const claimed = assistant.contextTokens;
|
|
529
|
+
const drift =
|
|
530
|
+
state.contextSource === 'manual' &&
|
|
531
|
+
claimed != null &&
|
|
532
|
+
Math.abs(claimed - measured) > FILL_DRIFT_WARN_TOKENS
|
|
533
|
+
? ` ⚠ fill claims ${n(claimed)} — the gate and the planner are looking at different conversations`
|
|
534
|
+
: '';
|
|
535
|
+
return `${assistant.messages.length} msgs · ~${n(measured)} tokens${drift}`;
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
const describePlan = (): string => {
|
|
539
|
+
const plan = assistant.compactionPlan;
|
|
540
|
+
if (!plan) return `none — ${assistant.compactHint}`;
|
|
541
|
+
return `cut @${plan.cut} · −${n(plan.reclaimed)} → ${n(plan.tokensAfter)}`;
|
|
542
|
+
};
|
|
543
|
+
|
|
293
544
|
// ─── Control panel ───────────────────────────────────────────────────────────
|
|
294
545
|
|
|
295
546
|
const GROUPS: ReadonlyArray<ControlGroup<SandboxState>> = [
|
|
@@ -384,7 +635,12 @@ const GROUPS: ReadonlyArray<ControlGroup<SandboxState>> = [
|
|
|
384
635
|
label: 'Persistence provider configured',
|
|
385
636
|
hint: 'The menu is gated on a provider — with none, ⋮ never renders.',
|
|
386
637
|
},
|
|
387
|
-
{
|
|
638
|
+
{
|
|
639
|
+
kind: 'toggle',
|
|
640
|
+
key: 'showSessionMenu',
|
|
641
|
+
label: 'chatConfig.ui.showSessionMenu',
|
|
642
|
+
hint: 'Turn this off (or the provider above) and push the fill past the warning — the gate forces ⋮ back on screen, showing Compact only.',
|
|
643
|
+
},
|
|
388
644
|
{
|
|
389
645
|
kind: 'action',
|
|
390
646
|
label: 'Open / close session menu',
|
|
@@ -393,6 +649,104 @@ const GROUPS: ReadonlyArray<ControlGroup<SandboxState>> = [
|
|
|
393
649
|
},
|
|
394
650
|
],
|
|
395
651
|
},
|
|
652
|
+
{
|
|
653
|
+
title: 'Context window (GENC-1567)',
|
|
654
|
+
controls: [
|
|
655
|
+
{
|
|
656
|
+
kind: 'select',
|
|
657
|
+
key: 'contextPreset',
|
|
658
|
+
label: 'Jump to state',
|
|
659
|
+
featured: true,
|
|
660
|
+
hint: 'Sets the whole group below to land on that state. It is a shortcut, not a readout — once you move anything yourself, the Gate line is what tells you where you actually are.',
|
|
661
|
+
options: [
|
|
662
|
+
...Object.entries(CONTEXT_PRESETS).map(([value, { label }]) => ({ value, label })),
|
|
663
|
+
{ value: CUSTOM_PRESET, label: 'Custom (hand-tuned)' },
|
|
664
|
+
],
|
|
665
|
+
},
|
|
666
|
+
{
|
|
667
|
+
kind: 'range',
|
|
668
|
+
key: 'contextWindow',
|
|
669
|
+
label: 'Window (tokens)',
|
|
670
|
+
min: 10_000,
|
|
671
|
+
max: 200_000,
|
|
672
|
+
step: 5_000,
|
|
673
|
+
hint: 'What the stub provider reports. Small on purpose — at a real 200k window the block sits at 175k, and seeding a transcript that big makes the page miserable.',
|
|
674
|
+
},
|
|
675
|
+
{
|
|
676
|
+
kind: 'select',
|
|
677
|
+
key: 'contextSource',
|
|
678
|
+
label: 'Fill from',
|
|
679
|
+
hint: 'The stub reports no usage, so this has to be simulated. `auto` measures the seeded transcript with the real estimator — gate and compaction planner then agree. `manual` can disagree with the transcript on purpose; watch the readouts.',
|
|
680
|
+
options: [
|
|
681
|
+
{ value: 'auto', label: 'Transcript (measured)' },
|
|
682
|
+
{ value: 'manual', label: 'Manual %' },
|
|
683
|
+
{ value: 'none', label: 'Unset (no usage reported)' },
|
|
684
|
+
],
|
|
685
|
+
},
|
|
686
|
+
{
|
|
687
|
+
kind: 'range',
|
|
688
|
+
key: 'contextFill',
|
|
689
|
+
label: 'Manual fill %',
|
|
690
|
+
min: 0,
|
|
691
|
+
max: 100,
|
|
692
|
+
hint: 'Only used when "Fill from" is Manual.',
|
|
693
|
+
},
|
|
694
|
+
{ kind: 'toggle', key: 'contextGateEnabled', label: 'chatConfig.context.enabled' },
|
|
695
|
+
{
|
|
696
|
+
kind: 'range',
|
|
697
|
+
key: 'reserveTokens',
|
|
698
|
+
label: 'Reserve (tokens)',
|
|
699
|
+
min: 1_000,
|
|
700
|
+
max: 40_000,
|
|
701
|
+
step: 1_000,
|
|
702
|
+
hint: 'Headroom kept free for one turn. Block sits one reserve below the window.',
|
|
703
|
+
},
|
|
704
|
+
{
|
|
705
|
+
kind: 'range',
|
|
706
|
+
key: 'warnMultiplier',
|
|
707
|
+
label: 'Warn multiplier',
|
|
708
|
+
min: 1,
|
|
709
|
+
max: 4,
|
|
710
|
+
step: 0.5,
|
|
711
|
+
hint: 'How many reserves ahead of the block the warning appears.',
|
|
712
|
+
},
|
|
713
|
+
{
|
|
714
|
+
kind: 'toggle',
|
|
715
|
+
key: 'flowInProgress',
|
|
716
|
+
label: 'Flow in progress',
|
|
717
|
+
hint: 'Sets a flow owner. A block downgrades to a warning while set, so a flow can always finish.',
|
|
718
|
+
},
|
|
719
|
+
{
|
|
720
|
+
kind: 'readout',
|
|
721
|
+
label: 'Gate',
|
|
722
|
+
render: () => describeGate(),
|
|
723
|
+
hint: 'Derived by the component, not the panel — this is the check.',
|
|
724
|
+
},
|
|
725
|
+
{
|
|
726
|
+
kind: 'readout',
|
|
727
|
+
label: 'Thresholds',
|
|
728
|
+
render: () => describeThresholds(),
|
|
729
|
+
},
|
|
730
|
+
{
|
|
731
|
+
kind: 'readout',
|
|
732
|
+
label: 'Transcript',
|
|
733
|
+
render: () => describeTranscript(),
|
|
734
|
+
hint: 'What the estimator actually measures. If this is far below the fill, a manual fill is claiming a conversation that is not there.',
|
|
735
|
+
},
|
|
736
|
+
{
|
|
737
|
+
kind: 'readout',
|
|
738
|
+
label: 'Compaction plan',
|
|
739
|
+
render: () => describePlan(),
|
|
740
|
+
hint: 'What Compact would actually reclaim. `none` here is why the button greys out.',
|
|
741
|
+
},
|
|
742
|
+
{
|
|
743
|
+
kind: 'action',
|
|
744
|
+
label: 'Compact now',
|
|
745
|
+
run: () => void assistant.compactSession(),
|
|
746
|
+
hint: 'Bypasses the menu. Seed `loop` or `huge` first — a small transcript is correctly not worth compacting.',
|
|
747
|
+
},
|
|
748
|
+
],
|
|
749
|
+
},
|
|
396
750
|
{
|
|
397
751
|
title: 'Simulated state',
|
|
398
752
|
controls: [
|
|
@@ -424,6 +778,9 @@ const GROUPS: ReadonlyArray<ControlGroup<SandboxState>> = [
|
|
|
424
778
|
{ value: 'short', label: 'Short' },
|
|
425
779
|
{ value: 'rich', label: 'Tools, markdown, agent switch' },
|
|
426
780
|
{ value: 'long', label: 'Long (28 turns)' },
|
|
781
|
+
{ value: 'huge', label: 'Huge (~20k tokens)' },
|
|
782
|
+
{ value: 'loop', label: 'Tool loop (~22k, no user turns inside)' },
|
|
783
|
+
{ value: 'wall', label: 'Wall (~24k in one indivisible tool result)' },
|
|
427
784
|
],
|
|
428
785
|
},
|
|
429
786
|
{
|
|
@@ -480,10 +837,10 @@ const GROUPS: ReadonlyArray<ControlGroup<SandboxState>> = [
|
|
|
480
837
|
kind: 'range',
|
|
481
838
|
key: 'width',
|
|
482
839
|
label: 'Width',
|
|
483
|
-
min:
|
|
840
|
+
min: MIN_STAGE_WIDTH,
|
|
484
841
|
max: 1600,
|
|
485
842
|
step: 10,
|
|
486
|
-
hint: 'The composer floats free of the transcript above 1500px (a container query).',
|
|
843
|
+
hint: 'Floors at 400px — the narrowest supported composer, and what the blocked-state copy is sized against. The composer floats free of the transcript above 1500px (a container query).',
|
|
487
844
|
},
|
|
488
845
|
{ kind: 'range', key: 'height', label: 'Height', min: 320, max: 1000, step: 10 },
|
|
489
846
|
{
|
|
@@ -514,12 +871,32 @@ const GROUPS: ReadonlyArray<ControlGroup<SandboxState>> = [
|
|
|
514
871
|
},
|
|
515
872
|
];
|
|
516
873
|
|
|
517
|
-
|
|
874
|
+
/**
|
|
875
|
+
* Replaced by `paint()`. A no-op until then because `mountAssistant()` calls
|
|
876
|
+
* `apply()` before the panel exists.
|
|
877
|
+
*/
|
|
878
|
+
let refreshReadouts: () => void = () => {};
|
|
879
|
+
|
|
880
|
+
const paint = (): void => {
|
|
881
|
+
refreshReadouts = renderControls(panel, GROUPS, state, (_s, key) => {
|
|
882
|
+
if (key === 'contextPreset') {
|
|
883
|
+
// Expand it, then let `apply()`'s refresh push the rewritten values back
|
|
884
|
+
// into the controls. No rebuild — that would drop focus and collapse any
|
|
885
|
+
// group the user had opened.
|
|
886
|
+
applyContextPreset(state);
|
|
887
|
+
} else if ((CONTEXT_PRESET_KEYS as readonly string[]).includes(key ?? '')) {
|
|
888
|
+
// Hand-tuned now, so stop claiming a preset. Reaching 'custom' is also what
|
|
889
|
+
// makes the next preset pick a real `change` event on the select.
|
|
890
|
+
state.contextPreset = CUSTOM_PRESET;
|
|
891
|
+
}
|
|
892
|
+
apply();
|
|
893
|
+
});
|
|
894
|
+
};
|
|
518
895
|
|
|
519
896
|
// ─── Viewport presets + live size readout ────────────────────────────────────
|
|
520
897
|
|
|
521
898
|
const PRESETS: ReadonlyArray<{ label: string; width: number; height: number }> = [
|
|
522
|
-
{ label: 'Bubble
|
|
899
|
+
{ label: 'Bubble 400', width: MIN_STAGE_WIDTH, height: 560 },
|
|
523
900
|
{ label: 'Panel 480', width: 480, height: 760 },
|
|
524
901
|
{ label: 'Dialog 640', width: 640, height: 720 },
|
|
525
902
|
{ label: 'Wide 1000', width: 1000, height: 800 },
|