@agi-fans/dsh-tui 0.24.0 → 0.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,22 +7,25 @@
7
7
  * decoding, and the prompt/settings/search branches that decide whether a
8
8
  * paste is an image at all stay in the provider.
9
9
  */
10
- import { imageMarker, probeImageDimensions, submissionDraftText } from "../input/image-paste.js";
10
+ import { imageMarker, probeImageDimensions, submissionDraft } from "../input/image-paste.js";
11
11
  export class ComposerImages {
12
12
  #deps;
13
- #images = [];
13
+ #images = new Map();
14
+ #next = 0;
14
15
  #revision = 0;
15
16
  #validate;
16
17
  constructor(deps) {
17
18
  this.#deps = deps;
18
19
  }
19
20
  get count() {
20
- return this.#images.length;
21
+ return this.#active().length;
21
22
  }
22
23
  get revision() { return this.#revision; }
23
24
  /** Fresh copies for a submission snapshot. */
24
- copies() {
25
- return this.#images.map(image => ({ ...image }));
25
+ copies(snapshot) {
26
+ if (snapshot === undefined)
27
+ this.reconcile();
28
+ return this.#active(snapshot).map(marker => ({ ...this.#images.get(marker.id) }));
26
29
  }
27
30
  setValidator(validate) {
28
31
  this.#validate = validate;
@@ -32,127 +35,133 @@ export class ComposerImages {
32
35
  * becomes an error notice and skips the draft, instead of failing the whole
33
36
  * submission after the user has typed a prompt around it.
34
37
  */
35
- async admit(image) {
38
+ async admit(image, signal) {
39
+ if (signal?.aborted === true)
40
+ return false;
36
41
  if (this.#validate === undefined)
37
42
  return true;
38
43
  try {
39
44
  await this.#validate(image);
40
- return true;
45
+ return !signal?.aborted;
41
46
  }
42
47
  catch (error) {
43
- this.#deps.notice(error instanceof Error ? error.message : String(error));
48
+ if (!signal?.aborted)
49
+ this.#deps.notice(error instanceof Error ? error.message : String(error));
44
50
  return false;
45
51
  }
46
52
  }
47
- /** Insert an `[Image #n]` marker at the cursor, padded with spaces. */
53
+ #active(snapshot) {
54
+ return (snapshot === undefined ? this.#deps.editor.markers() : snapshot.markers ?? []).filter(marker => this.#images.has(marker.id));
55
+ }
56
+ /** Expanded-text positions of the owned image markers, including their separators. */
57
+ ranges() {
58
+ const editor = this.#deps.editor;
59
+ return this.#active().map((marker, index) => ({ kind: 'image', index,
60
+ start: editor.textRange(0, marker.start).length, end: editor.textRange(0, marker.end).length }));
61
+ }
62
+ /** Insert an image as one undoable owned marker. */
48
63
  insert(input) {
49
- this.#revision++;
50
- const size = input.width === undefined || input.height === undefined
51
- ? probeImageDimensions(input.data, input.mediaType)
52
- : undefined;
53
- const image = {
54
- ...input,
64
+ const size = input.width === undefined || input.height === undefined ? probeImageDimensions(input.data, input.mediaType) : undefined;
65
+ const image = { ...input,
55
66
  ...(input.width === undefined && size !== undefined ? { width: size.width } : {}),
56
- ...(input.height === undefined && size !== undefined ? { height: size.height } : {}),
57
- };
58
- const marker = imageMarker(this.#images.length, image);
67
+ ...(input.height === undefined && size !== undefined ? { height: size.height } : {}) };
59
68
  const editor = this.#deps.editor;
60
- const before = editor.cursor > 0 && !/\s/u.test(editor.text[editor.cursor - 1] ?? '') ? ' ' : '';
61
- const after = editor.cursor >= editor.text.length || !/\s/u.test(editor.text[editor.cursor] ?? '')
62
- ? ' '
63
- : '';
64
- this.#images.push(image);
65
- editor.handle({ type: 'text', value: before + marker + after });
69
+ const start = editor.cursor;
70
+ const before = start > 0 && !/\s/u.test(editor.text[start - 1] ?? '') ? ' ' : '';
71
+ const after = start >= editor.text.length || !/\s/u.test(editor.text[start] ?? '') ? ' ' : '';
72
+ const text = before + imageMarker(this.count, image) + after;
73
+ editor.splice(start, start, text);
74
+ const id = `image:${++this.#next}`;
75
+ editor.mark(id, start, start + text.length);
76
+ this.#images.set(id, image);
77
+ this.#revision++;
78
+ this.reconcile();
66
79
  }
67
- /**
68
- * Delete the image whose marker touches the cursor, renumbering the rest.
69
- * Returns true when a draft was removed; the caller repaints.
70
- */
71
80
  removeAtCursor(key) {
72
81
  const editor = this.#deps.editor;
73
- const cursor = editor.cursor;
74
- for (let index = 0; index < this.#images.length; index += 1) {
75
- const image = this.#images[index];
76
- const marker = imageMarker(index, image);
77
- const start = editor.text.indexOf(marker);
78
- if (start < 0)
79
- continue;
80
- let from = start;
81
- let to = start + marker.length;
82
- const touches = key === 'backspace'
83
- ? cursor > start && cursor <= to
84
- : cursor >= start && cursor < to;
85
- if (!touches)
86
- continue;
87
- if (editor.text[to] === ' ')
88
- to += 1;
89
- else if (from > 0 && editor.text[from - 1] === ' ')
90
- from -= 1;
91
- const oldImages = this.#images;
92
- let text = editor.text.slice(0, from) + editor.text.slice(to);
93
- const nextImages = oldImages.filter((_, oldIndex) => oldIndex !== index);
94
- let nextIndex = 0;
95
- for (let oldIndex = 0; oldIndex < oldImages.length; oldIndex += 1) {
96
- if (oldIndex === index)
97
- continue;
98
- const remaining = oldImages[oldIndex];
99
- text = text.replaceAll(imageMarker(oldIndex, remaining), imageMarker(nextIndex, remaining));
100
- nextIndex += 1;
82
+ const marker = this.#active().find(marker => key === 'backspace'
83
+ ? editor.cursor > marker.start && editor.cursor <= marker.end
84
+ : editor.cursor >= marker.start && editor.cursor < marker.end);
85
+ if (marker === undefined)
86
+ return false;
87
+ editor.splice(marker.start, marker.end, '');
88
+ this.reconcile();
89
+ return true;
90
+ }
91
+ /** Keep payloads referenced by current text or undo history, and relabel active images. */
92
+ reconcile() {
93
+ const editor = this.#deps.editor;
94
+ const retained = editor.retainedMarkerIds();
95
+ for (const id of this.#images.keys()) {
96
+ if (!retained.has(id)) {
97
+ this.#images.delete(id);
98
+ this.#revision++;
101
99
  }
102
- this.#images = nextImages;
103
- this.#revision++;
104
- editor.replaceText(text, Math.min(from, text.length));
105
- return true;
106
100
  }
107
- return false;
101
+ for (const [index, { id }] of this.#active().entries()) {
102
+ const marker = editor.markers().find(marker => marker.id === id);
103
+ const current = editor.text.slice(marker.start, marker.end);
104
+ const text = (current.match(/^\s*/u)?.[0] ?? '') + imageMarker(index, this.#images.get(marker.id)) + (current.match(/\s*$/u)?.[0] ?? '');
105
+ if (text !== current)
106
+ editor.rewriteMarker(marker.id, text);
107
+ }
108
108
  }
109
- /** Drop drafts whose marker left the text, renumbering the survivors. */
110
- reconcile() {
111
- if (this.#images.length === 0)
112
- return;
109
+ /** Bind persisted payloads to the editor's saved marker identities. */
110
+ restoreSnapshot(images) {
111
+ this.#images.clear();
113
112
  const editor = this.#deps.editor;
114
- const oldImages = this.#images;
115
- const retained = oldImages.filter((image, index) => editor.text.includes(imageMarker(index, image)));
116
- if (retained.length === oldImages.length)
117
- return;
118
- let text = editor.text;
119
- let nextIndex = 0;
120
- for (let oldIndex = 0; oldIndex < oldImages.length; oldIndex += 1) {
121
- const image = oldImages[oldIndex];
122
- const oldMarker = imageMarker(oldIndex, image);
123
- if (!text.includes(oldMarker))
124
- continue;
125
- text = text.replaceAll(oldMarker, imageMarker(nextIndex, image));
126
- nextIndex += 1;
113
+ let markers = editor.markers().filter(marker => marker.id.startsWith('image:'));
114
+ if (markers.length === 0) {
115
+ // Drafts saved before marker ranges were recorded retain their old labels.
116
+ let cursor = 0;
117
+ for (const [index, image] of images.entries()) {
118
+ const label = imageMarker(index, image);
119
+ const start = editor.text.indexOf(label, cursor);
120
+ if (start < 0)
121
+ continue;
122
+ cursor = start + label.length;
123
+ editor.mark(`image:${++this.#next}`, start, cursor);
124
+ }
125
+ markers = editor.markers().filter(marker => marker.id.startsWith('image:'));
126
+ }
127
+ for (const [index, marker] of markers.entries()) {
128
+ this.#next = Math.max(this.#next, Number(marker.id.slice('image:'.length)) || 0);
129
+ if (images[index] !== undefined)
130
+ this.#images.set(marker.id, { ...images[index] });
127
131
  }
128
- this.#images = retained;
129
132
  this.#revision++;
130
- editor.replaceText(text, Math.min(editor.cursor, text.length));
131
133
  }
132
- /** Prepend a queued submission's drafts, rebasing the current markers. */
134
+ /** Prepend restored input with an exact edit so repeated paste labels keep their payloads. */
133
135
  restore(submission, fileMarkers) {
134
- this.#revision++;
135
136
  const editor = this.#deps.editor;
136
- const currentImages = this.#images;
137
- let rebasedCurrent = editor.text;
138
- for (let index = currentImages.length - 1; index >= 0; index -= 1) {
139
- const image = currentImages[index];
140
- rebasedCurrent = rebasedCurrent.replaceAll(imageMarker(index, image), imageMarker(index + submission.images.length, image));
137
+ const draft = submissionDraft(submission, fileMarkers);
138
+ const separator = draft.text !== '' && editor.text !== '' ? '\n' : '';
139
+ editor.splice(0, 0, draft.text + separator, draft.text.length + separator.length + editor.text.length);
140
+ const last = draft.ranges.at(-1);
141
+ if (submission.text === '' && last?.kind === 'image' && last.end === draft.text.length)
142
+ last.end += separator.length;
143
+ for (const range of draft.ranges) {
144
+ if (range.kind !== 'image')
145
+ continue;
146
+ const id = `image:${++this.#next}`;
147
+ editor.mark(id, range.start, range.end);
148
+ this.#images.set(id, { ...submission.images[range.index] });
141
149
  }
142
- const restored = submissionDraftText(submission, fileMarkers);
143
- const separator = restored !== '' && rebasedCurrent !== '' ? '\n' : '';
144
- this.#images = [...submission.images.map(image => ({ ...image })), ...currentImages];
145
- editor.replaceText(restored + separator + rebasedCurrent);
150
+ this.#revision++;
151
+ this.reconcile();
146
152
  }
147
- /** Replace drafts and text with a queued/edited submission. */
148
153
  replace(submission, fileMarkers) {
149
- this.#revision++;
150
- this.#images = submission.images.map(image => ({ ...image }));
151
- this.#deps.editor.setText(submissionDraftText(submission, fileMarkers));
154
+ const draft = submissionDraft(submission, fileMarkers);
155
+ this.#deps.editor.setText(draft.text);
156
+ for (const range of draft.ranges) {
157
+ if (range.kind === 'image')
158
+ this.#deps.editor.mark(`image:${++this.#next}`, range.start, range.end);
159
+ }
160
+ this.restoreSnapshot(submission.images);
152
161
  }
153
162
  clear() {
154
163
  this.#revision++;
155
- this.#images = [];
164
+ this.#images.clear();
156
165
  }
157
166
  }
158
167
  //# sourceMappingURL=composer-images.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"composer-images.js","sourceRoot":"","sources":["../../src/runtime/composer-images.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"composer-images.js","sourceRoot":"","sources":["../../src/runtime/composer-images.ts"],"names":[],"mappings":"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"}
@@ -87,7 +87,7 @@ import { edgeIndex, listNavigation, navigateIndex } from "../views/list-navigati
87
87
  import { refreshProjectContext, resolveGitStatusFiles, resolveProjectContext } from "../session/project-context.js";
88
88
  import { modelRouteReady } from "../session/model-readiness.js";
89
89
  import { formatEssentialHotkeysText, formatHotkeysText, keysForAction } from "../views/hotkeys.js";
90
- import { imagePathCandidates, splitComposerAttachmentMarkers, stripComposerImageMarkers, readImageFile, readImageFromClipboard, readMacClipboardFiles, } from "../input/image-paste.js";
90
+ import { imagePathCandidates, splitComposerAttachmentMarkers, readImageFile, readImageFromClipboard, readMacClipboardFiles, } from "../input/image-paste.js";
91
91
  import { APP_DISPLAY_NAME, APP_NAME, APP_VERSION } from "../session/package-metadata.js";
92
92
  const DOUBLE_CTRL_C_MS = 500;
93
93
  /**
@@ -316,6 +316,7 @@ export class LocalTui {
316
316
  #paste = false;
317
317
  #pasteBuf = '';
318
318
  #pasteInFlight = 0;
319
+ #pasteAbort;
319
320
  #deferredPasteEvents = [];
320
321
  #images = new ComposerImages({
321
322
  editor: this.#editor,
@@ -889,6 +890,7 @@ export class LocalTui {
889
890
  this.#render();
890
891
  }
891
892
  async reviewFiles(request) {
893
+ this.#cancelPaste();
892
894
  if (!this.#tty || this.#disposed || request.signal.aborted)
893
895
  return;
894
896
  this.#workspacePress = undefined;
@@ -1244,6 +1246,7 @@ export class LocalTui {
1244
1246
  this.notice(error instanceof Error ? error.message : String(error), { level: 'error' });
1245
1247
  }
1246
1248
  }
1249
+ this.#cancelPaste();
1247
1250
  this.#flushPasteBurst(true);
1248
1251
  this.#pasteBurst.clear();
1249
1252
  if (request.notify !== false) {
@@ -1607,8 +1610,8 @@ export class LocalTui {
1607
1610
  this.#reportDraftError('restore', error);
1608
1611
  }
1609
1612
  if (existing !== undefined) {
1610
- images.replace({ text: existing.editor.text, images: existing.images });
1611
1613
  editor.restore(existing.editor);
1614
+ images.restoreSnapshot(existing.images);
1612
1615
  files.restoreSnapshot(existing.files);
1613
1616
  }
1614
1617
  images.restore(submission, files.adopt(submission.files ?? []));
@@ -1842,6 +1845,7 @@ export class LocalTui {
1842
1845
  this.#flushDraft();
1843
1846
  this.#pendingInputs.flush(this.#pendingScope());
1844
1847
  this.#disposed = true;
1848
+ this.#cancelPaste();
1845
1849
  this.#imagePreviewAbort?.abort();
1846
1850
  this.#fileReviewAbort?.abort();
1847
1851
  this.#closeFileBrowser();
@@ -2049,11 +2053,11 @@ export class LocalTui {
2049
2053
  if (this.#historyIndex > 0 && this.#historyDraft !== undefined)
2050
2054
  return this.#historyDraft;
2051
2055
  const editor = this.#prompt?.savedInput ?? this.#editor.snapshot();
2052
- return { editor, images: this.#images.copies(), files: this.#files.snapshot(editor.text), literal: this.#literalInput };
2056
+ return { editor, images: this.#images.copies(editor), files: this.#files.snapshot(editor.text), literal: this.#literalInput };
2053
2057
  }
2054
2058
  #restoreDraft(draft) {
2055
- this.#images.replace({ text: draft.editor.text, images: draft.images });
2056
2059
  this.#editor.restore(draft.editor);
2060
+ this.#images.restoreSnapshot(draft.images);
2057
2061
  this.#files.restoreSnapshot(draft.files);
2058
2062
  this.#literalInput = draft.literal;
2059
2063
  }
@@ -2117,6 +2121,7 @@ export class LocalTui {
2117
2121
  #switchDraft(id) {
2118
2122
  if (id === this.#draftScope)
2119
2123
  return;
2124
+ this.#cancelPaste();
2120
2125
  this.#goalAbort?.abort();
2121
2126
  this.#goalAbort = undefined;
2122
2127
  this.#queueHotkeyAbort?.abort();
@@ -2509,14 +2514,26 @@ export class LocalTui {
2509
2514
  for (const event of this.#pasteBurst.flush(Date.now(), force))
2510
2515
  this.#deliverPasteEmission(event);
2511
2516
  }
2517
+ #cancelPaste() {
2518
+ this.#pasteAbort?.abort();
2519
+ this.#pasteAbort = undefined;
2520
+ this.#pasteInFlight = 0;
2521
+ this.#deferredPasteEvents = [];
2522
+ this.#paste = false;
2523
+ this.#pasteBuf = '';
2524
+ }
2512
2525
  #startAsyncPaste(operation) {
2513
- this.#pasteInFlight += 1;
2514
- void operation.catch((error) => {
2515
- this.notice('Paste failed: ' + (error instanceof Error ? error.message : String(error)), { level: 'error' });
2526
+ const abort = new AbortController();
2527
+ this.#pasteAbort = abort;
2528
+ this.#pasteInFlight = 1;
2529
+ void operation(abort.signal).catch((error) => {
2530
+ if (!abort.signal.aborted)
2531
+ this.notice('Paste failed: ' + (error instanceof Error ? error.message : String(error)), { level: 'error' });
2516
2532
  }).finally(() => {
2517
- this.#pasteInFlight = Math.max(0, this.#pasteInFlight - 1);
2518
- if (this.#pasteInFlight !== 0 || this.#deferredPasteEvents.length === 0)
2533
+ if (this.#pasteAbort !== abort)
2519
2534
  return;
2535
+ this.#pasteAbort = undefined;
2536
+ this.#pasteInFlight = 0;
2520
2537
  const deferred = this.#deferredPasteEvents.splice(0);
2521
2538
  for (let index = 0; index < deferred.length; index += 1) {
2522
2539
  this.#dispatch(deferred[index]);
@@ -2527,7 +2544,10 @@ export class LocalTui {
2527
2544
  }
2528
2545
  });
2529
2546
  }
2530
- async #acceptPastedText(text) {
2547
+ async #acceptPastedText(text, signal) {
2548
+ if (signal.aborted)
2549
+ return;
2550
+ text = text.replaceAll('\r\n', '\n').replaceAll('\r', '\n');
2531
2551
  if (this.#fileBrowser !== null) {
2532
2552
  this.#applyFileBrowserInput({ type: 'text', value: text });
2533
2553
  return;
@@ -2555,42 +2575,50 @@ export class LocalTui {
2555
2575
  }
2556
2576
  const paths = imagePathCandidates(text);
2557
2577
  if (paths.length > 0) {
2558
- const images = await Promise.all(paths.map(path => this.#readImagePath(path)));
2559
- if (images.every((image) => image !== null)) {
2560
- let inserted = false;
2561
- for (const image of images) {
2562
- if (await this.#images.admit(image)) {
2563
- this.#images.insert(image);
2564
- inserted = true;
2565
- }
2578
+ const images = await Promise.all(paths.map(async (path) => {
2579
+ try {
2580
+ return await this.#readImagePath(path);
2566
2581
  }
2567
- if (inserted) {
2568
- this.#refreshAutocomplete();
2569
- this.#render();
2582
+ catch {
2583
+ return null;
2570
2584
  }
2585
+ }));
2586
+ if (signal.aborted)
2571
2587
  return;
2588
+ let inserted = false;
2589
+ for (const [index, image] of images.entries()) {
2590
+ if (image === null) {
2591
+ this.notice(uiText(resolveUiLanguage(this.#uiLanguage), 'Could not read image {path}', { path: paths[index] }), { level: 'error' });
2592
+ continue;
2593
+ }
2594
+ const admitted = await this.#images.admit(image, signal);
2595
+ if (signal.aborted)
2596
+ return;
2597
+ if (admitted) {
2598
+ this.#images.insert(image);
2599
+ inserted = true;
2600
+ }
2572
2601
  }
2573
- // Screenshot tools sometimes paste a transient cache path and remove
2574
- // it before Node reads it. OMP falls back to the still-live clipboard
2575
- // image instead of leaking that stale path into the prompt.
2576
- const clipboardImage = await this.#readClipboardImage();
2577
- if (clipboardImage !== null) {
2578
- this.#images.insert(clipboardImage);
2602
+ if (inserted)
2579
2603
  this.#refreshAutocomplete();
2580
- this.#render();
2581
- return;
2582
- }
2604
+ this.#render();
2605
+ return;
2583
2606
  }
2584
2607
  this.#literalInput ||= this.#editor.cursor === 0 && text.startsWith('!');
2585
2608
  this.#editor.paste(text);
2586
2609
  this.#refreshAutocomplete();
2587
2610
  this.#render();
2588
2611
  }
2589
- async #pasteClipboard() {
2590
- if (this.#search === null && this.#settings === null) {
2612
+ async #pasteClipboard(signal) {
2613
+ if (this.#prompt === null && this.#search === null && this.#settings === null) {
2591
2614
  const image = await this.#readClipboardImage();
2615
+ if (signal.aborted)
2616
+ return;
2592
2617
  if (image !== null) {
2593
- if (await this.#images.admit(image)) {
2618
+ const admitted = await this.#images.admit(image, signal);
2619
+ if (signal.aborted)
2620
+ return;
2621
+ if (admitted) {
2594
2622
  this.#images.insert(image);
2595
2623
  this.#refreshAutocomplete();
2596
2624
  this.#render();
@@ -2598,29 +2626,17 @@ export class LocalTui {
2598
2626
  return;
2599
2627
  }
2600
2628
  const files = await this.#readClipboardFiles();
2629
+ if (signal.aborted)
2630
+ return;
2601
2631
  const imagePaths = files.filter(path => imagePathCandidates(path).length === 1);
2602
2632
  if (imagePaths.length > 0) {
2603
- const images = (await Promise.all(imagePaths.map(path => this.#readImagePath(path))))
2604
- .filter((candidate) => candidate !== null);
2605
- if (images.length > 0) {
2606
- let inserted = false;
2607
- for (const candidate of images) {
2608
- if (await this.#images.admit(candidate)) {
2609
- this.#images.insert(candidate);
2610
- inserted = true;
2611
- }
2612
- }
2613
- if (inserted) {
2614
- this.#refreshAutocomplete();
2615
- this.#render();
2616
- }
2617
- return;
2618
- }
2633
+ await this.#acceptPastedText(imagePaths.join('\n'), signal);
2634
+ return;
2619
2635
  }
2620
2636
  }
2621
2637
  const text = await this.#readClipboard();
2622
- if (text !== '')
2623
- await this.#acceptPastedText(text);
2638
+ if (text !== '' && !signal.aborted)
2639
+ await this.#acceptPastedText(text, signal);
2624
2640
  }
2625
2641
  /** Resolve one bounded tmux probe without overwriting an explicit preference. */
2626
2642
  setTmuxMouseEnabled(enabled) {
@@ -3208,6 +3224,8 @@ export class LocalTui {
3208
3224
  // Clipboard image inspection is asynchronous. Preserve the exact key
3209
3225
  // order so a fast Ctrl+V, Enter submits the finished image draft rather
3210
3226
  // than an empty prompt.
3227
+ if (this.#pasteInFlight > 0 && event.type === 'key' && (event.id === 'ctrl+c' || event.id === 'escape'))
3228
+ this.#cancelPaste();
3211
3229
  if (this.#pasteInFlight > 0) {
3212
3230
  this.#deferredPasteEvents.push(event);
3213
3231
  return;
@@ -3218,7 +3236,7 @@ export class LocalTui {
3218
3236
  const text = this.#pasteBuf.replaceAll('\r\n', '\n').replaceAll('\r', '\n');
3219
3237
  this.#pasteBuf = '';
3220
3238
  if (text !== '')
3221
- this.#startAsyncPaste(this.#acceptPastedText(text));
3239
+ this.#startAsyncPaste(signal => this.#acceptPastedText(text, signal));
3222
3240
  return;
3223
3241
  }
3224
3242
  if (event.type === 'text')
@@ -4606,11 +4624,20 @@ export class LocalTui {
4606
4624
  #buildSubmission(text) {
4607
4625
  const images = this.#images.copies();
4608
4626
  const files = this.#files.copies();
4609
- const typedText = this.#files.text(images.length > 0 ? text.trim() : text);
4627
+ const markers = this.#files.snapshot(this.#editor.text).entries.map(item => item.marker);
4628
+ const ranges = this.#images.ranges();
4629
+ for (const [index, marker] of markers.entries()) {
4630
+ const start = text.indexOf(marker);
4631
+ if (start < 0)
4632
+ continue;
4633
+ const end = start + marker.length;
4634
+ ranges.push({ kind: 'file', index, start: start > 0 && text[start - 1] === ' ' ? start - 1 : start,
4635
+ end: text[end] === ' ' ? end + 1 : end });
4636
+ }
4610
4637
  const segments = images.length === 0 && files.length === 0 ? []
4611
- : splitComposerAttachmentMarkers(text.trim(), images, this.#files.snapshot(text).entries.map(item => item.marker));
4612
- // Stripping a marker that sat on its own line leaves that line break behind.
4613
- const submittedText = images.length > 0 ? stripComposerImageMarkers(typedText, images).trim() : typedText;
4638
+ : splitComposerAttachmentMarkers(text, images, markers, images.length > 0 && text === this.#editor.expandedText ? ranges : undefined);
4639
+ const submittedText = images.length === 0 ? this.#files.text(text)
4640
+ : segments.flatMap(segment => segment.kind === 'text' ? [segment.text] : []).join('');
4614
4641
  const literal = text === this.#editor.expandedText && (this.#literalInput || this.#editor.startsWithPaste);
4615
4642
  const historyText = submittedText.replace(/[ \t]{2,}/gu, ' ').trim();
4616
4643
  return {
@@ -4829,6 +4856,7 @@ export class LocalTui {
4829
4856
  const pending = this.#prompt;
4830
4857
  if (pending === null)
4831
4858
  return;
4859
+ this.#cancelPaste();
4832
4860
  const draft = structuredPromptAnswer(pending, this.#editor.expandedText);
4833
4861
  try {
4834
4862
  pending.request.onAnswerDraft?.(draft);
@@ -4979,6 +5007,7 @@ export class LocalTui {
4979
5007
  }
4980
5008
  this.#search = null;
4981
5009
  this.#ac = null;
5010
+ this.#cancelPaste();
4982
5011
  this.#settings = createSettings(this.prefs(), undefined, this.#agentBehavior, this.#features, this.#pluginSettings?.entries(), discoverEditors(this.#externalEditor), this.#settingsActions());
4983
5012
  this.#refreshSettingsCatalog();
4984
5013
  this.#render();
@@ -5391,7 +5420,7 @@ export class LocalTui {
5391
5420
  return true;
5392
5421
  }
5393
5422
  if (action === 'paste-clipboard') {
5394
- this.#startAsyncPaste(this.#pasteClipboard());
5423
+ this.#startAsyncPaste(signal => this.#pasteClipboard(signal));
5395
5424
  return true;
5396
5425
  }
5397
5426
  // Explicit queue delivery is independent of the busy-Enter preference.
@@ -5407,7 +5436,8 @@ export class LocalTui {
5407
5436
  this.#handTerminal(() => {
5408
5437
  this.#literalInput ||= this.#editor.startsWithPaste;
5409
5438
  const text = editExternally(this.#editor.expandedText, this.#externalEditor);
5410
- this.#editor.setText(text);
5439
+ while (this.#editor.expandPaste()) { /* Keep marker positions aligned with expanded source. */ }
5440
+ this.#editor.replaceText(text);
5411
5441
  this.#images.reconcile();
5412
5442
  });
5413
5443
  }