@chatbridge/vscode 0.8.0 → 0.8.2

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.
@@ -1,4 +1,6 @@
1
1
  import { ChatBridgeError, MAX_FILE_BYTES, MAX_TOTAL_BYTES, closeOrKill, formatAttachment, formatSize, } from "@chatbridge/core";
2
+ /** The separator pushed into the history by `reopen()`. */
3
+ export const REOPENED_SEPARATOR = "reopened";
2
4
  export const CLOSE_TIMEOUT_MS = 5_000;
3
5
  const EMPTY = {
4
6
  ok: false,
@@ -16,6 +18,15 @@ export class SessionController {
16
18
  session;
17
19
  /** The full prompt of the last send, for retryLast(). */
18
20
  lastPrompt;
21
+ /** Turns sent while the controller was not ready, oldest first. */
22
+ queue = [];
23
+ /** Bumped by every reopen; a send from an older generation is stale. */
24
+ generation = 0;
25
+ /** The reopen in flight, so a second Ctrl+R joins it instead of racing. */
26
+ reopening;
27
+ /** The openSession in flight (first send or reopen), so close() can wait
28
+ * for it instead of orphaning the browser it is about to produce. */
29
+ opening;
19
30
  closeTimeoutMs;
20
31
  constructor(opts) {
21
32
  this.opts = opts;
@@ -32,6 +43,10 @@ export class SessionController {
32
43
  path,
33
44
  bytes,
34
45
  })),
46
+ queue: this.queue.map((q) => ({
47
+ text: q.text,
48
+ attachments: q.attachments.map(({ path, bytes }) => ({ path, bytes })),
49
+ })),
35
50
  };
36
51
  if (this.lastError !== undefined)
37
52
  state.lastError = this.lastError;
@@ -41,6 +56,10 @@ export class SessionController {
41
56
  this.status = status;
42
57
  this.emit();
43
58
  }
59
+ /** A `/help` listing, as a history entry. */
60
+ pushHelp(text) {
61
+ this.push({ role: "help", text });
62
+ }
44
63
  push(message) {
45
64
  this.messages.push(message);
46
65
  this.emit();
@@ -72,37 +91,103 @@ export class SessionController {
72
91
  this.pending.splice(index, 1);
73
92
  this.emit();
74
93
  }
75
- /** Sends the text plus the pending attachments as one turn. Never
76
- * throws: the outcome is the result and the history. */
94
+ /** True when a turn can start right now. `dead` counts: an explicit
95
+ * send is the user retrying, and only automatic draining must stop
96
+ * while the controller is dead. */
97
+ get canStartTurn() {
98
+ return (this.status === "idle" ||
99
+ this.status === "closed" ||
100
+ this.status === "dead");
101
+ }
102
+ /** Sends the text plus the pending attachments as one turn, or queues it
103
+ * when a turn is already running. Never throws: the outcome is the
104
+ * result and the history. */
77
105
  async send(text) {
78
- if (this.status === "busy" || this.status === "opening") {
79
- return {
80
- ok: false,
81
- code: "INVALID_STATE",
82
- message: "A send is already in progress.",
83
- };
84
- }
85
106
  const body = text.trim() === "" ? "" : text;
86
107
  if (body === "" && this.pending.length === 0)
87
108
  return EMPTY;
88
- const sections = this.pending.map((a) => formatAttachment(a.path, a.content));
89
- const prompt = [body, ...sections].filter((s) => s !== "").join("\n\n");
90
- const attachments = this.pending.map(({ path, bytes }) => ({
91
- path,
92
- bytes,
93
- }));
109
+ const attachments = this.pending;
94
110
  this.pending = [];
95
- this.push({ role: "user", text: body, attachments });
111
+ // A non-empty queue means the controller was not ready when the last
112
+ // entry arrived and has not become ready since — every ready transition
113
+ // drains first; `closed` after deactivate can still hold a queue, which
114
+ // the next send queues behind. Either way, keep FIFO by queueing.
115
+ if (!this.canStartTurn || this.queue.length > 0) {
116
+ this.queue.push({ text: body, attachments });
117
+ this.drain();
118
+ this.emit();
119
+ return { ok: true, queued: true };
120
+ }
121
+ return this.startTurn({ text: body, attachments });
122
+ }
123
+ /** Pushes the user entry and runs the turn. Shared by send and drain. */
124
+ startTurn(turn) {
125
+ const sections = turn.attachments.map((a) => formatAttachment(a.path, a.content));
126
+ const prompt = [turn.text, ...sections]
127
+ .filter((s) => s !== "")
128
+ .join("\n\n");
129
+ this.push({
130
+ role: "user",
131
+ text: turn.text,
132
+ attachments: turn.attachments.map(({ path, bytes }) => ({ path, bytes })),
133
+ });
96
134
  this.lastPrompt = prompt;
97
135
  return this.runTurn(prompt);
98
136
  }
137
+ /** Starts the oldest queued entry, if any, when the controller is ready
138
+ * for a turn. Called at every transition back to a ready state, before
139
+ * the caller emits, so the webview never sees an idle frame with a
140
+ * queue still waiting. */
141
+ drain() {
142
+ // markLoggedIn() can arrive in any status; never start a second turn
143
+ // on top of a running one. The other call sites are already ready.
144
+ if (!this.canStartTurn)
145
+ return;
146
+ const next = this.queue.shift();
147
+ if (next === undefined)
148
+ return;
149
+ void this.startTurn(next);
150
+ }
151
+ /** Empties the queue back into the composer: the entries are returned
152
+ * and their attachments become pending again. */
153
+ takeBack() {
154
+ if (this.queue.length === 0)
155
+ return { entries: [], droppedAttachments: 0 };
156
+ const entries = this.queue.splice(0);
157
+ let total = this.pending.reduce((n, p) => n + p.bytes, 0);
158
+ let dropped = 0;
159
+ for (const e of entries) {
160
+ for (const a of e.attachments) {
161
+ if (total + a.bytes > MAX_TOTAL_BYTES) {
162
+ dropped++;
163
+ continue;
164
+ }
165
+ total += a.bytes;
166
+ this.pending.push(a);
167
+ }
168
+ }
169
+ this.emit();
170
+ return {
171
+ entries: entries.map((e) => ({
172
+ text: e.text,
173
+ attachments: e.attachments.map(({ path, bytes }) => ({ path, bytes })),
174
+ })),
175
+ droppedAttachments: dropped,
176
+ };
177
+ }
178
+ removeQueued(index) {
179
+ if (index < 0 || index >= this.queue.length)
180
+ return;
181
+ this.queue.splice(index, 1);
182
+ this.emit();
183
+ }
99
184
  /** Re-runs the last prompt after a recoverable fatal error (a missing
100
185
  * browser that was just installed). Drops the trailing error entry so
101
186
  * the history reads user → assistant. */
102
187
  async retryLast() {
103
188
  if (this.lastPrompt === undefined)
104
189
  return EMPTY;
105
- if (this.status === "busy" || this.status === "opening") {
190
+ if (!this.canStartTurn) {
106
191
  return {
107
192
  ok: false,
108
193
  code: "INVALID_STATE",
@@ -122,22 +207,39 @@ export class SessionController {
122
207
  // `lastError` would keep the webview's error banner up after a
123
208
  // successful send from `dead`.
124
209
  this.lastError = undefined;
210
+ const generation = this.generation;
125
211
  try {
126
212
  if (this.session === undefined) {
127
213
  this.setStatus("opening");
128
- this.session = await this.opts.openSession();
214
+ const session = await this.trackOpen(this.opts.openSession());
215
+ // A reopen ran while we were opening: this browser is an orphan.
216
+ // dropSession may have closed it already; close/kill are idempotent.
217
+ if (generation !== this.generation) {
218
+ await closeOrKill(session, this.closeTimeoutMs);
219
+ return { ok: true };
220
+ }
221
+ this.session = session;
129
222
  }
130
223
  this.setStatus("busy");
131
224
  const reply = await this.session.send(prompt);
225
+ if (generation !== this.generation)
226
+ return { ok: true }; // stale
132
227
  this.messages.push({ role: "assistant", text: reply });
133
- this.setStatus("idle");
228
+ // Claim the next turn before emitting, so no idle frame is shown.
229
+ this.status = "idle";
230
+ this.drain();
231
+ this.emit();
134
232
  return { ok: true };
135
233
  }
136
234
  catch (err) {
235
+ if (generation !== this.generation)
236
+ return { ok: true }; // stale
137
237
  return this.fail(err);
138
238
  }
139
239
  }
140
- async fail(err) {
240
+ /** Pushes the error entry (message plus the extension's remedy, if any)
241
+ * and reports the code back to the caller. */
242
+ pushError(err) {
141
243
  const code = err instanceof ChatBridgeError ? err.code : "UNKNOWN";
142
244
  const message = err instanceof Error ? err.message : String(err);
143
245
  const hint = this.opts.hints?.[code];
@@ -145,10 +247,16 @@ export class SessionController {
145
247
  role: "error",
146
248
  text: hint === undefined ? message : `${message}\n${hint}`,
147
249
  });
250
+ return { code, message };
251
+ }
252
+ async fail(err) {
253
+ const { code, message } = this.pushError(err);
148
254
  // Show the error before `dropSession` (up to `closeTimeoutMs`) runs.
149
255
  this.emit();
150
256
  if (code === "RESPONSE_TIMEOUT") {
151
- this.setStatus("idle");
257
+ this.status = "idle";
258
+ this.drain();
259
+ this.emit();
152
260
  }
153
261
  else {
154
262
  this.lastError = code;
@@ -157,27 +265,89 @@ export class SessionController {
157
265
  }
158
266
  return { ok: false, code, message };
159
267
  }
268
+ /** Tracks an openSession so close()/dropSession can wait for it. */
269
+ async trackOpen(p) {
270
+ this.opening = p;
271
+ try {
272
+ return await p;
273
+ }
274
+ finally {
275
+ if (this.opening === p)
276
+ this.opening = undefined;
277
+ }
278
+ }
160
279
  async dropSession() {
280
+ // An open still in flight would assign `this.session` after we return,
281
+ // so wait for it and close the browser it produced here: the caller
282
+ // (close(), discard()) must not return with one still running.
283
+ const opened = await this.opening?.catch(() => undefined);
161
284
  const old = this.session;
162
285
  this.session = undefined;
163
286
  if (old !== undefined)
164
287
  await closeOrKill(old, this.closeTimeoutMs);
288
+ // The opener's own generation check will close this one too; close()
289
+ // and kill() are idempotent, so closing it twice is harmless.
290
+ else if (opened !== undefined) {
291
+ await closeOrKill(opened, this.closeTimeoutMs);
292
+ }
165
293
  }
166
- /** Ctrl+R of the TUI: drop the browser, mark the break, reopen lazily. */
294
+ /** Ctrl+R of the TUI: drop the browser, mark the break, reopen lazily.
295
+ * False when a turn is in flight, so the caller can warn. */
167
296
  async newChat() {
168
- await this.discard("New chat");
297
+ return this.discard("New chat");
298
+ }
299
+ /** Replaces the browser in every state. The in-flight turn, if any, is
300
+ * abandoned: its result is dropped by the generation check. A second
301
+ * call while one is running joins the first. */
302
+ reopen() {
303
+ if (this.reopening)
304
+ return this.reopening;
305
+ const run = this.runReopen().finally(() => {
306
+ this.reopening = undefined;
307
+ });
308
+ this.reopening = run;
309
+ return run;
310
+ }
311
+ async runReopen() {
312
+ const generation = ++this.generation;
313
+ this.setStatus("reopening");
314
+ await this.dropSession();
315
+ try {
316
+ const session = await this.trackOpen(this.opts.openSession());
317
+ // `close()` (deactivate) ran while the browser was opening: this one
318
+ // is an orphan, and the controller is closed. dropSession may have
319
+ // closed it already; close/kill are idempotent.
320
+ if (generation !== this.generation) {
321
+ await closeOrKill(session, this.closeTimeoutMs);
322
+ return;
323
+ }
324
+ this.session = session;
325
+ this.lastError = undefined;
326
+ this.messages.push({ role: "separator", text: REOPENED_SEPARATOR });
327
+ this.status = "idle";
328
+ this.drain();
329
+ this.emit();
330
+ }
331
+ catch (err) {
332
+ const { code } = this.pushError(err);
333
+ this.lastError = code;
334
+ this.setStatus("dead");
335
+ }
169
336
  }
170
337
  /** Closes the session (if any) and pushes `separator`. Refused (returns
171
338
  * false) while a turn is in flight. Clears a dead state. */
172
339
  async discard(separator) {
173
- if (this.status === "busy" || this.status === "opening")
340
+ if (!this.canStartTurn)
174
341
  return false;
175
342
  await this.dropSession();
176
343
  this.lastError = undefined;
177
344
  // A fresh chat must not re-send a prompt from before the break.
178
345
  this.lastPrompt = undefined;
179
346
  this.status = "closed";
180
- this.push({ role: "separator", text: separator });
347
+ this.messages.push({ role: "separator", text: separator });
348
+ // The queue outlives the break: drain it into the new chat.
349
+ this.drain();
350
+ this.emit();
181
351
  return true;
182
352
  }
183
353
  /** After a successful login command: a dead controller may try again. */
@@ -186,10 +356,15 @@ export class SessionController {
186
356
  this.lastError = undefined;
187
357
  this.status = "closed";
188
358
  }
189
- this.push({ role: "separator", text: "Logged in" });
359
+ this.messages.push({ role: "separator", text: "Logged in" });
360
+ this.drain();
361
+ this.emit();
190
362
  }
191
363
  /** deactivate: close the browser, keep the history. Idempotent. */
192
364
  async close() {
365
+ // A reopen in flight is stale from here on: its new browser must be
366
+ // closed rather than adopted by a controller the user has shut down.
367
+ this.generation++;
193
368
  await this.dropSession();
194
369
  if (this.status !== "dead")
195
370
  this.status = "closed";
@@ -0,0 +1,7 @@
1
+ /** Mirrors the CLI's parseTimeoutMs: finite and > 0, else the default.
2
+ * `invalid` is true when a value was set but unusable, so the caller can
3
+ * warn once. */
4
+ export declare function parseTimeoutSec(raw: unknown, fallbackMs: number): {
5
+ timeoutMs: number;
6
+ invalid: boolean;
7
+ };
@@ -0,0 +1,12 @@
1
+ /** Mirrors the CLI's parseTimeoutMs: finite and > 0, else the default.
2
+ * `invalid` is true when a value was set but unusable, so the caller can
3
+ * warn once. */
4
+ export function parseTimeoutSec(raw, fallbackMs) {
5
+ if (raw === undefined)
6
+ return { timeoutMs: fallbackMs, invalid: false };
7
+ const seconds = typeof raw === "number" ? raw : Number(raw);
8
+ if (!Number.isFinite(seconds) || seconds <= 0) {
9
+ return { timeoutMs: fallbackMs, invalid: true };
10
+ }
11
+ return { timeoutMs: seconds * 1000, invalid: false };
12
+ }
@@ -20,6 +20,11 @@ export interface VscodeUi {
20
20
  showInformationMessage(message: string): void;
21
21
  withProgress<T>(title: string, cancellable: boolean, task: (progress: ProgressReporter, signal: AbortSignal) => Promise<T>): Promise<T>;
22
22
  activeEditor(): EditorSnapshot | undefined;
23
+ /** True when the value is a real vscode.Uri: a command argument from an
24
+ * unexpected caller is not. */
25
+ isUri(value: unknown): boolean;
26
+ /** Parses a URI string; throws when it is not a valid URI. */
27
+ parseUri(uri: string): unknown;
23
28
  /** Opens the document behind an explorer Uri (or any Uri) read-only. */
24
29
  openDocument(uri: unknown): Promise<{
25
30
  path: string;
package/dist/vscode-ui.js CHANGED
@@ -29,6 +29,8 @@ export function createVscodeUi(api, id) {
29
29
  }
30
30
  return snap;
31
31
  },
32
+ isUri: (v) => v instanceof api.Uri,
33
+ parseUri: (uri) => api.Uri.parse(uri, true),
32
34
  openDocument: async (uri) => {
33
35
  const doc = await api.workspace.openTextDocument(uri);
34
36
  return { path: relPath(doc.uri), text: doc.getText() };
@@ -1,5 +1,25 @@
1
1
  "use strict";
2
2
  (() => {
3
+ // ../core/dist/slash-commands.js
4
+ var SLASH_COMMANDS = [
5
+ { name: "login", description: "Log in in a browser window" },
6
+ { name: "logout", description: "Delete the saved login and close the chat" },
7
+ { name: "new", description: "Start a new chat" },
8
+ { name: "reopen", description: "Reopen the browser (also Ctrl+R)" },
9
+ { name: "help", description: "List these commands" }
10
+ ];
11
+ var NAMES = new Set(SLASH_COMMANDS.map((c) => c.name));
12
+ var PATTERN = /^\/([a-z]+)$/;
13
+ function parseSlashCommand(text) {
14
+ const word = PATTERN.exec(text.trim())?.[1];
15
+ if (word === void 0)
16
+ return void 0;
17
+ return NAMES.has(word) ? { command: word } : { unknown: word };
18
+ }
19
+ function unknownCommandMessage(word) {
20
+ return `Unknown command: /${word}. Type /help.`;
21
+ }
22
+
3
23
  // src/webview/main.ts
4
24
  var vscode = acquireVsCodeApi();
5
25
  var history = document.getElementById("history");
@@ -12,7 +32,17 @@
12
32
  var welcomeText = document.getElementById("welcome-text");
13
33
  var banner = document.getElementById("banner");
14
34
  var footer = document.getElementById("footer");
35
+ var queue = document.getElementById("queue");
36
+ var inlineError = document.getElementById("inline-error");
37
+ function fitComposer() {
38
+ const atBottom = history.scrollHeight - history.scrollTop - history.clientHeight < 2;
39
+ input.style.height = "auto";
40
+ input.style.height = `${input.scrollHeight + input.offsetHeight - input.clientHeight}px`;
41
+ if (atBottom) history.scrollTop = history.scrollHeight;
42
+ }
15
43
  var config = {};
44
+ var lastState;
45
+ var lastProgress;
16
46
  function applyConfig(c) {
17
47
  config = c;
18
48
  if (c.sendButton?.background) {
@@ -43,8 +73,8 @@
43
73
  if (text !== void 0) e.textContent = text;
44
74
  return e;
45
75
  }
46
- function button(label, onClick) {
47
- const b = el("button", "action", label);
76
+ function button(label, onClick, className = "action") {
77
+ const b = el("button", className, label);
48
78
  b.setAttribute("type", "button");
49
79
  b.addEventListener("click", onClick);
50
80
  return b;
@@ -60,6 +90,10 @@
60
90
  box.textContent = `\u2014 ${m.text} \u2014`;
61
91
  return box;
62
92
  }
93
+ if (m.role === "help") {
94
+ box.textContent = m.text;
95
+ return box;
96
+ }
63
97
  box.appendChild(el("div", "text", m.text));
64
98
  for (const a of m.attachments ?? []) {
65
99
  box.appendChild(
@@ -68,16 +102,22 @@
68
102
  }
69
103
  return box;
70
104
  }
105
+ function waitingText(status2) {
106
+ if (status2 === "reopening") return "Reopening browser...";
107
+ if (status2 === "opening") return "Opening browser...";
108
+ return "Waiting...";
109
+ }
71
110
  function renderStatus(s) {
72
111
  status.replaceChildren();
73
112
  status.hidden = false;
74
- if (s.status === "busy" || s.status === "opening") {
113
+ if (s.status === "busy" || s.status === "opening" || s.status === "reopening") {
75
114
  status.appendChild(el("span", "spinner"));
115
+ const queued = s.queue.length > 0 ? ` \xB7 ${s.queue.length} queued` : "";
76
116
  status.appendChild(
77
117
  el(
78
118
  "span",
79
119
  "progress-text",
80
- s.status === "opening" ? "Opening browser..." : "Waiting..."
120
+ `${lastProgress ?? waitingText(s.status)}${queued}`
81
121
  )
82
122
  );
83
123
  return;
@@ -99,6 +139,12 @@
99
139
  )
100
140
  );
101
141
  }
142
+ status.appendChild(
143
+ button(
144
+ "Reopen",
145
+ () => vscode.postMessage({ type: "command", name: "reopen" })
146
+ )
147
+ );
102
148
  status.appendChild(
103
149
  button(
104
150
  "New chat",
@@ -109,36 +155,79 @@
109
155
  }
110
156
  status.hidden = true;
111
157
  }
158
+ function renderQueue(s) {
159
+ queue.replaceChildren();
160
+ queue.hidden = s.queue.length === 0;
161
+ s.queue.forEach((entry, index) => {
162
+ const li = document.createElement("li");
163
+ const firstLine = entry.text.split("\n")[0] ?? "";
164
+ const label = entry.attachments.length > 0 ? `${firstLine} \u{1F4CE} ${entry.attachments.length}` : firstLine;
165
+ li.appendChild(el("span", "queue-text", `\u25B9 ${label}`));
166
+ const x = button(
167
+ "\xD7",
168
+ () => vscode.postMessage({ type: "removeQueued", index }),
169
+ "chip-remove"
170
+ );
171
+ li.appendChild(x);
172
+ queue.appendChild(li);
173
+ });
174
+ }
112
175
  function renderAttachments(s) {
113
176
  attachments.replaceChildren();
114
177
  s.pendingAttachments.forEach((a, index) => {
115
178
  const chip = el("span", "chip", `\u{1F4CE} ${a.path} (${formatSize(a.bytes)})`);
116
179
  const x = button(
117
180
  "\xD7",
118
- () => vscode.postMessage({ type: "removeAttachment", index })
181
+ () => vscode.postMessage({ type: "removeAttachment", index }),
182
+ "chip-remove"
119
183
  );
120
- x.className = "chip-remove";
121
184
  chip.appendChild(x);
122
185
  attachments.appendChild(chip);
123
186
  });
124
187
  }
125
188
  function render(s) {
189
+ const active = s.status === "busy" || s.status === "opening" || s.status === "reopening";
190
+ if (!active) lastProgress = void 0;
126
191
  history.replaceChildren(...s.messages.map(renderMessage));
127
192
  history.scrollTop = history.scrollHeight;
128
193
  renderStatus(s);
194
+ renderQueue(s);
129
195
  renderAttachments(s);
130
196
  welcome.hidden = s.messages.length > 0 || !config.welcome && !config.bannerUri;
131
197
  history.hidden = !welcome.hidden;
132
- const locked = s.status === "busy" || s.status === "opening";
133
- input.disabled = locked;
134
- sendButton.disabled = locked;
135
- if (!locked) input.focus();
198
+ input.disabled = false;
199
+ sendButton.disabled = false;
200
+ sendButton.textContent = active ? "Queue" : "Send";
201
+ if (!lastState || lastState.status !== s.status) {
202
+ if (document.activeElement === null || document.activeElement === document.body) {
203
+ input.focus();
204
+ }
205
+ }
206
+ lastState = s;
207
+ }
208
+ function showInlineError(text) {
209
+ inlineError.textContent = text ?? "";
210
+ inlineError.hidden = text === void 0;
136
211
  }
137
212
  function submit() {
138
213
  const text = input.value;
139
214
  if (text.trim() === "" && attachments.childElementCount === 0) return;
215
+ const slash = parseSlashCommand(text);
216
+ if (slash && "unknown" in slash) {
217
+ showInlineError(unknownCommandMessage(slash.unknown));
218
+ return;
219
+ }
220
+ showInlineError(void 0);
221
+ if (slash) {
222
+ input.value = "";
223
+ fitComposer();
224
+ const name = slash.command === "new" ? "newChat" : slash.command;
225
+ vscode.postMessage({ type: "command", name });
226
+ return;
227
+ }
140
228
  vscode.postMessage({ type: "send", text });
141
229
  input.value = "";
230
+ fitComposer();
142
231
  }
143
232
  form.addEventListener("submit", (e) => {
144
233
  e.preventDefault();
@@ -148,10 +237,83 @@
148
237
  if (e.key === "Enter" && !e.shiftKey && !e.isComposing) {
149
238
  e.preventDefault();
150
239
  submit();
240
+ return;
241
+ }
242
+ if (e.key === "ArrowUp" && input.value === "" && (lastState?.queue.length ?? 0) > 0) {
243
+ e.preventDefault();
244
+ vscode.postMessage({ type: "takeBack" });
245
+ }
246
+ });
247
+ input.addEventListener("input", () => {
248
+ showInlineError(void 0);
249
+ fitComposer();
250
+ });
251
+ function urisFromDrop(dt) {
252
+ const list = dt?.getData("text/uri-list") ?? "";
253
+ return list.split(/\r?\n/).map((l) => l.trim()).filter((l) => l !== "" && !l.startsWith("#"));
254
+ }
255
+ function carriesFiles(dt) {
256
+ return dt?.types.includes("text/uri-list") ?? false;
257
+ }
258
+ var dragDepth = 0;
259
+ document.addEventListener("dragenter", (e) => {
260
+ if (!carriesFiles(e.dataTransfer)) return;
261
+ dragDepth++;
262
+ document.body.classList.add("drop-target");
263
+ });
264
+ document.addEventListener("dragover", (e) => {
265
+ if (carriesFiles(e.dataTransfer)) e.preventDefault();
266
+ });
267
+ document.addEventListener("dragleave", (e) => {
268
+ if (!carriesFiles(e.dataTransfer)) return;
269
+ if (--dragDepth <= 0) {
270
+ dragDepth = 0;
271
+ document.body.classList.remove("drop-target");
272
+ }
273
+ });
274
+ document.addEventListener("drop", (e) => {
275
+ if (!carriesFiles(e.dataTransfer)) return;
276
+ e.preventDefault();
277
+ dragDepth = 0;
278
+ document.body.classList.remove("drop-target");
279
+ const uris = urisFromDrop(e.dataTransfer);
280
+ if (uris.length > 0) vscode.postMessage({ type: "attachUris", uris });
281
+ });
282
+ var PASTE_TIMEOUT_MS = 500;
283
+ var pasteSeq = 0;
284
+ var pendingPastes = /* @__PURE__ */ new Map();
285
+ function insertAtCaret(text) {
286
+ input.focus();
287
+ if (!document.execCommand("insertText", false, text)) {
288
+ const { selectionStart, selectionEnd, value } = input;
289
+ input.value = value.slice(0, selectionStart) + text + value.slice(selectionEnd);
290
+ const pos = selectionStart + text.length;
291
+ input.setSelectionRange(pos, pos);
292
+ input.dispatchEvent(new Event("input", { bubbles: true }));
293
+ }
294
+ fitComposer();
295
+ }
296
+ input.addEventListener("paste", (e) => {
297
+ const text = e.clipboardData?.getData("text/plain") ?? "";
298
+ if (!text.includes("\n")) return;
299
+ e.preventDefault();
300
+ const id = ++pasteSeq;
301
+ const timer = setTimeout(() => {
302
+ pendingPastes.delete(id);
303
+ if (!input.disabled) insertAtCaret(text);
304
+ }, PASTE_TIMEOUT_MS);
305
+ pendingPastes.set(id, { text, timer });
306
+ vscode.postMessage({ type: "pasted", id, text });
307
+ });
308
+ document.addEventListener("keydown", (e) => {
309
+ if (e.key.toLowerCase() === "r" && (e.ctrlKey || e.metaKey) && !e.shiftKey && !e.altKey) {
310
+ e.preventDefault();
311
+ vscode.postMessage({ type: "command", name: "reopen" });
151
312
  }
152
313
  });
153
314
  window.addEventListener("message", (event) => {
154
315
  const m = event.data;
316
+ if (!m || typeof m !== "object" || typeof m.type !== "string") return;
155
317
  if (m.type === "state") {
156
318
  const { type: _type, ...state } = m;
157
319
  render(state);
@@ -159,9 +321,22 @@
159
321
  const { type: _type, ...rest } = m;
160
322
  applyConfig(rest);
161
323
  } else if (m.type === "progress") {
324
+ lastProgress = m.text;
162
325
  const t = status.querySelector(".progress-text");
163
326
  if (t) t.textContent = m.text;
327
+ } else if (m.type === "pasteResult") {
328
+ const p = pendingPastes.get(m.id);
329
+ if (!p) return;
330
+ clearTimeout(p.timer);
331
+ pendingPastes.delete(m.id);
332
+ if (!m.attached) insertAtCaret(p.text);
333
+ } else if (m.type === "tookBack") {
334
+ input.value = m.entries.map((q) => q.text).join("\n\n");
335
+ showInlineError(void 0);
336
+ fitComposer();
337
+ input.focus();
164
338
  }
165
339
  });
340
+ fitComposer();
166
341
  vscode.postMessage({ type: "ready" });
167
342
  })();