zidane-ui 9.1.6 → 9.1.11

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 (223) hide show
  1. package/dist/adapters/run-host.js +17 -20
  2. package/dist/adapters/server-files-host.js +3 -1
  3. package/dist/adapters/server-host.js +18 -11
  4. package/dist/adapters/server-machines-host.js +3 -1
  5. package/dist/adapters/server-session-host.js +2 -1
  6. package/dist/adapters/server-settings-accounts.js +2 -1
  7. package/dist/adapters/server-settings-host.js +3 -3
  8. package/dist/adapters/sse-host.js +9 -7
  9. package/dist/app/testing/index.js +1 -1
  10. package/dist/chat/SessionGroupHeader.js +2 -2
  11. package/dist/chat/SessionHostHeader.js +1 -1
  12. package/dist/chat/StatusDot.js +24 -20
  13. package/dist/chat/composer/Composer.d.ts +13 -8
  14. package/dist/chat/composer/Composer.js +14 -10
  15. package/dist/chat/composer/HostComposer.d.ts +1 -1
  16. package/dist/chat/composer/HostComposer.js +7 -10
  17. package/dist/chat/composer/RunStatusChip.js +13 -1
  18. package/dist/chat/composer/attachment-ingest.d.ts +4 -1
  19. package/dist/chat/composer/attachment-ingest.js +27 -10
  20. package/dist/chat/composer/composer-bus.d.ts +9 -1
  21. package/dist/chat/composer/composer-bus.js +49 -6
  22. package/dist/chat/composer/index.d.ts +2 -2
  23. package/dist/chat/composer/index.js +2 -2
  24. package/dist/chat/composer/use-draft-target.d.ts +4 -0
  25. package/dist/chat/composer/use-draft-target.js +15 -4
  26. package/dist/chat/composer/use-send-prompt.d.ts +5 -1
  27. package/dist/chat/composer/use-send-prompt.js +2 -12
  28. package/dist/chat/headless/chip-labels.d.ts +2 -0
  29. package/dist/chat/headless/chip-labels.js +3 -1
  30. package/dist/chat/headless/composer/labels-editor.d.ts +2 -0
  31. package/dist/chat/headless/composer/labels-editor.js +2 -0
  32. package/dist/chat/headless/file-chip.d.ts +12 -13
  33. package/dist/chat/headless/file-chip.js +40 -2
  34. package/dist/chat/headless/fixture-host.js +14 -17
  35. package/dist/chat/headless/host.d.ts +24 -6
  36. package/dist/chat/headless/index.d.ts +4 -4
  37. package/dist/chat/headless/index.js +3 -3
  38. package/dist/chat/headless/labels.d.ts +16 -0
  39. package/dist/chat/headless/labels.js +15 -2
  40. package/dist/chat/headless/send-flow.d.ts +21 -4
  41. package/dist/chat/headless/send-flow.js +25 -6
  42. package/dist/chat/headless/session-list.d.ts +11 -0
  43. package/dist/chat/headless/sidebar/labels.d.ts +12 -1
  44. package/dist/chat/headless/transcript-host.d.ts +8 -1
  45. package/dist/chat/headless/transcript-labels.d.ts +2 -0
  46. package/dist/chat/index.d.ts +3 -3
  47. package/dist/chat/index.js +2 -2
  48. package/dist/chat/interactions/InboxPanel.js +2 -2
  49. package/dist/chat/interactions/QuestionField.js +2 -2
  50. package/dist/chat/rich-composer/RichComposer.d.ts +9 -2
  51. package/dist/chat/rich-composer/RichComposer.js +28 -7
  52. package/dist/chat/rich-composer/composer-extensions.d.ts +2 -0
  53. package/dist/chat/rich-composer/composer-extensions.js +2 -2
  54. package/dist/chat/rich-composer/composer-keys.js +1 -1
  55. package/dist/chat/rich-composer/use-composer-bus.js +2 -1
  56. package/dist/chat/rich-composer/use-composer-editor.d.ts +4 -1
  57. package/dist/chat/rich-composer/use-composer-editor.js +2 -1
  58. package/dist/chat/sidebar/SessionSidebar.js +1 -2
  59. package/dist/chat/sidebar/SidebarSearchRow.js +1 -1
  60. package/dist/chat/sidebar/useSidebarKeyboardNav.d.ts +0 -2
  61. package/dist/chat/sidebar/useSidebarKeyboardNav.js +7 -13
  62. package/dist/chat/testing/contract.d.ts +1 -1
  63. package/dist/chat/testing/contract.js +8 -7
  64. package/dist/chat/transcript/BadgeMenu.d.ts +12 -3
  65. package/dist/chat/transcript/BadgeMenu.js +20 -3
  66. package/dist/chat/transcript/CodemodeScript.js +147 -44
  67. package/dist/chat/transcript/EditDiffBlock.js +1 -4
  68. package/dist/chat/transcript/FileChip.js +60 -14
  69. package/dist/chat/transcript/LiveOutput.js +7 -3
  70. package/dist/chat/transcript/badges.js +2 -2
  71. package/dist/chat/transcript/index.d.ts +2 -2
  72. package/dist/chat/transcript/markdown-context.d.ts +4 -1
  73. package/dist/chat/transcript/rolling.d.ts +12 -0
  74. package/dist/chat/transcript/rolling.js +22 -0
  75. package/dist/chat/transcript/script-input.d.ts +1 -1
  76. package/dist/chat/transcript/shell-block.js +70 -60
  77. package/dist/chat/transcript/tool-row.js +21 -5
  78. package/dist/code/editor/FileTree.d.ts +3 -1
  79. package/dist/code/editor/FileTree.js +29 -1
  80. package/dist/components/Restricted.d.ts +25 -0
  81. package/dist/components/Restricted.js +32 -0
  82. package/dist/components/StepFrame.d.ts +1 -1
  83. package/dist/components/ZidaneMark.d.ts +28 -0
  84. package/dist/components/ZidaneMark.js +114 -0
  85. package/dist/components/palette/PalettePanel.d.ts +7 -1
  86. package/dist/components/palette/PalettePanel.js +37 -11
  87. package/dist/components/zidane-mark.d.ts +52 -0
  88. package/dist/components/zidane-mark.js +94 -0
  89. package/dist/core.css +59 -6
  90. package/dist/dist/{index-DqCio7Nd.d.ts → index-Bq87OcGy.d.ts} +19 -12
  91. package/dist/dist/index.d.ts +1 -1
  92. package/dist/extensions/ExtensionSettingsSection.d.ts +18 -0
  93. package/dist/extensions/ExtensionSettingsSection.js +227 -0
  94. package/dist/extensions/headless/host.d.ts +1 -1
  95. package/dist/extensions/headless/labels.d.ts +41 -1
  96. package/dist/extensions/headless/labels.js +57 -0
  97. package/dist/extensions/headless/select.js +1 -2
  98. package/dist/extensions/index.d.ts +2 -1
  99. package/dist/extensions/index.js +2 -1
  100. package/dist/files/FilesTab.js +2 -0
  101. package/dist/files/TreeColumn.d.ts +5 -1
  102. package/dist/files/TreeColumn.js +6 -0
  103. package/dist/files/headless/fixture-host.js +5 -3
  104. package/dist/files/headless/host.d.ts +24 -6
  105. package/dist/files/headless/index.d.ts +2 -2
  106. package/dist/files/headless/labels.d.ts +1 -0
  107. package/dist/files/headless/labels.js +1 -0
  108. package/dist/files/testing/contract.js +9 -2
  109. package/dist/files/useWorkspace.d.ts +14 -2
  110. package/dist/files/useWorkspace.js +87 -9
  111. package/dist/git/BranchMenu.js +1 -1
  112. package/dist/git/MoveSessionDialog.js +1 -1
  113. package/dist/git/RemoteFolderPicker.js +1 -1
  114. package/dist/git/WherePicker.js +1 -1
  115. package/dist/git/headless/index.js +1 -1
  116. package/dist/git/useEnvironmentChoices.js +1 -1
  117. package/dist/index.d.ts +4 -2
  118. package/dist/index.js +4 -2
  119. package/dist/keybindings/headless/catalog.js +1 -1
  120. package/dist/labels.d.ts +1 -0
  121. package/dist/labels.js +1 -0
  122. package/dist/machines/DaemonMenu.js +1 -1
  123. package/dist/machines/HostCapabilities.d.ts +23 -1
  124. package/dist/machines/HostCapabilities.js +149 -65
  125. package/dist/machines/HostSyncPanel.js +129 -123
  126. package/dist/machines/HostsSection.js +5 -1
  127. package/dist/machines/ServerCard.d.ts +4 -2
  128. package/dist/machines/ServerCard.js +222 -99
  129. package/dist/machines/ServerMachinesPanel.js +16 -13
  130. package/dist/machines/ServerUpdatePanel.js +31 -21
  131. package/dist/machines/TokensPanel.js +75 -53
  132. package/dist/machines/headless/fixture-host.d.ts +5 -0
  133. package/dist/machines/headless/fixture-host.js +13 -4
  134. package/dist/machines/headless/fixtures.js +1 -1
  135. package/dist/machines/headless/host.d.ts +22 -2
  136. package/dist/machines/headless/index.d.ts +2 -2
  137. package/dist/machines/headless/labels.d.ts +22 -1
  138. package/dist/machines/headless/labels.js +22 -2
  139. package/dist/machines/testing/contract.d.ts +3 -1
  140. package/dist/machines/testing/contract.js +9 -0
  141. package/dist/motion/size.d.ts +22 -1
  142. package/dist/motion/size.js +82 -4
  143. package/dist/onboarding/demos/delivery.d.ts +16 -0
  144. package/dist/onboarding/demos/delivery.js +132 -0
  145. package/dist/onboarding/demos/index.d.ts +2 -1
  146. package/dist/onboarding/demos/index.js +2 -1
  147. package/dist/onboarding/headless/labels.d.ts +8 -0
  148. package/dist/onboarding/headless/labels.js +7 -0
  149. package/dist/onboarding/index.d.ts +2 -1
  150. package/dist/onboarding/index.js +2 -1
  151. package/dist/onboarding/steps/ConversationStep.js +46 -4
  152. package/dist/palette/CommandPalette.js +3 -0
  153. package/dist/palette/PaletteDialog.js +7 -3
  154. package/dist/palette/usePaletteController.d.ts +10 -0
  155. package/dist/palette/usePaletteController.js +25 -3
  156. package/dist/server/sse-handler.js +6 -1
  157. package/dist/session/ArtifactsPanel.js +1 -1
  158. package/dist/session/SideChat.js +1 -2
  159. package/dist/session/UsagePanel.js +1 -1
  160. package/dist/session/headless/host.d.ts +3 -1
  161. package/dist/session/headless/index.js +1 -1
  162. package/dist/settings/SettingsRestricted.d.ts +25 -0
  163. package/dist/settings/SettingsRestricted.js +44 -0
  164. package/dist/settings/SettingsRowsPage.js +31 -37
  165. package/dist/settings/headless/catalog.js +7 -6
  166. package/dist/settings/headless/labels.d.ts +4 -2
  167. package/dist/settings/headless/labels.js +4 -2
  168. package/dist/settings/headless/layout.js +1 -1
  169. package/dist/settings/headless/providers.d.ts +2 -0
  170. package/dist/settings/headless/search.d.ts +1 -1
  171. package/dist/settings/index.d.ts +2 -2
  172. package/dist/settings/index.js +2 -2
  173. package/dist/settings/rows.d.ts +51 -8
  174. package/dist/settings/rows.js +81 -26
  175. package/dist/settings/sections/enabled-list/EnabledListSection.js +3 -2
  176. package/dist/settings/sections/environments/EnvironmentsSection.js +4 -3
  177. package/dist/settings/sections/extensions/ExtensionConfigRows.d.ts +2 -2
  178. package/dist/settings/sections/extensions/ExtensionConfigRows.js +22 -25
  179. package/dist/settings/sections/extensions/ExtensionOptionsSection.js +3 -2
  180. package/dist/settings/sections/extensions/options-labels.d.ts +2 -2
  181. package/dist/settings/sections/extensions/options-labels.js +2 -2
  182. package/dist/settings/sections/host-catalog/HostCatalogSection.d.ts +5 -3
  183. package/dist/settings/sections/host-catalog/HostCatalogSection.js +7 -4
  184. package/dist/settings/sections/instructions/InstructionsSection.js +24 -17
  185. package/dist/settings/sections/logs/LogsSection.d.ts +2 -1
  186. package/dist/settings/sections/logs/LogsSection.js +64 -56
  187. package/dist/settings/sections/mcp/McpSection.d.ts +1 -1
  188. package/dist/settings/sections/mcp/McpSection.js +18 -17
  189. package/dist/settings/sections/models/ModelRows.d.ts +3 -3
  190. package/dist/settings/sections/models/ModelRows.js +8 -7
  191. package/dist/settings/sections/performance/PerformanceSection.js +13 -4
  192. package/dist/settings/sections/performance/labels.d.ts +0 -1
  193. package/dist/settings/sections/performance/labels.js +0 -1
  194. package/dist/settings/sections/providers/CredentialSources.js +18 -8
  195. package/dist/settings/sections/providers/ProvidersSection.d.ts +2 -2
  196. package/dist/settings/sections/providers/ProvidersSection.js +20 -17
  197. package/dist/settings/sections/providers/ServerProvidersSection.d.ts +1 -1
  198. package/dist/settings/sections/providers/ServerProvidersSection.js +10 -14
  199. package/dist/settings/sections/shared/catalog.js +3 -2
  200. package/dist/settings/sections/skills/SkillList.js +4 -3
  201. package/dist/settings/testing/accounts-demo.js +1 -0
  202. package/dist/settings/testing/accounts.contract.js +2 -0
  203. package/dist/styles.css +160 -24
  204. package/dist/terminal/HostTerminalSurface.js +1 -1
  205. package/dist/terminal/headless/links.d.ts +19 -1
  206. package/dist/terminal/headless/links.js +26 -2
  207. package/dist/theme/css-vars.js +1 -0
  208. package/dist/theme/derive.d.ts +4 -0
  209. package/dist/theme/derive.js +2 -0
  210. package/dist/workspace/PanelContentView.js +1 -1
  211. package/dist/workspace/PanelHome.d.ts +3 -1
  212. package/dist/workspace/PanelHome.js +4 -3
  213. package/dist/workspace/SidePanel.d.ts +3 -1
  214. package/dist/workspace/SidePanel.js +3 -2
  215. package/dist/workspace/headless/open-target.d.ts +1 -1
  216. package/dist/workspace/headless/open-target.js +2 -2
  217. package/package.json +2 -2
  218. package/src/styles/chat.css +10 -10
  219. package/src/styles/motion.css +37 -0
  220. package/src/styles/primitives.css +11 -3
  221. package/src/styles/sidebar.css +12 -0
  222. package/src/styles/tokens.css +6 -0
  223. package/src/styles/transcript.css +69 -5
@@ -125,7 +125,23 @@ function createRunHost(options) {
125
125
  if (!state.transcript(sessionId).state.get().hydrated) state.hydrate(sessionId, []);
126
126
  return state.readable(sessionId);
127
127
  },
128
- send,
128
+ async send(input) {
129
+ const active = runs.get(input.sessionId);
130
+ const { delivery, ...prompt } = input;
131
+ if (delivery === "steer" && active?.steer && !input.attachments?.length) {
132
+ state.apply(input.sessionId, {
133
+ kind: "user-prompt",
134
+ text: input.prompt
135
+ });
136
+ active.steer(input.prompt);
137
+ return;
138
+ }
139
+ if (delivery && active) {
140
+ active.queued.push(prompt);
141
+ return;
142
+ }
143
+ return send(prompt);
144
+ },
129
145
  interrupt(sessionId) {
130
146
  const active = runs.get(sessionId);
131
147
  if (!active) return;
@@ -134,25 +150,6 @@ function createRunHost(options) {
134
150
  for (const i of state.pending(sessionId).get()) settle(i.requestId, null);
135
151
  state.setRunning(sessionId, false, "idle");
136
152
  },
137
- async steer(sessionId, message) {
138
- const active = runs.get(sessionId);
139
- if (!active?.steer) return false;
140
- state.apply(sessionId, {
141
- kind: "user-prompt",
142
- text: message
143
- });
144
- active.steer(message);
145
- return true;
146
- },
147
- async followUp(sessionId, message) {
148
- const active = runs.get(sessionId);
149
- if (!active) return false;
150
- active.queued.push({
151
- sessionId,
152
- prompt: message
153
- });
154
- return true;
155
- },
156
153
  interactions: {
157
154
  pending: state.pending,
158
155
  async answer(requestId, response) {
@@ -142,13 +142,15 @@ function createServerFilesHost(options) {
142
142
  }), client.can("sessions.workspaceEmptyDirs") ? client.call("sessions.workspaceEmptyDirs", { sessionId }).catch(() => []) : []]);
143
143
  const paths = /* @__PURE__ */ new Set();
144
144
  for (const entry of entries) paths.add(relativePath(typeof entry === "string" ? entry : entry.path, cwd));
145
- return [...[...paths].map((path) => ({
145
+ const all = [...[...paths].map((path) => ({
146
146
  path,
147
147
  kind: "file"
148
148
  })), ...emptyDirs.map((path) => ({
149
149
  path: `${path}/`,
150
150
  kind: "dir"
151
151
  }))].sort((a, b) => a.path.localeCompare(b.path));
152
+ const dir = opts?.dir?.replace(/\/+$/, "");
153
+ return { entries: dir ? all.filter((e) => e.path.startsWith(`${dir}/`)) : all };
152
154
  },
153
155
  async read(cwd, path) {
154
156
  const sessionId = sessionOf(cwd);
@@ -1,6 +1,7 @@
1
1
  import { writable } from "../chat/headless/store.js";
2
2
  import { createLiveState } from "./live.js";
3
3
  import { createServerClient } from "./server-client.js";
4
+ import { wireOriginal } from "../chat/headless/send-flow.js";
4
5
  import { createServerTranscriptMembers } from "./server-transcript-members.js";
5
6
  //#region src/adapters/server-host.ts
6
7
  /** Transcript turns per page. */
@@ -360,17 +361,31 @@ function createServerHost(options) {
360
361
  } } : {}
361
362
  },
362
363
  transcript: (sessionId) => state.readable(sessionId, { loadEarlier: () => void loadEarlier(sessionId) }),
363
- async send({ sessionId, prompt, attachments, model, thinking, activatedSkills, echo, keepDraft }) {
364
- if (echo !== false) state.echoPrompt(sessionId, prompt, attachments);
364
+ async send({ sessionId, prompt, attachments, model, thinking, activatedSkills, echo, keepDraft, delivery }) {
365
+ if (echo !== false && !delivery) state.echoPrompt(sessionId, prompt, attachments?.map(({ original: _original, ...a }) => a));
365
366
  await applyChoice(sessionId, model, thinking);
366
367
  const uploads = (attachments ?? []).flatMap((a) => {
367
368
  const m = a.dataUrl?.match(/^data:([^;,]+)?;base64,(.*)$/s);
369
+ const original = wireOriginal(a.original);
368
370
  return m ? [{
369
371
  mediaType: m[1] || a.mediaType,
370
372
  data: m[2],
371
- name: a.name
373
+ name: a.name,
374
+ ...original ? { original } : {}
372
375
  }] : [];
373
376
  });
377
+ if (delivery) {
378
+ await client.call("queue.add", {
379
+ sessionId,
380
+ text: prompt,
381
+ delivery,
382
+ requestId: crypto.randomUUID(),
383
+ ...activatedSkills?.length ? { activatedSkills: [...activatedSkills] } : {},
384
+ ...uploads.length ? { attachments: uploads } : {},
385
+ ...keepDraft ? { keepDraft: true } : {}
386
+ });
387
+ return;
388
+ }
374
389
  await client.call("chat.send", {
375
390
  sessionId,
376
391
  prompt,
@@ -387,14 +402,6 @@ function createServerHost(options) {
387
402
  interrupt(sessionId) {
388
403
  client.call("chat.interrupt", { sessionId }).catch(onError);
389
404
  },
390
- steer: (sessionId, message) => client.call("chat.steer", {
391
- sessionId,
392
- message
393
- }),
394
- followUp: (sessionId, message) => client.call("chat.followUp", {
395
- sessionId,
396
- message
397
- }),
398
405
  interactions: {
399
406
  pending: state.pending,
400
407
  all: () => allPending,
@@ -77,7 +77,8 @@ function createServerMachinesHost(options) {
77
77
  status,
78
78
  ...error ? { error } : {},
79
79
  ...hello?.appVersion ? { serverVersion: hello.appVersion } : {},
80
- ...hello?.scope ? { scope: hello.scope } : {},
80
+ ...hello?.scope ? { scope: hello.scope === "admin" ? "admin" : "chat" } : {},
81
+ ...hello?.adminCommands ? { adminCommands: hello.adminCommands } : {},
81
82
  ...hello?.startedAt ? { startedAt: hello.startedAt } : {},
82
83
  ...hello?.machineServe ? { machineServe: true } : {},
83
84
  ...lastConnectedAt ? { lastConnectedAt } : {}
@@ -192,6 +193,7 @@ function createServerMachinesHost(options) {
192
193
  },
193
194
  signIn: (_hostId, target, onDone) => serverOAuthFlow(client, target, onDone ? { onDone: () => onDone() } : {}),
194
195
  capabilities: (_hostId, opts) => guard("read", () => client.call("host.capabilities", opts?.refresh ? { refresh: true } : {})),
196
+ installTool: (_hostId, name) => guard("install", () => client.call("host.installTool", { name })),
195
197
  presence: createPresenceStore((apply) => {
196
198
  let stopped = false;
197
199
  let sawSignal = false;
@@ -101,7 +101,8 @@ function createServerSessionHost(options) {
101
101
  text,
102
102
  requestId: crypto.randomUUID(),
103
103
  ...add?.activatedSkills ? { activatedSkills: add.activatedSkills } : {},
104
- ...add?.attachments ? { attachments: add.attachments } : {}
104
+ ...add?.attachments ? { attachments: add.attachments } : {},
105
+ ...add?.delivery ? { delivery: add.delivery } : {}
105
106
  })).delivered,
106
107
  remove: queue.drop,
107
108
  update: queue.edit,
@@ -26,7 +26,8 @@ function providerOf(auth) {
26
26
  detail: m.detail,
27
27
  active: m.active,
28
28
  ...m.key ? { key: m.key } : {},
29
- ...m.label ? { label: m.label } : {}
29
+ ...m.label ? { label: m.label } : {},
30
+ ...m.email ? { email: m.email } : {}
30
31
  })),
31
32
  ...auth.customFields ? { customFields: auth.customFields } : {},
32
33
  ...auth.configuredFields ? { configuredFields: auth.configuredFields } : {}
@@ -50,10 +50,10 @@ function createServerSettingsHost(options) {
50
50
  });
51
51
  client.onOpen(() => void refresh());
52
52
  if (client.state().status === "open") refresh();
53
- const canWrite = () => client.state().status === "open" && client.can("settings.set");
54
- const writableStore = writable(canWrite());
53
+ const writableStore = writable(client.state().status === "open" ? client.can("settings.set") : true);
55
54
  client.onState(() => {
56
- const next = canWrite();
55
+ if (client.state().status !== "open") return;
56
+ const next = client.can("settings.set");
57
57
  if (writableStore.get() !== next) writableStore.set(next);
58
58
  });
59
59
  const update = async (patch) => {
@@ -164,7 +164,15 @@ function createSseHost(options) {
164
164
  }
165
165
  },
166
166
  transcript: (sessionId) => state.readable(sessionId),
167
- async send({ sessionId, prompt, attachments, model, thinking }) {
167
+ async send({ sessionId, prompt, attachments, model, thinking, delivery }) {
168
+ if (delivery) {
169
+ const path = delivery === "steer" ? "steer" : "follow-up";
170
+ const body = {
171
+ message: prompt,
172
+ ...attachments?.length ? { attachments } : {}
173
+ };
174
+ if ((await request("POST", `/sessions/${sid(sessionId)}/${path}`, body)).ok) return;
175
+ }
168
176
  state.echoPrompt(sessionId, prompt, attachments);
169
177
  try {
170
178
  await request("POST", `/sessions/${sid(sessionId)}/send`, {
@@ -184,12 +192,6 @@ function createSseHost(options) {
184
192
  interrupt(sessionId) {
185
193
  request("POST", `/sessions/${sid(sessionId)}/interrupt`).catch(onError);
186
194
  },
187
- async steer(sessionId, message) {
188
- return (await request("POST", `/sessions/${sid(sessionId)}/steer`, { message })).ok;
189
- },
190
- async followUp(sessionId, message) {
191
- return (await request("POST", `/sessions/${sid(sessionId)}/follow-up`, { message })).ok;
192
- },
193
195
  interactions: {
194
196
  pending: state.pending,
195
197
  async answer(requestId, response) {
@@ -1,5 +1,5 @@
1
- import { createFixtureAppearanceHost } from "../../appearance/headless/fixture-host.js";
2
1
  import { createFixtureApprovalsHost } from "../../approvals/headless/fixture-host.js";
2
+ import { createFixtureAppearanceHost } from "../../appearance/headless/fixture-host.js";
3
3
  import { createFixtureHost } from "../../chat/headless/fixture-host.js";
4
4
  import { createFixtureComposerHost } from "../../chat/testing/fixture-composer-host.js";
5
5
  import { createFixtureSidebarHost } from "../../chat/testing/fixture-sidebar-host.js";
@@ -36,7 +36,7 @@ function SessionGroupHeader({ id, label, collapsed, icon, action, onToggle, onOp
36
36
  onClick: () => onOpen(id),
37
37
  "aria-current": active ? "page" : void 0,
38
38
  title: labels.openGroup(label),
39
- className: `zg-press-soft flex flex-1 items-center min-w-0 bg-transparent border-0 cursor-pointer p-0 font-medium hover:text-[var(--zg-text)] transition-colors ${active ? "text-[var(--zg-text)]" : "text-[var(--zg-text-muted)]"}`,
39
+ className: `zg-press-soft flex flex-1 items-center min-w-0 bg-transparent border-0 cursor-pointer p-0 font-medium hover:text-[var(--zg-text)] transition-colors ${active ? "text-[var(--zg-text)]" : "text-[var(--zg-sidebar-label-fg,var(--zg-text-muted))]"}`,
40
40
  children: /* @__PURE__ */ jsx("span", {
41
41
  className: "truncate",
42
42
  children: label
@@ -65,7 +65,7 @@ function SessionGroupHeader({ id, label, collapsed, icon, action, onToggle, onOp
65
65
  onClick: () => onToggle(id),
66
66
  "aria-expanded": !collapsed,
67
67
  title: toggleTitle,
68
- className: "zg-press-soft flex flex-1 items-center gap-1 min-w-0 bg-transparent border-0 cursor-pointer p-0 font-medium text-[var(--zg-text-muted)] hover:text-[var(--zg-text)] transition-colors",
68
+ className: "zg-press-soft flex flex-1 items-center gap-1 min-w-0 bg-transparent border-0 cursor-pointer p-0 font-medium text-[var(--zg-sidebar-label-fg,var(--zg-text-muted))] hover:text-[var(--zg-text)] transition-colors",
69
69
  children: [/* @__PURE__ */ jsx("span", {
70
70
  className: "truncate",
71
71
  children: label
@@ -12,7 +12,7 @@ function SessionHostHeader({ id, host, collapsed, onToggle, className = "", styl
12
12
  const labels = useChatLabels();
13
13
  const offline = !hostReachable(host);
14
14
  return /* @__PURE__ */ jsxs("li", {
15
- className: `group flex items-center gap-1.5 -mx-2 px-3 pt-4 pb-0.5 text-zg-meta uppercase tracking-wide text-[var(--zg-text-muted)] select-none ${className}`,
15
+ className: `group flex items-center gap-1.5 -mx-2 px-3 pt-4 pb-0.5 text-zg-meta text-[var(--zg-text-muted)] select-none ${className}`,
16
16
  style,
17
17
  "data-host-id": host.id,
18
18
  children: [/* @__PURE__ */ jsx("span", {
@@ -1,15 +1,17 @@
1
+ import { ZidaneMark } from "../components/ZidaneMark.js";
1
2
  import { Presence } from "../motion/swap.js";
2
3
  import { useChatLabels } from "./useChatLabels.js";
3
4
  import { jsx, jsxs } from "react/jsx-runtime";
4
5
  import { memo } from "react";
5
6
  //#region src/chat/StatusDot.tsx
6
7
  /**
7
- * A session's state as one 8px dot: idle ring, running core with an
8
- * orbiting arc, a rippling "needs you" for a pending approval, a breathing
8
+ * A session's state as one 8px dot: idle ring, the zidane mark (its bands
9
+ * flowing) while it runs, a rippling "needs you" for a pending approval, a breathing
9
10
  * "awaiting input", an attached ring, a popping completion, a glowing error.
10
- * Three layers (core, orbit, ripple) so every change tweens instead of
11
- * snapping (`.zg-state-tween`, styles/motion.css). The loops (orbit,
12
- * ripple, breathe) keep their own fixed timing and carry `.zg-loop`, which
11
+ * Three layers (core, mark, ripple) so every change tweens instead of
12
+ * snapping (`.zg-state-tween`, styles/motion.css). The mark spins out of
13
+ * the dot as the core swells into it, and back (`.zg-mark-swap`); it stills itself
14
+ * under reduced / off motion; the other loops (ripple, breathe) keep their own fixed timing and carry `.zg-loop`, which
13
15
  * stills them under reduced / off motion; the completion pop is the `snap`
14
16
  * spring with its opacity on `fade`. Colours are the decorative `-dot`
15
17
  * tones (a fill, not text: vivid in light rather than held to text floors).
@@ -35,9 +37,10 @@ const STYLES = {
35
37
  label: "dotRunning",
36
38
  style: {
37
39
  background: "var(--zg-accent-dot)",
38
- transform: "scale(0.62)"
40
+ transform: "scale(1.5)",
41
+ opacity: 0
39
42
  },
40
- ring: "orbit"
43
+ ring: "mark"
41
44
  },
42
45
  pending: {
43
46
  label: "dotPending",
@@ -75,13 +78,11 @@ const STYLES = {
75
78
  }
76
79
  }
77
80
  };
78
- const ORBIT_MASK = "radial-gradient(farthest-side, transparent calc(100% - 1.5px), #000 calc(100% - 1.5px))";
79
- const ORBIT_STYLE = {
80
- background: "conic-gradient(from 0deg, var(--zg-accent-dot) 0 32%, transparent 32% 100%)",
81
- WebkitMaskImage: ORBIT_MASK,
82
- maskImage: ORBIT_MASK,
83
- animation: "zg-orbit 1.1s linear infinite"
84
- };
81
+ const MARK_STYLE = { color: "var(--zg-accent-dot)" };
82
+ /** The mark grows from the dot's own size (8px of its 12px). */
83
+ const MARK_SWAP_STYLE = { "--zg-mark-from": 2 / 3 };
84
+ /** Lively but under the composer's pace: a list of running sessions should not shimmer. */
85
+ const MARK_SPEED = .9;
85
86
  const RIPPLE_STYLE = {
86
87
  background: "var(--zg-warn-dot)",
87
88
  animation: "zg-dot 1.8s ease-in-out infinite"
@@ -104,12 +105,15 @@ function StatusDotInner({ state, label, className, style }) {
104
105
  "data-core": true
105
106
  }),
106
107
  /* @__PURE__ */ jsx(Presence, {
107
- show: cfg.ring === "orbit",
108
- className: "absolute -inset-[3px]",
109
- children: /* @__PURE__ */ jsx("span", {
110
- "aria-hidden": true,
111
- className: "zg-loop absolute rounded-full inset-0",
112
- style: ORBIT_STYLE
108
+ show: cfg.ring === "mark",
109
+ className: "zg-mark-swap absolute -inset-[2px]",
110
+ style: MARK_SWAP_STYLE,
111
+ children: /* @__PURE__ */ jsx(ZidaneMark, {
112
+ size: 12,
113
+ speed: MARK_SPEED,
114
+ tone: "inherit",
115
+ decorative: true,
116
+ style: MARK_STYLE
113
117
  })
114
118
  }),
115
119
  /* @__PURE__ */ jsx(Presence, {
@@ -3,29 +3,34 @@ import { ComposerBadge } from "./ComposerBadges.js";
3
3
  import "./attachment-ingest.js";
4
4
  import { SuggestionSource } from "./suggestions.js";
5
5
  import { CSSProperties, ReactNode } from "react";
6
+ import { MessageDelivery } from "zidane/chat";
6
7
  //#region src/chat/composer/Composer.d.ts
7
- /** What the composer hands to `onSend`, `onSteer`, and `onQueue`. */
8
+ /** What the composer hands to `onSend`. */
8
9
  interface ComposerSubmit {
9
10
  /** The draft as plain text. Mentions and commands are serialized to text. */
10
11
  prompt: string;
11
12
  /** The staged attachments. */
12
13
  attachments: readonly ComposerAttachment[];
14
+ /** Set while `running`: steer the draft into the run, or queue it for when the run ends. */
15
+ delivery?: MessageDelivery;
13
16
  }
14
17
  /** `false` (or a rejection) keeps the draft: the composer puts it back. */
15
18
  type ComposerResult = void | boolean | Promise<void | boolean>;
16
19
  /** Props for {@link Composer}. */
17
20
  interface ComposerProps {
18
- /** Called when the user sends the draft with Enter or the Send button. Return `false` or reject to put the draft back. */
21
+ /** Called when the user sends the draft with Enter or the Send button, also while `running` (see `ComposerSubmit.delivery`). Return `false` or reject to put the draft back. */
19
22
  onSend(input: ComposerSubmit): ComposerResult;
20
- /** A run is in flight: Enter steers or queues, Escape and the button stop. */
23
+ /** A run is in flight: Enter steers or queues (`delivery`), Escape and the button stop. */
21
24
  running?: boolean;
25
+ /**
26
+ * What Enter does with the draft while `running`: `steer` it into the run or `queue` it for when the run ends.
27
+ * Alt+Enter does the other. Pass the user's "Messages during a run" setting.
28
+ * @default 'queue'
29
+ */
30
+ delivery?: MessageDelivery;
22
31
  /** Called when the user stops the running agent with Escape or the Stop button. Without it the composer offers no stop. */
23
32
  onStop?(): void;
24
- /** Enter while running adds the draft to the run in flight. */
25
- onSteer?(input: ComposerSubmit): ComposerResult;
26
- /** Enter while running (without `onSteer`) holds the draft for when the run ends. */
27
- onQueue?(input: ComposerSubmit): ComposerResult;
28
- /** A send / steer / queue handler threw (the draft is already back). Absent: the composer shows the error itself. */
33
+ /** The send handler threw (the draft is already back). Absent: the composer shows the error itself. */
29
34
  onError?(error: unknown): void;
30
35
  /** Controlled text of the draft. Use with `onChange`. Omit for an uncontrolled draft. */
31
36
  value?: string;
@@ -11,6 +11,7 @@ import { SendButton } from "./SendButton.js";
11
11
  import { SuggestionPopup } from "./suggestions.js";
12
12
  import { jsx, jsxs } from "react/jsx-runtime";
13
13
  import { forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
14
+ import { otherDelivery } from "zidane/chat/pure";
14
15
  //#region src/chat/composer/Composer.tsx
15
16
  const serialize = (editor) => editor.getText({ blockSeparator: "\n" });
16
17
  /** Renders the chat prompt box: a text editor with staged attachments, a controls row, and Send or Stop. Enter sends, Shift+Enter adds a line, Escape stops a running agent, and the arrow keys recall earlier prompts. */
@@ -19,7 +20,8 @@ const Composer = forwardRef(function Composer(props, ref) {
19
20
  const labels = useChatLabels();
20
21
  const propsRef = useRef(props);
21
22
  propsRef.current = props;
22
- const placeholder = props.placeholder ?? labels.composerPlaceholder;
23
+ const runningPlaceholder = (props.delivery ?? "queue") === "steer" ? labels.composerPlaceholderSteer : labels.composerPlaceholderQueue;
24
+ const placeholder = props.placeholder ?? (running ? runningPlaceholder : labels.composerPlaceholder);
23
25
  const placeholderRef = useRef(placeholder);
24
26
  placeholderRef.current = placeholder;
25
27
  const nameRef = useRef(props.ariaLabel ?? labels.composerLabel);
@@ -111,17 +113,18 @@ const Composer = forwardRef(function Composer(props, ref) {
111
113
  editor.commands.setContent(plainTextToDocJson(props.value), { emitUpdate: false });
112
114
  setText(props.value);
113
115
  }, [editor, props.value]);
114
- const route = !running ? "send" : props.onSteer ? "steer" : props.onQueue ? "queue" : "send";
115
- const canSend = !disabled && (text.trim() !== "" || ingest.attachments.length > 0) && (!running || route !== "send");
116
- const submit = useCallback(async () => {
116
+ const delivery = props.delivery ?? "queue";
117
+ const route = running ? delivery : "send";
118
+ const canSend = !disabled && (text.trim() !== "" || ingest.attachments.length > 0);
119
+ const submit = useCallback(async (alternate = false) => {
117
120
  const p = propsRef.current;
118
121
  if (!editor) shell.current.submit = true;
119
122
  if (!editor || p.disabled) return;
120
123
  const prompt = serialize(editor).trim();
121
124
  const { attachments: staged, clear, restore: putBack } = ingestRef.current;
122
125
  if (!prompt && staged.length === 0) return;
123
- const handler = p.running ? p.onSteer ?? p.onQueue : p.onSend;
124
- if (!handler) return;
126
+ const preferred = p.delivery ?? "queue";
127
+ const sending = p.running ? alternate ? otherDelivery(preferred) : preferred : void 0;
125
128
  setSendError(null);
126
129
  const draft = editor.getJSON();
127
130
  editor.commands.clearContent(true);
@@ -134,9 +137,10 @@ const Composer = forwardRef(function Composer(props, ref) {
134
137
  putBack(staged);
135
138
  };
136
139
  try {
137
- if (await handler({
140
+ if (await p.onSend({
138
141
  prompt,
139
- attachments: staged
142
+ attachments: staged,
143
+ ...sending ? { delivery: sending } : {}
140
144
  }) === false) restore();
141
145
  } catch (error) {
142
146
  restore();
@@ -171,7 +175,7 @@ const Composer = forwardRef(function Composer(props, ref) {
171
175
  if (event.key === "Enter") {
172
176
  if (event.shiftKey) return editor.commands.splitBlock();
173
177
  event.preventDefault();
174
- submit();
178
+ submit(event.altKey);
175
179
  return true;
176
180
  }
177
181
  if (event.key === "Escape" && p.running && p.onStop) {
@@ -300,7 +304,7 @@ const Composer = forwardRef(function Composer(props, ref) {
300
304
  const p = propsRef.current;
301
305
  if (event.key === "Enter" && !event.shiftKey) {
302
306
  event.preventDefault();
303
- submit();
307
+ submit(event.altKey);
304
308
  } else if (event.key === "Escape" && p.running && p.onStop) p.onStop();
305
309
  },
306
310
  onPaste: (event) => ingestRef.current.paste(event),
@@ -3,7 +3,7 @@ import { ThinkingLevel } from "./ModelOptionsPicker.js";
3
3
  import { ReactNode } from "react";
4
4
  //#region src/chat/composer/HostComposer.d.ts
5
5
  /** Props for {@link HostComposer}: the {@link ComposerProps} the host does not drive, plus `sessionId`, `defaultModel`, `defaultThinking`, and `footer`. */
6
- interface HostComposerProps extends Omit<ComposerProps, 'onSend' | 'running' | 'onStop' | 'onSteer' | 'onQueue' | 'footer'> {
6
+ interface HostComposerProps extends Omit<ComposerProps, 'onSend' | 'running' | 'onStop' | 'footer'> {
7
7
  /** Id of the session the composer sends to. */
8
8
  sessionId: string;
9
9
  /** The model to start on (`provider/model` or a bare id). Default: the session's, else the first offered. */
@@ -10,7 +10,8 @@ import { useMemo, useState } from "react";
10
10
  * `Composer` over a `ChatHost` for one session: Enter sends through
11
11
  * `host.send` (with the picked model and reasoning effort), Stop calls
12
12
  * `host.interrupt`, and while the session's transcript is running Enter
13
- * steers (`host.steer`) or queues (`host.followUp`). The model and
13
+ * sends with `delivery` (steer into the run or queue for its end; Alt+Enter
14
+ * the other), attachments included. The model and
14
15
  * options (reasoning effort) pickers show when the host offers `models`
15
16
  * (options only for a model with `thinking: true`). A capability the host leaves out hides
16
17
  * its control; without `send` there is no composer.
@@ -22,11 +23,12 @@ import { useMemo, useState } from "react";
22
23
  const NO_MODELS = constant([]);
23
24
  const NO_OPTIONS = [];
24
25
  const NO_ENABLED = {};
25
- const toWire = ({ attachments }) => attachments.map(({ name, mediaType, size, dataUrl }) => ({
26
+ const toWire = ({ attachments }) => attachments.map(({ name, mediaType, size, dataUrl, original }) => ({
26
27
  name,
27
28
  mediaType,
28
29
  size,
29
- dataUrl
30
+ dataUrl,
31
+ ...original ? { original } : {}
30
32
  }));
31
33
  /**
32
34
  * Renders a {@link Composer} bound to the {@link ChatHost} in context for one session. Controls the host leaves out are hidden; without `send` nothing renders.
@@ -49,12 +51,9 @@ function HostComposer({ sessionId, defaultModel, defaultThinking = "medium", foo
49
51
  prompt: input.prompt,
50
52
  ...input.attachments.length ? { attachments: toWire(input) } : {},
51
53
  ...choice ? { model: choice.key } : {},
52
- ...choice?.model.thinking ? { thinking } : {}
54
+ ...choice?.model.thinking ? { thinking } : {},
55
+ ...input.delivery ? { delivery: input.delivery } : {}
53
56
  });
54
- const relay = (fn) => async (input) => {
55
- if (input.attachments.length > 0) return false;
56
- if (!await fn(sessionId, input.prompt)) await send(input);
57
- };
58
57
  const pickers = host.models && providers.length > 0 ? /* @__PURE__ */ jsxs("div", {
59
58
  className: "zg-segment min-w-0 max-w-full",
60
59
  children: [/* @__PURE__ */ jsx(ModelPicker, {
@@ -80,8 +79,6 @@ function HostComposer({ sessionId, defaultModel, defaultThinking = "medium", foo
80
79
  running,
81
80
  onSend: send,
82
81
  ...host.interrupt ? { onStop: () => host.interrupt(sessionId) } : {},
83
- ...host.steer ? { onSteer: relay(host.steer.bind(host)) } : {},
84
- ...host.followUp ? { onQueue: relay(host.followUp.bind(host)) } : {},
85
82
  footer: pickers || footer ? /* @__PURE__ */ jsxs(Fragment, { children: [pickers, footer] }) : void 0
86
83
  });
87
84
  }
@@ -1,3 +1,4 @@
1
+ import { ZidaneMark } from "../../components/ZidaneMark.js";
1
2
  import { RollingText } from "../../motion/roll.js";
2
3
  import { SmoothWidth } from "../../motion/size.js";
3
4
  import { Presence, useCadencedValue } from "../../motion/swap.js";
@@ -11,8 +12,10 @@ import { EMPTY_RATE, fmtTokens, formatElapsed, stepStreamRate, streamRateValue }
11
12
  /**
12
13
  * The ONE live indicator for a running turn, set into the composer card:
13
14
  *
14
- * [1m 29s] [↓72.2k] [41 tok/s] [~$0.123] (one track chip per value)
15
+ * (mark) [1m 29s] [↓72.2k] [41 tok/s] [~$0.123] (one track chip per value)
15
16
  *
17
+ * The zidane mark leads the row: its bands flow fast while text streams and
18
+ * ease down to a slower pace between turns (tools, waiting on the model).
16
19
  * Visible from `run:start` to `run:end` (one fade in, one fade out), never
17
20
  * in between: the elapsed counter and the run-wide output count are always
18
21
  * defined, so tool calls between streaming turns do not blank the chip.
@@ -31,6 +34,9 @@ import { EMPTY_RATE, fmtTokens, formatElapsed, stepStreamRate, streamRateValue }
31
34
  * elapsed time reads the host's run counters (`ComposerHost.runs`).
32
35
  */
33
36
  const CHIP_CADENCE_MS = 320;
37
+ /** The mark's loops per second: streaming text, and the rest of the run. */
38
+ const MARK_STREAMING = 2.2;
39
+ const MARK_WORKING = 1.1;
34
40
  /**
35
41
  * The run's live indicator (see the header).
36
42
  *
@@ -57,6 +63,12 @@ function RunStatusChip({ sessionId, running, progress, settledOutputTokens, cost
57
63
  children: /* @__PURE__ */ jsxs("span", {
58
64
  className: "inline-flex items-center gap-1 whitespace-nowrap",
59
65
  children: [
66
+ /* @__PURE__ */ jsx(ZidaneMark, {
67
+ size: 14,
68
+ speed: progress ? MARK_STREAMING : MARK_WORKING,
69
+ decorative: true,
70
+ className: "zg-mark-in mx-0.5"
71
+ }),
60
72
  /* @__PURE__ */ jsx(Segment, { children: /* @__PURE__ */ jsx(RollingText, {
61
73
  text: formatElapsed(elapsedSec),
62
74
  value: elapsedSec
@@ -56,7 +56,10 @@ declare function shouldFoldPasteToAttachment(text: string): boolean;
56
56
  /**
57
57
  * Reads a file into a {@link ComposerAttachment} with a `data:` URL, as the
58
58
  * shared rule says (`planAttachment`): images, PDFs and text attach; an image
59
- * in another format, or over the image cap, is re-encoded to fit. Rejects with
59
+ * in another format, or over the image cap, is re-encoded to fit, and keeps
60
+ * the file as picked in `original` (up to the file cap) so the host can save
61
+ * it for the agent and say it was converted. An animated GIF under the cap
62
+ * goes as it is, every frame. Rejects with
60
63
  * an {@link AttachmentRefusedError} for a file the rule refuses (or an image
61
64
  * that cannot be decoded), and with the read error when the file cannot be read.
62
65
  *
@@ -1,4 +1,4 @@
1
- import { ATTACHMENT_SNIFF_BYTES, MAX_FILE_ATTACHMENT_BYTES, MAX_IMAGE_ATTACHMENT_BYTES, MAX_IMAGE_ATTACHMENT_BYTES as MAX_IMAGE_ATTACHMENT_BYTES$1, formatBytes, formatBytes as formatBytes$1, planAttachment } from "zidane/chat/pure";
1
+ import { ATTACHMENT_SNIFF_BYTES, MAX_FILE_ATTACHMENT_BYTES, MAX_FILE_ATTACHMENT_BYTES as MAX_FILE_ATTACHMENT_BYTES$1, MAX_IMAGE_ATTACHMENT_BYTES, MAX_IMAGE_ATTACHMENT_BYTES as MAX_IMAGE_ATTACHMENT_BYTES$1, formatBytes, formatBytes as formatBytes$1, planAttachment } from "zidane/chat/pure";
2
2
  //#region src/chat/composer/attachment-ingest.ts
3
3
  /**
4
4
  * Attachment-ingest helpers, shared by the kit's `Composer` and the desktop
@@ -75,7 +75,10 @@ function shouldFoldPasteToAttachment(text) {
75
75
  /**
76
76
  * Reads a file into a {@link ComposerAttachment} with a `data:` URL, as the
77
77
  * shared rule says (`planAttachment`): images, PDFs and text attach; an image
78
- * in another format, or over the image cap, is re-encoded to fit. Rejects with
78
+ * in another format, or over the image cap, is re-encoded to fit, and keeps
79
+ * the file as picked in `original` (up to the file cap) so the host can save
80
+ * it for the agent and say it was converted. An animated GIF under the cap
81
+ * goes as it is, every frame. Rejects with
79
82
  * an {@link AttachmentRefusedError} for a file the rule refuses (or an image
80
83
  * that cannot be decoded), and with the read error when the file cannot be read.
81
84
  *
@@ -94,19 +97,25 @@ async function readAttachment(file) {
94
97
  if (plan.kind === "convert-image") {
95
98
  const converted = await convertImage(file, plan.mediaType).catch(() => null);
96
99
  if (!converted) throw new AttachmentRefusedError(name, { reason: "unreadable-image" });
97
- return toComposerAttachment(converted, renameForType(name, converted.type), converted.type);
100
+ const attachment = await toComposerAttachment(converted, renameForType(name, converted.type), converted.type);
101
+ if (file.size > MAX_FILE_ATTACHMENT_BYTES$1) return attachment;
102
+ const original = await readDataUrl(file);
103
+ return {
104
+ ...attachment,
105
+ original: {
106
+ name,
107
+ mediaType: plan.mediaType,
108
+ size: file.size,
109
+ dataUrl: original
110
+ }
111
+ };
98
112
  }
99
113
  return toComposerAttachment(file, name, plan.mediaType);
100
114
  }
101
115
  /** The data URL's header carries `mediaType`: the send path reads the type from there. */
102
116
  async function toComposerAttachment(blob, name, mediaType) {
103
117
  const typed = blob.type === mediaType ? blob : new Blob([blob], { type: mediaType });
104
- const dataUrl = await new Promise((resolve, reject) => {
105
- const reader = new FileReader();
106
- reader.onload = () => resolve(reader.result);
107
- reader.onerror = () => reject(reader.error);
108
- reader.readAsDataURL(typed);
109
- });
118
+ const dataUrl = await readDataUrl(typed);
110
119
  return {
111
120
  id: crypto.randomUUID(),
112
121
  name,
@@ -115,12 +124,20 @@ async function toComposerAttachment(blob, name, mediaType) {
115
124
  size: typed.size
116
125
  };
117
126
  }
127
+ function readDataUrl(blob) {
128
+ return new Promise((resolve, reject) => {
129
+ const reader = new FileReader();
130
+ reader.onload = () => resolve(reader.result);
131
+ reader.onerror = () => reject(reader.error);
132
+ reader.readAsDataURL(blob);
133
+ });
134
+ }
118
135
  /** Largest edge a converted image keeps (Anthropic's ceiling). */
119
136
  const MAX_IMAGE_EDGE = 8e3;
120
137
  /**
121
138
  * Decode an image and encode it again, as PNG (JPEG for a JPEG: a photo grows
122
139
  * as PNG), scaled down until it fits the image cap. An animated GIF or WebP
123
- * keeps its first frame. Rejects when the image cannot be decoded (HEIC and
140
+ * over the cap keeps its first frame (`original` keeps them all). Rejects when the image cannot be decoded (HEIC and
124
141
  * TIFF in Chromium) or there is no DOM to draw it in.
125
142
  */
126
143
  async function convertImage(file, sourceType) {