@speechos/client 0.2.6 → 0.2.8

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/index.cjs CHANGED
@@ -145,6 +145,10 @@ class FormDetector {
145
145
  this.focusHandler = (event) => {
146
146
  const target = event.target;
147
147
  if (isFormField(target)) {
148
+ console.log("[SpeechOS] FormDetector: focus on form field", {
149
+ element: target,
150
+ tagName: target?.tagName,
151
+ });
148
152
  core.state.setFocusedElement(target);
149
153
  core.state.show();
150
154
  core.events.emit("form:focus", { element: target });
@@ -1400,6 +1404,71 @@ const transcriptStore = {
1400
1404
  deleteTranscript: deleteTranscript,
1401
1405
  };
1402
1406
 
1407
+ function isNativeField(field) {
1408
+ return field instanceof HTMLInputElement || field instanceof HTMLTextAreaElement;
1409
+ }
1410
+ /** Call a function after focusing a field and then restore the previous focus afterwards if necessary */
1411
+ function withFocus(field, callback) {
1412
+ const document = field.ownerDocument;
1413
+ const initialFocus = document.activeElement;
1414
+ if (initialFocus === field) {
1415
+ return callback();
1416
+ }
1417
+ try {
1418
+ field.focus();
1419
+ return callback();
1420
+ }
1421
+ finally {
1422
+ field.blur(); // Supports `intialFocus === body`
1423
+ if (initialFocus instanceof HTMLElement) {
1424
+ initialFocus.focus();
1425
+ }
1426
+ }
1427
+ }
1428
+ // This will insert into the focused field. It shouild always be called inside withFocus.
1429
+ // Use this one locally if there are multiple `insertTextIntoField` or `document.execCommand` calls
1430
+ function insertTextWhereverTheFocusIs(document, text) {
1431
+ if (text === '') {
1432
+ // https://github.com/fregante/text-field-edit/issues/16
1433
+ document.execCommand('delete');
1434
+ }
1435
+ else {
1436
+ document.execCommand('insertText', false, text);
1437
+ }
1438
+ }
1439
+ /** Inserts `text` at the cursor’s position, replacing any selection, with **undo** support and by firing the `input` event. */
1440
+ function insertTextIntoField(field, text) {
1441
+ withFocus(field, () => {
1442
+ insertTextWhereverTheFocusIs(field.ownerDocument, text);
1443
+ });
1444
+ }
1445
+ /** Replaces the entire content, equivalent to `field.value = text` but with **undo** support and by firing the `input` event. */
1446
+ function setFieldText(field, text) {
1447
+ if (isNativeField(field)) {
1448
+ field.select();
1449
+ insertTextIntoField(field, text);
1450
+ }
1451
+ else {
1452
+ const document = field.ownerDocument;
1453
+ withFocus(field, () => {
1454
+ document.execCommand('selectAll', false, text);
1455
+ insertTextWhereverTheFocusIs(document, text);
1456
+ });
1457
+ }
1458
+ }
1459
+ /** Get the selected text in a field or an empty string if nothing is selected. */
1460
+ function getFieldSelection(field) {
1461
+ if (isNativeField(field)) {
1462
+ return field.value.slice(field.selectionStart, field.selectionEnd);
1463
+ }
1464
+ const selection = field.ownerDocument.getSelection();
1465
+ if (selection && field.contains(selection.anchorNode)) {
1466
+ // The selection is inside the field
1467
+ return selection.toString();
1468
+ }
1469
+ return '';
1470
+ }
1471
+
1403
1472
  /**
1404
1473
  * @license
1405
1474
  * Copyright 2017 Google LLC
@@ -1760,6 +1829,7 @@ let SpeechOSMicButton = class SpeechOSMicButton extends i$1 {
1760
1829
  this.activeAction = null;
1761
1830
  this.editPreviewText = "";
1762
1831
  this.errorMessage = null;
1832
+ this.showRetryButton = true;
1763
1833
  this.actionFeedback = null;
1764
1834
  this.showNoAudioWarning = false;
1765
1835
  }
@@ -2469,10 +2539,14 @@ let SpeechOSMicButton = class SpeechOSMicButton extends i$1 {
2469
2539
  bottom: 72px; /* Above button */
2470
2540
  left: 50%;
2471
2541
  transform: translateX(-50%) translateY(8px);
2542
+ min-width: 200px;
2472
2543
  max-width: 280px;
2544
+ width: max-content;
2473
2545
  font-size: 13px;
2474
2546
  color: white;
2475
2547
  white-space: normal;
2548
+ word-wrap: break-word;
2549
+ overflow-wrap: break-word;
2476
2550
  text-align: center;
2477
2551
  padding: 12px 16px;
2478
2552
  border-radius: 12px;
@@ -2685,6 +2759,7 @@ let SpeechOSMicButton = class SpeechOSMicButton extends i$1 {
2685
2759
  .error-message {
2686
2760
  font-size: 15px;
2687
2761
  padding: 14px 18px;
2762
+ min-width: 220px;
2688
2763
  max-width: 300px;
2689
2764
  bottom: 94px;
2690
2765
  }
@@ -2910,9 +2985,13 @@ let SpeechOSMicButton = class SpeechOSMicButton extends i$1 {
2910
2985
  ? b `
2911
2986
  <div class="error-message ${showError ? "visible" : ""}">
2912
2987
  ${this.errorMessage}
2913
- <button class="retry-button" @click="${this.handleRetry}">
2914
- Retry Connection
2915
- </button>
2988
+ ${this.showRetryButton
2989
+ ? b `
2990
+ <button class="retry-button" @click="${this.handleRetry}">
2991
+ Retry Connection
2992
+ </button>
2993
+ `
2994
+ : ""}
2916
2995
  </div>
2917
2996
  `
2918
2997
  : ""}
@@ -3011,6 +3090,9 @@ __decorate([
3011
3090
  __decorate([
3012
3091
  n({ type: String })
3013
3092
  ], SpeechOSMicButton.prototype, "errorMessage", void 0);
3093
+ __decorate([
3094
+ n({ type: Boolean })
3095
+ ], SpeechOSMicButton.prototype, "showRetryButton", void 0);
3014
3096
  __decorate([
3015
3097
  n({ type: String })
3016
3098
  ], SpeechOSMicButton.prototype, "actionFeedback", void 0);
@@ -6046,6 +6128,7 @@ let SpeechOSDictationOutputModal = class SpeechOSDictationOutputModal extends i$
6046
6128
  super(...arguments);
6047
6129
  this.open = false;
6048
6130
  this.text = "";
6131
+ this.mode = "dictation";
6049
6132
  this.copied = false;
6050
6133
  this.copyTimeout = null;
6051
6134
  }
@@ -6125,6 +6208,41 @@ let SpeechOSDictationOutputModal = class SpeechOSDictationOutputModal extends i$
6125
6208
  color: #10b981;
6126
6209
  flex-shrink: 0;
6127
6210
  }
6211
+
6212
+ /* Edit mode styles */
6213
+ :host([mode="edit"]) .logo-icon {
6214
+ background: linear-gradient(135deg, #8b5cf6 0%, #6366f1 100%);
6215
+ }
6216
+
6217
+ :host([mode="edit"]) .modal-title {
6218
+ background: linear-gradient(135deg, #a78bfa 0%, #818cf8 100%);
6219
+ -webkit-background-clip: text;
6220
+ -webkit-text-fill-color: transparent;
6221
+ background-clip: text;
6222
+ }
6223
+
6224
+ :host([mode="edit"]) .hint {
6225
+ background: rgba(139, 92, 246, 0.08);
6226
+ }
6227
+
6228
+ :host([mode="edit"]) .hint-icon {
6229
+ color: #8b5cf6;
6230
+ }
6231
+
6232
+ :host([mode="edit"]) .btn-primary {
6233
+ background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
6234
+ box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
6235
+ }
6236
+
6237
+ :host([mode="edit"]) .btn-primary:hover {
6238
+ background: linear-gradient(135deg, #a78bfa 0%, #8b5cf6 100%);
6239
+ box-shadow: 0 6px 16px rgba(139, 92, 246, 0.4);
6240
+ }
6241
+
6242
+ :host([mode="edit"]) .btn-success {
6243
+ background: linear-gradient(135deg, #a78bfa 0%, #8b5cf6 100%);
6244
+ box-shadow: 0 4px 12px rgba(167, 139, 250, 0.3);
6245
+ }
6128
6246
  `,
6129
6247
  ]; }
6130
6248
  disconnectedCallback() {
@@ -6177,6 +6295,17 @@ let SpeechOSDictationOutputModal = class SpeechOSDictationOutputModal extends i$
6177
6295
  console.error("[SpeechOS] Failed to copy text:", err);
6178
6296
  }
6179
6297
  }
6298
+ get modalTitle() {
6299
+ return this.mode === "edit" ? "Edit Complete" : "Dictation Complete";
6300
+ }
6301
+ get modalIcon() {
6302
+ return this.mode === "edit" ? editIcon(18) : micIcon(18);
6303
+ }
6304
+ get hintText() {
6305
+ return this.mode === "edit"
6306
+ ? "Tip: The editor didn't accept the edit. Copy and paste manually."
6307
+ : "Tip: Focus a text field first to auto-insert next time";
6308
+ }
6180
6309
  render() {
6181
6310
  return b `
6182
6311
  <div
@@ -6186,8 +6315,8 @@ let SpeechOSDictationOutputModal = class SpeechOSDictationOutputModal extends i$
6186
6315
  <div class="modal-card">
6187
6316
  <div class="modal-header">
6188
6317
  <div class="header-content">
6189
- <div class="logo-icon">${micIcon(18)}</div>
6190
- <h2 class="modal-title">Dictation Complete</h2>
6318
+ <div class="logo-icon">${this.modalIcon}</div>
6319
+ <h2 class="modal-title">${this.modalTitle}</h2>
6191
6320
  </div>
6192
6321
  <button
6193
6322
  class="close-button"
@@ -6204,7 +6333,7 @@ let SpeechOSDictationOutputModal = class SpeechOSDictationOutputModal extends i$
6204
6333
  <svg class="hint-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
6205
6334
  <circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/>
6206
6335
  </svg>
6207
- <span>Tip: Focus a text field first to auto-insert next time</span>
6336
+ <span>${this.hintText}</span>
6208
6337
  </div>
6209
6338
  </div>
6210
6339
 
@@ -6231,6 +6360,9 @@ __decorate([
6231
6360
  __decorate([
6232
6361
  n({ type: String })
6233
6362
  ], SpeechOSDictationOutputModal.prototype, "text", void 0);
6363
+ __decorate([
6364
+ n({ type: String, reflect: true })
6365
+ ], SpeechOSDictationOutputModal.prototype, "mode", void 0);
6234
6366
  __decorate([
6235
6367
  r()
6236
6368
  ], SpeechOSDictationOutputModal.prototype, "copied", void 0);
@@ -6408,9 +6540,11 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
6408
6540
  this.settingsOpenFromWarning = false;
6409
6541
  this.dictationModalOpen = false;
6410
6542
  this.dictationModalText = "";
6543
+ this.dictationModalMode = "dictation";
6411
6544
  this.editHelpModalOpen = false;
6412
6545
  this.actionFeedback = null;
6413
6546
  this.showNoAudioWarning = false;
6547
+ this.isErrorRetryable = true;
6414
6548
  this.dictationTargetElement = null;
6415
6549
  this.editTargetElement = null;
6416
6550
  this.dictationCursorStart = null;
@@ -6559,6 +6693,8 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
6559
6693
  this.errorEventUnsubscribe = core.events.on("error", (payload) => {
6560
6694
  if (this.widgetState.recordingState !== "idle" &&
6561
6695
  this.widgetState.recordingState !== "error") {
6696
+ // Check if this is a non-retryable error (e.g., CSP blocked connection)
6697
+ this.isErrorRetryable = payload.code !== "connection_blocked";
6562
6698
  core.state.setError(payload.message);
6563
6699
  core.getBackend().disconnect().catch(() => { });
6564
6700
  }
@@ -6633,6 +6769,9 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
6633
6769
  if (changedProperties.has("dictationModalText") && this.dictationModalElement) {
6634
6770
  this.dictationModalElement.text = this.dictationModalText;
6635
6771
  }
6772
+ if (changedProperties.has("dictationModalMode") && this.dictationModalElement) {
6773
+ this.dictationModalElement.mode = this.dictationModalMode;
6774
+ }
6636
6775
  if (changedProperties.has("editHelpModalOpen") && this.editHelpModalElement) {
6637
6776
  this.editHelpModalElement.open = this.editHelpModalOpen;
6638
6777
  }
@@ -6836,13 +6975,24 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
6836
6975
  // Track result for consecutive failure detection
6837
6976
  this.trackActionResult(!!transcription);
6838
6977
  if (transcription) {
6978
+ if (core.getConfig().debug) {
6979
+ console.log("[SpeechOS] Transcription received:", {
6980
+ transcription,
6981
+ dictationTargetElement: this.dictationTargetElement,
6982
+ tagName: this.dictationTargetElement?.tagName,
6983
+ });
6984
+ }
6839
6985
  // Check if we have a target element to insert into
6840
6986
  if (this.dictationTargetElement) {
6841
6987
  this.insertTranscription(transcription);
6842
6988
  }
6843
6989
  else {
6844
6990
  // No target element - show dictation output modal
6991
+ if (core.getConfig().debug) {
6992
+ console.log("[SpeechOS] No target element, showing dictation modal");
6993
+ }
6845
6994
  this.dictationModalText = transcription;
6995
+ this.dictationModalMode = "dictation";
6846
6996
  this.dictationModalOpen = true;
6847
6997
  }
6848
6998
  transcriptStore.saveTranscript(transcription, "dictate");
@@ -6987,41 +7137,68 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
6987
7137
  return;
6988
7138
  }
6989
7139
  const tagName = target.tagName.toLowerCase();
7140
+ const originalContent = this.getElementContent(target) || "";
6990
7141
  if (tagName === "input" || tagName === "textarea") {
6991
7142
  const inputEl = target;
7143
+ // Ensure DOM focus is on the input before inserting
7144
+ inputEl.focus();
7145
+ // Restore cursor position before inserting
6992
7146
  const start = this.dictationCursorStart ?? inputEl.value.length;
6993
7147
  const end = this.dictationCursorEnd ?? inputEl.value.length;
6994
- const before = inputEl.value.substring(0, start);
6995
- const after = inputEl.value.substring(end);
6996
- inputEl.value = before + text + after;
6997
- if (this.supportsSelection(inputEl)) {
6998
- const newCursorPos = start + text.length;
6999
- inputEl.setSelectionRange(newCursorPos, newCursorPos);
7000
- }
7001
- inputEl.dispatchEvent(new Event("input", { bubbles: true }));
7002
- inputEl.focus();
7148
+ inputEl.setSelectionRange(start, end);
7149
+ // Use text-field-edit to insert text (handles undo, events, etc.)
7150
+ insertTextIntoField(inputEl, text);
7003
7151
  core.state.setFocusedElement(inputEl);
7004
7152
  }
7005
7153
  else if (target.isContentEditable) {
7006
7154
  target.focus();
7007
7155
  core.state.setFocusedElement(target);
7008
- const textNode = document.createTextNode(text);
7009
- target.appendChild(textNode);
7010
- const selection = window.getSelection();
7011
- if (selection) {
7012
- const range = document.createRange();
7013
- range.selectNodeContents(textNode);
7014
- range.collapse(false);
7015
- selection.removeAllRanges();
7016
- selection.addRange(range);
7017
- }
7018
- target.dispatchEvent(new Event("input", { bubbles: true }));
7156
+ // Use text-field-edit for contentEditable elements
7157
+ insertTextIntoField(target, text);
7019
7158
  }
7020
7159
  core.events.emit("transcription:inserted", { text, element: target });
7160
+ // Verify insertion was applied after DOM updates
7161
+ this.verifyInsertionApplied(target, text, originalContent);
7021
7162
  this.dictationTargetElement = null;
7022
7163
  this.dictationCursorStart = null;
7023
7164
  this.dictationCursorEnd = null;
7024
7165
  }
7166
+ /**
7167
+ * Verify that a dictation insertion was actually applied to the target element.
7168
+ * Some custom editors (CodeMirror, Monaco, Slate, etc.) don't respond to
7169
+ * standard DOM editing methods. If the insertion fails, show a fallback modal.
7170
+ */
7171
+ verifyInsertionApplied(target, insertedText, originalContent) {
7172
+ // Use requestAnimationFrame to check after DOM updates
7173
+ requestAnimationFrame(() => {
7174
+ const tagName = target.tagName.toLowerCase();
7175
+ let currentContent = "";
7176
+ if (tagName === "input" || tagName === "textarea") {
7177
+ currentContent = target.value;
7178
+ }
7179
+ else if (target.isContentEditable) {
7180
+ currentContent = target.textContent || "";
7181
+ }
7182
+ // Check if the insertion was applied:
7183
+ // - Content should contain the inserted text
7184
+ // - Or content should be different from original (for empty fields)
7185
+ const insertionApplied = currentContent.includes(insertedText) ||
7186
+ (originalContent === "" && currentContent !== "");
7187
+ if (!insertionApplied) {
7188
+ if (core.getConfig().debug) {
7189
+ console.log("[SpeechOS] Dictation failed to insert, showing fallback modal", {
7190
+ insertedText,
7191
+ currentContent,
7192
+ originalContent,
7193
+ });
7194
+ }
7195
+ // Show fallback modal with dictation mode styling
7196
+ this.dictationModalText = insertedText;
7197
+ this.dictationModalMode = "dictation";
7198
+ this.dictationModalOpen = true;
7199
+ }
7200
+ });
7201
+ }
7025
7202
  handleActionSelect(event) {
7026
7203
  const { action } = event.detail;
7027
7204
  // Clear any existing command feedback when a new action is selected
@@ -7060,6 +7237,13 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
7060
7237
  this.dictationTargetElement = this.widgetState.focusedElement;
7061
7238
  this.dictationCursorStart = null;
7062
7239
  this.dictationCursorEnd = null;
7240
+ if (core.getConfig().debug) {
7241
+ console.log("[SpeechOS] startDictation:", {
7242
+ focusedElement: this.widgetState.focusedElement,
7243
+ dictationTargetElement: this.dictationTargetElement,
7244
+ tagName: this.dictationTargetElement?.tagName,
7245
+ });
7246
+ }
7063
7247
  if (this.dictationTargetElement) {
7064
7248
  const tagName = this.dictationTargetElement.tagName.toLowerCase();
7065
7249
  if (tagName === "input" || tagName === "textarea") {
@@ -7085,15 +7269,18 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
7085
7269
  // Ensure minimum animation duration before transitioning to recording
7086
7270
  const elapsed = Date.now() - connectingStartTime;
7087
7271
  const remainingDelay = MIN_CONNECTING_ANIMATION_MS - elapsed;
7272
+ const startRecording = () => {
7273
+ if (core.state.getState().recordingState === "error") {
7274
+ return;
7275
+ }
7276
+ core.state.setRecordingState("recording");
7277
+ this.startNoAudioWarningTracking();
7278
+ };
7088
7279
  if (remainingDelay > 0) {
7089
- setTimeout(() => {
7090
- core.state.setRecordingState("recording");
7091
- this.startNoAudioWarningTracking();
7092
- }, remainingDelay);
7280
+ setTimeout(startRecording, remainingDelay);
7093
7281
  }
7094
7282
  else {
7095
- core.state.setRecordingState("recording");
7096
- this.startNoAudioWarningTracking();
7283
+ startRecording();
7097
7284
  }
7098
7285
  },
7099
7286
  });
@@ -7114,6 +7301,13 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
7114
7301
  this.editSelectionStart = null;
7115
7302
  this.editSelectionEnd = null;
7116
7303
  this.editSelectedText = "";
7304
+ if (core.getConfig().debug) {
7305
+ console.log("[SpeechOS] startEdit:", {
7306
+ focusedElement: this.widgetState.focusedElement,
7307
+ editTargetElement: this.editTargetElement,
7308
+ tagName: this.editTargetElement?.tagName,
7309
+ });
7310
+ }
7117
7311
  if (this.editTargetElement) {
7118
7312
  const tagName = this.editTargetElement.tagName.toLowerCase();
7119
7313
  if (tagName === "input" || tagName === "textarea") {
@@ -7124,7 +7318,8 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
7124
7318
  const start = this.editSelectionStart ?? 0;
7125
7319
  const end = this.editSelectionEnd ?? 0;
7126
7320
  if (start !== end) {
7127
- this.editSelectedText = inputEl.value.substring(start, end);
7321
+ // Use getFieldSelection from text-field-edit
7322
+ this.editSelectedText = getFieldSelection(inputEl);
7128
7323
  }
7129
7324
  }
7130
7325
  else {
@@ -7133,13 +7328,11 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
7133
7328
  }
7134
7329
  }
7135
7330
  else if (this.editTargetElement.isContentEditable) {
7136
- const selection = window.getSelection();
7137
- if (selection && selection.rangeCount > 0) {
7138
- const selectedText = selection.toString();
7139
- this.editSelectionStart = 0;
7140
- this.editSelectionEnd = selectedText.length;
7141
- this.editSelectedText = selectedText;
7142
- }
7331
+ // Use getFieldSelection from text-field-edit for contentEditable too
7332
+ const selectedText = getFieldSelection(this.editTargetElement);
7333
+ this.editSelectionStart = 0;
7334
+ this.editSelectionEnd = selectedText.length;
7335
+ this.editSelectedText = selectedText;
7143
7336
  }
7144
7337
  }
7145
7338
  // Capture the content to edit at start time (sent with auth message)
@@ -7156,15 +7349,18 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
7156
7349
  // Ensure minimum animation duration before transitioning to recording
7157
7350
  const elapsed = Date.now() - connectingStartTime;
7158
7351
  const remainingDelay = MIN_CONNECTING_ANIMATION_MS - elapsed;
7352
+ const startRecording = () => {
7353
+ if (core.state.getState().recordingState === "error") {
7354
+ return;
7355
+ }
7356
+ core.state.setRecordingState("recording");
7357
+ this.startNoAudioWarningTracking();
7358
+ };
7159
7359
  if (remainingDelay > 0) {
7160
- setTimeout(() => {
7161
- core.state.setRecordingState("recording");
7162
- this.startNoAudioWarningTracking();
7163
- }, remainingDelay);
7360
+ setTimeout(startRecording, remainingDelay);
7164
7361
  }
7165
7362
  else {
7166
- core.state.setRecordingState("recording");
7167
- this.startNoAudioWarningTracking();
7363
+ startRecording();
7168
7364
  }
7169
7365
  },
7170
7366
  });
@@ -7232,15 +7428,18 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
7232
7428
  // Ensure minimum animation duration before transitioning to recording
7233
7429
  const elapsed = Date.now() - connectingStartTime;
7234
7430
  const remainingDelay = MIN_CONNECTING_ANIMATION_MS - elapsed;
7431
+ const startRecording = () => {
7432
+ if (core.state.getState().recordingState === "error") {
7433
+ return;
7434
+ }
7435
+ core.state.setRecordingState("recording");
7436
+ this.startNoAudioWarningTracking();
7437
+ };
7235
7438
  if (remainingDelay > 0) {
7236
- setTimeout(() => {
7237
- core.state.setRecordingState("recording");
7238
- this.startNoAudioWarningTracking();
7239
- }, remainingDelay);
7439
+ setTimeout(startRecording, remainingDelay);
7240
7440
  }
7241
7441
  else {
7242
- core.state.setRecordingState("recording");
7243
- this.startNoAudioWarningTracking();
7442
+ startRecording();
7244
7443
  }
7245
7444
  },
7246
7445
  });
@@ -7279,8 +7478,6 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
7279
7478
  // Note: command:complete event is already emitted by the backend
7280
7479
  // when the command_result message is received, so we don't emit here
7281
7480
  core.state.completeRecording();
7282
- // Keep widget visible but collapsed (just mic button, no action bubbles)
7283
- core.state.setState({ isExpanded: false });
7284
7481
  // Show command feedback
7285
7482
  this.showActionFeedback(result ? "command-success" : "command-none");
7286
7483
  backend.disconnect().catch(() => { });
@@ -7416,21 +7613,14 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
7416
7613
  const tagName = element.tagName.toLowerCase();
7417
7614
  if (tagName === "input" || tagName === "textarea") {
7418
7615
  const inputEl = element;
7419
- const fullContent = inputEl.value;
7420
- const start = this.editSelectionStart ?? 0;
7421
- const end = this.editSelectionEnd ?? fullContent.length;
7422
- const hasSelection = start !== end;
7423
- if (hasSelection) {
7424
- return fullContent.substring(start, end);
7425
- }
7426
- return fullContent;
7616
+ const selectedText = getFieldSelection(inputEl);
7617
+ // If there's selected text, return it; otherwise return full content
7618
+ return selectedText || inputEl.value;
7427
7619
  }
7428
7620
  else if (element.isContentEditable) {
7429
- const selection = window.getSelection();
7430
- if (selection && selection.toString().length > 0) {
7431
- return selection.toString();
7432
- }
7433
- return element.textContent || "";
7621
+ const selectedText = getFieldSelection(element);
7622
+ // If there's selected text, return it; otherwise return full content
7623
+ return selectedText || element.textContent || "";
7434
7624
  }
7435
7625
  return "";
7436
7626
  }
@@ -7445,40 +7635,46 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
7445
7635
  if (tagName === "input" || tagName === "textarea") {
7446
7636
  const inputEl = target;
7447
7637
  originalContent = inputEl.value;
7638
+ // Ensure DOM focus is on the input before editing
7448
7639
  inputEl.focus();
7449
- if (this.supportsSelection(inputEl)) {
7450
- const selectionStart = this.editSelectionStart ?? 0;
7451
- const selectionEnd = this.editSelectionEnd ?? inputEl.value.length;
7452
- const hasSelection = selectionStart !== selectionEnd;
7453
- if (hasSelection) {
7454
- inputEl.setSelectionRange(selectionStart, selectionEnd);
7455
- }
7456
- else {
7457
- inputEl.setSelectionRange(0, inputEl.value.length);
7458
- }
7459
- document.execCommand("insertText", false, editedText);
7640
+ // Restore the original selection/cursor position
7641
+ const selectionStart = this.editSelectionStart ?? 0;
7642
+ const selectionEnd = this.editSelectionEnd ?? inputEl.value.length;
7643
+ const hasSelection = selectionStart !== selectionEnd;
7644
+ if (hasSelection) {
7645
+ // Restore selection, then use insertTextIntoField() to replace it
7646
+ inputEl.setSelectionRange(selectionStart, selectionEnd);
7647
+ insertTextIntoField(inputEl, editedText);
7460
7648
  }
7461
7649
  else {
7462
- inputEl.value = editedText;
7463
- inputEl.dispatchEvent(new Event("input", { bubbles: true }));
7650
+ // No selection - replace entire content using setFieldText()
7651
+ setFieldText(inputEl, editedText);
7464
7652
  }
7465
7653
  core.state.setFocusedElement(inputEl);
7466
7654
  }
7467
7655
  else if (target.isContentEditable) {
7468
7656
  originalContent = target.textContent || "";
7469
- target.focus();
7470
- core.state.setFocusedElement(target);
7471
7657
  const hasSelection = this.editSelectionStart !== null &&
7472
7658
  this.editSelectionEnd !== null &&
7473
7659
  this.editSelectionStart !== this.editSelectionEnd;
7474
- if (!hasSelection) {
7660
+ if (hasSelection) {
7661
+ // Selection exists - focus and insert (assumes selection is still active or we restore it)
7662
+ target.focus();
7663
+ insertTextIntoField(target, editedText);
7664
+ }
7665
+ else {
7666
+ // No selection - select all content first, then replace with insertTextIntoField()
7667
+ target.focus();
7475
7668
  const selection = window.getSelection();
7476
- const range = document.createRange();
7477
- range.selectNodeContents(target);
7478
- selection?.removeAllRanges();
7479
- selection?.addRange(range);
7669
+ if (selection) {
7670
+ const range = document.createRange();
7671
+ range.selectNodeContents(target);
7672
+ selection.removeAllRanges();
7673
+ selection.addRange(range);
7674
+ }
7675
+ insertTextIntoField(target, editedText);
7480
7676
  }
7481
- document.execCommand("insertText", false, editedText);
7677
+ core.state.setFocusedElement(target);
7482
7678
  }
7483
7679
  transcriptStore.saveTranscript(editedText, "edit", originalContent);
7484
7680
  core.events.emit("edit:applied", {
@@ -7487,11 +7683,54 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
7487
7683
  element: target,
7488
7684
  });
7489
7685
  core.state.completeRecording();
7686
+ // Verify edit was applied after DOM updates
7687
+ this.verifyEditApplied(target, editedText, originalContent);
7490
7688
  this.editTargetElement = null;
7491
7689
  this.editSelectionStart = null;
7492
7690
  this.editSelectionEnd = null;
7493
7691
  this.editSelectedText = "";
7494
7692
  }
7693
+ /**
7694
+ * Verify that an edit was actually applied to the target element.
7695
+ * Some custom editors (CodeMirror, Monaco, Slate, etc.) don't respond to
7696
+ * standard DOM editing methods. If the edit fails, show a fallback modal.
7697
+ */
7698
+ verifyEditApplied(target, editedText, originalContent) {
7699
+ // Use requestAnimationFrame to check after DOM updates
7700
+ requestAnimationFrame(() => {
7701
+ const tagName = target.tagName.toLowerCase();
7702
+ let currentContent = "";
7703
+ if (tagName === "input" || tagName === "textarea") {
7704
+ currentContent = target.value;
7705
+ }
7706
+ else if (target.isContentEditable) {
7707
+ currentContent = target.textContent || "";
7708
+ }
7709
+ // Normalize whitespace for comparison
7710
+ const normalizedCurrent = currentContent.trim();
7711
+ const normalizedEdited = editedText.trim();
7712
+ const normalizedOriginal = originalContent.trim();
7713
+ // Check if the edit was applied:
7714
+ // - Content should be different from original (unless edit was no-op)
7715
+ // - Content should contain or match the edited text
7716
+ const editApplied = normalizedCurrent !== normalizedOriginal ||
7717
+ normalizedCurrent === normalizedEdited ||
7718
+ normalizedCurrent.includes(normalizedEdited);
7719
+ if (!editApplied) {
7720
+ if (core.getConfig().debug) {
7721
+ console.log("[SpeechOS] Edit failed to apply, showing fallback modal", {
7722
+ expected: editedText,
7723
+ actual: currentContent,
7724
+ original: originalContent,
7725
+ });
7726
+ }
7727
+ // Show fallback modal with edit mode styling
7728
+ this.dictationModalText = editedText;
7729
+ this.dictationModalMode = "edit";
7730
+ this.dictationModalOpen = true;
7731
+ }
7732
+ });
7733
+ }
7495
7734
  render() {
7496
7735
  if (!this.widgetState.isVisible) {
7497
7736
  this.setAttribute("hidden", "");
@@ -7519,6 +7758,7 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
7519
7758
  activeAction="${this.widgetState.activeAction || ""}"
7520
7759
  editPreviewText="${this.editSelectedText}"
7521
7760
  errorMessage="${this.widgetState.errorMessage || ""}"
7761
+ ?showRetryButton="${this.isErrorRetryable}"
7522
7762
  .actionFeedback="${this.actionFeedback}"
7523
7763
  ?showNoAudioWarning="${this.showNoAudioWarning}"
7524
7764
  @mic-click="${this.handleMicClick}"
@@ -7545,6 +7785,9 @@ __decorate([
7545
7785
  __decorate([
7546
7786
  r()
7547
7787
  ], SpeechOSWidget.prototype, "dictationModalText", void 0);
7788
+ __decorate([
7789
+ r()
7790
+ ], SpeechOSWidget.prototype, "dictationModalMode", void 0);
7548
7791
  __decorate([
7549
7792
  r()
7550
7793
  ], SpeechOSWidget.prototype, "editHelpModalOpen", void 0);
@@ -7554,6 +7797,9 @@ __decorate([
7554
7797
  __decorate([
7555
7798
  r()
7556
7799
  ], SpeechOSWidget.prototype, "showNoAudioWarning", void 0);
7800
+ __decorate([
7801
+ r()
7802
+ ], SpeechOSWidget.prototype, "isErrorRetryable", void 0);
7557
7803
  SpeechOSWidget = SpeechOSWidget_1 = __decorate([
7558
7804
  t$1("speechos-widget")
7559
7805
  ], SpeechOSWidget);