@kitn.ai/ui 0.22.0 → 0.22.1

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/mcp.es.js CHANGED
@@ -737,14 +737,14 @@ Call component_reference with no name (or name: "list") to list all ${all.length
737
737
  lines.push(`- **${slot.name}**${desc ? ` — ${desc}` : ""}`);
738
738
  }
739
739
  }
740
- const parts = el.cssParts ?? [];
741
- if (parts.length > 0) {
740
+ const parts2 = el.cssParts ?? [];
741
+ if (parts2.length > 0) {
742
742
  lines.push(
743
743
  "",
744
744
  "### Styleable parts (`::part`)",
745
745
  "Restyle these from outside the Shadow DOM via `" + tag + "::part(name) { … }`."
746
746
  );
747
- for (const part of parts) {
747
+ for (const part of parts2) {
748
748
  const desc = part.description?.trim() ?? "";
749
749
  lines.push(`- **${part.name}**${desc ? ` — ${desc}` : ""}`);
750
750
  if (part.recipe) {
@@ -2594,6 +2594,83 @@ function listIntegrations() {
2594
2594
  function listArchetypes() {
2595
2595
  return archetypes;
2596
2596
  }
2597
+ const ENCODABLE = [
2598
+ { pattern: "image/jpeg", kind: "image" },
2599
+ { pattern: "image/png", kind: "image" },
2600
+ { pattern: "image/gif", kind: "image" },
2601
+ { pattern: "image/webp", kind: "image" },
2602
+ { pattern: "application/pdf", kind: "document" },
2603
+ { pattern: "text/*", kind: "text" },
2604
+ { pattern: "application/json", kind: "text" },
2605
+ { pattern: "application/xml", kind: "text" },
2606
+ { pattern: "application/x-yaml", kind: "text" },
2607
+ { pattern: "application/yaml", kind: "text" }
2608
+ ];
2609
+ const UNNAMED_TEXT_MEDIA_TYPE = "text/plain";
2610
+ const NAMES_NOTHING = ["", "application/octet-stream"];
2611
+ const normalize = (value) => value.trim().toLowerCase();
2612
+ const namesNothing = (mediaType) => NAMES_NOTHING.includes(mediaType === void 0 ? "" : normalize(mediaType));
2613
+ const assertMediaType = (entry) => {
2614
+ if (!entry.startsWith(".")) return entry;
2615
+ throw new Error(
2616
+ `\`accept\` entry "${entry}" is a file extension, and this kit takes media types only. HTML's own accept attribute does take extensions, which is exactly why this throws rather than quietly matching nothing. Use a media type instead -- "text/*" covers source files, "image/*" images, "application/pdf" PDFs, and \`encodableMediaTypes()\` is the full set. You lose nothing by dropping the extension: a file the browser cannot name is decided by decoding its bytes, which is stronger than matching its name.`
2617
+ );
2618
+ };
2619
+ const splitFilter = (filter) => (typeof filter === "string" ? filter.split(",") : filter).map(normalize).filter((v2) => v2 !== "").map(assertMediaType);
2620
+ const parts = (pattern) => {
2621
+ const slash = pattern.indexOf("/");
2622
+ return slash === -1 ? [pattern, ""] : [pattern.slice(0, slash), pattern.slice(slash + 1)];
2623
+ };
2624
+ const matches = (pattern, mediaType) => {
2625
+ const [pType, pSub] = parts(pattern);
2626
+ const [mType, mSub] = parts(mediaType);
2627
+ if (pType !== "*" && pType !== mType) return false;
2628
+ return pSub === "*" || pSub === mSub;
2629
+ };
2630
+ const intersect = (capability, filter) => {
2631
+ if (matches(capability, filter)) return filter;
2632
+ if (matches(filter, capability)) return capability;
2633
+ return void 0;
2634
+ };
2635
+ function encodableMediaTypes() {
2636
+ return ENCODABLE.map((entry) => entry.pattern);
2637
+ }
2638
+ function resolveMediaPolicy(options = {}) {
2639
+ const filter = options.accept === void 0 ? void 0 : splitFilter(options.accept);
2640
+ const effective = [];
2641
+ for (const capability of ENCODABLE) {
2642
+ if (filter === void 0) {
2643
+ effective.push({ ...capability });
2644
+ continue;
2645
+ }
2646
+ for (const wanted of filter) {
2647
+ const overlap = intersect(capability.pattern, wanted);
2648
+ if (overlap !== void 0 && !effective.some((e) => e.pattern === overlap)) {
2649
+ effective.push({ pattern: overlap, kind: capability.kind });
2650
+ }
2651
+ }
2652
+ }
2653
+ return {
2654
+ types: effective.map((e) => e.pattern),
2655
+ accept: effective.map((e) => e.pattern).join(","),
2656
+ decide(mediaType) {
2657
+ const type = mediaType === void 0 ? "" : normalize(mediaType);
2658
+ if (namesNothing(type)) {
2659
+ return effective.some((e) => matches(e.pattern, UNNAMED_TEXT_MEDIA_TYPE)) ? { status: "undetermined" } : (
2660
+ // `unsupported` rather than `filtered`, deliberately: `filtered`
2661
+ // claims the kit COULD have encoded this file, and nobody has
2662
+ // established what it is. Unread and unnamed, it is not a text
2663
+ // file the filter excluded -- it is a file with no known type.
2664
+ { status: "unsupported" }
2665
+ );
2666
+ }
2667
+ const hit = effective.find((e) => matches(e.pattern, type));
2668
+ if (hit) return { status: "allowed", kind: hit.kind };
2669
+ return ENCODABLE.some((e) => matches(e.pattern, type)) ? { status: "filtered" } : { status: "unsupported" };
2670
+ }
2671
+ };
2672
+ }
2673
+ resolveMediaPolicy();
2597
2674
  const text = (s) => ({
2598
2675
  content: [{ type: "text", text: s }]
2599
2676
  });
@@ -3015,6 +3092,7 @@ const ATTACHMENT_TAGS = /* @__PURE__ */ new Set(["kai-file-upload", "kai-attachm
3015
3092
  function hasAttachments(components) {
3016
3093
  return components.includes("kai-file-upload") && components.includes("kai-attachments");
3017
3094
  }
3095
+ const ATTACHMENT_ACCEPT = encodableMediaTypes().join(",");
3018
3096
  const ATTACHMENT_WIRE_NOTE = [
3019
3097
  "// The staged files ride along on the message as `file` parts, so they RENDER",
3020
3098
  "// in the thread AND reach the model: toOpenAIMessages / toAnthropicMessages",
@@ -3119,7 +3197,7 @@ function componentTags(components, chatFill) {
3119
3197
  ` <!-- Drop files here to stage them for the NEXT message. src/main.ts wires`,
3120
3198
  ` kai-files-added -> the staged list -> kai-attachments' items property. -->`,
3121
3199
  ` <div style="flex: 0 0 auto; display: flex; flex-direction: column; gap: 0.5rem; padding: 0.75rem;">`,
3122
- ` <kai-file-upload id="upload" accept="image/*,application/pdf"></kai-file-upload>`,
3200
+ ` <kai-file-upload id="upload" accept="${ATTACHMENT_ACCEPT}"></kai-file-upload>`,
3123
3201
  ` <!-- items is a JS PROPERTY (arrays can't be attributes); 'removable' fires kai-remove. -->`,
3124
3202
  ` <kai-attachments id="attachments" variant="inline" removable></kai-attachments>`,
3125
3203
  ` </div>`
@@ -3382,7 +3460,7 @@ function renderJsx(components, ctx, framework) {
3382
3460
  const attachmentJsx = attachments ? [
3383
3461
  ` {/* Drop files here to stage them for the NEXT message. */}`,
3384
3462
  ` <div style={{ flex: '0 0 auto', display: 'flex', flexDirection: 'column', gap: '0.5rem', padding: '0.75rem' }}>`,
3385
- ` <FileUpload accept="image/*,application/pdf" onFilesAdded={onFilesAdded} />`,
3463
+ ` <FileUpload accept="${ATTACHMENT_ACCEPT}" onFilesAdded={onFilesAdded} />`,
3386
3464
  ` {/* items is an ARRAY, so the wrapper sets it as a DOM property, never an attribute. */}`,
3387
3465
  ` <Attachments items={staged} variant="inline" removable onRemove={onRemoveAttachment} />`,
3388
3466
  ` </div>`
@@ -3659,7 +3737,7 @@ function renderVue(components, ctx) {
3659
3737
  const attachmentTemplate = attachments ? [
3660
3738
  ` <!-- Drop files here to stage them for the NEXT message. -->`,
3661
3739
  ` <div style="flex: 0 0 auto; display: flex; flex-direction: column; gap: 0.5rem; padding: 0.75rem;">`,
3662
- ` <kai-file-upload accept="image/*,application/pdf" @kai-files-added="onFilesAdded" />`,
3740
+ ` <kai-file-upload accept="${ATTACHMENT_ACCEPT}" @kai-files-added="onFilesAdded" />`,
3663
3741
  ` <kai-attachments :items.prop="staged" variant="inline" removable @kai-remove="onRemoveAttachment" />`,
3664
3742
  ` </div>`
3665
3743
  ].join("\n") : "";
@@ -3914,7 +3992,7 @@ function renderSvelte(components, ctx) {
3914
3992
  const attachmentMarkup = attachments ? [
3915
3993
  ` <!-- Drop files here to stage them for the NEXT message. -->`,
3916
3994
  ` <div style="flex: 0 0 auto; display: flex; flex-direction: column; gap: 0.5rem; padding: 0.75rem;">`,
3917
- ` <kai-file-upload accept="image/*,application/pdf" onkai-files-added={onFilesAdded}></kai-file-upload>`,
3995
+ ` <kai-file-upload accept="${ATTACHMENT_ACCEPT}" onkai-files-added={onFilesAdded}></kai-file-upload>`,
3918
3996
  ` <kai-attachments bind:this={attachmentsEl} variant="inline" removable onkai-remove={onRemoveAttachment}></kai-attachments>`,
3919
3997
  ` </div>`
3920
3998
  ] : [];
@@ -4033,7 +4111,7 @@ function renderTanstackStart(components, ctx) {
4033
4111
  const attachmentJsx = attachments ? [
4034
4112
  ` {/* Drop files here to stage them for the NEXT message. */}`,
4035
4113
  ` <div style={{ flex: '0 0 auto', display: 'flex', flexDirection: 'column', gap: '0.5rem', padding: '0.75rem' }}>`,
4036
- ` <FileUpload accept="image/*,application/pdf" onFilesAdded={onFilesAdded} />`,
4114
+ ` <FileUpload accept="${ATTACHMENT_ACCEPT}" onFilesAdded={onFilesAdded} />`,
4037
4115
  ` {/* items is an ARRAY, so the wrapper sets it as a DOM property, never an attribute. */}`,
4038
4116
  ` <Attachments items={staged} variant="inline" removable onRemove={onRemoveAttachment} />`,
4039
4117
  ` </div>`
@@ -4235,7 +4313,7 @@ function renderAngular(components, ctx) {
4235
4313
  const attachmentTemplate = attachments ? [
4236
4314
  ` <!-- Drop files here to stage them for the NEXT message. -->`,
4237
4315
  ` <div style="flex: 0 0 auto; display: flex; flex-direction: column; gap: 0.5rem; padding: 0.75rem;">`,
4238
- ` <kai-file-upload accept="image/*,application/pdf" (kai-files-added)="onFilesAdded($event)"></kai-file-upload>`,
4316
+ ` <kai-file-upload accept="${ATTACHMENT_ACCEPT}" (kai-files-added)="onFilesAdded($event)"></kai-file-upload>`,
4239
4317
  ` <kai-attachments [items]="staged()" variant="inline" removable (kai-remove)="onRemoveAttachment($event)"></kai-attachments>`,
4240
4318
  ` </div>`
4241
4319
  ] : [];
@@ -4598,7 +4676,7 @@ function renderSolid(components, ctx) {
4598
4676
  ` {/* Read every file BEFORE appending: a per-file append would order`,
4599
4677
  ` the list by whichever finished reading first. */}`,
4600
4678
  ` <FileUpload`,
4601
- ` accept="image/*,application/pdf"`,
4679
+ ` accept="${ATTACHMENT_ACCEPT}"`,
4602
4680
  ` onFilesAdded={async (files) => setStaged([...staged(), ...(await Promise.all(files.map(toAttachment)))])}`,
4603
4681
  ` >`,
4604
4682
  ` <FileUploadTrigger class="border-border text-muted-foreground w-full rounded-xl border border-dashed px-4 py-3 text-center text-sm">`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kitn.ai/ui",
3
- "version": "0.22.0",
3
+ "version": "0.22.1",
4
4
  "nx": {
5
5
  "name": "ui",
6
6
  "targets": {
@@ -178,6 +178,8 @@
178
178
  "verify:ssr:render": "node scripts/verify-ssr-render.mjs",
179
179
  "verify:pack": "node scripts/verify-pack-weight.mjs",
180
180
  "verify:starters": "node scripts/verify-starters.mjs",
181
+ "lint:attachment-object-urls": "node scripts/lint-attachment-object-urls.mjs --self-test && node scripts/lint-attachment-object-urls.mjs",
182
+ "lint:silent-drops": "node scripts/lint-silent-drops.mjs --self-test && node scripts/lint-silent-drops.mjs",
181
183
  "test": "vitest run",
182
184
  "test:react": "vitest run --config vitest.react.config.ts",
183
185
  "test:storybook": "vitest run --project=storybook",
@@ -8,6 +8,12 @@ import {
8
8
  listArchetypes,
9
9
  listIntegrations,
10
10
  } from '../../registry';
11
+ // The kit's media-type declaration, read rather than restated. This is the one
12
+ // import in agent-tooling that reaches outside itself, and the module it reaches
13
+ // for is the reason: `wire/media-types.ts` is pure (no I/O, no DOM, no solid-js),
14
+ // so the Node MCP pass typechecks and bundles it unchanged. See
15
+ // `ATTACHMENT_ACCEPT`.
16
+ import { encodableMediaTypes } from '../../../wire/media-types';
11
17
 
12
18
  /**
13
19
  * scaffold — the keystone tool. Composes a working chat surface from four axes:
@@ -1081,6 +1087,40 @@ function hasAttachments(components: readonly string[]): boolean {
1081
1087
  return components.includes('kai-file-upload') && components.includes('kai-attachments');
1082
1088
  }
1083
1089
 
1090
+ /**
1091
+ * ★ THE EMITTED `accept`, DERIVED. The seven renderers below interpolate this;
1092
+ * none of them writes a media type.
1093
+ *
1094
+ * It used to be the literal `image/*,application/pdf` in all seven, which was
1095
+ * right when only images and PDFs could be encoded and silently stopped being
1096
+ * right when #190 made `text/*`, `application/json`, `application/xml` and YAML
1097
+ * encodable. The list did not become wrong in this file — it became wrong in
1098
+ * every app scaffolded from it, which is the worst place for a copy to live and
1099
+ * exactly what `wire/media-types.ts` exists to prevent. An `accept` attribute is
1100
+ * a harder case than the prose in `ATTACHMENT_WIRE_NOTE` only in that a comment
1101
+ * can point at a function and an attribute has to CONTAIN the answer; the fix is
1102
+ * the same either way, which is to read the declaration rather than restate it.
1103
+ *
1104
+ * WHY EMIT IT AT ALL, RATHER THAN NOTHING. An absent filter means the full
1105
+ * capability set to `resolveMediaPolicy`, so omitting looks equivalent and is
1106
+ * not, because the tags emitted here never call it. `<kai-file-upload>` (and the
1107
+ * Solid `<FileUpload>`) pass `accept` straight to `<input type="file">` and do no
1108
+ * JS filtering at all — unlike `<kai-chat accept>`, which resolves the policy in
1109
+ * `default-input.tsx`. So on THESE tags an absent attribute is not the capability
1110
+ * set, it is wider than the capability set: the dialog offers `.zip`, the emitted
1111
+ * `toAttachment` stages it, and the encoder throws on a file the picker
1112
+ * volunteered. Deriving the attribute is the only form that tracks the
1113
+ * declaration; omitting it is a third behaviour that matches neither layer.
1114
+ *
1115
+ * IT IS A STEER, NOT A GATE, and nothing here should be read as claiming
1116
+ * otherwise. `accept` filters the OS dialog, which always offers an "All Files"
1117
+ * escape, and it does not apply to drag-and-drop — `file-upload.tsx` hands
1118
+ * `dataTransfer.files` straight to `onFilesAdded`. A scaffolded app that wants a
1119
+ * filter that HOLDS calls `resolveMediaPolicy().decide()` on what it staged; the
1120
+ * emitted note points at it.
1121
+ */
1122
+ const ATTACHMENT_ACCEPT = encodableMediaTypes().join(',');
1123
+
1084
1124
  /**
1085
1125
  * What the scaffolder INTENDS to emit for one components list, so a guard can
1086
1126
  * check the seven renderers against the decision instead of restating it.
@@ -1161,15 +1201,35 @@ export const ATTACHMENT_WIRE_NOTE = [
1161
1201
  *
1162
1202
  * `<kai-file-upload>` hands over `File` objects and `<kai-attachments>` renders
1163
1203
  * `AttachmentData`, so something has to bridge them and it may as well be code
1164
- * the consumer can edit. The object URL is what makes an image preview a real
1165
- * thumbnail rather than a generic icon; it is deliberately not revoked here,
1166
- * and the comment says why rather than leaving a silent leak.
1204
+ * the consumer can edit.
1205
+ *
1206
+ * It reads the file with `FileReader.readAsDataURL` and stages a `data:` URI —
1207
+ * the same call `readAsDataUrl` in `elements/default-input.tsx` makes, for the
1208
+ * same reason. `URL.createObjectURL` would draw an identical thumbnail and be
1209
+ * meaningless to anything downstream: an object URL resolves only inside the tab
1210
+ * that minted it, so `toOpenAIMessages` / `toAnthropicMessages` refuse it rather
1211
+ * than send a provider an address it cannot fetch. A `data:` URI previews the
1212
+ * same and is the one form both APIs actually take. Reading is async, which is
1213
+ * the only reason the emitted function is.
1214
+ *
1215
+ * WHICH files survive that encoding is deliberately not stated here, and the
1216
+ * emitted code does not state it either: `encodableMediaTypes()` is the set, and
1217
+ * is public precisely so that nothing has to keep a second copy of it (the
1218
+ * reasoning is on `ATTACHMENT_WIRE_NOTE` above).
1167
1219
  *
1168
1220
  * The parameter type is derived from the property it feeds
1169
1221
  * (`KaiAttachmentsElement['items']` / the wrapper's `items` prop) at each call
1170
1222
  * site rather than importing `AttachmentData`, for the reason the html target
1171
1223
  * already derives its message type from `KaiChatElement['messages']`: a type
1172
1224
  * read off the assignment target cannot drift out of step with it.
1225
+ *
1226
+ * This comment taught the reverse until now, and approvingly — it described the
1227
+ * object URL as what made a preview a real thumbnail and called leaving it
1228
+ * unrevoked a deliberate choice, having gone untouched since #185 while #186
1229
+ * rewrote the function under it. That is the second time in this file the code
1230
+ * was corrected and the prose above it was not; the paragraph on
1231
+ * `ATTACHMENT_WIRE_NOTE` records the first. Neither claim had anything to
1232
+ * disagree with, which is the whole hazard of explaining emitted code in prose.
1173
1233
  */
1174
1234
  function fileToAttachmentLines(pad: string, typeName: string): string[] {
1175
1235
  return [
@@ -1404,7 +1464,7 @@ function componentTags(components: readonly string[], chatFill: string): string
1404
1464
  ` <!-- Drop files here to stage them for the NEXT message. src/main.ts wires`,
1405
1465
  ` kai-files-added -> the staged list -> kai-attachments' items property. -->`,
1406
1466
  ` <div style="flex: 0 0 auto; display: flex; flex-direction: column; gap: 0.5rem; padding: 0.75rem;">`,
1407
- ` <kai-file-upload id="upload" accept="image/*,application/pdf"></kai-file-upload>`,
1467
+ ` <kai-file-upload id="upload" accept="${ATTACHMENT_ACCEPT}"></kai-file-upload>`,
1408
1468
  ` <!-- items is a JS PROPERTY (arrays can't be attributes); 'removable' fires kai-remove. -->`,
1409
1469
  ` <kai-attachments id="attachments" variant="inline" removable></kai-attachments>`,
1410
1470
  ` </div>`,
@@ -1815,7 +1875,7 @@ function renderJsx(components: readonly string[], ctx: RenderCtx, framework: str
1815
1875
  ? [
1816
1876
  ` {/* Drop files here to stage them for the NEXT message. */}`,
1817
1877
  ` <div style={{ flex: '0 0 auto', display: 'flex', flexDirection: 'column', gap: '0.5rem', padding: '0.75rem' }}>`,
1818
- ` <FileUpload accept="image/*,application/pdf" onFilesAdded={onFilesAdded} />`,
1878
+ ` <FileUpload accept="${ATTACHMENT_ACCEPT}" onFilesAdded={onFilesAdded} />`,
1819
1879
  ` {/* items is an ARRAY, so the wrapper sets it as a DOM property, never an attribute. */}`,
1820
1880
  ` <Attachments items={staged} variant="inline" removable onRemove={onRemoveAttachment} />`,
1821
1881
  ` </div>`,
@@ -2176,7 +2236,7 @@ function renderVue(components: readonly string[], ctx: RenderCtx): string {
2176
2236
  ? [
2177
2237
  ` <!-- Drop files here to stage them for the NEXT message. -->`,
2178
2238
  ` <div style="flex: 0 0 auto; display: flex; flex-direction: column; gap: 0.5rem; padding: 0.75rem;">`,
2179
- ` <kai-file-upload accept="image/*,application/pdf" @kai-files-added="onFilesAdded" />`,
2239
+ ` <kai-file-upload accept="${ATTACHMENT_ACCEPT}" @kai-files-added="onFilesAdded" />`,
2180
2240
  ` <kai-attachments :items.prop="staged" variant="inline" removable @kai-remove="onRemoveAttachment" />`,
2181
2241
  ` </div>`,
2182
2242
  ].join('\n')
@@ -2533,7 +2593,7 @@ function renderSvelte(components: readonly string[], ctx: RenderCtx): string {
2533
2593
  ? [
2534
2594
  ` <!-- Drop files here to stage them for the NEXT message. -->`,
2535
2595
  ` <div style="flex: 0 0 auto; display: flex; flex-direction: column; gap: 0.5rem; padding: 0.75rem;">`,
2536
- ` <kai-file-upload accept="image/*,application/pdf" onkai-files-added={onFilesAdded}></kai-file-upload>`,
2596
+ ` <kai-file-upload accept="${ATTACHMENT_ACCEPT}" onkai-files-added={onFilesAdded}></kai-file-upload>`,
2537
2597
  ` <kai-attachments bind:this={attachmentsEl} variant="inline" removable onkai-remove={onRemoveAttachment}></kai-attachments>`,
2538
2598
  ` </div>`,
2539
2599
  ]
@@ -2714,7 +2774,7 @@ function renderTanstackStart(components: readonly string[], ctx: RenderCtx): str
2714
2774
  ? [
2715
2775
  ` {/* Drop files here to stage them for the NEXT message. */}`,
2716
2776
  ` <div style={{ flex: '0 0 auto', display: 'flex', flexDirection: 'column', gap: '0.5rem', padding: '0.75rem' }}>`,
2717
- ` <FileUpload accept="image/*,application/pdf" onFilesAdded={onFilesAdded} />`,
2777
+ ` <FileUpload accept="${ATTACHMENT_ACCEPT}" onFilesAdded={onFilesAdded} />`,
2718
2778
  ` {/* items is an ARRAY, so the wrapper sets it as a DOM property, never an attribute. */}`,
2719
2779
  ` <Attachments items={staged} variant="inline" removable onRemove={onRemoveAttachment} />`,
2720
2780
  ` </div>`,
@@ -2989,7 +3049,7 @@ function renderAngular(components: readonly string[], ctx: RenderCtx): string {
2989
3049
  ? [
2990
3050
  ` <!-- Drop files here to stage them for the NEXT message. -->`,
2991
3051
  ` <div style="flex: 0 0 auto; display: flex; flex-direction: column; gap: 0.5rem; padding: 0.75rem;">`,
2992
- ` <kai-file-upload accept="image/*,application/pdf" (kai-files-added)="onFilesAdded($event)"></kai-file-upload>`,
3052
+ ` <kai-file-upload accept="${ATTACHMENT_ACCEPT}" (kai-files-added)="onFilesAdded($event)"></kai-file-upload>`,
2993
3053
  ` <kai-attachments [items]="staged()" variant="inline" removable (kai-remove)="onRemoveAttachment($event)"></kai-attachments>`,
2994
3054
  ` </div>`,
2995
3055
  ]
@@ -3498,7 +3558,7 @@ function renderSolid(components: readonly string[], ctx: RenderCtx): string {
3498
3558
  ` {/* Read every file BEFORE appending: a per-file append would order`,
3499
3559
  ` the list by whichever finished reading first. */}`,
3500
3560
  ` <FileUpload`,
3501
- ` accept="image/*,application/pdf"`,
3561
+ ` accept="${ATTACHMENT_ACCEPT}"`,
3502
3562
  ` onFilesAdded={async (files) => setStaged([...staged(), ...(await Promise.all(files.map(toAttachment)))])}`,
3503
3563
  ` >`,
3504
3564
  ` <FileUploadTrigger class="border-border text-muted-foreground w-full rounded-xl border border-dashed px-4 py-3 text-center text-sm">`,
@@ -143,6 +143,7 @@ type SettledTool = ToolPart & { toolCallId: string };
143
143
 
144
144
  const isTextPart = (p: MessagePart): p is TextPart => p.type === 'text';
145
145
 
146
+ // lint-silent-drops: drops reasoning,tool,card,source,file -- text projection by name and contract, used for the plain-string form of a turn; every caller that needs the other variants encodes them itself before falling back here.
146
147
  const textOf = (parts: MessagePart[]): string =>
147
148
  parts
148
149
  .filter(isTextPart)
@@ -423,6 +424,7 @@ function reasoningDetailOf(part: ReasoningPart): OpenAIReasoningDetail | undefin
423
424
  * or document content in an assistant message, so there is nothing to encode it
424
425
  * to; attachments belong to the user turn that sent them.
425
426
  */
427
+ // lint-silent-drops: drops card,source -- kit-side parts with no OpenAI wire representation; cards come from tool calls, which ARE encoded, and sources are UI citations.
426
428
  export function toOpenAIMessages(
427
429
  messages: ChatMessage[],
428
430
  options: OpenAIEncodeOptions = {},
@@ -449,6 +451,7 @@ export function toOpenAIMessages(
449
451
  buffered = '';
450
452
  };
451
453
 
454
+ // lint-silent-drops: drops reasoning,tool,card,source -- a USER turn carries authored content only; reasoning and tool parts are assistant-side, cards and sources are kit-side UI with no OpenAI wire form.
452
455
  message.parts.forEach((part, partIndex) => {
453
456
  if (part.type === 'text') {
454
457
  buffered += part.text;
@@ -593,6 +596,7 @@ export function toAnthropicMessages(
593
596
  if (message.role === 'user') {
594
597
  // Part order, for the same reason as `toOpenAIMessages`.
595
598
  const userBlocks: AnthropicContentBlock[] = [];
599
+ // lint-silent-drops: drops reasoning,tool,card,source -- a USER turn carries authored content only; reasoning and tool parts are assistant-side, cards and sources are kit-side UI with no Anthropic wire form.
596
600
  message.parts.forEach((part, partIndex) => {
597
601
  if (part.type === 'text') {
598
602
  if (part.text !== '') userBlocks.push({ type: 'text', text: part.text });
@@ -619,6 +623,7 @@ export function toAnthropicMessages(
619
623
  results = [];
620
624
  };
621
625
 
626
+ // lint-silent-drops: drops card,source,file -- cards and sources are kit-side UI; a file part on an ASSISTANT turn has no Anthropic representation, since the API takes image and document content on user turns only.
622
627
  message.parts.forEach((part, partIndex) => {
623
628
  switch (part.type) {
624
629
  case 'reasoning': {