@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
@@ -100,15 +100,26 @@ const platformContextUsageTemplate = (progressTag) => html `
100
100
  <div class="settings-context-usage" part="context-usage">
101
101
  <div class="settings-context-header">
102
102
  <span class="settings-context-label">Context window</span>
103
+ <!--
104
+ Read through the pre-formatted \`settingsContextUsage\` rather than
105
+ \`contextTokens\`/\`contextLimit\` directly. These bindings used to carry
106
+ non-null assertions, and they re-evaluate in the SAME notify batch as the
107
+ \`when\` above that unmounts them — so a session Clear with this tab open
108
+ threw \`Cannot read properties of undefined (reading 'toLocaleString')\`
109
+ twice per render. Optional access on a shape that can be absent cannot.
110
+ The \`?? ''\` fallbacks matter for the same reason: these bindings provably
111
+ DO evaluate while the figures are absent, so without them a lost race
112
+ with the guard would flash the literal text "undefined".
113
+ -->
103
114
  <span class="settings-context-tokens">
104
- ${(x) => x.contextTokens.toLocaleString()} /
105
- ${(x) => x.contextLimit.toLocaleString()} tokens
106
- (${(x) => Math.round((x.contextTokens / x.contextLimit) * 100)}%)
115
+ ${(x) => { var _a, _b; return (_b = (_a = x.settingsContextUsage) === null || _a === void 0 ? void 0 : _a.used) !== null && _b !== void 0 ? _b : ''; }} /
116
+ ${(x) => { var _a, _b; return (_b = (_a = x.settingsContextUsage) === null || _a === void 0 ? void 0 : _a.limit) !== null && _b !== void 0 ? _b : ''; }} tokens
117
+ (${(x) => { var _a, _b; return (_b = (_a = x.settingsContextUsage) === null || _a === void 0 ? void 0 : _a.percent) !== null && _b !== void 0 ? _b : ''; }}%)
107
118
  </span>
108
119
  </div>
109
120
  <${progressTag}
110
121
  part="context-progress"
111
- value="${(x) => Math.min(100, Math.round((x.contextTokens / x.contextLimit) * 100))}"
122
+ value="${(x) => { var _a, _b; return (_b = (_a = x.settingsContextUsage) === null || _a === void 0 ? void 0 : _a.barValue) !== null && _b !== void 0 ? _b : 0; }}"
112
123
  ></${progressTag}>
113
124
  </div>
114
125
  `)}
@@ -0,0 +1,254 @@
1
+ import { __awaiter, __decorate } from "tslib";
2
+ import { observable } from '@genesislcap/web-core';
3
+ import { logger } from '../utils/logger';
4
+ /**
5
+ * The image types BOTH vendors accept, and therefore the only ones sent as image blocks.
6
+ *
7
+ * A host's `acceptedFiles` may well say `image/*`, which covers HEIC, BMP, TIFF, AVIF and SVG —
8
+ * none of which the APIs take. Sending one fails the whole turn the user is waiting on, so
9
+ * anything outside this set falls through to the text branch instead: harmless for a binary, and
10
+ * genuinely useful for SVG, which is text a model can read.
11
+ *
12
+ * `image/jpg` is absent on purpose — `normalizeImageMime` rewrites it to `image/jpeg` at the
13
+ * transport, but a file arriving with that spelling is still a real JPEG, so it is accepted here.
14
+ */
15
+ const VENDOR_IMAGE_MIME_TYPES = new Set([
16
+ 'image/jpeg',
17
+ 'image/jpg',
18
+ 'image/png',
19
+ 'image/gif',
20
+ 'image/webp',
21
+ ]);
22
+ /**
23
+ * Ceiling on ONE image attachment, under Anthropic's ~5MB-per-image limit with room for the ~33%
24
+ * base64 overhead. Refused here, with the size named, rather than at the vendor: a 400 arrives
25
+ * only after the user has waited out the turn.
26
+ *
27
+ * A VENDOR guard, and only that — it does not protect the session's storage quota, and the
28
+ * arithmetic says so: one file at this ceiling is ~4.7MB of base64, which alone can exceed a 5MB
29
+ * `localStorage` budget before the transcript is counted. Attachments riding into persisted
30
+ * storage is a real and separate problem; it is not this constant's to solve.
31
+ */
32
+ const MAX_IMAGE_ATTACHMENT_BYTES = 3500000;
33
+ /**
34
+ * Ceiling on ALL attachments carried by one message, across every type and every successive pick.
35
+ *
36
+ * The per-file check above is blind to the total: the picker APPENDS, so ten files each just under
37
+ * the per-image limit are ten passes and one ~34MB request. Sized against the vendors' request
38
+ * ceilings — comfortably inside the smaller of them with room for the transcript itself — so a
39
+ * turn that would be rejected wholesale is refused here, where the user can still act on it,
40
+ * naming the file that crossed the line.
41
+ *
42
+ * Like its sibling, this does NOT make attachments safe to persist — 8MB of attachments is still
43
+ * ~10.7MB of base64. What a persisted session should keep of its images is GENC-1531; note the
44
+ * transports also REPLAY history attachments on every later call, so it is a recurring cost as
45
+ * well as a storage one.
46
+ */
47
+ const MAX_ATTACHMENTS_TOTAL_BYTES = 8000000;
48
+ /** Base64 carries 3 bytes in every 4 characters, so decoded size is 3/4 of the encoded length. */
49
+ const BASE64_DECODED_RATIO = 0.75;
50
+ /** Bytes per megabyte, for the size the refusal message quotes back to the user. */
51
+ const BYTES_PER_MB = 1000000;
52
+ /** A rough byte size for an already-accepted attachment, for budgeting against the total cap. */
53
+ function attachmentBytes(a) {
54
+ var _a, _b, _c, _d;
55
+ return a.kind === 'image'
56
+ ? Math.ceil(((_b = (_a = a.data) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0) * BASE64_DECODED_RATIO)
57
+ : ((_d = (_c = a.content) === null || _c === void 0 ? void 0 : _c.length) !== null && _d !== void 0 ? _d : 0);
58
+ }
59
+ /**
60
+ * The composer's file attachments: what is pending on the next message, what was
61
+ * rejected and why, and the reading and validation that gets a picked `File` into
62
+ * a {@link ChatAttachment}.
63
+ *
64
+ * @internal
65
+ */
66
+ export class AttachmentController {
67
+ constructor(
68
+ /** Typed `HTMLElement` — the hidden file input is all this needs from the DOM. */
69
+ host, deps) {
70
+ this.host = host;
71
+ this.deps = deps;
72
+ /** Attachments pending on the next message. */
73
+ this.attachments = [];
74
+ /** Rejection messages for files the last pick refused. */
75
+ this.errors = [];
76
+ }
77
+ /** Open the OS file picker via the hidden input the template renders. */
78
+ triggerFileInput() {
79
+ var _a, _b;
80
+ (_b = (_a = this.host.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('.file-input')) === null || _b === void 0 ? void 0 : _b.click();
81
+ }
82
+ /** `@change` on the hidden input. Fire-and-forget; the read is async. */
83
+ handleFileSelect(e) {
84
+ void this.loadSelected(e);
85
+ }
86
+ remove(attachment) {
87
+ this.attachments = this.attachments.filter((a) => a !== attachment);
88
+ }
89
+ dismissError(message) {
90
+ const idx = this.errors.indexOf(message);
91
+ if (idx !== -1) {
92
+ this.errors = [...this.errors.slice(0, idx), ...this.errors.slice(idx + 1)];
93
+ }
94
+ }
95
+ /**
96
+ * Hand the pending attachments to a send and clear both lists in one step.
97
+ *
98
+ * Read-and-clear as one method rather than two statements: the previous inline
99
+ * form copied the array and then blanked the fields separately, leaving a state
100
+ * the element could be observed in between the two.
101
+ */
102
+ take() {
103
+ const pending = this.attachments.length ? [...this.attachments] : undefined;
104
+ this.attachments = [];
105
+ this.errors = [];
106
+ return pending;
107
+ }
108
+ /** Drop everything pending, without handing it anywhere. */
109
+ clear() {
110
+ this.attachments = [];
111
+ this.errors = [];
112
+ }
113
+ /** Total bytes already committed, for budgeting a further pick against the cap. */
114
+ pendingBytes() {
115
+ return this.attachments.reduce((n, a) => n + attachmentBytes(a), 0);
116
+ }
117
+ /**
118
+ * Read and validate a batch of files without committing them — for
119
+ * `submitMessage({ files })`, which must be able to refuse the whole message
120
+ * rather than half-attach it.
121
+ */
122
+ process(files_1) {
123
+ return __awaiter(this, arguments, void 0, function* (files, existingBytes = 0) {
124
+ // Budget the batch BEFORE reading any of it. `file.size` needs no I/O, so the verdict is
125
+ // deterministic and in the user's own pick order; deciding inside the parallel map below would
126
+ // hand the last slot to whichever read happened to finish first. `existingBytes` is what the
127
+ // caller has already committed to — the picker appends to a running list, so its files have to
128
+ // count against what is already attached rather than starting from zero each time.
129
+ let running = existingBytes;
130
+ const overBudget = new Map();
131
+ for (const file of files) {
132
+ if (running + file.size > MAX_ATTACHMENTS_TOTAL_BYTES) {
133
+ overBudget.set(file, `"${file.name}" would take this message past the ` +
134
+ `${MAX_ATTACHMENTS_TOTAL_BYTES / BYTES_PER_MB}MB attachment limit ` +
135
+ `(${(running / BYTES_PER_MB).toFixed(1)}MB already attached).`);
136
+ continue;
137
+ }
138
+ running += file.size;
139
+ }
140
+ const results = yield Promise.all(files.map((file) => __awaiter(this, void 0, void 0, function* () {
141
+ if (!this.isAccepted(file)) {
142
+ return { ok: false, message: `"${file.name}" is not an accepted file type.` };
143
+ }
144
+ const tooMuch = overBudget.get(file);
145
+ if (tooMuch)
146
+ return { ok: false, message: tooMuch };
147
+ try {
148
+ // An image file becomes a real image attachment — the transports render those as
149
+ // image blocks the model SEES. Reading it as text (the only path that existed)
150
+ // produced mojibake the model "read" as prose: an accepted .png was worse than a
151
+ // rejected one. Text stays the default for everything else.
152
+ //
153
+ // Only the types the vendors actually accept take this branch. `image/*` covers far
154
+ // more than that, and sending one an API rejects fails the whole turn the user is
155
+ // waiting on — where the text branch merely degrades. It also keeps SVG working:
156
+ // SVG *is* text, and reading it as such gives the model something it can genuinely
157
+ // read, which a base64 image block of it would not.
158
+ if (VENDOR_IMAGE_MIME_TYPES.has(file.type.toLowerCase())) {
159
+ if (file.size > MAX_IMAGE_ATTACHMENT_BYTES) {
160
+ return {
161
+ ok: false,
162
+ message: `"${file.name}" is ${Math.ceil(file.size / BYTES_PER_MB)}MB; images are limited to ` +
163
+ `${MAX_IMAGE_ATTACHMENT_BYTES / BYTES_PER_MB}MB.`,
164
+ };
165
+ }
166
+ const data = yield this.readAsBase64(file);
167
+ return {
168
+ ok: true,
169
+ attachment: { kind: 'image', name: file.name, mimeType: file.type, data },
170
+ };
171
+ }
172
+ const content = yield this.readAsText(file);
173
+ return {
174
+ ok: true,
175
+ attachment: { name: file.name, content, mimeType: file.type || 'text/plain' },
176
+ };
177
+ }
178
+ catch (readError) {
179
+ logger.error('Failed to read file:', readError);
180
+ this.deps.logMeta('file.read-failed', {
181
+ file: file.name,
182
+ message: readError instanceof Error ? readError.message : String(readError),
183
+ });
184
+ return { ok: false, message: `Failed to read "${file.name}".` };
185
+ }
186
+ })));
187
+ return {
188
+ attachments: results
189
+ .filter((r) => r.ok)
190
+ .map((r) => r.attachment),
191
+ errors: results
192
+ .filter((r) => !r.ok)
193
+ .map((r) => r.message),
194
+ };
195
+ });
196
+ }
197
+ isAccepted(file) {
198
+ var _a, _b;
199
+ const accepted = this.deps.acceptedFiles();
200
+ if (!accepted)
201
+ return false;
202
+ const acceptedList = accepted.split(',').map((s) => s.trim().toLowerCase());
203
+ const ext = `.${(_b = (_a = file.name.split('.').pop()) === null || _a === void 0 ? void 0 : _a.toLowerCase()) !== null && _b !== void 0 ? _b : ''}`;
204
+ return acceptedList.some((a) => a === ext ||
205
+ a === file.type ||
206
+ (a.endsWith('/*') && file.type.startsWith(a.replace('/*', '/'))));
207
+ }
208
+ // Blob's own promise APIs, not FileReader: UTF-8 only (a `charset` parameter on the blob's
209
+ // type is ignored, where FileReader would honour it), and they exist in the node test runtime
210
+ // too — FileReader does not, so neither reader was testable before.
211
+ readAsText(file) {
212
+ return file.text();
213
+ }
214
+ /**
215
+ * The base64 payload of a file, without any `data:` prefix — the raw form
216
+ * `ChatImageAttachment.data` specifies (transports add their own framing).
217
+ */
218
+ readAsBase64(file) {
219
+ return __awaiter(this, void 0, void 0, function* () {
220
+ const bytes = new Uint8Array(yield file.arrayBuffer());
221
+ let binary = '';
222
+ const CHUNK = 0x8000;
223
+ for (let i = 0; i < bytes.length; i += CHUNK) {
224
+ binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
225
+ }
226
+ return btoa(binary);
227
+ });
228
+ }
229
+ /** The picker path: read the picked files and APPEND whatever passed. */
230
+ loadSelected(e) {
231
+ return __awaiter(this, void 0, void 0, function* () {
232
+ var _a;
233
+ const input = e.target;
234
+ const files = Array.from((_a = input.files) !== null && _a !== void 0 ? _a : []);
235
+ input.value = '';
236
+ const { attachments, errors } = yield this.process(files, this.pendingBytes());
237
+ if (attachments.length) {
238
+ this.attachments = [...this.attachments, ...attachments];
239
+ this.deps.logMeta('attachment.added', {
240
+ count: attachments.length,
241
+ names: attachments.map((a) => a.name),
242
+ });
243
+ }
244
+ if (errors.length)
245
+ this.errors = [...this.errors, ...errors];
246
+ });
247
+ }
248
+ }
249
+ __decorate([
250
+ observable
251
+ ], AttachmentController.prototype, "attachments", void 0);
252
+ __decorate([
253
+ observable
254
+ ], AttachmentController.prototype, "errors", void 0);