@speechos/client 0.2.6 → 0.2.7

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.js CHANGED
@@ -142,6 +142,10 @@ class FormDetector {
142
142
  this.focusHandler = (event) => {
143
143
  const target = event.target;
144
144
  if (isFormField(target)) {
145
+ console.log("[SpeechOS] FormDetector: focus on form field", {
146
+ element: target,
147
+ tagName: target?.tagName,
148
+ });
145
149
  state.setFocusedElement(target);
146
150
  state.show();
147
151
  events.emit("form:focus", { element: target });
@@ -1397,6 +1401,71 @@ const transcriptStore = {
1397
1401
  deleteTranscript: deleteTranscript,
1398
1402
  };
1399
1403
 
1404
+ function isNativeField(field) {
1405
+ return field instanceof HTMLInputElement || field instanceof HTMLTextAreaElement;
1406
+ }
1407
+ /** Call a function after focusing a field and then restore the previous focus afterwards if necessary */
1408
+ function withFocus(field, callback) {
1409
+ const document = field.ownerDocument;
1410
+ const initialFocus = document.activeElement;
1411
+ if (initialFocus === field) {
1412
+ return callback();
1413
+ }
1414
+ try {
1415
+ field.focus();
1416
+ return callback();
1417
+ }
1418
+ finally {
1419
+ field.blur(); // Supports `intialFocus === body`
1420
+ if (initialFocus instanceof HTMLElement) {
1421
+ initialFocus.focus();
1422
+ }
1423
+ }
1424
+ }
1425
+ // This will insert into the focused field. It shouild always be called inside withFocus.
1426
+ // Use this one locally if there are multiple `insertTextIntoField` or `document.execCommand` calls
1427
+ function insertTextWhereverTheFocusIs(document, text) {
1428
+ if (text === '') {
1429
+ // https://github.com/fregante/text-field-edit/issues/16
1430
+ document.execCommand('delete');
1431
+ }
1432
+ else {
1433
+ document.execCommand('insertText', false, text);
1434
+ }
1435
+ }
1436
+ /** Inserts `text` at the cursor’s position, replacing any selection, with **undo** support and by firing the `input` event. */
1437
+ function insertTextIntoField(field, text) {
1438
+ withFocus(field, () => {
1439
+ insertTextWhereverTheFocusIs(field.ownerDocument, text);
1440
+ });
1441
+ }
1442
+ /** Replaces the entire content, equivalent to `field.value = text` but with **undo** support and by firing the `input` event. */
1443
+ function setFieldText(field, text) {
1444
+ if (isNativeField(field)) {
1445
+ field.select();
1446
+ insertTextIntoField(field, text);
1447
+ }
1448
+ else {
1449
+ const document = field.ownerDocument;
1450
+ withFocus(field, () => {
1451
+ document.execCommand('selectAll', false, text);
1452
+ insertTextWhereverTheFocusIs(document, text);
1453
+ });
1454
+ }
1455
+ }
1456
+ /** Get the selected text in a field or an empty string if nothing is selected. */
1457
+ function getFieldSelection(field) {
1458
+ if (isNativeField(field)) {
1459
+ return field.value.slice(field.selectionStart, field.selectionEnd);
1460
+ }
1461
+ const selection = field.ownerDocument.getSelection();
1462
+ if (selection && field.contains(selection.anchorNode)) {
1463
+ // The selection is inside the field
1464
+ return selection.toString();
1465
+ }
1466
+ return '';
1467
+ }
1468
+
1400
1469
  /**
1401
1470
  * @license
1402
1471
  * Copyright 2017 Google LLC
@@ -1757,6 +1826,7 @@ let SpeechOSMicButton = class SpeechOSMicButton extends i$1 {
1757
1826
  this.activeAction = null;
1758
1827
  this.editPreviewText = "";
1759
1828
  this.errorMessage = null;
1829
+ this.showRetryButton = true;
1760
1830
  this.actionFeedback = null;
1761
1831
  this.showNoAudioWarning = false;
1762
1832
  }
@@ -2466,10 +2536,14 @@ let SpeechOSMicButton = class SpeechOSMicButton extends i$1 {
2466
2536
  bottom: 72px; /* Above button */
2467
2537
  left: 50%;
2468
2538
  transform: translateX(-50%) translateY(8px);
2539
+ min-width: 200px;
2469
2540
  max-width: 280px;
2541
+ width: max-content;
2470
2542
  font-size: 13px;
2471
2543
  color: white;
2472
2544
  white-space: normal;
2545
+ word-wrap: break-word;
2546
+ overflow-wrap: break-word;
2473
2547
  text-align: center;
2474
2548
  padding: 12px 16px;
2475
2549
  border-radius: 12px;
@@ -2682,6 +2756,7 @@ let SpeechOSMicButton = class SpeechOSMicButton extends i$1 {
2682
2756
  .error-message {
2683
2757
  font-size: 15px;
2684
2758
  padding: 14px 18px;
2759
+ min-width: 220px;
2685
2760
  max-width: 300px;
2686
2761
  bottom: 94px;
2687
2762
  }
@@ -2907,9 +2982,13 @@ let SpeechOSMicButton = class SpeechOSMicButton extends i$1 {
2907
2982
  ? b `
2908
2983
  <div class="error-message ${showError ? "visible" : ""}">
2909
2984
  ${this.errorMessage}
2910
- <button class="retry-button" @click="${this.handleRetry}">
2911
- Retry Connection
2912
- </button>
2985
+ ${this.showRetryButton
2986
+ ? b `
2987
+ <button class="retry-button" @click="${this.handleRetry}">
2988
+ Retry Connection
2989
+ </button>
2990
+ `
2991
+ : ""}
2913
2992
  </div>
2914
2993
  `
2915
2994
  : ""}
@@ -3008,6 +3087,9 @@ __decorate([
3008
3087
  __decorate([
3009
3088
  n({ type: String })
3010
3089
  ], SpeechOSMicButton.prototype, "errorMessage", void 0);
3090
+ __decorate([
3091
+ n({ type: Boolean })
3092
+ ], SpeechOSMicButton.prototype, "showRetryButton", void 0);
3011
3093
  __decorate([
3012
3094
  n({ type: String })
3013
3095
  ], SpeechOSMicButton.prototype, "actionFeedback", void 0);
@@ -6043,6 +6125,7 @@ let SpeechOSDictationOutputModal = class SpeechOSDictationOutputModal extends i$
6043
6125
  super(...arguments);
6044
6126
  this.open = false;
6045
6127
  this.text = "";
6128
+ this.mode = "dictation";
6046
6129
  this.copied = false;
6047
6130
  this.copyTimeout = null;
6048
6131
  }
@@ -6122,6 +6205,41 @@ let SpeechOSDictationOutputModal = class SpeechOSDictationOutputModal extends i$
6122
6205
  color: #10b981;
6123
6206
  flex-shrink: 0;
6124
6207
  }
6208
+
6209
+ /* Edit mode styles */
6210
+ :host([mode="edit"]) .logo-icon {
6211
+ background: linear-gradient(135deg, #8b5cf6 0%, #6366f1 100%);
6212
+ }
6213
+
6214
+ :host([mode="edit"]) .modal-title {
6215
+ background: linear-gradient(135deg, #a78bfa 0%, #818cf8 100%);
6216
+ -webkit-background-clip: text;
6217
+ -webkit-text-fill-color: transparent;
6218
+ background-clip: text;
6219
+ }
6220
+
6221
+ :host([mode="edit"]) .hint {
6222
+ background: rgba(139, 92, 246, 0.08);
6223
+ }
6224
+
6225
+ :host([mode="edit"]) .hint-icon {
6226
+ color: #8b5cf6;
6227
+ }
6228
+
6229
+ :host([mode="edit"]) .btn-primary {
6230
+ background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
6231
+ box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
6232
+ }
6233
+
6234
+ :host([mode="edit"]) .btn-primary:hover {
6235
+ background: linear-gradient(135deg, #a78bfa 0%, #8b5cf6 100%);
6236
+ box-shadow: 0 6px 16px rgba(139, 92, 246, 0.4);
6237
+ }
6238
+
6239
+ :host([mode="edit"]) .btn-success {
6240
+ background: linear-gradient(135deg, #a78bfa 0%, #8b5cf6 100%);
6241
+ box-shadow: 0 4px 12px rgba(167, 139, 250, 0.3);
6242
+ }
6125
6243
  `,
6126
6244
  ]; }
6127
6245
  disconnectedCallback() {
@@ -6174,6 +6292,17 @@ let SpeechOSDictationOutputModal = class SpeechOSDictationOutputModal extends i$
6174
6292
  console.error("[SpeechOS] Failed to copy text:", err);
6175
6293
  }
6176
6294
  }
6295
+ get modalTitle() {
6296
+ return this.mode === "edit" ? "Edit Complete" : "Dictation Complete";
6297
+ }
6298
+ get modalIcon() {
6299
+ return this.mode === "edit" ? editIcon(18) : micIcon(18);
6300
+ }
6301
+ get hintText() {
6302
+ return this.mode === "edit"
6303
+ ? "Tip: The editor didn't accept the edit. Copy and paste manually."
6304
+ : "Tip: Focus a text field first to auto-insert next time";
6305
+ }
6177
6306
  render() {
6178
6307
  return b `
6179
6308
  <div
@@ -6183,8 +6312,8 @@ let SpeechOSDictationOutputModal = class SpeechOSDictationOutputModal extends i$
6183
6312
  <div class="modal-card">
6184
6313
  <div class="modal-header">
6185
6314
  <div class="header-content">
6186
- <div class="logo-icon">${micIcon(18)}</div>
6187
- <h2 class="modal-title">Dictation Complete</h2>
6315
+ <div class="logo-icon">${this.modalIcon}</div>
6316
+ <h2 class="modal-title">${this.modalTitle}</h2>
6188
6317
  </div>
6189
6318
  <button
6190
6319
  class="close-button"
@@ -6201,7 +6330,7 @@ let SpeechOSDictationOutputModal = class SpeechOSDictationOutputModal extends i$
6201
6330
  <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">
6202
6331
  <circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/>
6203
6332
  </svg>
6204
- <span>Tip: Focus a text field first to auto-insert next time</span>
6333
+ <span>${this.hintText}</span>
6205
6334
  </div>
6206
6335
  </div>
6207
6336
 
@@ -6228,6 +6357,9 @@ __decorate([
6228
6357
  __decorate([
6229
6358
  n({ type: String })
6230
6359
  ], SpeechOSDictationOutputModal.prototype, "text", void 0);
6360
+ __decorate([
6361
+ n({ type: String, reflect: true })
6362
+ ], SpeechOSDictationOutputModal.prototype, "mode", void 0);
6231
6363
  __decorate([
6232
6364
  r()
6233
6365
  ], SpeechOSDictationOutputModal.prototype, "copied", void 0);
@@ -6405,9 +6537,11 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
6405
6537
  this.settingsOpenFromWarning = false;
6406
6538
  this.dictationModalOpen = false;
6407
6539
  this.dictationModalText = "";
6540
+ this.dictationModalMode = "dictation";
6408
6541
  this.editHelpModalOpen = false;
6409
6542
  this.actionFeedback = null;
6410
6543
  this.showNoAudioWarning = false;
6544
+ this.isErrorRetryable = true;
6411
6545
  this.dictationTargetElement = null;
6412
6546
  this.editTargetElement = null;
6413
6547
  this.dictationCursorStart = null;
@@ -6556,6 +6690,8 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
6556
6690
  this.errorEventUnsubscribe = events.on("error", (payload) => {
6557
6691
  if (this.widgetState.recordingState !== "idle" &&
6558
6692
  this.widgetState.recordingState !== "error") {
6693
+ // Check if this is a non-retryable error (e.g., CSP blocked connection)
6694
+ this.isErrorRetryable = payload.code !== "connection_blocked";
6559
6695
  state.setError(payload.message);
6560
6696
  getBackend().disconnect().catch(() => { });
6561
6697
  }
@@ -6630,6 +6766,9 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
6630
6766
  if (changedProperties.has("dictationModalText") && this.dictationModalElement) {
6631
6767
  this.dictationModalElement.text = this.dictationModalText;
6632
6768
  }
6769
+ if (changedProperties.has("dictationModalMode") && this.dictationModalElement) {
6770
+ this.dictationModalElement.mode = this.dictationModalMode;
6771
+ }
6633
6772
  if (changedProperties.has("editHelpModalOpen") && this.editHelpModalElement) {
6634
6773
  this.editHelpModalElement.open = this.editHelpModalOpen;
6635
6774
  }
@@ -6833,13 +6972,24 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
6833
6972
  // Track result for consecutive failure detection
6834
6973
  this.trackActionResult(!!transcription);
6835
6974
  if (transcription) {
6975
+ if (getConfig().debug) {
6976
+ console.log("[SpeechOS] Transcription received:", {
6977
+ transcription,
6978
+ dictationTargetElement: this.dictationTargetElement,
6979
+ tagName: this.dictationTargetElement?.tagName,
6980
+ });
6981
+ }
6836
6982
  // Check if we have a target element to insert into
6837
6983
  if (this.dictationTargetElement) {
6838
6984
  this.insertTranscription(transcription);
6839
6985
  }
6840
6986
  else {
6841
6987
  // No target element - show dictation output modal
6988
+ if (getConfig().debug) {
6989
+ console.log("[SpeechOS] No target element, showing dictation modal");
6990
+ }
6842
6991
  this.dictationModalText = transcription;
6992
+ this.dictationModalMode = "dictation";
6843
6993
  this.dictationModalOpen = true;
6844
6994
  }
6845
6995
  transcriptStore.saveTranscript(transcription, "dictate");
@@ -6984,41 +7134,66 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
6984
7134
  return;
6985
7135
  }
6986
7136
  const tagName = target.tagName.toLowerCase();
7137
+ const originalContent = this.getElementContent(target) || "";
6987
7138
  if (tagName === "input" || tagName === "textarea") {
6988
7139
  const inputEl = target;
7140
+ // Restore cursor position before inserting
6989
7141
  const start = this.dictationCursorStart ?? inputEl.value.length;
6990
7142
  const end = this.dictationCursorEnd ?? inputEl.value.length;
6991
- const before = inputEl.value.substring(0, start);
6992
- const after = inputEl.value.substring(end);
6993
- inputEl.value = before + text + after;
6994
- if (this.supportsSelection(inputEl)) {
6995
- const newCursorPos = start + text.length;
6996
- inputEl.setSelectionRange(newCursorPos, newCursorPos);
6997
- }
6998
- inputEl.dispatchEvent(new Event("input", { bubbles: true }));
6999
- inputEl.focus();
7143
+ inputEl.setSelectionRange(start, end);
7144
+ // Use text-field-edit to insert text (handles undo, events, etc.)
7145
+ insertTextIntoField(inputEl, text);
7000
7146
  state.setFocusedElement(inputEl);
7001
7147
  }
7002
7148
  else if (target.isContentEditable) {
7003
7149
  target.focus();
7004
7150
  state.setFocusedElement(target);
7005
- const textNode = document.createTextNode(text);
7006
- target.appendChild(textNode);
7007
- const selection = window.getSelection();
7008
- if (selection) {
7009
- const range = document.createRange();
7010
- range.selectNodeContents(textNode);
7011
- range.collapse(false);
7012
- selection.removeAllRanges();
7013
- selection.addRange(range);
7014
- }
7015
- target.dispatchEvent(new Event("input", { bubbles: true }));
7151
+ // Use text-field-edit for contentEditable elements
7152
+ insertTextIntoField(target, text);
7016
7153
  }
7017
7154
  events.emit("transcription:inserted", { text, element: target });
7155
+ // Verify insertion was applied after DOM updates
7156
+ this.verifyInsertionApplied(target, text, originalContent);
7018
7157
  this.dictationTargetElement = null;
7019
7158
  this.dictationCursorStart = null;
7020
7159
  this.dictationCursorEnd = null;
7021
7160
  }
7161
+ /**
7162
+ * Verify that a dictation insertion was actually applied to the target element.
7163
+ * Some custom editors (CodeMirror, Monaco, Slate, etc.) don't respond to
7164
+ * standard DOM editing methods. If the insertion fails, show a fallback modal.
7165
+ */
7166
+ verifyInsertionApplied(target, insertedText, originalContent) {
7167
+ // Use requestAnimationFrame to check after DOM updates
7168
+ requestAnimationFrame(() => {
7169
+ const tagName = target.tagName.toLowerCase();
7170
+ let currentContent = "";
7171
+ if (tagName === "input" || tagName === "textarea") {
7172
+ currentContent = target.value;
7173
+ }
7174
+ else if (target.isContentEditable) {
7175
+ currentContent = target.textContent || "";
7176
+ }
7177
+ // Check if the insertion was applied:
7178
+ // - Content should contain the inserted text
7179
+ // - Or content should be different from original (for empty fields)
7180
+ const insertionApplied = currentContent.includes(insertedText) ||
7181
+ (originalContent === "" && currentContent !== "");
7182
+ if (!insertionApplied) {
7183
+ if (getConfig().debug) {
7184
+ console.log("[SpeechOS] Dictation failed to insert, showing fallback modal", {
7185
+ insertedText,
7186
+ currentContent,
7187
+ originalContent,
7188
+ });
7189
+ }
7190
+ // Show fallback modal with dictation mode styling
7191
+ this.dictationModalText = insertedText;
7192
+ this.dictationModalMode = "dictation";
7193
+ this.dictationModalOpen = true;
7194
+ }
7195
+ });
7196
+ }
7022
7197
  handleActionSelect(event) {
7023
7198
  const { action } = event.detail;
7024
7199
  // Clear any existing command feedback when a new action is selected
@@ -7057,6 +7232,13 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
7057
7232
  this.dictationTargetElement = this.widgetState.focusedElement;
7058
7233
  this.dictationCursorStart = null;
7059
7234
  this.dictationCursorEnd = null;
7235
+ if (getConfig().debug) {
7236
+ console.log("[SpeechOS] startDictation:", {
7237
+ focusedElement: this.widgetState.focusedElement,
7238
+ dictationTargetElement: this.dictationTargetElement,
7239
+ tagName: this.dictationTargetElement?.tagName,
7240
+ });
7241
+ }
7060
7242
  if (this.dictationTargetElement) {
7061
7243
  const tagName = this.dictationTargetElement.tagName.toLowerCase();
7062
7244
  if (tagName === "input" || tagName === "textarea") {
@@ -7082,15 +7264,18 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
7082
7264
  // Ensure minimum animation duration before transitioning to recording
7083
7265
  const elapsed = Date.now() - connectingStartTime;
7084
7266
  const remainingDelay = MIN_CONNECTING_ANIMATION_MS - elapsed;
7267
+ const startRecording = () => {
7268
+ if (state.getState().recordingState === "error") {
7269
+ return;
7270
+ }
7271
+ state.setRecordingState("recording");
7272
+ this.startNoAudioWarningTracking();
7273
+ };
7085
7274
  if (remainingDelay > 0) {
7086
- setTimeout(() => {
7087
- state.setRecordingState("recording");
7088
- this.startNoAudioWarningTracking();
7089
- }, remainingDelay);
7275
+ setTimeout(startRecording, remainingDelay);
7090
7276
  }
7091
7277
  else {
7092
- state.setRecordingState("recording");
7093
- this.startNoAudioWarningTracking();
7278
+ startRecording();
7094
7279
  }
7095
7280
  },
7096
7281
  });
@@ -7111,6 +7296,13 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
7111
7296
  this.editSelectionStart = null;
7112
7297
  this.editSelectionEnd = null;
7113
7298
  this.editSelectedText = "";
7299
+ if (getConfig().debug) {
7300
+ console.log("[SpeechOS] startEdit:", {
7301
+ focusedElement: this.widgetState.focusedElement,
7302
+ editTargetElement: this.editTargetElement,
7303
+ tagName: this.editTargetElement?.tagName,
7304
+ });
7305
+ }
7114
7306
  if (this.editTargetElement) {
7115
7307
  const tagName = this.editTargetElement.tagName.toLowerCase();
7116
7308
  if (tagName === "input" || tagName === "textarea") {
@@ -7121,7 +7313,8 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
7121
7313
  const start = this.editSelectionStart ?? 0;
7122
7314
  const end = this.editSelectionEnd ?? 0;
7123
7315
  if (start !== end) {
7124
- this.editSelectedText = inputEl.value.substring(start, end);
7316
+ // Use getFieldSelection from text-field-edit
7317
+ this.editSelectedText = getFieldSelection(inputEl);
7125
7318
  }
7126
7319
  }
7127
7320
  else {
@@ -7130,13 +7323,11 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
7130
7323
  }
7131
7324
  }
7132
7325
  else if (this.editTargetElement.isContentEditable) {
7133
- const selection = window.getSelection();
7134
- if (selection && selection.rangeCount > 0) {
7135
- const selectedText = selection.toString();
7136
- this.editSelectionStart = 0;
7137
- this.editSelectionEnd = selectedText.length;
7138
- this.editSelectedText = selectedText;
7139
- }
7326
+ // Use getFieldSelection from text-field-edit for contentEditable too
7327
+ const selectedText = getFieldSelection(this.editTargetElement);
7328
+ this.editSelectionStart = 0;
7329
+ this.editSelectionEnd = selectedText.length;
7330
+ this.editSelectedText = selectedText;
7140
7331
  }
7141
7332
  }
7142
7333
  // Capture the content to edit at start time (sent with auth message)
@@ -7153,15 +7344,18 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
7153
7344
  // Ensure minimum animation duration before transitioning to recording
7154
7345
  const elapsed = Date.now() - connectingStartTime;
7155
7346
  const remainingDelay = MIN_CONNECTING_ANIMATION_MS - elapsed;
7347
+ const startRecording = () => {
7348
+ if (state.getState().recordingState === "error") {
7349
+ return;
7350
+ }
7351
+ state.setRecordingState("recording");
7352
+ this.startNoAudioWarningTracking();
7353
+ };
7156
7354
  if (remainingDelay > 0) {
7157
- setTimeout(() => {
7158
- state.setRecordingState("recording");
7159
- this.startNoAudioWarningTracking();
7160
- }, remainingDelay);
7355
+ setTimeout(startRecording, remainingDelay);
7161
7356
  }
7162
7357
  else {
7163
- state.setRecordingState("recording");
7164
- this.startNoAudioWarningTracking();
7358
+ startRecording();
7165
7359
  }
7166
7360
  },
7167
7361
  });
@@ -7229,15 +7423,18 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
7229
7423
  // Ensure minimum animation duration before transitioning to recording
7230
7424
  const elapsed = Date.now() - connectingStartTime;
7231
7425
  const remainingDelay = MIN_CONNECTING_ANIMATION_MS - elapsed;
7426
+ const startRecording = () => {
7427
+ if (state.getState().recordingState === "error") {
7428
+ return;
7429
+ }
7430
+ state.setRecordingState("recording");
7431
+ this.startNoAudioWarningTracking();
7432
+ };
7232
7433
  if (remainingDelay > 0) {
7233
- setTimeout(() => {
7234
- state.setRecordingState("recording");
7235
- this.startNoAudioWarningTracking();
7236
- }, remainingDelay);
7434
+ setTimeout(startRecording, remainingDelay);
7237
7435
  }
7238
7436
  else {
7239
- state.setRecordingState("recording");
7240
- this.startNoAudioWarningTracking();
7437
+ startRecording();
7241
7438
  }
7242
7439
  },
7243
7440
  });
@@ -7413,21 +7610,14 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
7413
7610
  const tagName = element.tagName.toLowerCase();
7414
7611
  if (tagName === "input" || tagName === "textarea") {
7415
7612
  const inputEl = element;
7416
- const fullContent = inputEl.value;
7417
- const start = this.editSelectionStart ?? 0;
7418
- const end = this.editSelectionEnd ?? fullContent.length;
7419
- const hasSelection = start !== end;
7420
- if (hasSelection) {
7421
- return fullContent.substring(start, end);
7422
- }
7423
- return fullContent;
7613
+ const selectedText = getFieldSelection(inputEl);
7614
+ // If there's selected text, return it; otherwise return full content
7615
+ return selectedText || inputEl.value;
7424
7616
  }
7425
7617
  else if (element.isContentEditable) {
7426
- const selection = window.getSelection();
7427
- if (selection && selection.toString().length > 0) {
7428
- return selection.toString();
7429
- }
7430
- return element.textContent || "";
7618
+ const selectedText = getFieldSelection(element);
7619
+ // If there's selected text, return it; otherwise return full content
7620
+ return selectedText || element.textContent || "";
7431
7621
  }
7432
7622
  return "";
7433
7623
  }
@@ -7442,40 +7632,44 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
7442
7632
  if (tagName === "input" || tagName === "textarea") {
7443
7633
  const inputEl = target;
7444
7634
  originalContent = inputEl.value;
7445
- inputEl.focus();
7446
- if (this.supportsSelection(inputEl)) {
7447
- const selectionStart = this.editSelectionStart ?? 0;
7448
- const selectionEnd = this.editSelectionEnd ?? inputEl.value.length;
7449
- const hasSelection = selectionStart !== selectionEnd;
7450
- if (hasSelection) {
7451
- inputEl.setSelectionRange(selectionStart, selectionEnd);
7452
- }
7453
- else {
7454
- inputEl.setSelectionRange(0, inputEl.value.length);
7455
- }
7456
- document.execCommand("insertText", false, editedText);
7635
+ // Restore the original selection/cursor position
7636
+ const selectionStart = this.editSelectionStart ?? 0;
7637
+ const selectionEnd = this.editSelectionEnd ?? inputEl.value.length;
7638
+ const hasSelection = selectionStart !== selectionEnd;
7639
+ if (hasSelection) {
7640
+ // Restore selection, then use insertTextIntoField() to replace it
7641
+ inputEl.setSelectionRange(selectionStart, selectionEnd);
7642
+ insertTextIntoField(inputEl, editedText);
7457
7643
  }
7458
7644
  else {
7459
- inputEl.value = editedText;
7460
- inputEl.dispatchEvent(new Event("input", { bubbles: true }));
7645
+ // No selection - replace entire content using setFieldText()
7646
+ setFieldText(inputEl, editedText);
7461
7647
  }
7462
7648
  state.setFocusedElement(inputEl);
7463
7649
  }
7464
7650
  else if (target.isContentEditable) {
7465
7651
  originalContent = target.textContent || "";
7466
- target.focus();
7467
- state.setFocusedElement(target);
7468
7652
  const hasSelection = this.editSelectionStart !== null &&
7469
7653
  this.editSelectionEnd !== null &&
7470
7654
  this.editSelectionStart !== this.editSelectionEnd;
7471
- if (!hasSelection) {
7655
+ if (hasSelection) {
7656
+ // Selection exists - focus and insert (assumes selection is still active or we restore it)
7657
+ target.focus();
7658
+ insertTextIntoField(target, editedText);
7659
+ }
7660
+ else {
7661
+ // No selection - select all content first, then replace with insertTextIntoField()
7662
+ target.focus();
7472
7663
  const selection = window.getSelection();
7473
- const range = document.createRange();
7474
- range.selectNodeContents(target);
7475
- selection?.removeAllRanges();
7476
- selection?.addRange(range);
7664
+ if (selection) {
7665
+ const range = document.createRange();
7666
+ range.selectNodeContents(target);
7667
+ selection.removeAllRanges();
7668
+ selection.addRange(range);
7669
+ }
7670
+ insertTextIntoField(target, editedText);
7477
7671
  }
7478
- document.execCommand("insertText", false, editedText);
7672
+ state.setFocusedElement(target);
7479
7673
  }
7480
7674
  transcriptStore.saveTranscript(editedText, "edit", originalContent);
7481
7675
  events.emit("edit:applied", {
@@ -7484,11 +7678,54 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
7484
7678
  element: target,
7485
7679
  });
7486
7680
  state.completeRecording();
7681
+ // Verify edit was applied after DOM updates
7682
+ this.verifyEditApplied(target, editedText, originalContent);
7487
7683
  this.editTargetElement = null;
7488
7684
  this.editSelectionStart = null;
7489
7685
  this.editSelectionEnd = null;
7490
7686
  this.editSelectedText = "";
7491
7687
  }
7688
+ /**
7689
+ * Verify that an edit was actually applied to the target element.
7690
+ * Some custom editors (CodeMirror, Monaco, Slate, etc.) don't respond to
7691
+ * standard DOM editing methods. If the edit fails, show a fallback modal.
7692
+ */
7693
+ verifyEditApplied(target, editedText, originalContent) {
7694
+ // Use requestAnimationFrame to check after DOM updates
7695
+ requestAnimationFrame(() => {
7696
+ const tagName = target.tagName.toLowerCase();
7697
+ let currentContent = "";
7698
+ if (tagName === "input" || tagName === "textarea") {
7699
+ currentContent = target.value;
7700
+ }
7701
+ else if (target.isContentEditable) {
7702
+ currentContent = target.textContent || "";
7703
+ }
7704
+ // Normalize whitespace for comparison
7705
+ const normalizedCurrent = currentContent.trim();
7706
+ const normalizedEdited = editedText.trim();
7707
+ const normalizedOriginal = originalContent.trim();
7708
+ // Check if the edit was applied:
7709
+ // - Content should be different from original (unless edit was no-op)
7710
+ // - Content should contain or match the edited text
7711
+ const editApplied = normalizedCurrent !== normalizedOriginal ||
7712
+ normalizedCurrent === normalizedEdited ||
7713
+ normalizedCurrent.includes(normalizedEdited);
7714
+ if (!editApplied) {
7715
+ if (getConfig().debug) {
7716
+ console.log("[SpeechOS] Edit failed to apply, showing fallback modal", {
7717
+ expected: editedText,
7718
+ actual: currentContent,
7719
+ original: originalContent,
7720
+ });
7721
+ }
7722
+ // Show fallback modal with edit mode styling
7723
+ this.dictationModalText = editedText;
7724
+ this.dictationModalMode = "edit";
7725
+ this.dictationModalOpen = true;
7726
+ }
7727
+ });
7728
+ }
7492
7729
  render() {
7493
7730
  if (!this.widgetState.isVisible) {
7494
7731
  this.setAttribute("hidden", "");
@@ -7516,6 +7753,7 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
7516
7753
  activeAction="${this.widgetState.activeAction || ""}"
7517
7754
  editPreviewText="${this.editSelectedText}"
7518
7755
  errorMessage="${this.widgetState.errorMessage || ""}"
7756
+ ?showRetryButton="${this.isErrorRetryable}"
7519
7757
  .actionFeedback="${this.actionFeedback}"
7520
7758
  ?showNoAudioWarning="${this.showNoAudioWarning}"
7521
7759
  @mic-click="${this.handleMicClick}"
@@ -7542,6 +7780,9 @@ __decorate([
7542
7780
  __decorate([
7543
7781
  r()
7544
7782
  ], SpeechOSWidget.prototype, "dictationModalText", void 0);
7783
+ __decorate([
7784
+ r()
7785
+ ], SpeechOSWidget.prototype, "dictationModalMode", void 0);
7545
7786
  __decorate([
7546
7787
  r()
7547
7788
  ], SpeechOSWidget.prototype, "editHelpModalOpen", void 0);
@@ -7551,6 +7792,9 @@ __decorate([
7551
7792
  __decorate([
7552
7793
  r()
7553
7794
  ], SpeechOSWidget.prototype, "showNoAudioWarning", void 0);
7795
+ __decorate([
7796
+ r()
7797
+ ], SpeechOSWidget.prototype, "isErrorRetryable", void 0);
7554
7798
  SpeechOSWidget = SpeechOSWidget_1 = __decorate([
7555
7799
  t$1("speechos-widget")
7556
7800
  ], SpeechOSWidget);