@agent-native/core 0.84.10 → 0.84.13

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 (140) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +48 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/durable-background.ts +30 -0
  5. package/corpus/core/src/agent/production-agent.ts +34 -6
  6. package/corpus/core/src/agent/run-manager.ts +45 -2
  7. package/corpus/core/src/agent/run-store.ts +29 -6
  8. package/corpus/core/src/chat-threads/store.ts +2 -0
  9. package/corpus/core/src/cli/skills.ts +10 -6
  10. package/corpus/core/src/client/AssistantChat.tsx +40 -9
  11. package/corpus/core/src/client/agent-chat-adapter.ts +23 -2
  12. package/corpus/core/src/client/chat/run-recovery.tsx +52 -44
  13. package/corpus/core/src/client/chat/tool-call-display.tsx +7 -2
  14. package/corpus/core/src/client/require-session.tsx +20 -1
  15. package/corpus/core/src/client/sharing/ShareButton.tsx +9 -3
  16. package/corpus/core/src/collab/storage.ts +40 -5
  17. package/corpus/core/src/server/agent-chat-plugin.ts +1 -0
  18. package/corpus/core/src/server/auth.ts +6 -0
  19. package/corpus/core/src/shared/streaming-text-smoothing.ts +10 -5
  20. package/corpus/templates/assets/.agents/skills/logo-composite/SKILL.md +7 -6
  21. package/corpus/templates/assets/AGENTS.md +15 -0
  22. package/corpus/templates/assets/actions/_helpers.ts +3 -1
  23. package/corpus/templates/assets/actions/create-generation-preset.ts +12 -1
  24. package/corpus/templates/assets/actions/generate-asset.ts +5 -1
  25. package/corpus/templates/assets/actions/generate-image-batch.ts +6 -1
  26. package/corpus/templates/assets/actions/generate-image.ts +18 -11
  27. package/corpus/templates/assets/actions/open-asset-picker.ts +4 -2
  28. package/corpus/templates/assets/actions/update-generation-preset.ts +16 -3
  29. package/corpus/templates/assets/app/i18n/zh-TW.ts +3 -0
  30. package/corpus/templates/assets/app/i18n-data.ts +30 -0
  31. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +42 -7
  32. package/corpus/templates/assets/app/routes/library.tsx +4 -2
  33. package/corpus/templates/assets/changelog/2026-07-01-generation-presets-can-now-composite-your-brand-s-canonical-.md +6 -0
  34. package/corpus/templates/assets/changelog/2026-07-01-tagging-a-preset-now-briefs-the-agent-on-that-preset-s-aesth.md +6 -0
  35. package/corpus/templates/assets/server/lib/preset-chat-context.ts +118 -0
  36. package/corpus/templates/assets/server/plugins/agent-chat.ts +5 -0
  37. package/corpus/templates/assets/shared/api.ts +1 -0
  38. package/corpus/templates/clips/app/components/library/recording-card.tsx +14 -4
  39. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +10 -12
  40. package/corpus/templates/clips/app/hooks/use-library.ts +7 -4
  41. package/corpus/templates/clips/app/lib/recording-status.ts +32 -0
  42. package/corpus/templates/clips/changelog/2026-07-01-dictation-keeps-listening-through-natural-pauses-and-gives-c.md +6 -0
  43. package/corpus/templates/clips/changelog/2026-07-01-redoing-a-paused-desktop-recording-no-longer-leaves-the-recording-controls-disabled.md +6 -0
  44. package/corpus/templates/clips/changelog/2026-07-01-stalled-clip-uploads-now-update-in-the-library-and-surface-as-failed.md +6 -0
  45. package/corpus/templates/clips/desktop/src/lib/recorder.ts +36 -18
  46. package/corpus/templates/clips/desktop/src/lib/voice-dictation.ts +99 -27
  47. package/corpus/templates/clips/desktop/src/overlays/flow-bar.tsx +4 -43
  48. package/corpus/templates/clips/desktop/src/overlays/toolbar.tsx +3 -0
  49. package/corpus/templates/clips/desktop/src/styles.css +5 -48
  50. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +4 -5
  51. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +15 -6
  52. package/corpus/templates/design/AGENTS.md +8 -4
  53. package/corpus/templates/design/actions/create-design.ts +1 -0
  54. package/corpus/templates/design/actions/duplicate-design.ts +3 -1
  55. package/corpus/templates/design/actions/edit-design.ts +43 -13
  56. package/corpus/templates/design/actions/export-html.ts +4 -2
  57. package/corpus/templates/design/actions/export-pdf.ts +3 -1
  58. package/corpus/templates/design/actions/export-svg.ts +4 -2
  59. package/corpus/templates/design/actions/export-zip.ts +10 -3
  60. package/corpus/templates/design/actions/generate-design.ts +5 -1
  61. package/corpus/templates/design/actions/present-design-variants.ts +17 -12
  62. package/corpus/templates/design/actions/update-file.ts +68 -17
  63. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +3 -2
  64. package/corpus/templates/design/app/components/design/EditPanel.tsx +35 -3
  65. package/corpus/templates/design/app/components/design/MotionDock.tsx +46 -11
  66. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +80 -30
  67. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +60 -10
  68. package/corpus/templates/design/app/components/design/canvas-primitive-style.ts +2 -0
  69. package/corpus/templates/design/app/components/design/inspector/InspectorAiActions.tsx +4 -0
  70. package/corpus/templates/design/app/hooks/use-question-flow.ts +2 -2
  71. package/corpus/templates/design/app/hooks/useAgentEditRequest.ts +2 -2
  72. package/corpus/templates/design/app/i18n-data.ts +1 -1
  73. package/corpus/templates/design/app/pages/DesignEditor.tsx +493 -109
  74. package/corpus/templates/design/app/pages/VisualEdit.tsx +4 -16
  75. package/corpus/templates/design/changelog/2026-07-01-org-members-can-see-design-projects-created-in-their-workspace.md +6 -0
  76. package/corpus/templates/design/e2e/helpers.ts +2 -2
  77. package/corpus/templates/design/server/plugins/db.ts +9 -0
  78. package/dist/agent/durable-background.d.ts +3 -0
  79. package/dist/agent/durable-background.d.ts.map +1 -1
  80. package/dist/agent/durable-background.js +19 -0
  81. package/dist/agent/durable-background.js.map +1 -1
  82. package/dist/agent/production-agent.d.ts +1 -0
  83. package/dist/agent/production-agent.d.ts.map +1 -1
  84. package/dist/agent/production-agent.js +29 -6
  85. package/dist/agent/production-agent.js.map +1 -1
  86. package/dist/agent/run-manager.d.ts.map +1 -1
  87. package/dist/agent/run-manager.js +40 -2
  88. package/dist/agent/run-manager.js.map +1 -1
  89. package/dist/agent/run-store.d.ts.map +1 -1
  90. package/dist/agent/run-store.js +28 -6
  91. package/dist/agent/run-store.js.map +1 -1
  92. package/dist/chat-threads/store.d.ts +1 -0
  93. package/dist/chat-threads/store.d.ts.map +1 -1
  94. package/dist/chat-threads/store.js +2 -0
  95. package/dist/chat-threads/store.js.map +1 -1
  96. package/dist/cli/skills.d.ts.map +1 -1
  97. package/dist/cli/skills.js +10 -6
  98. package/dist/cli/skills.js.map +1 -1
  99. package/dist/client/AssistantChat.d.ts +10 -0
  100. package/dist/client/AssistantChat.d.ts.map +1 -1
  101. package/dist/client/AssistantChat.js +26 -8
  102. package/dist/client/AssistantChat.js.map +1 -1
  103. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  104. package/dist/client/agent-chat-adapter.js +21 -2
  105. package/dist/client/agent-chat-adapter.js.map +1 -1
  106. package/dist/client/chat/run-recovery.d.ts.map +1 -1
  107. package/dist/client/chat/run-recovery.js +15 -10
  108. package/dist/client/chat/run-recovery.js.map +1 -1
  109. package/dist/client/chat/tool-call-display.d.ts +1 -0
  110. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  111. package/dist/client/chat/tool-call-display.js +3 -2
  112. package/dist/client/chat/tool-call-display.js.map +1 -1
  113. package/dist/client/require-session.d.ts +14 -0
  114. package/dist/client/require-session.d.ts.map +1 -1
  115. package/dist/client/require-session.js +19 -1
  116. package/dist/client/require-session.js.map +1 -1
  117. package/dist/client/sharing/ShareButton.d.ts +4 -2
  118. package/dist/client/sharing/ShareButton.d.ts.map +1 -1
  119. package/dist/client/sharing/ShareButton.js +3 -1
  120. package/dist/client/sharing/ShareButton.js.map +1 -1
  121. package/dist/collab/routes.d.ts +2 -2
  122. package/dist/collab/storage.d.ts.map +1 -1
  123. package/dist/collab/storage.js +31 -5
  124. package/dist/collab/storage.js.map +1 -1
  125. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  126. package/dist/notifications/routes.d.ts +2 -2
  127. package/dist/observability/routes.d.ts +5 -5
  128. package/dist/progress/routes.d.ts +1 -1
  129. package/dist/resources/handlers.d.ts +3 -3
  130. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  131. package/dist/server/agent-chat-plugin.js +1 -1
  132. package/dist/server/agent-chat-plugin.js.map +1 -1
  133. package/dist/server/auth.d.ts.map +1 -1
  134. package/dist/server/auth.js +7 -0
  135. package/dist/server/auth.js.map +1 -1
  136. package/dist/server/transcribe-voice.d.ts +1 -1
  137. package/dist/shared/streaming-text-smoothing.d.ts.map +1 -1
  138. package/dist/shared/streaming-text-smoothing.js +8 -5
  139. package/dist/shared/streaming-text-smoothing.js.map +1 -1
  140. package/package.json +1 -1
@@ -1278,6 +1278,18 @@ async function trashRecording(
1278
1278
  }
1279
1279
  }
1280
1280
 
1281
+ async function cleanupCancelledRemoteRecording(
1282
+ serverUrl: string,
1283
+ recordingId: string,
1284
+ ): Promise<void> {
1285
+ await abortRecordingUpload(
1286
+ serverUrl,
1287
+ recordingId,
1288
+ "Recording cancelled by user",
1289
+ );
1290
+ await trashRecording(serverUrl, recordingId);
1291
+ }
1292
+
1281
1293
  class CountdownCancelledError extends Error {
1282
1294
  constructor() {
1283
1295
  super("Recording cancelled during countdown");
@@ -2029,7 +2041,12 @@ async function startNativeFullscreenRecording(
2029
2041
  }
2030
2042
  stateUnlistens.forEach((u) => u());
2031
2043
  stateUnlistens = [];
2032
- await transcriptionCapture?.cancel().catch(() => {});
2044
+ void transcriptionCapture?.cancel().catch((err) => {
2045
+ console.warn(
2046
+ "[clips-recorder] native transcription cancel failed:",
2047
+ err,
2048
+ );
2049
+ });
2033
2050
  await localCameraExport?.cancel().catch(() => {});
2034
2051
  await invoke("native_fullscreen_recording_cancel").catch((err) =>
2035
2052
  console.warn(
@@ -2049,12 +2066,14 @@ async function startNativeFullscreenRecording(
2049
2066
  streamCleanups.forEach((cleanup) => cleanup());
2050
2067
  await invoke("hide_overlays").catch(() => {});
2051
2068
  if (!localOnly && id) {
2052
- await abortRecordingUpload(
2053
- params.serverUrl,
2054
- id,
2055
- "Recording cancelled by user",
2069
+ void cleanupCancelledRemoteRecording(params.serverUrl, id).catch(
2070
+ (err) => {
2071
+ console.warn(
2072
+ "[clips-recorder] cancelled recording cleanup failed:",
2073
+ err,
2074
+ );
2075
+ },
2056
2076
  );
2057
- await trashRecording(params.serverUrl, id);
2058
2077
  }
2059
2078
  })();
2060
2079
  return cancelPromise;
@@ -3244,7 +3263,9 @@ async function startRecordingInner(
3244
3263
  if (tickHandle) clearInterval(tickHandle);
3245
3264
  stateUnlistens.forEach((u) => u());
3246
3265
  stateUnlistens = [];
3247
- transcriptionCapture?.cancel().catch(() => {});
3266
+ void transcriptionCapture?.cancel().catch((err) => {
3267
+ console.warn("[clips-recorder] transcription cancel failed:", err);
3268
+ });
3248
3269
  // Remove MediaRecorder's data handler so any final `ondataavailable`
3249
3270
  // from the stop() below doesn't push a new Blob into `inflight`
3250
3271
  // after we've decided to discard everything.
@@ -3282,18 +3303,15 @@ async function startRecordingInner(
3282
3303
  inflight.clear();
3283
3304
  await invoke("hide_recording_chrome").catch(() => {});
3284
3305
  // Tell the server to abort the partial recording (drops chunks from
3285
- // application_state, flips the recording row to 'failed'). Fire and
3286
- // forget with a short-circuit on failure we don't want to keep the
3287
- // user waiting on a network call to a dev server that may be down.
3288
- try {
3289
- await abortRecordingUpload(
3290
- params.serverUrl,
3291
- id,
3292
- "Recording cancelled by user",
3306
+ // application_state, flips the recording row to 'failed'), then trash
3307
+ // it. This is best-effort background cleanup: redo/cancel must release
3308
+ // the desktop chrome immediately even if the server is slow or offline.
3309
+ if (id) {
3310
+ void cleanupCancelledRemoteRecording(params.serverUrl, id).catch(
3311
+ (err) => {
3312
+ console.warn("[clips-recorder] abort failed (non-fatal):", err);
3313
+ },
3293
3314
  );
3294
- await trashRecording(params.serverUrl, id);
3295
- } catch (err) {
3296
- console.warn("[clips-recorder] abort failed (non-fatal):", err);
3297
3315
  }
3298
3316
  await deleteBrowserRecordingBackup(id).catch((err) => {
3299
3317
  console.warn("[clips-recorder] local backup cleanup failed:", err);
@@ -102,6 +102,11 @@ interface VoiceSession {
102
102
  // Accumulated final transcript from interim webkit results, in case
103
103
  // the recognition session ends before we ask it to stop.
104
104
  browserTranscript: string;
105
+ // Native / Whisper engines can emit one final segment per pause. Keep
106
+ // committed segments separate from the current partial so long dictations
107
+ // survive natural pauses.
108
+ finalTranscriptParts: string[];
109
+ interimTranscript: string;
105
110
  // browser-only: monotonic timestamp of the most recent
106
111
  // recognition.onresult event. stop() reads this to decide whether
107
112
  // the user was actively speaking up to Fn release (worth a tail-
@@ -229,6 +234,73 @@ function setFlowState(state: FlowState): void {
229
234
  emit("voice:state-change", { state }).catch(() => {});
230
235
  }
231
236
 
237
+ let lastStartPingAt = 0;
238
+
239
+ function playStartPing(): void {
240
+ const now = Date.now();
241
+ if (now - lastStartPingAt < 250) return;
242
+ lastStartPingAt = now;
243
+ try {
244
+ const AudioCtx = window.AudioContext || (window as any).webkitAudioContext;
245
+ if (!AudioCtx) return;
246
+ const ctx = new AudioCtx();
247
+ const osc = ctx.createOscillator();
248
+ const gain = ctx.createGain();
249
+ const start = ctx.currentTime;
250
+ osc.type = "sine";
251
+ osc.frequency.setValueAtTime(880, start);
252
+ osc.frequency.exponentialRampToValueAtTime(1174.66, start + 0.08);
253
+ gain.gain.setValueAtTime(0.0001, start);
254
+ gain.gain.exponentialRampToValueAtTime(0.08, start + 0.015);
255
+ gain.gain.exponentialRampToValueAtTime(0.0001, start + 0.13);
256
+ osc.connect(gain);
257
+ gain.connect(ctx.destination);
258
+ osc.start(start);
259
+ osc.stop(start + 0.14);
260
+ window.setTimeout(() => {
261
+ ctx.close().catch(() => {});
262
+ }, 250);
263
+ } catch {
264
+ // Sound feedback is best-effort; the moving bars remain the visual cue.
265
+ }
266
+ }
267
+
268
+ function enterRecordingState(): void {
269
+ setFlowState("recording");
270
+ playStartPing();
271
+ }
272
+
273
+ function joinedTranscript(parts: string[], interim: string): string {
274
+ return [...parts, interim].filter(Boolean).join(" ").trim();
275
+ }
276
+
277
+ function appendFinalTranscript(session: VoiceSession, text: string): void {
278
+ const clean = text.trim();
279
+ if (!clean) return;
280
+ const committed = joinedTranscript(session.finalTranscriptParts, "");
281
+ if (!committed) {
282
+ session.finalTranscriptParts = [clean];
283
+ } else if (clean === committed || clean.startsWith(`${committed} `)) {
284
+ // Some recognizers send the whole dictation as their final result.
285
+ session.finalTranscriptParts = [clean];
286
+ } else if (session.finalTranscriptParts.at(-1) !== clean) {
287
+ session.finalTranscriptParts.push(clean);
288
+ }
289
+ session.interimTranscript = "";
290
+ session.browserTranscript = joinedTranscript(
291
+ session.finalTranscriptParts,
292
+ "",
293
+ );
294
+ }
295
+
296
+ function setInterimTranscript(session: VoiceSession, text: string): void {
297
+ session.interimTranscript = text.trim();
298
+ session.browserTranscript = joinedTranscript(
299
+ session.finalTranscriptParts,
300
+ session.interimTranscript,
301
+ );
302
+ }
303
+
232
304
  function stopMeter(session: VoiceSession): void {
233
305
  if (session.raf != null) {
234
306
  cancelAnimationFrame(session.raf);
@@ -698,7 +770,6 @@ export function installDesktopVoiceDictation(
698
770
  } catch (err) {
699
771
  console.warn("[voice-dictation] vocab learn-monitor failed:", err);
700
772
  }
701
- emit("voice:partial-transcript", { text }).catch(() => {});
702
773
  if (target.cleanupProvider) setFlowState("idle");
703
774
  return text;
704
775
  };
@@ -808,7 +879,7 @@ export function installDesktopVoiceDictation(
808
879
  abortPendingStart();
809
880
  return;
810
881
  }
811
- setFlowState("recording");
882
+ enterRecordingState();
812
883
  const mimeType = pickMimeType();
813
884
  const recorder = new MediaRecorder(stream, { mimeType });
814
885
  const next: VoiceSession = {
@@ -822,6 +893,8 @@ export function installDesktopVoiceDictation(
822
893
  mimeType: recorder.mimeType || mimeType,
823
894
  recognition: null,
824
895
  browserTranscript: "",
896
+ finalTranscriptParts: [],
897
+ interimTranscript: "",
825
898
  lastResultAt: 0,
826
899
  startedAt: Date.now(),
827
900
  stopping: false,
@@ -948,7 +1021,7 @@ export function installDesktopVoiceDictation(
948
1021
  abortPendingStart();
949
1022
  return;
950
1023
  }
951
- setFlowState("recording");
1024
+ enterRecordingState();
952
1025
  // Reset any prior partial transcript display in the flow-bar.
953
1026
  emit("voice:partial-transcript", { text: "" }).catch(() => {});
954
1027
  const next: VoiceSession = {
@@ -962,6 +1035,8 @@ export function installDesktopVoiceDictation(
962
1035
  mimeType: "",
963
1036
  recognition: null,
964
1037
  browserTranscript: "",
1038
+ finalTranscriptParts: [],
1039
+ interimTranscript: "",
965
1040
  lastResultAt: 0,
966
1041
  startedAt: Date.now(),
967
1042
  stopping: false,
@@ -1043,7 +1118,7 @@ export function installDesktopVoiceDictation(
1043
1118
  abortPendingStart();
1044
1119
  return;
1045
1120
  }
1046
- setFlowState("recording");
1121
+ enterRecordingState();
1047
1122
  emit("voice:partial-transcript", { text: "" }).catch(() => {});
1048
1123
  const next: VoiceSession = {
1049
1124
  kind: "whisper",
@@ -1056,6 +1131,8 @@ export function installDesktopVoiceDictation(
1056
1131
  mimeType: "",
1057
1132
  recognition: null,
1058
1133
  browserTranscript: "",
1134
+ finalTranscriptParts: [],
1135
+ interimTranscript: "",
1059
1136
  lastResultAt: 0,
1060
1137
  startedAt: Date.now(),
1061
1138
  stopping: false,
@@ -1112,7 +1189,7 @@ export function installDesktopVoiceDictation(
1112
1189
  abortPendingStart();
1113
1190
  return;
1114
1191
  }
1115
- setFlowState("recording");
1192
+ enterRecordingState();
1116
1193
  // Reset any prior partial transcript display in the flow-bar.
1117
1194
  emit("voice:partial-transcript", { text: "" }).catch(() => {});
1118
1195
  const recognition = new Ctor();
@@ -1131,6 +1208,8 @@ export function installDesktopVoiceDictation(
1131
1208
  mimeType: "",
1132
1209
  recognition,
1133
1210
  browserTranscript: "",
1211
+ finalTranscriptParts: [],
1212
+ interimTranscript: "",
1134
1213
  lastResultAt: 0,
1135
1214
  startedAt: Date.now(),
1136
1215
  stopping: false,
@@ -1203,10 +1282,6 @@ export function installDesktopVoiceDictation(
1203
1282
  // the tail because Web Speech only marks a segment as `isFinal`
1204
1283
  // after a confidence-threshold pass.
1205
1284
  next.browserTranscript = (finalSoFar + interim).trim();
1206
- // Stream the live transcript to the flow-bar.
1207
- emit("voice:partial-transcript", {
1208
- text: next.browserTranscript,
1209
- }).catch(() => {});
1210
1285
  };
1211
1286
  recognition.onerror = (ev) => {
1212
1287
  if (ev.error !== "no-speech" && ev.error !== "aborted") {
@@ -1732,31 +1807,28 @@ export function installDesktopVoiceDictation(
1732
1807
  if (!current || (current.kind !== "native" && current.kind !== "whisper"))
1733
1808
  return;
1734
1809
  if (current.cancelled || current.stopping) return;
1735
- current.browserTranscript = text.trim();
1810
+ setInterimTranscript(current, text);
1736
1811
  })
1737
1812
  .then((u) => unlistens.push(u))
1738
1813
  .catch(() => {});
1739
1814
  onFinalTranscript(({ text }) => {
1740
- // Final transcripts are ONLY emitted by Rust after `endAudio()`,
1741
- // which we call in stop(). At that point the session has been
1742
- // moved from `session` to `lingeringSession`, so route there
1743
- // exclusively. Do NOT fall back to the active `session`: a
1744
- // late-arriving final from the previous session would otherwise
1745
- // overwrite the new session's transcript with stale text.
1746
- const current =
1747
- lingeringSession &&
1748
- (lingeringSession.kind === "native" ||
1749
- lingeringSession.kind === "whisper")
1750
- ? lingeringSession
1815
+ // Whisper emits final segments throughout a long dictation whenever the
1816
+ // speaker pauses. Native speech emits its final after stop, when the
1817
+ // stopped session is parked in lingeringSession. Native final events do not
1818
+ // carry a session id, so never fall back to an active native session here:
1819
+ // a late final from the previous stop could otherwise be appended to a new
1820
+ // dictation.
1821
+ const current = lingeringSession
1822
+ ? lingeringSession
1823
+ : session && session.kind === "whisper" && !session.stopping
1824
+ ? session
1751
1825
  : null;
1752
1826
  if (!current) return;
1753
1827
  if (current.cancelled) return;
1754
- // Final beats partial — overwrite so a `complete_voice_dictation`
1755
- // from a late stop() picks up the better text.
1756
- current.browserTranscript = text.trim();
1757
- // If stop() is waiting on this event before lingering, trigger the
1758
- // finalize sequence now (paste → 1s linger → dismiss).
1759
- current.onNativeFinalize?.();
1828
+ appendFinalTranscript(current, text);
1829
+ if (current === lingeringSession) {
1830
+ current.onNativeFinalize?.();
1831
+ }
1760
1832
  })
1761
1833
  .then((u) => unlistens.push(u))
1762
1834
  .catch(() => {});
@@ -3,11 +3,7 @@ import { invoke } from "@tauri-apps/api/core";
3
3
  import { emit, listen } from "@tauri-apps/api/event";
4
4
  import { useEffect, useRef, useState } from "react";
5
5
 
6
- import {
7
- onAudioLevel,
8
- onFinalTranscript,
9
- onPartialTranscript,
10
- } from "../lib/transcription-engine";
6
+ import { onAudioLevel } from "../lib/transcription-engine";
11
7
 
12
8
  type FlowState = "idle" | "recording" | "processing" | "complete" | "error";
13
9
 
@@ -28,7 +24,6 @@ export function FlowBar() {
28
24
  // "idle" caused the bar to flash an "EN" language pill that never went
29
25
  // away if the start event was missed.
30
26
  const [state, setState] = useState<FlowState>("recording");
31
- const [partialTranscript, setPartialTranscript] = useState("");
32
27
  const canvasRef = useRef<HTMLCanvasElement | null>(null);
33
28
  const levelRef = useRef(0);
34
29
  const rafRef = useRef<number | null>(null);
@@ -63,23 +58,6 @@ export function FlowBar() {
63
58
  }),
64
59
  );
65
60
 
66
- trackListen(
67
- onPartialTranscript(({ text }) => {
68
- // Live transcript as the user speaks — rendered above the pill.
69
- // Empty payload clears the display (sent at session start/end).
70
- setPartialTranscript(text);
71
- }),
72
- );
73
-
74
- trackListen(
75
- onFinalTranscript(({ text }) => {
76
- // Final result from the recognizer (only fires after stop is
77
- // requested). Show it on the bar — the last word lingers there
78
- // for ~1s before voice-dictation.ts dismisses everything.
79
- if (text) setPartialTranscript(text);
80
- }),
81
- );
82
-
83
61
  return () => {
84
62
  stopped = true;
85
63
  unlistens.forEach((u) => {
@@ -166,29 +144,12 @@ export function FlowBar() {
166
144
  }, 250);
167
145
  };
168
146
 
169
- // The transcript chip is independent of the pill — it can linger on
170
- // its own after Fn release while the pill dismisses snappily. Voice-
171
- // dictation.ts emits an empty payload to clear it once the linger
172
- // window expires.
173
- const showTranscript = partialTranscript.length > 0;
174
-
175
147
  return (
176
148
  <div className="flow-bar-root">
177
- {showTranscript && (
178
- <div className="flow-bar-transcript">
179
- {/* <bdi> + unicode-bidi: plaintext (in CSS) keeps Latin text
180
- in its natural LTR order while the parent's direction:rtl
181
- clips overflow from the visual left. Without this, the
182
- last few characters of the newest words were being pushed
183
- past the visible right edge by bidi reordering. */}
184
- <bdi>{partialTranscript}</bdi>
185
- </div>
186
- )}
187
149
  {/* Pill is ALWAYS mounted — when state goes idle we fade the
188
- opacity to 0 (see CSS) instead of removing it from the DOM,
189
- so the transcript chip above doesn't reflow when the pill
190
- "goes away". Inner content keeps its last frame rendered
191
- during the fade so the canvas doesn't pop. */}
150
+ opacity to 0 (see CSS) instead of removing it from the DOM.
151
+ Inner content keeps its last frame rendered during the fade
152
+ so the canvas doesn't pop. */}
192
153
  <div className={`flow-bar flow-bar-${state}`}>
193
154
  {(state === "recording" || state === "idle") && (
194
155
  <div className="flow-bar-recording">
@@ -98,8 +98,11 @@ export function Toolbar() {
98
98
  trackListen(
99
99
  listen<boolean>("clips:toolbar-enabled", (ev) => {
100
100
  setEnabled(!!ev.payload);
101
+ setPendingAction(null);
101
102
  if (!ev.payload) {
102
103
  setDiskSpaceLevel("ok");
104
+ setPaused(false);
105
+ setElapsed(0);
103
106
  }
104
107
  }),
105
108
  );
@@ -3663,57 +3663,18 @@ body[data-clips-route="recording-pill"] #root {
3663
3663
 
3664
3664
  /* Wispr Flow-style dictation pill: small capsule anchored to the bottom
3665
3665
  of its (transparent) overlay window. The window itself is positioned
3666
- bottom-center on the primary display by show_flow_bar in Rust. We
3667
- stack the live transcript above the pill in a column so the user sees
3668
- the words being recognized in real time. */
3666
+ bottom-center on the primary display by show_flow_bar in Rust. */
3669
3667
  .flow-bar-root {
3670
3668
  position: fixed;
3671
3669
  inset: var(--overlay-shadow-gutter);
3672
3670
  display: flex;
3673
- flex-direction: column;
3674
3671
  align-items: center;
3675
3672
  justify-content: flex-end;
3676
3673
  padding-bottom: 8px;
3677
- gap: 6px;
3678
3674
  background: transparent;
3679
3675
  pointer-events: none;
3680
3676
  }
3681
3677
 
3682
- /* Live partial-transcript chip floating above the pill. Same dark glass
3683
- look as the pill, smaller, only there when transcript is non-empty.
3684
- Caps width and clips from the left so the newest words stay visible
3685
- when a long sentence reaches the edge of the transparent window. */
3686
- .flow-bar-transcript {
3687
- max-width: 360px;
3688
- padding: 4px 10px;
3689
- border-radius: 12px;
3690
- background: rgba(18, 18, 20, 0.92);
3691
- backdrop-filter: blur(20px);
3692
- -webkit-backdrop-filter: blur(20px);
3693
- border: 1px solid rgba(255, 255, 255, 0.06);
3694
- color: rgba(255, 255, 255, 0.92);
3695
- font-size: 12px;
3696
- line-height: 1.35;
3697
- text-align: left;
3698
- white-space: nowrap;
3699
- overflow: hidden;
3700
- text-overflow: ellipsis;
3701
- /* RTL on the OUTER box so overflow clips from the visual left
3702
- (older text), keeping the newest characters pinned to the right.
3703
- `unicode-bidi: plaintext` on the inner <bdi> makes the actual
3704
- transcript text follow its own (LTR) direction — without it,
3705
- bidi reordering pushes the last few Latin characters past the
3706
- visible right edge and they get clipped a few chars short. */
3707
- direction: rtl;
3708
- pointer-events: none;
3709
- animation: flow-bar-in 100ms ease-out;
3710
- }
3711
-
3712
- .flow-bar-transcript > bdi {
3713
- unicode-bidi: plaintext;
3714
- direction: ltr;
3715
- }
3716
-
3717
3678
  .flow-bar {
3718
3679
  display: flex;
3719
3680
  align-items: center;
@@ -3729,17 +3690,13 @@ body[data-clips-route="recording-pill"] #root {
3729
3690
  /* Subtle fade-in so the pill doesn't hard-pop on appear. */
3730
3691
  animation: flow-bar-in 140ms ease-out;
3731
3692
  /* Fade out to opacity 0 when state goes idle (after Fn release).
3732
- Keeping the pill in the DOM means the transcript chip above
3733
- doesn't reflow when the pill "goes away" — important for the
3734
- linger UX where the chip stays put while the pill dissolves. */
3693
+ Keeping the pill in the DOM lets the waveform fade instead of pop. */
3735
3694
  transition: opacity 220ms ease-out;
3736
3695
  }
3737
3696
 
3738
- /* When the dictation engine reports idle (e.g. mid-linger after Fn
3739
- release on the browser path), fade the pill out without unmounting
3740
- it. The canvas keeps its last-frame so the user sees the waveform
3741
- smoothly fade rather than pop into nothing. Pointer-events go off
3742
- so the now-invisible chip can't catch clicks. */
3697
+ /* When the dictation engine reports idle, fade the pill out without
3698
+ unmounting it. Pointer-events go off so the invisible pill cannot
3699
+ catch clicks. */
3743
3700
  .flow-bar-idle {
3744
3701
  opacity: 0;
3745
3702
  pointer-events: none;
@@ -1210,11 +1210,10 @@ pub async fn show_flow_bar(app: AppHandle) -> Result<(), String> {
1210
1210
 
1211
1211
  let (mx, my, mw, mh) = tray_monitor_physical_rect(&app);
1212
1212
  let scale = overlay_scale_factor(&app);
1213
- // Wider + taller than the pill alone so the live transcript chip
1214
- // can stack above it. Height accommodates: bottom-anchored 32px pill
1215
- // + 6px gap + ~28px transcript chip + transparent window margin.
1216
- let content_w: u32 = (420.0 * scale).round() as u32;
1217
- let content_h: u32 = (120.0 * scale).round() as u32;
1213
+ // Compact Wispr-style pill window: just enough transparent canvas for
1214
+ // the bottom-centered waveform bar and its shadow gutter.
1215
+ let content_w: u32 = (160.0 * scale).round() as u32;
1216
+ let content_h: u32 = (56.0 * scale).round() as u32;
1218
1217
  let bottom_margin: i32 = (14.0 * scale).round() as i32;
1219
1218
  let gutter = overlay_shadow_gutter_physical(&app);
1220
1219
  let w: u32 = content_w + gutter * 2;
@@ -119,14 +119,21 @@ Each `content` is a complete, self-contained document (Alpine.js + Tailwind via
119
119
 
120
120
  Wait for the user's pick before refining. Once they choose, keep the selected
121
121
  screen, delete the unchosen variant screens with `delete-file`, and continue
122
- from the kept screen. If inline chat choice buttons are unavailable in the host,
123
- ask the user to tell you the preferred screen name. Do not ask them to paste HTML
124
- or a generated handoff summary; the variants are already real screens on the
125
- board.
122
+ from the kept screen by calling `get-design-snapshot` with the selected
123
+ screen's `fileId`, then calling `edit-design` on that same `fileId`. Use
124
+ `mode: "replace-file"` when expanding the representative placeholder into the
125
+ full chosen direction. Do not call `generate-design` after a variant pick. If
126
+ inline chat choice buttons are unavailable in the host, ask the user to tell you
127
+ the preferred screen name. Do not ask them to paste HTML or a generated handoff
128
+ summary; the variants are already real screens on the board.
126
129
 
127
130
  ### Phase 3 — Save with `generate-design` (when not using variants)
128
131
 
129
- Skip variants and call `generate-design` directly for: refinements to an already-picked design, multi-screen additions to an existing design, or one-shot prompts where the direction is unambiguous.
132
+ Skip variants and call `generate-design` directly for: a brand-new first
133
+ renderable file, multi-screen additions to an existing design, or one-shot
134
+ prompts where the direction is unambiguous. For refinements to an already-picked
135
+ design or selected screen, use `get-design-snapshot` followed by `edit-design`
136
+ instead.
130
137
 
131
138
  ```bash
132
139
  pnpm action generate-design \
@@ -476,7 +483,9 @@ regeneration is slow, expensive, and regresses unrelated parts.
476
483
  Each `search` must match the file **exactly and uniquely** — include enough
477
484
  surrounding context to be unambiguous. Wrapping an element in a new div is
478
485
  just a search/replace whose `replace` adds the wrapper around the original.
479
- 3. **Reserve `generate-design` for** net-new files or large structural rewrites.
486
+ 3. **Reserve `generate-design` for** net-new files. For large structural
487
+ rewrites of an existing selected file, call `edit-design` with
488
+ `mode: "replace-file"` and the exact `fileId` from `get-design-snapshot`.
480
489
  Never resend files you aren't changing.
481
490
  4. **Treat `:root` as the global spec.** For theme-wide restyles, edit the
482
491
  tokens in `:root` rather than touching every element.
@@ -64,8 +64,9 @@ patterns live in `.agents/skills/`.
64
64
  URLs, which would navigate the preview iframe to the app itself.
65
65
  - To refine an existing design, make the smallest change: read it with
66
66
  `get-design-snapshot`, then use `edit-design` (search/replace). Reserve
67
- `generate-design` for new files or large structural rewrites; never resend
68
- files you aren't changing.
67
+ `generate-design` for new files. For broad rewrites of an existing selected
68
+ file, use `edit-design` with `mode: "replace-file"` and the exact `fileId`;
69
+ never resend files you aren't changing.
69
70
  - When the user asks to add tweak controls, preserve existing useful tweaks,
70
71
  add or update the requested `tweaks` definitions, and make sure each control
71
72
  is backed by a CSS custom property the rendered file actually uses. If source
@@ -222,8 +223,11 @@ patterns live in `.agents/skills/`.
222
223
  - For human-in-the-loop UI exploration, create a design shell, call
223
224
  `present-design-variants` with 2-5 concise directions (three by default),
224
225
  wait for the user to pick one in chat, delete the other generated variant
225
- screens with `delete-file`, then use `get-design-snapshot` and
226
- `generate-design` or `edit-design` for follow-up refinements.
226
+ screens with `delete-file`, call `get-design-snapshot` with the selected
227
+ screen's `fileId`, then call `edit-design` on that same `fileId` for
228
+ follow-up refinement. Use `mode: "replace-file"` when expanding the
229
+ representative placeholder into the full chosen direction. Do not call
230
+ `generate-design` after a variant pick.
227
231
  - If inline chat choice buttons are unavailable, the user can tell you the
228
232
  preferred screen name. Do not show a separate variant picker or ask them to
229
233
  paste a copyable handoff summary.
@@ -85,6 +85,7 @@ export default defineAction({
85
85
  data: "{}",
86
86
  ownerEmail,
87
87
  orgId,
88
+ visibility: orgId ? "org" : "private",
88
89
  createdAt: now,
89
90
  updatedAt: now,
90
91
  });
@@ -63,6 +63,7 @@ export default defineAction({
63
63
  }
64
64
 
65
65
  // Copy the design with remapped canvasFrames
66
+ const orgId = getRequestOrgId() || null;
66
67
  await db.insert(schema.designs).values({
67
68
  id: newId,
68
69
  title: newTitle,
@@ -75,7 +76,8 @@ export default defineAction({
75
76
  if (!e) throw new Error("no authenticated user");
76
77
  return e;
77
78
  })(),
78
- orgId: getRequestOrgId() || null,
79
+ orgId,
80
+ visibility: orgId ? "org" : "private",
79
81
  createdAt: now,
80
82
  updatedAt: now,
81
83
  });