editprompt 1.7.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +132 -12
  2. package/dist/index.js +1154 -833
  3. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -1,96 +1,411 @@
1
1
  #!/usr/bin/env node
2
2
  import { configureSync, getAnsiColorFormatter, getLogger, getTextFormatter, parseLogLevel, withFilter } from "@logtape/logtape";
3
3
  import { cli, define } from "gunshi";
4
- import { appendFileSync } from "node:fs";
4
+ import { appendFileSync, existsSync } from "node:fs";
5
+ import { homedir, tmpdir } from "node:os";
6
+ import { join } from "node:path";
7
+ import { exec, spawn } from "node:child_process";
8
+ import Conf from "conf";
5
9
  import { createHash, randomBytes } from "node:crypto";
6
10
  import { createConnection } from "node:net";
7
- import Conf from "conf";
8
- import { exec, spawn } from "node:child_process";
9
11
  import { promisify } from "node:util";
10
12
  import clipboardy from "clipboardy";
11
13
  import { mkdir, readFile, writeFile } from "node:fs/promises";
12
- import { tmpdir } from "node:os";
13
- import { join } from "node:path";
14
14
 
15
- //#region \0rolldown/runtime.js
16
- var __defProp = Object.defineProperty;
17
- var __exportAll = (all, no_symbols) => {
18
- let target = {};
19
- for (var name in all) {
20
- __defProp(target, name, {
21
- get: all[name],
22
- enumerable: true
23
- });
24
- }
25
- if (!no_symbols) {
26
- __defProp(target, Symbol.toStringTag, { value: "Module" });
27
- }
28
- return target;
29
- };
15
+ //#region src/modules/conf.ts
16
+ const projectName = process.env.NODE_ENV === "test" ? "editprompt-test" : "editprompt";
17
+ const conf = new Conf({ projectName });
30
18
 
31
19
  //#endregion
32
- //#region src/modules/logger.ts
33
- function resolveLogFilePath(options) {
34
- if (options.logFile) return options.logFile;
35
- const envPath = process.env.EDITPROMPT_LOG_FILE;
36
- if (envPath) return envPath;
37
- }
38
- function resolveFileLogLevel(options) {
39
- if (options.verbose) return "debug";
40
- const envLevel = process.env.EDITPROMPT_LOG_LEVEL;
41
- if (envLevel) return parseLogLevel(envLevel);
42
- return "info";
20
+ //#region src/backends/stores.ts
21
+ const VAR_EDITOR_PANE = "editor_pane";
22
+ const VAR_IS_EDITOR = "is_editor";
23
+ const VAR_TARGET_PANES = "target_panes";
24
+ const VAR_QUOTE = "quote";
25
+ /**
26
+ * State kept in variables on the panes themselves (tmux pane options, niwaterm
27
+ * tab variables). Variable names are `<prefix>editor_pane`, `<prefix>is_editor`,
28
+ * `<prefix>target_panes` and `<prefix>quote`.
29
+ *
30
+ * The variables disappear with their pane, so forgetting an editor pane is a
31
+ * no-op here: only the link on the target pane is cleared.
32
+ */
33
+ function paneVariableStore(io, prefix) {
34
+ const name = (key) => `${prefix}${key}`;
35
+ const read = async (paneId, key) => (await io.get(paneId, name(key))).trim();
36
+ return {
37
+ getEditorPaneId: (targetPaneId) => read(targetPaneId, VAR_EDITOR_PANE),
38
+ setEditorPaneId: (targetPaneId, editorPaneId) => io.set(targetPaneId, name(VAR_EDITOR_PANE), editorPaneId),
39
+ removeEditorPaneId: (targetPaneId) => io.set(targetPaneId, name(VAR_EDITOR_PANE), ""),
40
+ isEditorPane: async (paneId) => await read(paneId, VAR_IS_EDITOR) === "1",
41
+ getTargetPaneIds: async (editorPaneId) => {
42
+ const value = await read(editorPaneId, VAR_TARGET_PANES);
43
+ if (value === "") return [];
44
+ return value.split(",").map((id) => id.trim());
45
+ },
46
+ setTargetPaneIds: async (editorPaneId, targetPaneIds) => {
47
+ await io.set(editorPaneId, name(VAR_IS_EDITOR), "1");
48
+ await io.set(editorPaneId, name(VAR_TARGET_PANES), targetPaneIds.join(","));
49
+ },
50
+ removeEditorPane: async () => {},
51
+ keepsEditorPaneMarks: true,
52
+ getQuote: (targetPaneId) => io.get(targetPaneId, name(VAR_QUOTE)),
53
+ setQuote: (targetPaneId, text) => io.set(targetPaneId, name(VAR_QUOTE), text),
54
+ removeQuote: (targetPaneId) => io.set(targetPaneId, name(VAR_QUOTE), "")
55
+ };
43
56
  }
44
- function resolveLogLevel(options) {
45
- if (options.quiet) return null;
46
- if (options.verbose) return "debug";
47
- const envLevel = process.env.EDITPROMPT_LOG_LEVEL;
48
- if (envLevel) return parseLogLevel(envLevel);
49
- return "info";
57
+ function readRecord(key) {
58
+ const data = conf.get(key);
59
+ return typeof data === "object" && data !== null && !Array.isArray(data) ? data : void 0;
50
60
  }
51
- function setupLogger(options = {}) {
52
- const stderrLevel = resolveLogLevel(options);
53
- const stderrFormatter = getAnsiColorFormatter({
54
- timestamp: "time-timezone",
55
- level: "ABBR"
61
+ function setField(key, field, value) {
62
+ conf.set(key, {
63
+ ...readRecord(key),
64
+ [field]: value
56
65
  });
57
- const rawStderrSink = (record) => {
58
- process.stderr.write(stderrFormatter(record));
66
+ }
67
+ function removeField(key, field) {
68
+ const data = readRecord(key);
69
+ if (!data || !(field in data)) return;
70
+ const { [field]: _, ...remaining } = data;
71
+ if (Object.keys(remaining).length === 0) conf.delete(key);
72
+ else conf.set(key, remaining);
73
+ }
74
+ /**
75
+ * State kept in editprompt's Conf storage, for backends whose panes cannot hold
76
+ * variables. The layout is:
77
+ *
78
+ * - `<namespace>.targetPane.pane_<id>`: `{ editorPaneId?, quote_text? }`
79
+ * - `<namespace>.editorPane.pane_<id>`: `{ targetPaneIds }`
80
+ *
81
+ * The namespace is resolved on every access so that it can depend on the
82
+ * environment (Herdr scopes it to the session socket).
83
+ */
84
+ function confStore(namespace, options = {}) {
85
+ const targetKey = (paneId) => `${namespace()}.targetPane.pane_${paneId}`;
86
+ const editorKey = (paneId) => `${namespace()}.editorPane.pane_${paneId}`;
87
+ const store = {
88
+ getEditorPaneId: async (targetPaneId) => {
89
+ const data = readRecord(targetKey(targetPaneId));
90
+ return data && "editorPaneId" in data ? String(data.editorPaneId) : "";
91
+ },
92
+ setEditorPaneId: async (targetPaneId, editorPaneId) => {
93
+ setField(targetKey(targetPaneId), "editorPaneId", editorPaneId);
94
+ },
95
+ removeEditorPaneId: async (targetPaneId) => {
96
+ removeField(targetKey(targetPaneId), "editorPaneId");
97
+ },
98
+ isEditorPane: async (paneId) => conf.has(editorKey(paneId)),
99
+ getTargetPaneIds: async (editorPaneId) => {
100
+ const targetPaneIds = readRecord(editorKey(editorPaneId))?.targetPaneIds;
101
+ return Array.isArray(targetPaneIds) ? targetPaneIds.map((id) => String(id)) : [];
102
+ },
103
+ setTargetPaneIds: async (editorPaneId, targetPaneIds) => {
104
+ conf.set(editorKey(editorPaneId), { targetPaneIds });
105
+ },
106
+ removeEditorPane: async (editorPaneId) => {
107
+ conf.delete(editorKey(editorPaneId));
108
+ },
109
+ getQuote: async (targetPaneId) => {
110
+ const data = readRecord(targetKey(targetPaneId));
111
+ return data && "quote_text" in data ? String(data.quote_text) : "";
112
+ },
113
+ setQuote: async (targetPaneId, text) => {
114
+ setField(targetKey(targetPaneId), "quote_text", text);
115
+ },
116
+ removeQuote: async (targetPaneId) => {
117
+ removeField(targetKey(targetPaneId), "quote_text");
118
+ }
59
119
  };
60
- const sinks = { stderr: stderrLevel === null ? () => {} : withFilter(rawStderrSink, stderrLevel) };
61
- const loggerSinks = ["stderr"];
62
- const logFilePath = resolveLogFilePath(options);
63
- const fileLogLevel = resolveFileLogLevel(options);
64
- if (logFilePath) try {
65
- appendFileSync(logFilePath, "");
66
- const fileFormatter = getTextFormatter({
67
- timestamp: "date-time-timezone",
68
- level: "ABBR"
69
- });
70
- const rawFileSink = (record) => {
71
- appendFileSync(logFilePath, fileFormatter(record));
120
+ if (!options.wholeTargetRecord) return store;
121
+ const logger = getLogger([...options.logCategory ?? ["editprompt", "state"]]);
122
+ return swallowErrors(logger, {
123
+ ...store,
124
+ setEditorPaneId: async (targetPaneId, editorPaneId) => {
125
+ logger.debug(`Saving editor pane ID to conf key: ${namespace()}.targetPane.pane_{targetPaneId}`, { targetPaneId });
126
+ conf.set(targetKey(targetPaneId), { editorPaneId });
127
+ },
128
+ removeEditorPaneId: async (targetPaneId) => {
129
+ conf.delete(targetKey(targetPaneId));
130
+ },
131
+ removeQuote: async (targetPaneId) => {
132
+ const key = `${targetKey(targetPaneId)}.quote_text`;
133
+ if (conf.has(key)) conf.delete(key);
134
+ }
135
+ });
136
+ }
137
+ const FAILURE_RESULTS = {
138
+ getEditorPaneId: "",
139
+ setEditorPaneId: void 0,
140
+ removeEditorPaneId: void 0,
141
+ isEditorPane: false,
142
+ getTargetPaneIds: [],
143
+ setTargetPaneIds: void 0,
144
+ removeEditorPane: void 0,
145
+ getQuote: "",
146
+ setQuote: void 0,
147
+ removeQuote: void 0
148
+ };
149
+ const LEGACY_METHOD_NAMES = {
150
+ getEditorPaneId: "getEditorPaneId",
151
+ setEditorPaneId: "saveEditorPaneId",
152
+ removeEditorPaneId: "clearEditorPaneId",
153
+ isEditorPane: "isEditorPaneFromConf",
154
+ getTargetPaneIds: "getTargetPaneIds",
155
+ setTargetPaneIds: "markAsEditorPane",
156
+ removeEditorPane: "clearEditorPaneId",
157
+ getQuote: "getQuoteText",
158
+ setQuote: "appendToQuoteText",
159
+ removeQuote: "clearQuoteText"
160
+ };
161
+ function swallowErrors(logger, store) {
162
+ const wrapped = {};
163
+ for (const method of Object.keys(FAILURE_RESULTS)) {
164
+ const call = store[method];
165
+ wrapped[method] = async (...args) => {
166
+ try {
167
+ return await call(...args);
168
+ } catch (error) {
169
+ logger.debug(`${LEGACY_METHOD_NAMES[method]} failed: {error}`, { error });
170
+ return FAILURE_RESULTS[method];
171
+ }
72
172
  };
73
- sinks.file = withFilter(rawFileSink, fileLogLevel);
74
- loggerSinks.push("file");
75
- } catch {
76
- process.stderr.write(`Warning: Cannot write to log file '${logFilePath}', continuing without file logging.\n`);
77
173
  }
78
- configureSync({
79
- sinks,
80
- loggers: [{
81
- category: ["editprompt"],
82
- sinks: loggerSinks,
83
- lowestLevel: "trace"
84
- }, {
85
- category: ["logtape", "meta"],
86
- lowestLevel: null
87
- }]
88
- });
174
+ return wrapped;
89
175
  }
90
176
 
91
177
  //#endregion
92
- //#region package.json
93
- var version = "1.7.0";
178
+ //#region src/backends/types.ts
179
+ function canSplitPane(backend) {
180
+ return typeof backend.splitPane === "function" && typeof backend.runInPane === "function" && typeof backend.closePane === "function";
181
+ }
182
+ /** Identity helper that gives backend definitions full type checking. */
183
+ function defineBackend(backend) {
184
+ return backend;
185
+ }
186
+
187
+ //#endregion
188
+ //#region src/backends/command.ts
189
+ const logger$16 = getLogger(["editprompt", "command-backend"]);
190
+ const COMMAND_VARIABLES = {
191
+ currentPane: [],
192
+ paneExists: ["EDITPROMPT_PANE"],
193
+ focus: ["EDITPROMPT_PANE"],
194
+ sendText: ["EDITPROMPT_PANE"],
195
+ sendKey: ["EDITPROMPT_PANE", "EDITPROMPT_KEY"],
196
+ splitPane: [
197
+ "EDITPROMPT_PANE",
198
+ "EDITPROMPT_ROWS",
199
+ "EDITPROMPT_CWD"
200
+ ],
201
+ runInPane: ["EDITPROMPT_PANE", "EDITPROMPT_COMMAND"],
202
+ closePane: ["EDITPROMPT_PANE"]
203
+ };
204
+ const COMMAND_VARIABLE_NAMES = new Set(Object.values(COMMAND_VARIABLES).flat());
205
+ const REQUIRED_COMMANDS = [
206
+ "paneExists",
207
+ "focus",
208
+ "sendText",
209
+ "sendKey"
210
+ ];
211
+ const OPTIONAL_COMMANDS = [
212
+ "splitPane",
213
+ "runInPane",
214
+ "closePane"
215
+ ];
216
+ const KNOWN_FIELDS = new Set([
217
+ "detect",
218
+ "currentPane",
219
+ ...REQUIRED_COMMANDS,
220
+ ...OPTIONAL_COMMANDS,
221
+ "maxChunkBytes",
222
+ "submitKey",
223
+ "quoteSeparator",
224
+ "collectInput"
225
+ ]);
226
+ const DEFAULT_MAX_CHUNK_BYTES = 8192;
227
+ const SHELL = "/bin/sh";
228
+ const VARIABLE_PATTERN = /\$\{?(EDITPROMPT_[A-Z_]+)/g;
229
+ var CommandBackendConfigError = class extends Error {
230
+ name = "CommandBackendConfigError";
231
+ };
232
+ function isRecord(value) {
233
+ return typeof value === "object" && value !== null && !Array.isArray(value);
234
+ }
235
+ function parseScript(value, path, field, errors) {
236
+ if (typeof value !== "string" || value.trim() === "") {
237
+ errors.push(`${path} must be a non-empty string`);
238
+ return;
239
+ }
240
+ const allowed = COMMAND_VARIABLES[field];
241
+ const reported = /* @__PURE__ */ new Set();
242
+ for (const [, name] of value.matchAll(VARIABLE_PATTERN)) {
243
+ const variable = name;
244
+ if (COMMAND_VARIABLE_NAMES.has(variable) && !allowed.includes(variable)) {
245
+ if (!reported.has(variable)) {
246
+ reported.add(variable);
247
+ const hint = allowed.length > 0 ? ` (available: ${allowed.map((n) => `$${n}`).join(", ")})` : "";
248
+ errors.push(`${path} uses $${variable}, which is not set for this command${hint}`);
249
+ }
250
+ }
251
+ }
252
+ return {
253
+ path,
254
+ source: value
255
+ };
256
+ }
257
+ function readEnvRef(value) {
258
+ if (isRecord(value) && Object.keys(value).length === 1 && typeof value.env === "string") return value.env;
259
+ }
260
+ function shellQuote(value) {
261
+ return `'${value.replace(/'/g, "'\\''")}'`;
262
+ }
263
+ function runScript(script, values, input) {
264
+ return new Promise((resolve, reject) => {
265
+ const child = spawn(SHELL, ["-c", script.source], {
266
+ env: {
267
+ ...process.env,
268
+ ...values
269
+ },
270
+ stdio: [
271
+ input === void 0 ? "ignore" : "pipe",
272
+ "pipe",
273
+ "pipe"
274
+ ]
275
+ });
276
+ let stdout = "";
277
+ let stderr = "";
278
+ child.stdout?.setEncoding("utf8");
279
+ child.stdout?.on("data", (chunk) => {
280
+ stdout += chunk;
281
+ });
282
+ child.stderr?.setEncoding("utf8");
283
+ child.stderr?.on("data", (chunk) => {
284
+ stderr += chunk;
285
+ });
286
+ child.on("error", (error) => {
287
+ reject(/* @__PURE__ */ new Error(`Failed to run ${script.path}: ${error.message}`));
288
+ });
289
+ child.on("close", (code) => {
290
+ resolve({
291
+ code,
292
+ stdout,
293
+ stderr
294
+ });
295
+ });
296
+ if (input !== void 0) {
297
+ child.stdin?.on("error", (error) => {
298
+ reject(/* @__PURE__ */ new Error(`Failed to write to ${script.path}: ${error.message}`));
299
+ });
300
+ child.stdin?.end(input);
301
+ }
302
+ });
303
+ }
304
+ async function runChecked(script, values, input) {
305
+ const result = await runScript(script, values, input);
306
+ if (result.code !== 0) throw new Error(`${script.path} exited with ${result.code}: ${result.stderr.trim()}`);
307
+ return result.stdout;
308
+ }
309
+ /**
310
+ * Validates a backend declared in the configuration file and builds it.
311
+ * Throws CommandBackendConfigError listing every problem found.
312
+ */
313
+ function createCommandBackend(name, spec) {
314
+ const path = `backends.${name}`;
315
+ const errors = [];
316
+ if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(name)) errors.push(`${path}: the name may only contain letters, digits, "-" and "_"`);
317
+ if (!isRecord(spec)) throw new CommandBackendConfigError(`${path} must be an object`);
318
+ for (const key of Object.keys(spec)) if (!KNOWN_FIELDS.has(key)) errors.push(`${path}.${key} is not a known option`);
319
+ let detectEnv;
320
+ if (spec.detect !== void 0) {
321
+ detectEnv = readEnvRef(spec.detect);
322
+ if (detectEnv === void 0) errors.push(`${path}.detect must be { env = "VARIABLE_NAME" }`);
323
+ }
324
+ const currentPaneEnv = readEnvRef(spec.currentPane);
325
+ let currentPaneScript;
326
+ if (spec.currentPane === void 0) errors.push(`${path}.currentPane is required`);
327
+ else if (currentPaneEnv === void 0) currentPaneScript = parseScript(spec.currentPane, `${path}.currentPane`, "currentPane", errors);
328
+ const commands = {};
329
+ for (const field of REQUIRED_COMMANDS) if (spec[field] === void 0) errors.push(`${path}.${field} is required`);
330
+ else commands[field] = parseScript(spec[field], `${path}.${field}`, field, errors);
331
+ const optionalCount = OPTIONAL_COMMANDS.filter((field) => spec[field] !== void 0).length;
332
+ if (optionalCount > 0 && optionalCount < OPTIONAL_COMMANDS.length) errors.push(`${path}: splitPane, runInPane and closePane must be set together`);
333
+ for (const field of OPTIONAL_COMMANDS) if (spec[field] !== void 0) commands[field] = parseScript(spec[field], `${path}.${field}`, field, errors);
334
+ const maxChunkBytes = spec.maxChunkBytes ?? DEFAULT_MAX_CHUNK_BYTES;
335
+ if (typeof maxChunkBytes !== "number" || !Number.isInteger(maxChunkBytes) || maxChunkBytes < 4) errors.push(`${path}.maxChunkBytes must be an integer of at least 4`);
336
+ const submitKey = spec.submitKey ?? "Enter";
337
+ if (typeof submitKey !== "string" || submitKey === "") errors.push(`${path}.submitKey must be a non-empty string`);
338
+ const quoteSeparator = spec.quoteSeparator ?? "\n\n";
339
+ if (typeof quoteSeparator !== "string") errors.push(`${path}.quoteSeparator must be a string`);
340
+ const collectInput = spec.collectInput ?? "argument";
341
+ if (collectInput !== "stdin" && collectInput !== "argument") errors.push(`${path}.collectInput must be "stdin" or "argument"`);
342
+ if (errors.length > 0) throw new CommandBackendConfigError(errors.join("\n"));
343
+ const command = (field) => commands[field];
344
+ const namespace = () => `state.${name}`;
345
+ const backend = defineBackend({
346
+ name,
347
+ detect: detectEnv ? (env) => (env[detectEnv]?.trim() ?? "") !== "" : void 0,
348
+ getCurrentPaneId: async () => {
349
+ if (currentPaneEnv !== void 0) {
350
+ const paneId = process.env[currentPaneEnv]?.trim();
351
+ if (!paneId) throw new Error(`${currentPaneEnv} is not set`);
352
+ return paneId;
353
+ }
354
+ return (await runChecked(currentPaneScript, {})).trim();
355
+ },
356
+ paneExists: async (paneId) => {
357
+ try {
358
+ return (await runScript(command("paneExists"), { EDITPROMPT_PANE: paneId })).code === 0;
359
+ } catch (error) {
360
+ logger$16.debug("paneExists failed: {error}", { error });
361
+ return false;
362
+ }
363
+ },
364
+ focusPane: async (paneId) => {
365
+ await runChecked(command("focus"), { EDITPROMPT_PANE: paneId });
366
+ },
367
+ sendText: async (paneId, chunk) => {
368
+ await runChecked(command("sendText"), { EDITPROMPT_PANE: paneId }, chunk);
369
+ },
370
+ sendKey: async (paneId, key) => {
371
+ await runChecked(command("sendKey"), {
372
+ EDITPROMPT_PANE: paneId,
373
+ EDITPROMPT_KEY: key
374
+ });
375
+ },
376
+ maxChunkBytes,
377
+ defaultSubmitKey: submitKey,
378
+ quoteSeparator,
379
+ collectInput,
380
+ storageNamespace: namespace,
381
+ resumeAcceptsEditorPane: true,
382
+ keepNewerEditorLink: true,
383
+ quietResumeErrors: "all",
384
+ state: confStore(namespace)
385
+ });
386
+ if (commands.splitPane && commands.runInPane && commands.closePane) {
387
+ const { splitPane, runInPane, closePane } = commands;
388
+ backend.splitPane = async (targetPaneId, options) => {
389
+ const paneId = (await runChecked(splitPane, {
390
+ EDITPROMPT_PANE: targetPaneId,
391
+ EDITPROMPT_ROWS: String(options.rows),
392
+ EDITPROMPT_CWD: options.cwd ?? process.cwd()
393
+ })).trim();
394
+ if (paneId === "") throw new Error(`${path}.splitPane did not print the new pane ID`);
395
+ return paneId;
396
+ };
397
+ backend.runInPane = async (paneId, argv) => {
398
+ await runChecked(runInPane, {
399
+ EDITPROMPT_PANE: paneId,
400
+ EDITPROMPT_COMMAND: argv.map(shellQuote).join(" ")
401
+ });
402
+ };
403
+ backend.closePane = async (paneId) => {
404
+ await runChecked(closePane, { EDITPROMPT_PANE: paneId });
405
+ };
406
+ }
407
+ return backend;
408
+ }
94
409
 
95
410
  //#endregion
96
411
  //#region src/config/constants.ts
@@ -103,65 +418,12 @@ const WEZTERM_SEND_CHUNK_BYTES = TMUX_SEND_CHUNK_BYTES;
103
418
  const HERDR_REQUEST_LIMIT_BYTES = 1024 * 1024;
104
419
  const HERDR_SEND_CHUNK_BYTES = HERDR_REQUEST_LIMIT_BYTES / 8;
105
420
  const NIWATERM_SEND_CHUNK_BYTES = WEZTERM_SEND_CHUNK_BYTES;
106
-
107
- //#endregion
108
- //#region src/utils/contentChunker.ts
109
- /**
110
- * Split content into chunks not exceeding maxBytes (UTF-8 byte count),
111
- * never breaking a Unicode code point across chunks.
112
- *
113
- * Content shorter than maxBytes is returned as a single chunk, so short
114
- * input keeps the previous behavior of being sent in one shot.
115
- */
116
- function splitByByteSize(content, maxBytes) {
117
- if (content === "") return [];
118
- const chunks = [];
119
- let current = "";
120
- let currentBytes = 0;
121
- for (const char of content) {
122
- const charBytes = Buffer.byteLength(char, "utf8");
123
- if (current !== "" && currentBytes + charBytes > maxBytes) {
124
- chunks.push(current);
125
- current = "";
126
- currentBytes = 0;
127
- }
128
- current += char;
129
- currentBytes += charBytes;
130
- }
131
- if (current !== "") chunks.push(current);
132
- return chunks;
133
- }
134
-
135
- //#endregion
136
- //#region src/modules/conf.ts
137
- const projectName = process.env.NODE_ENV === "test" ? "editprompt-test" : "editprompt";
138
- const conf = new Conf({ projectName });
421
+ const REX_CONTROL_LIMIT_BYTES = 64 * 1024;
422
+ const REX_SEND_CHUNK_BYTES = REX_CONTROL_LIMIT_BYTES / 8;
139
423
 
140
424
  //#endregion
141
425
  //#region src/modules/herdr.ts
142
- var herdr_exports = /* @__PURE__ */ __exportAll({
143
- appendToQuoteText: () => appendToQuoteText$1,
144
- calculateEditorSplitRatio: () => calculateEditorSplitRatio,
145
- checkPaneExists: () => checkPaneExists$3,
146
- clearEditorPaneId: () => clearEditorPaneId$3,
147
- clearQuoteText: () => clearQuoteText$1,
148
- closePane: () => closePane,
149
- focusPane: () => focusPane$3,
150
- getCurrentPaneId: () => getCurrentPaneId$3,
151
- getEditorPaneId: () => getEditorPaneId$3,
152
- getQuoteText: () => getQuoteText$1,
153
- getStorageNamespace: () => getStorageNamespace,
154
- getTargetPaneIds: () => getTargetPaneIds$3,
155
- inputToHerdrPane: () => inputToHerdrPane,
156
- isEditorPaneFromConf: () => isEditorPaneFromConf$1,
157
- markAsEditorPane: () => markAsEditorPane$3,
158
- resumeEditorPane: () => resumeEditorPane,
159
- runCommandInPane: () => runCommandInPane,
160
- saveEditorPaneId: () => saveEditorPaneId$3,
161
- sendKeyToHerdrPane: () => sendKeyToHerdrPane,
162
- splitEditorPane: () => splitEditorPane
163
- });
164
- const logger$14 = getLogger(["editprompt", "herdr"]);
426
+ const logger$15 = getLogger(["editprompt", "herdr"]);
165
427
  const HERDR_REQUEST_TIMEOUT_MS = 6e3;
166
428
  let requestSequence = 0;
167
429
  function getSocketPath() {
@@ -242,27 +504,21 @@ async function sendRequest(method, params) {
242
504
  function getStorageNamespace() {
243
505
  return `herdr.session_${createHash("sha256").update(getSocketPath()).digest("hex").slice(0, 16)}`;
244
506
  }
245
- function targetPaneKey(paneId) {
246
- return `${getStorageNamespace()}.targetPane.pane_${paneId}`;
247
- }
248
- function editorPaneKey(paneId) {
249
- return `${getStorageNamespace()}.editorPane.pane_${paneId}`;
250
- }
251
- async function getCurrentPaneId$3() {
507
+ async function getCurrentPaneId$4() {
252
508
  const paneId = process.env.HERDR_PANE_ID?.trim() || process.env.HERDR_ACTIVE_PANE_ID?.trim();
253
509
  if (!paneId) throw new Error("HERDR_PANE_ID or HERDR_ACTIVE_PANE_ID is not set");
254
510
  return paneId;
255
511
  }
256
- async function checkPaneExists$3(paneId) {
512
+ async function checkPaneExists$4(paneId) {
257
513
  try {
258
514
  await sendRequest("pane.get", { pane_id: paneId });
259
515
  return true;
260
516
  } catch (error) {
261
- logger$14.debug("checkPaneExists failed: {error}", { error });
517
+ logger$15.debug("checkPaneExists failed: {error}", { error });
262
518
  return false;
263
519
  }
264
520
  }
265
- async function focusPane$3(paneId) {
521
+ async function focusPane$4(paneId) {
266
522
  await sendRequest("pane.focus", { pane_id: paneId });
267
523
  }
268
524
  function calculateEditorSplitRatio(paneHeight, editorPaneRows) {
@@ -299,123 +555,73 @@ async function runCommandInPane(paneId, argv) {
299
555
  async function closePane(paneId) {
300
556
  await sendRequest("pane.close", { pane_id: paneId });
301
557
  }
302
- async function inputToHerdrPane(paneId, content) {
303
- const chunks = splitByByteSize(content, HERDR_SEND_CHUNK_BYTES);
304
- for (const chunk of chunks) await sendRequest("pane.send_text", {
558
+ async function sendTextChunk$4(paneId, chunk) {
559
+ await sendRequest("pane.send_text", {
305
560
  pane_id: paneId,
306
561
  text: chunk
307
562
  });
308
- logger$14.debug("Content sent to Herdr pane: {paneId} ({chunkCount} chunks)", {
309
- paneId,
310
- chunkCount: chunks.length
311
- });
312
563
  }
313
- async function sendKeyToHerdrPane(paneId, key, delay = 1e3) {
314
- await new Promise((resolve) => setTimeout(resolve, delay));
564
+ async function sendKey$4(paneId, key) {
315
565
  await sendRequest("pane.send_keys", {
316
566
  pane_id: paneId,
317
567
  keys: [key]
318
568
  });
319
569
  }
320
- async function saveEditorPaneId$3(targetPaneId, editorPaneId) {
321
- const data = conf.get(targetPaneKey(targetPaneId));
322
- const existing = typeof data === "object" && data !== null ? data : {};
323
- conf.set(targetPaneKey(targetPaneId), {
324
- ...existing,
325
- editorPaneId
570
+ function logContentSent$4(paneId, chunkCount) {
571
+ logger$15.debug("Content sent to Herdr pane: {paneId} ({chunkCount} chunks)", {
572
+ paneId,
573
+ chunkCount
326
574
  });
327
575
  }
328
- async function getEditorPaneId$3(targetPaneId) {
329
- const data = conf.get(targetPaneKey(targetPaneId));
330
- if (typeof data === "object" && data !== null && "editorPaneId" in data) return String(data.editorPaneId);
331
- return "";
332
- }
333
- async function clearEditorPaneId$3(targetPaneId, expectedEditorPaneId) {
334
- const editorPaneId = await getEditorPaneId$3(targetPaneId);
335
- if (expectedEditorPaneId && editorPaneId !== expectedEditorPaneId) {
336
- conf.delete(editorPaneKey(expectedEditorPaneId));
337
- return;
338
- }
339
- const data = conf.get(targetPaneKey(targetPaneId));
340
- if (typeof data === "object" && data !== null && "editorPaneId" in data) {
341
- const { editorPaneId: _, ...remaining } = data;
342
- if (Object.keys(remaining).length === 0) conf.delete(targetPaneKey(targetPaneId));
343
- else conf.set(targetPaneKey(targetPaneId), remaining);
344
- }
345
- if (editorPaneId) conf.delete(editorPaneKey(editorPaneId));
346
- }
347
- async function markAsEditorPane$3(editorPaneId, targetPaneIds) {
348
- const uniqueTargetPaneIds = [...new Set(targetPaneIds)];
349
- conf.set(editorPaneKey(editorPaneId), { targetPaneIds: uniqueTargetPaneIds });
350
- for (const targetPaneId of uniqueTargetPaneIds) await saveEditorPaneId$3(targetPaneId, editorPaneId);
351
- }
352
- async function getTargetPaneIds$3(editorPaneId) {
353
- const data = conf.get(editorPaneKey(editorPaneId));
354
- if (typeof data === "object" && data !== null && "targetPaneIds" in data) {
355
- const targetPaneIds = data.targetPaneIds;
356
- if (Array.isArray(targetPaneIds)) return targetPaneIds.map((id) => String(id));
357
- }
358
- return [];
359
- }
360
- function isEditorPaneFromConf$1(paneId) {
361
- return conf.has(editorPaneKey(paneId));
362
- }
363
- async function resumeEditorPane(targetPaneId) {
364
- const currentPaneId = await getCurrentPaneId$3();
365
- const editorPaneId = isEditorPaneFromConf$1(currentPaneId) ? currentPaneId : isEditorPaneFromConf$1(targetPaneId) ? targetPaneId : "";
366
- if (editorPaneId) {
367
- const targetPaneIds = await getTargetPaneIds$3(editorPaneId);
368
- for (const paneId of targetPaneIds) if (await checkPaneExists$3(paneId)) {
369
- await focusPane$3(paneId);
370
- return true;
371
- }
372
- return false;
373
- }
374
- const registeredEditorPaneId = await getEditorPaneId$3(targetPaneId);
375
- if (registeredEditorPaneId === "") return false;
376
- if (!await checkPaneExists$3(registeredEditorPaneId)) {
377
- await clearEditorPaneId$3(targetPaneId);
378
- return false;
379
- }
380
- await focusPane$3(registeredEditorPaneId);
381
- return true;
576
+
577
+ //#endregion
578
+ //#region src/backends/env.ts
579
+ function hasValue(value) {
580
+ return typeof value === "string" && value.trim() !== "";
382
581
  }
383
- async function appendToQuoteText$1(paneId, content) {
384
- const data = conf.get(targetPaneKey(paneId));
385
- const existing = typeof data === "object" && data !== null ? data : {};
386
- const existingQuoteText = "quote_text" in existing ? String(existing.quote_text) : "";
387
- const quoteText = existingQuoteText.trim() !== "" ? `${existingQuoteText}\n\n${content}` : content;
388
- conf.set(targetPaneKey(paneId), {
389
- ...existing,
390
- quote_text: quoteText
391
- });
582
+ function isHerdrEnvironment(env = process.env) {
583
+ return hasValue(env.HERDR_SOCKET_PATH) && (hasValue(env.HERDR_PANE_ID) || hasValue(env.HERDR_ACTIVE_PANE_ID));
392
584
  }
393
- async function getQuoteText$1(paneId) {
394
- const data = conf.get(targetPaneKey(paneId));
395
- if (typeof data === "object" && data !== null && "quote_text" in data) return String(data.quote_text);
396
- return "";
585
+ function isNiwatermEnvironment(env = process.env) {
586
+ return hasValue(env.NIWATERM_TAB_ID);
397
587
  }
398
- async function clearQuoteText$1(paneId) {
399
- const data = conf.get(targetPaneKey(paneId));
400
- if (typeof data === "object" && data !== null && "quote_text" in data) {
401
- const { quote_text: _, ...remaining } = data;
402
- if (Object.keys(remaining).length === 0) conf.delete(targetPaneKey(paneId));
403
- else conf.set(targetPaneKey(paneId), remaining);
404
- }
588
+ function isRexEnvironment(env = process.env) {
589
+ return hasValue(env.REX_BLOCK);
405
590
  }
406
591
 
592
+ //#endregion
593
+ //#region src/backends/herdr.ts
594
+ const herdrBackend = defineBackend({
595
+ name: "herdr",
596
+ detect: (env) => isHerdrEnvironment(env),
597
+ getCurrentPaneId: () => getCurrentPaneId$4(),
598
+ paneExists: (paneId) => checkPaneExists$4(paneId),
599
+ focusPane: (paneId) => focusPane$4(paneId),
600
+ sendText: (paneId, chunk) => sendTextChunk$4(paneId, chunk),
601
+ sendKey: (paneId, key) => sendKey$4(paneId, key),
602
+ logContentSent: (paneId, chunkCount) => logContentSent$4(paneId, chunkCount),
603
+ maxChunkBytes: HERDR_SEND_CHUNK_BYTES,
604
+ defaultSubmitKey: "enter",
605
+ quoteSeparator: "\n\n",
606
+ collectInput: "argument",
607
+ quietResumeErrors: "all",
608
+ resumeAcceptsEditorPane: true,
609
+ keepNewerEditorLink: true,
610
+ storageNamespace: () => getStorageNamespace(),
611
+ state: confStore(() => getStorageNamespace()),
612
+ splitPane: (targetPaneId, { rows, cwd, env }) => splitEditorPane(targetPaneId, rows, cwd ?? process.env.HERDR_ACTIVE_PANE_CWD, env),
613
+ runInPane: (paneId, argv) => runCommandInPane(paneId, argv),
614
+ closePane: (paneId) => closePane(paneId)
615
+ });
616
+
407
617
  //#endregion
408
618
  //#region src/modules/niwaterm.ts
409
619
  const execAsync$2 = promisify(exec);
410
- const logger$13 = getLogger(["editprompt", "niwaterm"]);
411
- const VAR_EDITOR_PANE = "editprompt_editor_pane";
412
- const VAR_IS_EDITOR = "editprompt_is_editor";
413
- const VAR_TARGET_PANES = "editprompt_target_panes";
414
- const VAR_QUOTE = "editprompt_quote";
620
+ const logger$14 = getLogger(["editprompt", "niwaterm"]);
415
621
  function quoteShellWord(value) {
416
622
  return `'${value.replace(/'/g, "'\\''")}'`;
417
623
  }
418
- async function getCurrentPaneId$2() {
624
+ async function getCurrentPaneId$3() {
419
625
  const tabId = process.env.NIWATERM_TAB_ID?.trim();
420
626
  if (!tabId) throw new Error("NIWATERM_TAB_ID is not set");
421
627
  return tabId;
@@ -425,71 +631,184 @@ async function getTabVar(tabId, key) {
425
631
  const { stdout } = await execAsync$2(`niwa tab var get -t ${quoteShellWord(tabId)} ${quoteShellWord(key)}`);
426
632
  return stdout.trim();
427
633
  } catch (error) {
428
- logger$13.debug("getTabVar failed: {error}", { error });
634
+ logger$14.debug("getTabVar failed: {error}", { error });
429
635
  return "";
430
636
  }
431
637
  }
432
638
  async function setTabVar(tabId, key, value) {
433
639
  await execAsync$2(`niwa tab var set -t ${quoteShellWord(tabId)} ${quoteShellWord(key)} ${quoteShellWord(value)}`);
434
640
  }
435
- async function checkPaneExists$2(paneId) {
641
+ async function checkPaneExists$3(paneId) {
436
642
  try {
437
643
  await execAsync$2(`niwa tab var list -t ${quoteShellWord(paneId)}`);
438
644
  return true;
439
645
  } catch (error) {
440
- logger$13.debug("checkPaneExists failed: {error}", { error });
646
+ logger$14.debug("checkPaneExists failed: {error}", { error });
441
647
  return false;
442
648
  }
443
649
  }
444
- async function focusPane$2(paneId) {
650
+ async function focusPane$3(paneId) {
445
651
  await execAsync$2(`niwa tab focus -t ${quoteShellWord(paneId)}`);
446
652
  }
447
- async function saveEditorPaneId$2(targetPaneId, editorPaneId) {
448
- await setTabVar(targetPaneId, VAR_EDITOR_PANE, editorPaneId);
449
- }
450
- async function clearEditorPaneId$2(targetPaneId) {
451
- await setTabVar(targetPaneId, VAR_EDITOR_PANE, "");
452
- }
453
- async function getEditorPaneId$2(targetPaneId) {
454
- return getTabVar(targetPaneId, VAR_EDITOR_PANE);
455
- }
456
- async function markAsEditorPane$2(editorPaneId, targetPaneIds) {
457
- const uniqueTargetPaneIds = [...new Set(targetPaneIds)];
458
- await setTabVar(editorPaneId, VAR_IS_EDITOR, "1");
459
- await setTabVar(editorPaneId, VAR_TARGET_PANES, uniqueTargetPaneIds.join(","));
460
- for (const targetPaneId of uniqueTargetPaneIds) await saveEditorPaneId$2(targetPaneId, editorPaneId);
653
+ async function sendKey$3(paneId, key) {
654
+ await execAsync$2(`niwa tab send-keys -t ${quoteShellWord(paneId)} ${quoteShellWord(key)}`);
461
655
  }
462
- async function getTargetPaneIds$2(editorPaneId) {
463
- const value = await getTabVar(editorPaneId, VAR_TARGET_PANES);
464
- if (value === "") return [];
465
- return value.split(",").map((id) => id.trim());
656
+ async function sendTextChunk$3(paneId, chunk) {
657
+ await execAsync$2(`niwa tab send-keys -t ${quoteShellWord(paneId)} -- ${quoteShellWord(chunk)}`);
466
658
  }
467
- async function isEditorPane$1(paneId) {
468
- return await getTabVar(paneId, VAR_IS_EDITOR) === "1";
659
+ function logContentSent$3(paneId, chunkCount) {
660
+ logger$14.debug("Content sent to niwaterm pane: {paneId} ({chunks} chunk(s))", {
661
+ paneId,
662
+ chunks: chunkCount
663
+ });
469
664
  }
470
- async function getQuoteVariableContent$1(paneId) {
471
- return getTabVar(paneId, VAR_QUOTE);
665
+
666
+ //#endregion
667
+ //#region src/backends/niwaterm.ts
668
+ const niwatermBackend = defineBackend({
669
+ name: "niwaterm",
670
+ detect: (env) => isNiwatermEnvironment(env),
671
+ getCurrentPaneId: () => getCurrentPaneId$3(),
672
+ paneExists: (paneId) => checkPaneExists$3(paneId),
673
+ focusPane: (paneId) => focusPane$3(paneId),
674
+ sendText: (paneId, chunk) => sendTextChunk$3(paneId, chunk),
675
+ sendKey: (paneId, key) => sendKey$3(paneId, key),
676
+ logContentSent: (paneId, chunkCount) => logContentSent$3(paneId, chunkCount),
677
+ maxChunkBytes: NIWATERM_SEND_CHUNK_BYTES,
678
+ defaultSubmitKey: "C-m",
679
+ quoteSeparator: "\n",
680
+ collectInput: "argument",
681
+ quietResumeErrors: "editorFocus",
682
+ traceResume: true,
683
+ storageNamespace: () => "niwaterm",
684
+ state: paneVariableStore({
685
+ get: (paneId, name) => getTabVar(paneId, name),
686
+ set: (paneId, name, value) => setTabVar(paneId, name, value)
687
+ }, "editprompt_")
688
+ });
689
+
690
+ //#endregion
691
+ //#region src/modules/rex.ts
692
+ const logger$13 = getLogger(["editprompt", "rex"]);
693
+ const REX_COMMAND = "rex";
694
+ const REX_GLOBAL_ARGS = ["--autostart=false"];
695
+ function rexCommand() {
696
+ const binDir = process.env.REX_BIN_DIR?.trim();
697
+ return binDir ? join(binDir, REX_COMMAND) : REX_COMMAND;
698
+ }
699
+ function rexArgs(args) {
700
+ return [...REX_GLOBAL_ARGS, ...args];
701
+ }
702
+ function runRex(args, input) {
703
+ const command = rexCommand();
704
+ const argv = rexArgs(args);
705
+ return new Promise((resolve, reject) => {
706
+ const child = spawn(command, argv, { stdio: [
707
+ input === void 0 ? "ignore" : "pipe",
708
+ "pipe",
709
+ "pipe"
710
+ ] });
711
+ let stdout = "";
712
+ let stderr = "";
713
+ child.stdout?.setEncoding("utf8");
714
+ child.stdout?.on("data", (chunk) => {
715
+ stdout += chunk;
716
+ });
717
+ child.stderr?.setEncoding("utf8");
718
+ child.stderr?.on("data", (chunk) => {
719
+ stderr += chunk;
720
+ });
721
+ child.on("error", (error) => {
722
+ reject(/* @__PURE__ */ new Error(`Failed to run rex: ${error.message}`));
723
+ });
724
+ child.on("close", (code) => {
725
+ if (code === 0) {
726
+ resolve(stdout);
727
+ return;
728
+ }
729
+ reject(/* @__PURE__ */ new Error(`rex ${argv.join(" ")} exited with ${code}: ${stderr.trim()}`));
730
+ });
731
+ if (input !== void 0) {
732
+ if (!child.stdin) {
733
+ reject(/* @__PURE__ */ new Error("rex was started without a standard input to write to"));
734
+ return;
735
+ }
736
+ child.stdin.on("error", (error) => {
737
+ reject(/* @__PURE__ */ new Error(`Failed to write to rex: ${error.message}`));
738
+ });
739
+ child.stdin.end(input);
740
+ }
741
+ });
472
742
  }
473
- async function appendToQuoteVariable$1(paneId, content) {
474
- const existingContent = await getQuoteVariableContent$1(paneId);
475
- await setTabVar(paneId, VAR_QUOTE, existingContent.trim() !== "" ? `${existingContent}\n${content}` : content);
743
+ async function getCurrentPaneId$2() {
744
+ const paneId = process.env.REX_BLOCK?.trim();
745
+ if (!paneId) throw new Error("REX_BLOCK is not set");
746
+ return paneId;
476
747
  }
477
- async function clearQuoteVariable$1(paneId) {
478
- await setTabVar(paneId, VAR_QUOTE, "");
748
+ async function checkPaneExists$2(paneId) {
749
+ try {
750
+ await runRex([
751
+ "block",
752
+ "inspect",
753
+ paneId,
754
+ "--json"
755
+ ]);
756
+ return true;
757
+ } catch (error) {
758
+ logger$13.debug("checkPaneExists failed: {error}", { error });
759
+ return false;
760
+ }
479
761
  }
480
- async function sendKeyToNiwatermPane(paneId, key, delay = 1e3) {
481
- await new Promise((resolve) => setTimeout(resolve, delay));
482
- await execAsync$2(`niwa tab send-keys -t ${quoteShellWord(paneId)} ${quoteShellWord(key)}`);
762
+ async function focusPane$2(paneId) {
763
+ await runRex([
764
+ "block",
765
+ "focus",
766
+ paneId
767
+ ]);
768
+ }
769
+ async function sendTextChunk$2(paneId, chunk) {
770
+ await runRex([
771
+ "send",
772
+ "-b",
773
+ paneId
774
+ ], chunk);
775
+ }
776
+ async function sendKey$2(paneId, key) {
777
+ await runRex([
778
+ "send-key",
779
+ "-b",
780
+ paneId,
781
+ key
782
+ ]);
483
783
  }
484
- async function inputToNiwatermPane(paneId, content) {
485
- const chunks = splitByByteSize(content, NIWATERM_SEND_CHUNK_BYTES);
486
- for (const chunk of chunks) await execAsync$2(`niwa tab send-keys -t ${quoteShellWord(paneId)} ${quoteShellWord(chunk)}`);
487
- logger$13.debug("Content sent to niwaterm pane: {paneId} ({chunks} chunk(s))", {
784
+ function logContentSent$2(paneId, chunkCount) {
785
+ logger$13.debug("Content sent to Rex pane: {paneId} ({chunks} chunk(s))", {
488
786
  paneId,
489
- chunks: chunks.length
787
+ chunks: chunkCount
490
788
  });
491
789
  }
492
790
 
791
+ //#endregion
792
+ //#region src/backends/rex.ts
793
+ const rexBackend = defineBackend({
794
+ name: "rex",
795
+ detect: (env) => isRexEnvironment(env),
796
+ getCurrentPaneId: () => getCurrentPaneId$2(),
797
+ paneExists: (paneId) => checkPaneExists$2(paneId),
798
+ focusPane: (paneId) => focusPane$2(paneId),
799
+ sendText: (paneId, chunk) => sendTextChunk$2(paneId, chunk),
800
+ sendKey: (paneId, key) => sendKey$2(paneId, key),
801
+ logContentSent: (paneId, chunkCount) => logContentSent$2(paneId, chunkCount),
802
+ maxChunkBytes: REX_SEND_CHUNK_BYTES,
803
+ defaultSubmitKey: "enter",
804
+ quoteSeparator: "\n\n",
805
+ collectInput: "argument",
806
+ quietResumeErrors: "all",
807
+ resumeAcceptsEditorPane: true,
808
+ storageNamespace: () => "rex",
809
+ state: confStore(() => "rex")
810
+ });
811
+
493
812
  //#endregion
494
813
  //#region src/modules/tmux.ts
495
814
  const execAsync$1 = promisify(exec);
@@ -500,20 +819,6 @@ async function getCurrentPaneId$1() {
500
819
  const { stdout } = await execAsync$1("tmux display-message -p \"#{pane_id}\"");
501
820
  return stdout.trim();
502
821
  }
503
- async function saveEditorPaneId$1(targetPaneId, editorPaneId) {
504
- await execAsync$1(`tmux set-option -pt '${targetPaneId}' @editprompt_editor_pane '${editorPaneId}'`);
505
- }
506
- async function clearEditorPaneId$1(targetPaneId) {
507
- await execAsync$1(`tmux set-option -pt '${targetPaneId}' @editprompt_editor_pane ""`);
508
- }
509
- async function getEditorPaneId$1(targetPaneId) {
510
- try {
511
- const { stdout } = await execAsync$1(`tmux show -pt '${targetPaneId}' -v @editprompt_editor_pane`);
512
- return stdout.trim();
513
- } catch {
514
- return "";
515
- }
516
- }
517
822
  async function checkPaneExists$1(paneId) {
518
823
  try {
519
824
  const { stdout } = await execAsync$1("tmux list-panes -a -F \"#{pane_id}\"");
@@ -525,62 +830,55 @@ async function checkPaneExists$1(paneId) {
525
830
  async function focusPane$1(paneId) {
526
831
  await execAsync$1(`tmux select-pane -t '${paneId}'`);
527
832
  }
528
- async function markAsEditorPane$1(editorPaneId, targetPaneIds) {
529
- await execAsync$1(`tmux set-option -pt '${editorPaneId}' @editprompt_is_editor 1`);
530
- const uniqueTargetPaneIds = [...new Set(targetPaneIds)];
531
- await execAsync$1(`tmux set-option -pt '${editorPaneId}' @editprompt_target_panes '${uniqueTargetPaneIds.join(",")}'`);
532
- for (const targetPaneId of uniqueTargetPaneIds) await saveEditorPaneId$1(targetPaneId, editorPaneId);
533
- }
534
- async function getTargetPaneIds$1(editorPaneId) {
833
+ async function getPaneOption(paneId, name) {
535
834
  try {
536
- const { stdout } = await execAsync$1(`tmux show -pt '${editorPaneId}' -v @editprompt_target_panes`);
537
- const value = stdout.trim();
538
- if (value === "") return [];
539
- return value.split(",").map((id) => id.trim());
540
- } catch {
541
- return [];
542
- }
543
- }
544
- async function isEditorPane(paneId) {
545
- try {
546
- const { stdout } = await execAsync$1(`tmux show -pt '${paneId}' -v @editprompt_is_editor`);
547
- return stdout.trim() === "1";
548
- } catch {
549
- return false;
550
- }
551
- }
552
- async function getQuoteVariableContent(paneId) {
553
- try {
554
- const { stdout } = await execAsync$1(`tmux show -pt '${paneId}' -v @editprompt_quote`);
835
+ const { stdout } = await execAsync$1(`tmux show -pt '${paneId}' -v ${name}`);
555
836
  return stdout;
556
837
  } catch {
557
838
  return "";
558
839
  }
559
840
  }
560
- async function appendToQuoteVariable(paneId, content) {
561
- let newContent = "";
562
- const existingContent = await getQuoteVariableContent(paneId);
563
- if (existingContent.trim() !== "") newContent = `${existingContent}\n${content}`;
564
- else newContent = content;
565
- await execAsync$1(`tmux set-option -pt '${paneId}' @editprompt_quote '${newContent.replace(/'/g, "'\\''")}' `);
566
- }
567
- async function clearQuoteVariable(targetPaneId) {
568
- await execAsync$1(`tmux set-option -pt '${targetPaneId}' @editprompt_quote ""`);
841
+ async function setPaneOption(paneId, name, value) {
842
+ await execAsync$1(`tmux set-option -pt '${paneId}' ${name} '${value.replace(/'/g, "'\\''")}'`);
569
843
  }
570
- async function sendKeyToTmuxPane(paneId, key, delay = 1e3) {
571
- await new Promise((resolve) => setTimeout(resolve, delay));
844
+ async function sendKey$1(paneId, key) {
572
845
  await execAsync$1(`tmux send-keys -t '${paneId}' '${key}'`);
573
846
  }
574
- async function inputToTmuxPane(paneId, content) {
847
+ async function exitCopyMode(paneId) {
575
848
  await execAsync$1(`tmux if-shell -t '${paneId}' '[ "#{pane_in_mode}" = "1" ]' "copy-mode -q -t '${paneId}'"`);
576
- const chunks = splitByByteSize(content, TMUX_SEND_CHUNK_BYTES);
577
- for (const chunk of chunks) await execAsync$1(`tmux send-keys -t '${paneId}' -- '${chunk.replace(/'/g, "'\\''")}'`);
849
+ }
850
+ async function sendTextChunk$1(paneId, chunk) {
851
+ await execAsync$1(`tmux send-keys -t '${paneId}' -- '${chunk.replace(/'/g, "'\\''")}'`);
852
+ }
853
+ function logContentSent$1(paneId, chunkCount) {
578
854
  logger$12.debug("Content sent to tmux pane: {paneId} ({chunks} chunk(s))", {
579
855
  paneId,
580
- chunks: chunks.length
856
+ chunks: chunkCount
581
857
  });
582
858
  }
583
859
 
860
+ //#endregion
861
+ //#region src/backends/tmux.ts
862
+ const tmuxBackend = defineBackend({
863
+ name: "tmux",
864
+ getCurrentPaneId: () => getCurrentPaneId$1(),
865
+ paneExists: (paneId) => checkPaneExists$1(paneId),
866
+ focusPane: (paneId) => focusPane$1(paneId),
867
+ beforeSendText: (paneId) => exitCopyMode(paneId),
868
+ sendText: (paneId, chunk) => sendTextChunk$1(paneId, chunk),
869
+ sendKey: (paneId, key) => sendKey$1(paneId, key),
870
+ logContentSent: (paneId, chunkCount) => logContentSent$1(paneId, chunkCount),
871
+ maxChunkBytes: TMUX_SEND_CHUNK_BYTES,
872
+ defaultSubmitKey: "C-m",
873
+ quoteSeparator: "\n",
874
+ collectInput: "stdin",
875
+ storageNamespace: () => "tmux",
876
+ state: paneVariableStore({
877
+ get: (paneId, name) => getPaneOption(paneId, name),
878
+ set: (paneId, name, value) => setPaneOption(paneId, name, value)
879
+ }, "@editprompt_")
880
+ });
881
+
584
882
  //#endregion
585
883
  //#region src/modules/wezterm.ts
586
884
  const logger$11 = getLogger(["editprompt", "wezterm"]);
@@ -605,114 +903,270 @@ async function checkPaneExists(paneId) {
605
903
  return false;
606
904
  }
607
905
  }
608
- async function saveEditorPaneId(targetPaneId, editorPaneId) {
609
- logger$11.debug("Saving editor pane ID to conf key: wezterm.targetPane.pane_{targetPaneId}", { targetPaneId });
610
- try {
611
- conf.set(`wezterm.targetPane.pane_${targetPaneId}`, { editorPaneId });
612
- } catch (error) {
613
- logger$11.debug("saveEditorPaneId failed: {error}", { error });
614
- }
906
+ async function focusPane(paneId) {
907
+ await execAsync(`wezterm cli activate-pane --pane-id '${paneId}'`);
615
908
  }
616
- async function getEditorPaneId(targetPaneId) {
617
- try {
618
- const data = conf.get(`wezterm.targetPane.pane_${targetPaneId}`);
619
- if (typeof data === "object" && data !== null && "editorPaneId" in data) return String(data.editorPaneId);
620
- return "";
621
- } catch (error) {
622
- logger$11.debug("getEditorPaneId failed: {error}", { error });
623
- return "";
624
- }
909
+ async function sendKey(paneId, key) {
910
+ await execAsync(`wezterm cli send-text --no-paste --pane-id '${paneId}' $'${key}'`);
625
911
  }
626
- async function clearEditorPaneId(targetPaneId) {
627
- try {
628
- const editorPaneId = await getEditorPaneId(targetPaneId);
629
- conf.delete(`wezterm.targetPane.pane_${targetPaneId}`);
630
- if (editorPaneId) conf.delete(`wezterm.editorPane.pane_${editorPaneId}`);
631
- } catch (error) {
632
- logger$11.debug("clearEditorPaneId failed: {error}", { error });
633
- }
912
+ async function sendTextChunk(paneId, chunk) {
913
+ await execAsync(`wezterm cli send-text --no-paste --pane-id '${paneId}' -- '${chunk.replace(/'/g, "'\\''")}'`);
634
914
  }
635
- async function focusPane(paneId) {
636
- await execAsync(`wezterm cli activate-pane --pane-id '${paneId}'`);
915
+ function logContentSent(paneId, chunkCount) {
916
+ logger$11.debug("Content sent to wezterm pane: {paneId} ({chunks} chunk(s))", {
917
+ paneId,
918
+ chunks: chunkCount
919
+ });
637
920
  }
638
- async function markAsEditorPane(editorPaneId, targetPaneIds) {
639
- try {
640
- const uniqueTargetPaneIds = [...new Set(targetPaneIds)];
641
- conf.set(`wezterm.editorPane.pane_${editorPaneId}`, { targetPaneIds: uniqueTargetPaneIds });
642
- for (const targetPaneId of uniqueTargetPaneIds) await saveEditorPaneId(targetPaneId, editorPaneId);
643
- } catch (error) {
644
- logger$11.debug("markAsEditorPane failed: {error}", { error });
921
+
922
+ //#endregion
923
+ //#region src/backends/wezterm.ts
924
+ const weztermBackend = defineBackend({
925
+ name: "wezterm",
926
+ getCurrentPaneId: () => getCurrentPaneId(),
927
+ paneExists: (paneId) => checkPaneExists(paneId),
928
+ focusPane: (paneId) => focusPane(paneId),
929
+ sendText: (paneId, chunk) => sendTextChunk(paneId, chunk),
930
+ sendKey: (paneId, key) => sendKey(paneId, key),
931
+ logContentSent: (paneId, chunkCount) => logContentSent(paneId, chunkCount),
932
+ maxChunkBytes: WEZTERM_SEND_CHUNK_BYTES,
933
+ defaultSubmitKey: "\\r",
934
+ quoteSeparator: "\n\n",
935
+ collectInput: "argument",
936
+ quietResumeErrors: "editorFocus",
937
+ traceResume: true,
938
+ storageNamespace: () => "wezterm",
939
+ state: confStore(() => "wezterm", {
940
+ wholeTargetRecord: true,
941
+ logCategory: ["editprompt", "wezterm"]
942
+ })
943
+ });
944
+
945
+ //#endregion
946
+ //#region src/backends/registry.ts
947
+ /** Used when nothing is configured and no backend detects its environment. */
948
+ const FALLBACK_BACKEND_NAME = "tmux";
949
+ const BUILTIN_BACKENDS = [
950
+ tmuxBackend,
951
+ weztermBackend,
952
+ herdrBackend,
953
+ niwatermBackend,
954
+ rexBackend
955
+ ];
956
+ const backends = new Map(BUILTIN_BACKENDS.map((b) => [b.name, b]));
957
+ function isBuiltinBackendName(name) {
958
+ return BUILTIN_BACKENDS.some((backend) => backend.name === name);
959
+ }
960
+ function registerBackend(backend) {
961
+ if (backends.has(backend.name)) throw new Error(`A backend named '${backend.name}' is already registered`);
962
+ backends.set(backend.name, backend);
963
+ }
964
+ function hasBackend(name) {
965
+ return typeof name === "string" && backends.has(name);
966
+ }
967
+ function listBackendNames() {
968
+ return [...backends.keys()];
969
+ }
970
+ function getBackend(name) {
971
+ const backend = backends.get(name);
972
+ if (!backend) throw new Error(`Invalid multiplexer type '${name}'. Supported values: ${listBackendNames().join(", ")}`);
973
+ return backend;
974
+ }
975
+ function detectBackendName(env = process.env) {
976
+ for (const backend of backends.values()) if (backend.detect?.(env)) return backend.name;
977
+ return FALLBACK_BACKEND_NAME;
978
+ }
979
+ /**
980
+ * Resolves the backend name from, in order: the explicit `--mux` value,
981
+ * `EDITPROMPT_MUX`, and auto-detection. Throws for an unknown name.
982
+ */
983
+ function resolveBackendName(explicit, env = process.env) {
984
+ const value = explicit || env.EDITPROMPT_MUX || detectBackendName(env);
985
+ if (!hasBackend(value)) {
986
+ const displayValue = typeof value === "string" ? value : `<${typeof value}>`;
987
+ throw new Error(`Invalid multiplexer type '${displayValue}'. Supported values: ${listBackendNames().join(", ")}`);
645
988
  }
989
+ return value;
646
990
  }
647
- async function getTargetPaneIds(editorPaneId) {
648
- try {
649
- const data = conf.get(`wezterm.editorPane.pane_${editorPaneId}`);
650
- if (typeof data === "object" && data !== null && "targetPaneIds" in data) {
651
- const targetPaneIds = data.targetPaneIds;
652
- if (Array.isArray(targetPaneIds)) return targetPaneIds.map((id) => String(id));
991
+
992
+ //#endregion
993
+ //#region src/config/userConfig.ts
994
+ const RC_FILE = ".editpromptrc";
995
+ /**
996
+ * The directory holding the configuration file: `EDITPROMPT_CONFIG_DIR`, or
997
+ * `editprompt` under `XDG_CONFIG_HOME` (default `~/.config`).
998
+ */
999
+ function getConfigDir(env = process.env) {
1000
+ const explicit = env.EDITPROMPT_CONFIG_DIR?.trim();
1001
+ if (explicit) return explicit;
1002
+ return join(env.XDG_CONFIG_HOME?.trim() || join(homedir(), ".config"), "editprompt");
1003
+ }
1004
+ /**
1005
+ * Loads the configuration with c12, so every format c12 supports works:
1006
+ * `config.{json,jsonc,json5,yaml,yml,toml,js,ts,mjs,cjs,mts,cts}` in the
1007
+ * config directory, and the `.editpromptrc` RC file there, in
1008
+ * `$XDG_CONFIG_HOME` or in the home directory.
1009
+ *
1010
+ * Configuration is read from the user's config directory only, never from the
1011
+ * current working directory: a repository must not be able to change how
1012
+ * editprompt talks to the terminal, or run code through a config file.
1013
+ */
1014
+ async function loadUserConfig(configDir = getConfigDir()) {
1015
+ const { loadConfig } = await import("c12");
1016
+ const { config } = await loadConfig({
1017
+ name: "editprompt",
1018
+ cwd: configDir,
1019
+ configFile: "config",
1020
+ rcFile: RC_FILE,
1021
+ globalRc: true,
1022
+ dotenv: false,
1023
+ packageJson: false
1024
+ });
1025
+ return config ?? {};
1026
+ }
1027
+ /**
1028
+ * Registers the backends declared in the configuration. Returns one message
1029
+ * per backend that could not be registered, so that a broken entry does not
1030
+ * take the built-in backends down with it.
1031
+ */
1032
+ function registerConfiguredBackends(config) {
1033
+ const errors = [];
1034
+ const { backends } = config;
1035
+ if (backends === void 0) return errors;
1036
+ if (typeof backends !== "object" || backends === null || Array.isArray(backends)) return ["backends must be an object keyed by backend name"];
1037
+ for (const [name, spec] of Object.entries(backends)) {
1038
+ if (isBuiltinBackendName(name)) {
1039
+ errors.push(`backends.${name}: '${name}' is a built-in backend and cannot be redefined`);
1040
+ continue;
1041
+ }
1042
+ try {
1043
+ registerBackend(createCommandBackend(name, spec));
1044
+ } catch (error) {
1045
+ errors.push(error instanceof Error ? error.message : String(error));
653
1046
  }
654
- return [];
655
- } catch (error) {
656
- logger$11.debug("getTargetPaneIds failed: {error}", { error });
657
- return [];
658
1047
  }
1048
+ return errors;
659
1049
  }
660
- function isEditorPaneFromConf(paneId) {
661
- try {
662
- return conf.has(`wezterm.editorPane.pane_${paneId}`);
663
- } catch (error) {
664
- logger$11.debug("isEditorPaneFromConf failed: {error}", { error });
665
- return false;
1050
+ /**
1051
+ * Returns the `--mux` value given on the command line, ignoring anything after
1052
+ * `--` (content passed to input, collect or stash).
1053
+ */
1054
+ function findMuxArgument(argv) {
1055
+ for (let i = 0; i < argv.length; i++) {
1056
+ const arg = argv[i];
1057
+ if (arg === "--") return;
1058
+ if (arg === "--mux" || arg === "-m") return argv[i + 1];
1059
+ if (arg.startsWith("--mux=")) return arg.slice(6);
666
1060
  }
667
1061
  }
668
- async function appendToQuoteText(paneId, content) {
1062
+ /**
1063
+ * Decides whether this invocation can use a configured backend at all, so that
1064
+ * the common case skips loading c12. Configured backends cannot replace
1065
+ * built-in ones and are detected after them, so they only matter when an
1066
+ * explicit `--mux`/EDITPROMPT_MUX names a non-built-in backend, or when
1067
+ * auto-detection would otherwise fall back to tmux.
1068
+ */
1069
+ function mayUseConfiguredBackend(argv, env = process.env) {
1070
+ const explicit = findMuxArgument(argv) || env.EDITPROMPT_MUX;
1071
+ if (explicit) return !isBuiltinBackendName(explicit);
1072
+ return detectBackendName(env) === FALLBACK_BACKEND_NAME;
1073
+ }
1074
+ /** Returns true when one of the files c12 would read exists. */
1075
+ function hasConfigFiles(configDir = getConfigDir(), env = process.env) {
1076
+ return existsSync(configDir) || existsSync(join(env.XDG_CONFIG_HOME?.trim() || homedir(), RC_FILE));
1077
+ }
1078
+ /** Loads the configuration and registers its backends, reporting problems on stderr. */
1079
+ async function setupUserConfig(configDir = getConfigDir()) {
1080
+ if (!hasConfigFiles(configDir)) return;
1081
+ let config;
669
1082
  try {
670
- const data = conf.get(`wezterm.targetPane.pane_${paneId}`);
671
- let newData;
672
- if (typeof data === "object" && data !== null) {
673
- const existingQuoteText = "quote_text" in data ? String(data.quote_text) : "";
674
- const newQuoteText = existingQuoteText.trim() !== "" ? `${existingQuoteText}\n\n${content}` : content;
675
- newData = {
676
- ...data,
677
- quote_text: newQuoteText
678
- };
679
- } else newData = { quote_text: content };
680
- conf.set(`wezterm.targetPane.pane_${paneId}`, newData);
1083
+ config = await loadUserConfig(configDir);
681
1084
  } catch (error) {
682
- logger$11.debug("appendToQuoteText failed: {error}", { error });
1085
+ console.error(`editprompt: failed to load the configuration: ${error instanceof Error ? error.message : String(error)}`);
1086
+ return;
683
1087
  }
1088
+ for (const message of registerConfiguredBackends(config)) console.error(`editprompt: invalid configuration: ${message}`);
684
1089
  }
685
- async function getQuoteText(paneId) {
686
- try {
687
- const data = conf.get(`wezterm.targetPane.pane_${paneId}`);
688
- if (typeof data === "object" && data !== null && "quote_text" in data) return String(data.quote_text);
689
- return "";
690
- } catch (error) {
691
- logger$11.debug("getQuoteText failed: {error}", { error });
692
- return "";
693
- }
1090
+
1091
+ //#endregion
1092
+ //#region src/modules/logger.ts
1093
+ function resolveLogFilePath(options) {
1094
+ if (options.logFile) return options.logFile;
1095
+ const envPath = process.env.EDITPROMPT_LOG_FILE;
1096
+ if (envPath) return envPath;
694
1097
  }
695
- async function clearQuoteText(paneId) {
696
- try {
697
- const key = `wezterm.targetPane.pane_${paneId}.quote_text`;
698
- if (conf.has(key)) conf.delete(key);
699
- } catch (error) {
700
- logger$11.debug("clearQuoteText failed: {error}", { error });
701
- }
1098
+ function resolveFileLogLevel(options) {
1099
+ if (options.verbose) return "debug";
1100
+ const envLevel = process.env.EDITPROMPT_LOG_LEVEL;
1101
+ if (envLevel) return parseLogLevel(envLevel);
1102
+ return "info";
702
1103
  }
703
- async function sendKeyToWeztermPane(paneId, key, delay = 1e3) {
704
- await new Promise((resolve) => setTimeout(resolve, delay));
705
- await execAsync(`wezterm cli send-text --no-paste --pane-id '${paneId}' $'${key}'`);
1104
+ function resolveLogLevel(options) {
1105
+ if (options.quiet) return null;
1106
+ if (options.verbose) return "debug";
1107
+ const envLevel = process.env.EDITPROMPT_LOG_LEVEL;
1108
+ if (envLevel) return parseLogLevel(envLevel);
1109
+ return "info";
706
1110
  }
707
- async function inputToWeztermPane(paneId, content) {
708
- const chunks = splitByByteSize(content, WEZTERM_SEND_CHUNK_BYTES);
709
- for (const chunk of chunks) await execAsync(`wezterm cli send-text --no-paste --pane-id '${paneId}' -- '${chunk.replace(/'/g, "'\\''")}'`);
710
- logger$11.debug("Content sent to wezterm pane: {paneId} ({chunks} chunk(s))", {
711
- paneId,
712
- chunks: chunks.length
1111
+ function setupLogger(options = {}) {
1112
+ const stderrLevel = resolveLogLevel(options);
1113
+ const stderrFormatter = getAnsiColorFormatter({
1114
+ timestamp: "time-timezone",
1115
+ level: "ABBR"
1116
+ });
1117
+ const rawStderrSink = (record) => {
1118
+ process.stderr.write(stderrFormatter(record));
1119
+ };
1120
+ const sinks = { stderr: stderrLevel === null ? () => {} : withFilter(rawStderrSink, stderrLevel) };
1121
+ const loggerSinks = ["stderr"];
1122
+ const logFilePath = resolveLogFilePath(options);
1123
+ const fileLogLevel = resolveFileLogLevel(options);
1124
+ if (logFilePath) try {
1125
+ appendFileSync(logFilePath, "");
1126
+ const fileFormatter = getTextFormatter({
1127
+ timestamp: "date-time-timezone",
1128
+ level: "ABBR"
1129
+ });
1130
+ const rawFileSink = (record) => {
1131
+ appendFileSync(logFilePath, fileFormatter(record));
1132
+ };
1133
+ sinks.file = withFilter(rawFileSink, fileLogLevel);
1134
+ loggerSinks.push("file");
1135
+ } catch {
1136
+ process.stderr.write(`Warning: Cannot write to log file '${logFilePath}', continuing without file logging.\n`);
1137
+ }
1138
+ configureSync({
1139
+ sinks,
1140
+ loggers: [{
1141
+ category: ["editprompt"],
1142
+ sinks: loggerSinks,
1143
+ lowestLevel: "trace"
1144
+ }, {
1145
+ category: ["logtape", "meta"],
1146
+ lowestLevel: null
1147
+ }]
713
1148
  });
714
1149
  }
715
1150
 
1151
+ //#endregion
1152
+ //#region package.json
1153
+ var version = "2.0.0";
1154
+
1155
+ //#endregion
1156
+ //#region src/core/quote.ts
1157
+ /** Appends a quote to the target pane's quote buffer. */
1158
+ async function appendQuote(backend, targetPaneId, content) {
1159
+ const existing = await backend.state.getQuote(targetPaneId);
1160
+ const quote = existing.trim() !== "" ? `${existing}${backend.quoteSeparator}${content}` : content;
1161
+ await backend.state.setQuote(targetPaneId, quote);
1162
+ }
1163
+ /** Returns the target pane's quote buffer and clears it. */
1164
+ async function takeQuote(backend, targetPaneId) {
1165
+ const quote = await backend.state.getQuote(targetPaneId);
1166
+ await backend.state.removeQuote(targetPaneId);
1167
+ return quote;
1168
+ }
1169
+
716
1170
  //#endregion
717
1171
  //#region src/utils/argumentParser.ts
718
1172
  /**
@@ -852,31 +1306,8 @@ function processQuoteText(text, options) {
852
1306
 
853
1307
  //#endregion
854
1308
  //#region src/utils/mux.ts
855
- const SUPPORTED_MUXES = [
856
- "tmux",
857
- "wezterm",
858
- "herdr",
859
- "niwaterm"
860
- ];
861
- function isMuxType(value) {
862
- return value === "tmux" || value === "wezterm" || value === "herdr" || value === "niwaterm";
863
- }
864
- function hasValue(value) {
865
- return typeof value === "string" && value.trim() !== "";
866
- }
867
- function isHerdrEnvironment(env = process.env) {
868
- return hasValue(env.HERDR_SOCKET_PATH) && (hasValue(env.HERDR_PANE_ID) || hasValue(env.HERDR_ACTIVE_PANE_ID));
869
- }
870
- function isNiwatermEnvironment(env = process.env) {
871
- return hasValue(env.NIWATERM_TAB_ID);
872
- }
873
1309
  function resolveMux(explicitMux, env = process.env) {
874
- const muxValue = explicitMux || env.EDITPROMPT_MUX || (isHerdrEnvironment(env) ? "herdr" : isNiwatermEnvironment(env) ? "niwaterm" : "tmux");
875
- if (!isMuxType(muxValue)) {
876
- const displayValue = typeof muxValue === "string" ? muxValue : `<${typeof muxValue}>`;
877
- throw new Error(`Invalid multiplexer type '${displayValue}'. Supported values: ${SUPPORTED_MUXES.join(", ")}`);
878
- }
879
- return muxValue;
1310
+ return resolveBackendName(explicitMux, env);
880
1311
  }
881
1312
 
882
1313
  //#endregion
@@ -884,7 +1315,7 @@ function resolveMux(explicitMux, env = process.env) {
884
1315
  const logger$10 = getLogger(["editprompt"]);
885
1316
  const ARG_MUX = {
886
1317
  short: "m",
887
- description: "Multiplexer type (tmux, wezterm, herdr, or niwaterm; auto-detects Herdr and niwaterm, otherwise tmux)",
1318
+ description: "Multiplexer type (tmux, wezterm, herdr, niwaterm, or rex; auto-detects Herdr, niwaterm and Rex, otherwise tmux)",
888
1319
  type: "string"
889
1320
  };
890
1321
  const ARG_TARGET_PANE_SINGLE = {
@@ -994,10 +1425,7 @@ async function runCollectMode(mux, targetPaneId, rawContent, outputs = ["buffer"
994
1425
  if (rawContent !== void 0) selection = rawContent;
995
1426
  else selection = await readStdin();
996
1427
  const processedText = processQuoteText(selection, { withQuote });
997
- for (const output of outputs) if (output === "buffer") if (mux === "tmux") await appendToQuoteVariable(targetPaneId, processedText);
998
- else if (mux === "wezterm") await appendToQuoteText(targetPaneId, processedText);
999
- else if (mux === "niwaterm") await appendToQuoteVariable$1(targetPaneId, processedText);
1000
- else await appendToQuoteText$1(targetPaneId, processedText);
1428
+ for (const output of outputs) if (output === "buffer") await appendQuote(getBackend(mux), targetPaneId, processedText);
1001
1429
  else if (output === "stdout") process.stdout.write(processedText);
1002
1430
  } catch (error) {
1003
1431
  logger$9.error(`${error instanceof Error ? error.message : "Unknown error"}`);
@@ -1027,7 +1455,7 @@ const collectCommand = define({
1027
1455
  const outputs = normalizeCollectOutputs(ctx.values.output);
1028
1456
  const withQuote = !ctx.values["no-quote"];
1029
1457
  let rawContent;
1030
- if (mux === "wezterm" || mux === "herdr" || mux === "niwaterm") {
1458
+ if (getBackend(mux).collectInput === "argument") {
1031
1459
  rawContent = extractRawContent(ctx.rest, ctx.positionals);
1032
1460
  if (rawContent === void 0) {
1033
1461
  logger$9.error(`Text content is required for collect mode with ${mux}. Use: editprompt collect --mux ${mux} --target-pane <id> -- "<text>"`);
@@ -1053,111 +1481,124 @@ function readSendConfig() {
1053
1481
  }
1054
1482
 
1055
1483
  //#endregion
1056
- //#region src/modes/dump.ts
1057
- const logger$8 = getLogger(["editprompt", "dump"]);
1058
- async function runDumpMode() {
1059
- try {
1060
- const config = readSendConfig();
1061
- let currentPaneId;
1062
- let isEditor;
1063
- if (config.mux === "tmux") {
1064
- currentPaneId = await getCurrentPaneId$1();
1065
- isEditor = await isEditorPane(currentPaneId);
1066
- } else if (config.mux === "wezterm") {
1067
- currentPaneId = await getCurrentPaneId();
1068
- isEditor = isEditorPaneFromConf(currentPaneId);
1069
- } else if (config.mux === "niwaterm") {
1070
- currentPaneId = await getCurrentPaneId$2();
1071
- isEditor = await isEditorPane$1(currentPaneId);
1072
- } else {
1073
- currentPaneId = await getCurrentPaneId$3();
1074
- isEditor = isEditorPaneFromConf$1(currentPaneId);
1075
- }
1076
- if (!isEditor) {
1077
- logger$8.error("Current pane is not an editor pane");
1078
- process.exit(1);
1079
- }
1080
- let targetPanes;
1081
- if (config.mux === "tmux") targetPanes = await getTargetPaneIds$1(currentPaneId);
1082
- else if (config.mux === "wezterm") targetPanes = await getTargetPaneIds(currentPaneId);
1083
- else if (config.mux === "niwaterm") targetPanes = await getTargetPaneIds$2(currentPaneId);
1084
- else targetPanes = await getTargetPaneIds$3(currentPaneId);
1085
- if (targetPanes.length === 0) {
1086
- logger$8.error("No target panes registered for this editor pane");
1087
- process.exit(1);
1088
- }
1089
- const quoteContents = [];
1090
- for (const targetPane of targetPanes) {
1091
- let content;
1092
- if (config.mux === "tmux") {
1093
- content = await getQuoteVariableContent(targetPane);
1094
- await clearQuoteVariable(targetPane);
1095
- } else if (config.mux === "wezterm") {
1096
- content = await getQuoteText(targetPane);
1097
- await clearQuoteText(targetPane);
1098
- } else if (config.mux === "niwaterm") {
1099
- content = await getQuoteVariableContent$1(targetPane);
1100
- await clearQuoteVariable$1(targetPane);
1101
- } else {
1102
- content = await getQuoteText$1(targetPane);
1103
- await clearQuoteText$1(targetPane);
1104
- }
1105
- if (content.trim() !== "") quoteContents.push(content);
1106
- }
1107
- const combinedContent = quoteContents.join("\n");
1108
- process.stdout.write(combinedContent.replace(/\n{3,}$/, "\n\n"));
1109
- process.exit(0);
1110
- } catch (error) {
1111
- logger$8.error(`${error instanceof Error ? error.message : "Unknown error"}`);
1112
- process.exit(1);
1113
- }
1484
+ //#region src/core/link.ts
1485
+ /**
1486
+ * Marks a pane as an editor pane serving the target panes, and records the
1487
+ * editor pane on each target pane so that resume can find it.
1488
+ */
1489
+ async function markAsEditorPane(backend, editorPaneId, targetPaneIds) {
1490
+ const uniqueTargetPaneIds = [...new Set(targetPaneIds)];
1491
+ await backend.state.setTargetPaneIds(editorPaneId, uniqueTargetPaneIds);
1492
+ for (const targetPaneId of uniqueTargetPaneIds) await backend.state.setEditorPaneId(targetPaneId, editorPaneId);
1114
1493
  }
1115
- const dumpCommand = define({
1116
- name: "dump",
1117
- description: "Output and clear collected quoted text from environment variables",
1118
- args: {
1119
- "log-file": ARG_LOG_FILE,
1120
- quiet: ARG_QUIET,
1121
- verbose: ARG_VERBOSE
1122
- },
1123
- async run(ctx) {
1124
- setupLogger({
1125
- quiet: Boolean(ctx.values.quiet),
1126
- verbose: Boolean(ctx.values.verbose),
1127
- logFile: ctx.values["log-file"]
1128
- });
1129
- await runDumpMode();
1494
+ function isEditorPane(backend, paneId) {
1495
+ return backend.state.isEditorPane(paneId);
1496
+ }
1497
+ function getTargetPaneIds(backend, editorPaneId) {
1498
+ return backend.state.getTargetPaneIds(editorPaneId);
1499
+ }
1500
+ /**
1501
+ * Clears the editor pane registered on a target pane.
1502
+ *
1503
+ * When `expectedEditorPaneId` is given and the target pane has meanwhile been
1504
+ * linked to a newer editor pane, the newer link is kept and only the expected
1505
+ * editor pane is forgotten. This lets an editor that exits late leave a newer
1506
+ * editor's registration alone.
1507
+ */
1508
+ async function clearEditorLink(backend, targetPaneId, expectedEditorPaneId) {
1509
+ if (!expectedEditorPaneId && backend.state.keepsEditorPaneMarks) {
1510
+ await backend.state.removeEditorPaneId(targetPaneId);
1511
+ return;
1130
1512
  }
1131
- });
1513
+ const editorPaneId = await backend.state.getEditorPaneId(targetPaneId);
1514
+ if (expectedEditorPaneId && editorPaneId !== expectedEditorPaneId) {
1515
+ await backend.state.removeEditorPane(expectedEditorPaneId);
1516
+ return;
1517
+ }
1518
+ await backend.state.removeEditorPaneId(targetPaneId);
1519
+ if (editorPaneId) await backend.state.removeEditorPane(editorPaneId);
1520
+ }
1521
+ var EditorContextError = class extends Error {
1522
+ name = "EditorContextError";
1523
+ };
1524
+ /**
1525
+ * Resolves the editor pane editprompt runs in and the target panes registered
1526
+ * on it. Throws EditorContextError when the current pane is not an editor pane
1527
+ * or has no target panes.
1528
+ */
1529
+ async function resolveEditorContext(backend) {
1530
+ const editorPaneId = await backend.getCurrentPaneId();
1531
+ if (!await backend.state.isEditorPane(editorPaneId)) throw new EditorContextError("Current pane is not an editor pane");
1532
+ const targetPaneIds = await backend.state.getTargetPaneIds(editorPaneId);
1533
+ if (targetPaneIds.length === 0) throw new EditorContextError("No target panes registered for this editor pane");
1534
+ return {
1535
+ editorPaneId,
1536
+ targetPaneIds
1537
+ };
1538
+ }
1132
1539
 
1133
1540
  //#endregion
1134
- //#region src/utils/contentProcessor.ts
1135
- function processContent(content) {
1136
- let processed = content.replace(/\n$/, "");
1137
- if (/@[^\n]*$/.test(processed)) processed += " ";
1138
- return processed;
1541
+ //#region src/utils/contentChunker.ts
1542
+ /**
1543
+ * Split content into chunks not exceeding maxBytes (UTF-8 byte count),
1544
+ * never breaking a Unicode code point across chunks.
1545
+ *
1546
+ * Content shorter than maxBytes is returned as a single chunk, so short
1547
+ * input keeps the previous behavior of being sent in one shot.
1548
+ */
1549
+ function splitByByteSize(content, maxBytes) {
1550
+ if (content === "") return [];
1551
+ const chunks = [];
1552
+ let current = "";
1553
+ let currentBytes = 0;
1554
+ for (const char of content) {
1555
+ const charBytes = Buffer.byteLength(char, "utf8");
1556
+ if (current !== "" && currentBytes + charBytes > maxBytes) {
1557
+ chunks.push(current);
1558
+ current = "";
1559
+ currentBytes = 0;
1560
+ }
1561
+ current += char;
1562
+ currentBytes += charBytes;
1563
+ }
1564
+ if (current !== "") chunks.push(current);
1565
+ return chunks;
1139
1566
  }
1140
1567
 
1141
1568
  //#endregion
1142
- //#region src/modes/common.ts
1143
- const logger$7 = getLogger(["editprompt", "delivery"]);
1569
+ //#region src/core/delivery.ts
1570
+ const logger$8 = getLogger(["editprompt", "delivery"]);
1144
1571
  async function copyToClipboard(content) {
1145
1572
  await clipboardy.write(content);
1146
1573
  }
1147
- async function inputContentToPane(content, mux, targetPaneId) {
1148
- if (mux === "wezterm") await inputToWeztermPane(targetPaneId, content);
1149
- else if (mux === "herdr") await inputToHerdrPane(targetPaneId, content);
1150
- else if (mux === "niwaterm") await inputToNiwatermPane(targetPaneId, content);
1151
- else await inputToTmuxPane(targetPaneId, content);
1574
+ /**
1575
+ * Types content into a pane without changing focus. Long content is split
1576
+ * into chunks the backend accepts and sent in order.
1577
+ */
1578
+ async function sendContentToPane(backend, paneId, content) {
1579
+ await backend.beforeSendText?.(paneId);
1580
+ const chunks = splitByByteSize(content, backend.maxChunkBytes);
1581
+ for (const chunk of chunks) await backend.sendText(paneId, chunk);
1582
+ if (backend.logContentSent) backend.logContentSent(paneId, chunks.length);
1583
+ else logger$8.debug("Content sent to {backend} pane: {paneId} ({chunks} chunk(s))", {
1584
+ backend: backend.name,
1585
+ paneId,
1586
+ chunks: chunks.length
1587
+ });
1588
+ }
1589
+ /**
1590
+ * Sends a key after a delay. The delay keeps some CLIs (e.g., codex) from
1591
+ * treating the key as part of a paste.
1592
+ */
1593
+ async function sendKeyToPane(backend, paneId, key, delay = 1e3) {
1594
+ await new Promise((resolve) => setTimeout(resolve, delay));
1595
+ await backend.sendKey(paneId, key);
1152
1596
  }
1153
- async function focusFirstSuccessPane(mux, targetPanes, failedPanes) {
1597
+ async function focusFirstSuccessPane(backend, targetPanes, failedPanes) {
1154
1598
  const firstSuccessPane = targetPanes.find((p) => !failedPanes.includes(p));
1155
- if (firstSuccessPane) if (mux === "tmux") await focusPane$1(firstSuccessPane);
1156
- else if (mux === "wezterm") await focusPane(firstSuccessPane);
1157
- else if (mux === "niwaterm") await focusPane$2(firstSuccessPane);
1158
- else await focusPane$3(firstSuccessPane);
1599
+ if (firstSuccessPane) await backend.focusPane(firstSuccessPane);
1159
1600
  }
1160
- async function handleContentDelivery(content, mux, targetPanes) {
1601
+ async function handleContentDelivery(content, backend, targetPanes) {
1161
1602
  if (!content) return {
1162
1603
  successCount: 0,
1163
1604
  totalCount: 0,
@@ -1168,9 +1609,9 @@ async function handleContentDelivery(content, mux, targetPanes) {
1168
1609
  if (targetPanes.length === 0) {
1169
1610
  try {
1170
1611
  await copyToClipboard(content);
1171
- logger$7.info("Content copied to clipboard.");
1612
+ logger$8.info("Content copied to clipboard.");
1172
1613
  } catch (error) {
1173
- logger$7.warn(`Failed to copy to clipboard: ${error instanceof Error ? error.message : "Unknown error"}`);
1614
+ logger$8.warn(`Failed to copy to clipboard: ${error instanceof Error ? error.message : "Unknown error"}`);
1174
1615
  }
1175
1616
  return {
1176
1617
  successCount: 0,
@@ -1182,13 +1623,13 @@ async function handleContentDelivery(content, mux, targetPanes) {
1182
1623
  }
1183
1624
  const results = [];
1184
1625
  for (const targetPane of targetPanes) try {
1185
- await inputContentToPane(content, mux, targetPane);
1626
+ await sendContentToPane(backend, targetPane, content);
1186
1627
  results.push({
1187
1628
  pane: targetPane,
1188
1629
  success: true
1189
1630
  });
1190
1631
  } catch (error) {
1191
- logger$7.warn(`Failed to send to pane ${targetPane}: ${error instanceof Error ? error.message : "Unknown error"}`);
1632
+ logger$8.warn(`Failed to send to pane ${targetPane}: ${error instanceof Error ? error.message : "Unknown error"}`);
1192
1633
  results.push({
1193
1634
  pane: targetPane,
1194
1635
  success: false
@@ -1198,13 +1639,13 @@ async function handleContentDelivery(content, mux, targetPanes) {
1198
1639
  const failedPanes = results.filter((r) => !r.success).map((r) => r.pane);
1199
1640
  const allSuccess = successCount === targetPanes.length;
1200
1641
  const allFailed = successCount === 0;
1201
- if (allSuccess) logger$7.info("Content sent successfully to all panes!");
1642
+ if (allSuccess) logger$8.info("Content sent successfully to all panes!");
1202
1643
  else if (allFailed) {
1203
- logger$7.error("All target panes failed to receive content.");
1204
- logger$7.info("Falling back to clipboard...");
1644
+ logger$8.error("All target panes failed to receive content.");
1645
+ logger$8.info("Falling back to clipboard...");
1205
1646
  await copyToClipboard(content);
1206
- logger$7.info("Content copied to clipboard.");
1207
- } else logger$7.warn(`Content sent to ${successCount}/${targetPanes.length} panes. Failed panes: ${failedPanes.join(", ")}`);
1647
+ logger$8.info("Content copied to clipboard.");
1648
+ } else logger$8.warn(`Content sent to ${successCount}/${targetPanes.length} panes. Failed panes: ${failedPanes.join(", ")}`);
1208
1649
  return {
1209
1650
  successCount,
1210
1651
  totalCount: targetPanes.length,
@@ -1214,6 +1655,71 @@ async function handleContentDelivery(content, mux, targetPanes) {
1214
1655
  };
1215
1656
  }
1216
1657
 
1658
+ //#endregion
1659
+ //#region src/modes/common.ts
1660
+ /**
1661
+ * Resolves the editor pane and its target panes for commands that run inside
1662
+ * an editor pane, exiting with an error (logged to the command's logger) when
1663
+ * there are none.
1664
+ */
1665
+ async function requireEditorContext(backend, logger) {
1666
+ try {
1667
+ return await resolveEditorContext(backend);
1668
+ } catch (error) {
1669
+ if (error instanceof EditorContextError) {
1670
+ logger.error(error.message);
1671
+ process.exit(1);
1672
+ }
1673
+ throw error;
1674
+ }
1675
+ }
1676
+
1677
+ //#endregion
1678
+ //#region src/modes/dump.ts
1679
+ const logger$7 = getLogger(["editprompt", "dump"]);
1680
+ async function runDumpMode() {
1681
+ try {
1682
+ const backend = getBackend(readSendConfig().mux);
1683
+ const { targetPaneIds } = await requireEditorContext(backend, logger$7);
1684
+ const quoteContents = [];
1685
+ for (const targetPane of targetPaneIds) {
1686
+ const content = await takeQuote(backend, targetPane);
1687
+ if (content.trim() !== "") quoteContents.push(content);
1688
+ }
1689
+ const combinedContent = quoteContents.join("\n");
1690
+ process.stdout.write(combinedContent.replace(/\n{3,}$/, "\n\n"));
1691
+ process.exit(0);
1692
+ } catch (error) {
1693
+ logger$7.error(`${error instanceof Error ? error.message : "Unknown error"}`);
1694
+ process.exit(1);
1695
+ }
1696
+ }
1697
+ const dumpCommand = define({
1698
+ name: "dump",
1699
+ description: "Output and clear collected quoted text from environment variables",
1700
+ args: {
1701
+ "log-file": ARG_LOG_FILE,
1702
+ quiet: ARG_QUIET,
1703
+ verbose: ARG_VERBOSE
1704
+ },
1705
+ async run(ctx) {
1706
+ setupLogger({
1707
+ quiet: Boolean(ctx.values.quiet),
1708
+ verbose: Boolean(ctx.values.verbose),
1709
+ logFile: ctx.values["log-file"]
1710
+ });
1711
+ await runDumpMode();
1712
+ }
1713
+ });
1714
+
1715
+ //#endregion
1716
+ //#region src/utils/contentProcessor.ts
1717
+ function processContent(content) {
1718
+ let processed = content.replace(/\n$/, "");
1719
+ if (/@[^\n]*$/.test(processed)) processed += " ";
1720
+ return processed;
1721
+ }
1722
+
1217
1723
  //#endregion
1218
1724
  //#region src/modes/input.ts
1219
1725
  const logger$6 = getLogger(["editprompt", "input"]);
@@ -1224,52 +1730,15 @@ async function runInputMode(rawContent, autoSend, sendKey, sendKeyDelay) {
1224
1730
  return;
1225
1731
  }
1226
1732
  const config = readSendConfig();
1227
- let currentPaneId;
1228
- let isEditor;
1229
- if (config.mux === "tmux") {
1230
- currentPaneId = await getCurrentPaneId$1();
1231
- isEditor = await isEditorPane(currentPaneId);
1232
- } else if (config.mux === "wezterm") {
1233
- currentPaneId = await getCurrentPaneId();
1234
- isEditor = isEditorPaneFromConf(currentPaneId);
1235
- } else if (config.mux === "niwaterm") {
1236
- currentPaneId = await getCurrentPaneId$2();
1237
- isEditor = await isEditorPane$1(currentPaneId);
1238
- } else {
1239
- currentPaneId = await getCurrentPaneId$3();
1240
- isEditor = isEditorPaneFromConf$1(currentPaneId);
1241
- }
1242
- if (!isEditor) {
1243
- logger$6.error("Current pane is not an editor pane");
1244
- process.exit(1);
1245
- }
1246
- let targetPanes;
1247
- if (config.mux === "tmux") targetPanes = await getTargetPaneIds$1(currentPaneId);
1248
- else if (config.mux === "wezterm") targetPanes = await getTargetPaneIds(currentPaneId);
1249
- else if (config.mux === "niwaterm") targetPanes = await getTargetPaneIds$2(currentPaneId);
1250
- else targetPanes = await getTargetPaneIds$3(currentPaneId);
1251
- if (targetPanes.length === 0) {
1252
- logger$6.error("No target panes registered for this editor pane");
1253
- process.exit(1);
1254
- }
1733
+ const backend = getBackend(config.mux);
1734
+ const { targetPaneIds: targetPanes } = await requireEditorContext(backend, logger$6);
1255
1735
  if (autoSend) {
1256
- const key = sendKey || (config.mux === "wezterm" ? "\\r" : config.mux === "herdr" ? "enter" : "C-m");
1736
+ const key = sendKey || backend.defaultSubmitKey;
1257
1737
  const delay = /\.(png|webp|avif|jpe?g|gif)\b/i.test(content) ? sendKeyDelay ?? 1e3 : 200;
1258
1738
  let successCount = 0;
1259
1739
  for (const targetPane of targetPanes) try {
1260
- if (config.mux === "wezterm") {
1261
- await inputToWeztermPane(targetPane, content);
1262
- await sendKeyToWeztermPane(targetPane, key, delay);
1263
- } else if (config.mux === "herdr") {
1264
- await inputToHerdrPane(targetPane, content);
1265
- await sendKeyToHerdrPane(targetPane, key, delay);
1266
- } else if (config.mux === "niwaterm") {
1267
- await inputToNiwatermPane(targetPane, content);
1268
- await sendKeyToNiwatermPane(targetPane, key, delay);
1269
- } else {
1270
- await inputToTmuxPane(targetPane, content);
1271
- await sendKeyToTmuxPane(targetPane, key, delay);
1272
- }
1740
+ await sendContentToPane(backend, targetPane, content);
1741
+ await sendKeyToPane(backend, targetPane, key, delay);
1273
1742
  successCount++;
1274
1743
  } catch (error) {
1275
1744
  logger$6.error(`Failed to send to pane ${targetPane}: ${error instanceof Error ? error.message : "Unknown error"}`);
@@ -1286,12 +1755,12 @@ async function runInputMode(rawContent, autoSend, sendKey, sendKeyDelay) {
1286
1755
  return;
1287
1756
  }
1288
1757
  try {
1289
- const result = await handleContentDelivery(content, config.mux, targetPanes);
1758
+ const result = await handleContentDelivery(content, backend, targetPanes);
1290
1759
  if (config.alwaysCopy && !result.allFailed) {
1291
1760
  await copyToClipboard(content);
1292
1761
  logger$6.info("Also copied to clipboard.");
1293
1762
  }
1294
- if (result.successCount > 0) await focusFirstSuccessPane(config.mux, targetPanes, result.failedPanes);
1763
+ if (result.successCount > 0) await focusFirstSuccessPane(backend, targetPanes, result.failedPanes);
1295
1764
  if (result.allFailed) process.exit(1);
1296
1765
  } catch (error) {
1297
1766
  logger$6.error(`${error instanceof Error ? error.message : "Unknown error"}`);
@@ -1424,22 +1893,11 @@ async function openEditorAndGetContent(editorOption, envVars, sendConfig) {
1424
1893
  //#region src/modes/openEditor.ts
1425
1894
  const logger$5 = getLogger(["editprompt", "open"]);
1426
1895
  async function runOpenEditorMode(options) {
1427
- let herdrEditorPaneId;
1428
- if (options.targetPanes.length > 0 && options.mux === "tmux") try {
1429
- await markAsEditorPane$1(await getCurrentPaneId$1(), options.targetPanes);
1430
- } catch {}
1431
- else if (options.targetPanes.length > 0 && options.mux === "wezterm") try {
1432
- const currentPaneId = await getCurrentPaneId();
1433
- await markAsEditorPane(currentPaneId, options.targetPanes);
1434
- } catch {}
1435
- else if (options.targetPanes.length > 0 && options.mux === "herdr") try {
1436
- const currentPaneId = await getCurrentPaneId$3();
1437
- herdrEditorPaneId = currentPaneId;
1438
- await markAsEditorPane$3(currentPaneId, options.targetPanes);
1439
- } catch {}
1440
- else if (options.targetPanes.length > 0 && options.mux === "niwaterm") try {
1441
- const currentPaneId = await getCurrentPaneId$2();
1442
- await markAsEditorPane$2(currentPaneId, options.targetPanes);
1896
+ const backend = getBackend(options.mux);
1897
+ let editorPaneId;
1898
+ if (options.targetPanes.length > 0) try {
1899
+ editorPaneId = await backend.getCurrentPaneId();
1900
+ await markAsEditorPane(backend, editorPaneId, options.targetPanes);
1443
1901
  } catch {}
1444
1902
  try {
1445
1903
  const sendConfig = {
@@ -1454,31 +1912,23 @@ async function runOpenEditorMode(options) {
1454
1912
  return;
1455
1913
  }
1456
1914
  try {
1457
- const result = await handleContentDelivery(content, options.mux, options.targetPanes);
1915
+ const result = await handleContentDelivery(content, backend, options.targetPanes);
1458
1916
  console.log("---");
1459
1917
  console.log(content);
1460
1918
  if (options.alwaysCopy && !result.allFailed) {
1461
1919
  await copyToClipboard(content);
1462
1920
  logger$5.info("Also copied to clipboard.");
1463
1921
  }
1464
- if (options.targetPanes.length > 0 && result.successCount > 0) await focusFirstSuccessPane(options.mux, options.targetPanes, result.failedPanes);
1922
+ if (options.targetPanes.length > 0 && result.successCount > 0) await focusFirstSuccessPane(backend, options.targetPanes, result.failedPanes);
1465
1923
  if (!result.allSuccess) process.exit(1);
1466
1924
  } catch (error) {
1467
1925
  logger$5.error(`${error instanceof Error ? error.message : "Unknown error"}`);
1468
1926
  process.exit(1);
1469
1927
  }
1470
1928
  } finally {
1471
- if (options.targetPanes.length > 0 && options.mux === "tmux") try {
1472
- for (const targetPane of options.targetPanes) await clearEditorPaneId$1(targetPane);
1473
- } catch {}
1474
- else if (options.targetPanes.length > 0 && options.mux === "wezterm") try {
1475
- for (const targetPane of options.targetPanes) await clearEditorPaneId(targetPane);
1476
- } catch {}
1477
- else if (options.targetPanes.length > 0 && options.mux === "herdr") try {
1478
- for (const targetPane of options.targetPanes) await clearEditorPaneId$3(targetPane, herdrEditorPaneId);
1479
- } catch {}
1480
- else if (options.targetPanes.length > 0 && options.mux === "niwaterm") try {
1481
- for (const targetPane of options.targetPanes) await clearEditorPaneId$2(targetPane);
1929
+ if (options.targetPanes.length > 0) try {
1930
+ const expectedEditorPaneId = backend.keepNewerEditorLink ? editorPaneId : void 0;
1931
+ for (const targetPane of options.targetPanes) await clearEditorLink(backend, targetPane, expectedEditorPaneId);
1482
1932
  } catch {}
1483
1933
  }
1484
1934
  }
@@ -1519,47 +1969,20 @@ async function runRegisterMode(options) {
1519
1969
  logger$4.error("--target-pane is required for register command");
1520
1970
  process.exit(1);
1521
1971
  }
1972
+ const backend = getBackend(options.mux);
1522
1973
  let editorPaneId;
1523
1974
  if (options.editorPane) editorPaneId = options.editorPane;
1524
- else if (options.mux === "tmux") {
1525
- editorPaneId = await getCurrentPaneId$1();
1526
- if (!await isEditorPane(editorPaneId)) {
1527
- logger$4.error("Current pane is not an editor pane. Please run this command from an editor pane or specify --editor-pane.");
1528
- process.exit(1);
1529
- }
1530
- } else if (options.mux === "wezterm") {
1531
- editorPaneId = await getCurrentPaneId();
1532
- if (!isEditorPaneFromConf(editorPaneId)) {
1533
- logger$4.error("Current pane is not an editor pane. Please run this command from an editor pane or specify --editor-pane.");
1534
- process.exit(1);
1535
- }
1536
- } else if (options.mux === "herdr") {
1537
- editorPaneId = await getCurrentPaneId$3();
1538
- if (!isEditorPaneFromConf$1(editorPaneId)) {
1539
- logger$4.error("Current pane is not an editor pane. Please run this command from an editor pane or specify --editor-pane.");
1540
- process.exit(1);
1541
- }
1542
- } else if (options.mux === "niwaterm") {
1543
- editorPaneId = await getCurrentPaneId$2();
1544
- if (!await isEditorPane$1(editorPaneId)) {
1975
+ else {
1976
+ editorPaneId = await backend.getCurrentPaneId();
1977
+ if (!await isEditorPane(backend, editorPaneId)) {
1545
1978
  logger$4.error("Current pane is not an editor pane. Please run this command from an editor pane or specify --editor-pane.");
1546
1979
  process.exit(1);
1547
1980
  }
1548
- } else {
1549
- logger$4.error("Unsupported multiplexer");
1550
- process.exit(1);
1551
1981
  }
1552
1982
  try {
1553
- let existingPanes = [];
1554
- if (options.mux === "tmux") existingPanes = await getTargetPaneIds$1(editorPaneId);
1555
- else if (options.mux === "wezterm") existingPanes = await getTargetPaneIds(editorPaneId);
1556
- else if (options.mux === "herdr") existingPanes = await getTargetPaneIds$3(editorPaneId);
1557
- else if (options.mux === "niwaterm") existingPanes = await getTargetPaneIds$2(editorPaneId);
1983
+ const existingPanes = await getTargetPaneIds(backend, editorPaneId);
1558
1984
  const mergedTargetPanes = [...new Set([...existingPanes, ...options.targetPanes])];
1559
- if (options.mux === "tmux") await markAsEditorPane$1(editorPaneId, mergedTargetPanes);
1560
- else if (options.mux === "wezterm") await markAsEditorPane(editorPaneId, mergedTargetPanes);
1561
- else if (options.mux === "herdr") await markAsEditorPane$3(editorPaneId, mergedTargetPanes);
1562
- else if (options.mux === "niwaterm") await markAsEditorPane$2(editorPaneId, mergedTargetPanes);
1985
+ await markAsEditorPane(backend, editorPaneId, mergedTargetPanes);
1563
1986
  logger$4.info(`Editor pane ${editorPaneId} registered with target panes: ${mergedTargetPanes.join(", ")}`);
1564
1987
  } catch (error) {
1565
1988
  logger$4.error(`${error instanceof Error ? error.message : "Unknown error"}`);
@@ -1596,124 +2019,74 @@ const registerCommand = define({
1596
2019
  });
1597
2020
 
1598
2021
  //#endregion
1599
- //#region src/modes/resume.ts
1600
- const logger$3 = getLogger(["editprompt", "resume"]);
1601
- async function runResumeMode(targetPane, mux) {
1602
- if (mux === "herdr") try {
1603
- process.exit(await resumeEditorPane(targetPane) ? 0 : 1);
1604
- } catch (error) {
1605
- logger$3.debug("Herdr resume failed: {error}", { error });
1606
- process.exit(1);
2022
+ //#region src/core/resume.ts
2023
+ /** Raised when focusing the editor pane from its target pane fails. */
2024
+ var EditorFocusError = class extends Error {
2025
+ name = "EditorFocusError";
2026
+ constructor(editorPaneId, cause) {
2027
+ super(`Can't focus editor pane ${editorPaneId}: ${cause instanceof Error ? cause.message : String(cause)}`);
2028
+ this.editorPaneId = editorPaneId;
2029
+ this.cause = cause;
1607
2030
  }
1608
- if (mux === "niwaterm") {
1609
- const currentPaneId = await getCurrentPaneId$2();
1610
- if (await isEditorPane$1(currentPaneId)) {
1611
- logger$3.debug("Current pane is an editor pane");
1612
- const originalTargetPaneIds = await getTargetPaneIds$2(currentPaneId);
1613
- if (originalTargetPaneIds.length === 0) {
1614
- logger$3.debug("No target pane IDs found for editor pane");
1615
- process.exit(1);
1616
- }
1617
- let focused = false;
1618
- for (const paneId of originalTargetPaneIds) if (await checkPaneExists$2(paneId)) {
1619
- await focusPane$2(paneId);
1620
- focused = true;
1621
- break;
1622
- }
1623
- if (!focused) {
1624
- logger$3.debug("All target panes do not exist");
1625
- process.exit(1);
1626
- }
1627
- process.exit(0);
1628
- }
1629
- logger$3.debug("Current pane is not an editor pane");
1630
- const editorPaneId = await getEditorPaneId$2(targetPane);
1631
- logger$3.debug("niwaterm editorPaneId: {editorPaneId}", { editorPaneId });
1632
- if (editorPaneId === "") {
1633
- logger$3.debug("Editor pane ID not found");
1634
- process.exit(1);
1635
- }
1636
- if (!await checkPaneExists$2(editorPaneId)) {
1637
- logger$3.debug("Editor pane does not exist");
1638
- await clearEditorPaneId$2(targetPane);
1639
- process.exit(1);
2031
+ };
2032
+ async function switchEditorFocus(backend, targetPaneId, trace = () => {}) {
2033
+ const currentPaneId = await backend.getCurrentPaneId();
2034
+ const editorPaneId = await backend.state.isEditorPane(currentPaneId) ? currentPaneId : backend.resumeAcceptsEditorPane && await backend.state.isEditorPane(targetPaneId) ? targetPaneId : "";
2035
+ if (editorPaneId) {
2036
+ trace("Current pane is an editor pane");
2037
+ const targetPaneIds = await backend.state.getTargetPaneIds(editorPaneId);
2038
+ if (targetPaneIds.length === 0) {
2039
+ trace("No target pane IDs found for editor pane");
2040
+ return false;
1640
2041
  }
1641
- try {
1642
- await focusPane$2(editorPaneId);
1643
- process.exit(0);
1644
- } catch (error) {
1645
- logger$3.debug("Can't focus editorPaneId: {editorPaneId}, error: {error}", {
1646
- editorPaneId,
1647
- error
1648
- });
1649
- process.exit(1);
2042
+ for (const paneId of targetPaneIds) if (await backend.paneExists(paneId)) {
2043
+ await backend.focusPane(paneId);
2044
+ return true;
1650
2045
  }
2046
+ trace("All target panes do not exist");
2047
+ return false;
1651
2048
  }
1652
- if (mux === "wezterm") {
1653
- const currentPaneId = await getCurrentPaneId();
1654
- if (isEditorPaneFromConf(currentPaneId)) {
1655
- logger$3.debug("Current pane is an editor pane");
1656
- const originalTargetPaneIds = await getTargetPaneIds(currentPaneId);
1657
- if (originalTargetPaneIds.length === 0) {
1658
- logger$3.debug("No target pane IDs found for editor pane");
1659
- process.exit(1);
1660
- }
1661
- let focused = false;
1662
- for (const paneId of originalTargetPaneIds) if (await checkPaneExists(paneId)) {
1663
- await focusPane(paneId);
1664
- focused = true;
1665
- break;
1666
- }
1667
- if (!focused) {
1668
- logger$3.debug("All target panes do not exist");
1669
- process.exit(1);
1670
- }
1671
- process.exit(0);
1672
- }
1673
- logger$3.debug("Current pane is not an editor pane");
1674
- const editorPaneId = await getEditorPaneId(targetPane);
1675
- logger$3.debug("wezterm editorPaneId: {editorPaneId}", { editorPaneId });
1676
- if (editorPaneId === "") {
1677
- logger$3.debug("Editor pane ID not found");
1678
- process.exit(1);
1679
- }
1680
- if (!await checkPaneExists(editorPaneId)) {
1681
- logger$3.debug("Editor pane does not exist");
1682
- await clearEditorPaneId(targetPane);
1683
- process.exit(1);
1684
- }
1685
- try {
1686
- await focusPane(editorPaneId);
1687
- process.exit(0);
1688
- } catch (error) {
1689
- logger$3.debug("Can't focus editorPaneId: {editorPaneId}, error: {error}", {
1690
- editorPaneId,
1691
- error
1692
- });
1693
- process.exit(1);
1694
- }
2049
+ trace("Current pane is not an editor pane");
2050
+ const registeredEditorPaneId = await backend.state.getEditorPaneId(targetPaneId);
2051
+ trace(`${backend.name} editorPaneId: {editorPaneId}`, { editorPaneId: registeredEditorPaneId });
2052
+ if (registeredEditorPaneId === "") {
2053
+ trace("Editor pane ID not found");
2054
+ return false;
1695
2055
  }
1696
- const currentPaneId = await getCurrentPaneId$1();
1697
- if (await isEditorPane(currentPaneId)) {
1698
- const originalTargetPaneIds = await getTargetPaneIds$1(currentPaneId);
1699
- if (originalTargetPaneIds.length === 0) process.exit(1);
1700
- let focused = false;
1701
- for (const paneId of originalTargetPaneIds) if (await checkPaneExists$1(paneId)) {
1702
- await focusPane$1(paneId);
1703
- focused = true;
1704
- break;
1705
- }
1706
- if (!focused) process.exit(1);
1707
- process.exit(0);
2056
+ if (!await backend.paneExists(registeredEditorPaneId)) {
2057
+ trace("Editor pane does not exist");
2058
+ await clearEditorLink(backend, targetPaneId);
2059
+ return false;
1708
2060
  }
1709
- const editorPaneId = await getEditorPaneId$1(targetPane);
1710
- if (editorPaneId === "") process.exit(1);
1711
- if (!await checkPaneExists$1(editorPaneId)) {
1712
- await clearEditorPaneId$1(targetPane);
2061
+ try {
2062
+ await backend.focusPane(registeredEditorPaneId);
2063
+ } catch (error) {
2064
+ throw new EditorFocusError(registeredEditorPaneId, error);
2065
+ }
2066
+ return true;
2067
+ }
2068
+
2069
+ //#endregion
2070
+ //#region src/modes/resume.ts
2071
+ const logger$3 = getLogger(["editprompt", "resume"]);
2072
+ async function runResumeMode(targetPane, mux) {
2073
+ const backend = getBackend(mux);
2074
+ let switched;
2075
+ try {
2076
+ switched = await switchEditorFocus(backend, targetPane, backend.traceResume ? (message, properties) => logger$3.debug(message, properties) : void 0);
2077
+ } catch (error) {
2078
+ if (!(backend.quietResumeErrors === "all" || backend.quietResumeErrors === "editorFocus" && error instanceof EditorFocusError)) throw error instanceof EditorFocusError ? error.cause : error;
2079
+ if (error instanceof EditorFocusError) logger$3.debug("Can't focus editorPaneId: {editorPaneId}, error: {error}", {
2080
+ editorPaneId: error.editorPaneId,
2081
+ error: error.cause
2082
+ });
2083
+ else {
2084
+ const label = backend.name.charAt(0).toUpperCase() + backend.name.slice(1);
2085
+ logger$3.debug(`${label} resume failed: {error}`, { error });
2086
+ }
1713
2087
  process.exit(1);
1714
2088
  }
1715
- await focusPane$1(editorPaneId);
1716
- process.exit(0);
2089
+ process.exit(switched ? 0 : 1);
1717
2090
  }
1718
2091
  const resumeCommand = define({
1719
2092
  name: "resume",
@@ -1743,41 +2116,11 @@ async function runPressMode(key, delay = 0) {
1743
2116
  logger$2.error("Key is required");
1744
2117
  process.exit(1);
1745
2118
  }
1746
- const config = readSendConfig();
1747
- let currentPaneId;
1748
- let isEditor;
1749
- if (config.mux === "tmux") {
1750
- currentPaneId = await getCurrentPaneId$1();
1751
- isEditor = await isEditorPane(currentPaneId);
1752
- } else if (config.mux === "wezterm") {
1753
- currentPaneId = await getCurrentPaneId();
1754
- isEditor = isEditorPaneFromConf(currentPaneId);
1755
- } else if (config.mux === "niwaterm") {
1756
- currentPaneId = await getCurrentPaneId$2();
1757
- isEditor = await isEditorPane$1(currentPaneId);
1758
- } else {
1759
- currentPaneId = await getCurrentPaneId$3();
1760
- isEditor = isEditorPaneFromConf$1(currentPaneId);
1761
- }
1762
- if (!isEditor) {
1763
- logger$2.error("Current pane is not an editor pane");
1764
- process.exit(1);
1765
- }
1766
- let targetPanes;
1767
- if (config.mux === "tmux") targetPanes = await getTargetPaneIds$1(currentPaneId);
1768
- else if (config.mux === "wezterm") targetPanes = await getTargetPaneIds(currentPaneId);
1769
- else if (config.mux === "niwaterm") targetPanes = await getTargetPaneIds$2(currentPaneId);
1770
- else targetPanes = await getTargetPaneIds$3(currentPaneId);
1771
- if (targetPanes.length === 0) {
1772
- logger$2.error("No target panes registered for this editor pane");
1773
- process.exit(1);
1774
- }
2119
+ const backend = getBackend(readSendConfig().mux);
2120
+ const { targetPaneIds } = await requireEditorContext(backend, logger$2);
1775
2121
  let successCount = 0;
1776
- for (const targetPane of targetPanes) try {
1777
- if (config.mux === "wezterm") await sendKeyToWeztermPane(targetPane, key, delay);
1778
- else if (config.mux === "herdr") await sendKeyToHerdrPane(targetPane, key, delay);
1779
- else if (config.mux === "niwaterm") await sendKeyToNiwatermPane(targetPane, key, delay);
1780
- else await sendKeyToTmuxPane(targetPane, key, delay);
2122
+ for (const targetPane of targetPaneIds) try {
2123
+ await sendKeyToPane(backend, targetPane, key, delay);
1781
2124
  successCount++;
1782
2125
  } catch (error) {
1783
2126
  logger$2.warn(`Failed to send key to pane ${targetPane}: ${error instanceof Error ? error.message : "Unknown error"}`);
@@ -1820,7 +2163,7 @@ const pressCommand = define({
1820
2163
  //#region src/modes/stash.ts
1821
2164
  const logger$1 = getLogger(["editprompt", "stash"]);
1822
2165
  function getStashKey(mux, targetPaneId) {
1823
- return `${mux === "herdr" ? getStorageNamespace() : mux}.targetPane.pane_${targetPaneId}.stash`;
2166
+ return `${getBackend(mux).storageNamespace()}.targetPane.pane_${targetPaneId}.stash`;
1824
2167
  }
1825
2168
  async function pushStash(mux, targetPaneId, content) {
1826
2169
  const key = (/* @__PURE__ */ new Date()).toISOString();
@@ -1862,37 +2205,10 @@ function dropStash(mux, targetPaneId, key) {
1862
2205
  }
1863
2206
  async function getTargetPaneForStash() {
1864
2207
  const config = readSendConfig();
1865
- let currentPaneId;
1866
- let isEditor;
1867
- if (config.mux === "tmux") {
1868
- currentPaneId = await getCurrentPaneId$1();
1869
- isEditor = await isEditorPane(currentPaneId);
1870
- } else if (config.mux === "wezterm") {
1871
- currentPaneId = await getCurrentPaneId();
1872
- isEditor = isEditorPaneFromConf(currentPaneId);
1873
- } else if (config.mux === "niwaterm") {
1874
- currentPaneId = await getCurrentPaneId$2();
1875
- isEditor = await isEditorPane$1(currentPaneId);
1876
- } else {
1877
- currentPaneId = await getCurrentPaneId$3();
1878
- isEditor = isEditorPaneFromConf$1(currentPaneId);
1879
- }
1880
- if (!isEditor) {
1881
- logger$1.error("Current pane is not an editor pane");
1882
- process.exit(1);
1883
- }
1884
- let targetPanes;
1885
- if (config.mux === "tmux") targetPanes = await getTargetPaneIds$1(currentPaneId);
1886
- else if (config.mux === "wezterm") targetPanes = await getTargetPaneIds(currentPaneId);
1887
- else if (config.mux === "niwaterm") targetPanes = await getTargetPaneIds$2(currentPaneId);
1888
- else targetPanes = await getTargetPaneIds$3(currentPaneId);
1889
- if (targetPanes.length === 0) {
1890
- logger$1.error("No target panes registered for this editor pane");
1891
- process.exit(1);
1892
- }
2208
+ const { targetPaneIds } = await requireEditorContext(getBackend(config.mux), logger$1);
1893
2209
  return {
1894
2210
  mux: config.mux,
1895
- targetPaneId: targetPanes[0]
2211
+ targetPaneId: targetPaneIds[0]
1896
2212
  };
1897
2213
  }
1898
2214
  async function runPush(rest, positionals) {
@@ -2034,18 +2350,21 @@ function buildOpenEditorArgv(options) {
2034
2350
  if (options.verbose) argv.push("--verbose");
2035
2351
  return argv;
2036
2352
  }
2037
- async function runToggleMode(options, herdrApi = herdr_exports) {
2038
- if (options.mux !== "herdr") throw new Error("toggle currently supports only the Herdr multiplexer");
2353
+ async function runToggleMode(options, backend = getBackend(options.mux)) {
2354
+ if (!canSplitPane(backend)) throw new Error(isBuiltinBackendName(backend.name) ? "toggle currently supports only the Herdr multiplexer" : `toggle is not supported by the '${backend.name}' multiplexer because it cannot split panes`);
2039
2355
  if (!Number.isInteger(options.paneRows) || options.paneRows <= 0) throw new Error("--pane-rows must be a positive integer");
2040
- if (await herdrApi.resumeEditorPane(options.targetPane)) return;
2356
+ if (await switchEditorFocus(backend, options.targetPane)) return;
2041
2357
  const launchEnv = {};
2042
2358
  if (process.env.NODE_NO_WARNINGS !== void 0) launchEnv.NODE_NO_WARNINGS = process.env.NODE_NO_WARNINGS;
2043
- const editorPaneId = await herdrApi.splitEditorPane(options.targetPane, options.paneRows, process.env.HERDR_ACTIVE_PANE_CWD, launchEnv);
2359
+ const editorPaneId = await backend.splitPane(options.targetPane, {
2360
+ rows: options.paneRows,
2361
+ env: launchEnv
2362
+ });
2044
2363
  try {
2045
- await herdrApi.runCommandInPane(editorPaneId, buildOpenEditorArgv(options));
2364
+ await backend.runInPane(editorPaneId, buildOpenEditorArgv(options));
2046
2365
  } catch (error) {
2047
2366
  try {
2048
- await herdrApi.closePane(editorPaneId);
2367
+ await backend.closePane(editorPaneId);
2049
2368
  } catch (closeError) {
2050
2369
  logger.warn("Failed to close editor pane after launch failure: {error}", { error: closeError });
2051
2370
  }
@@ -2102,7 +2421,9 @@ const toggleCommand = define({
2102
2421
 
2103
2422
  //#endregion
2104
2423
  //#region src/index.ts
2105
- await cli(process.argv.slice(2), {
2424
+ const argv = process.argv.slice(2);
2425
+ if (mayUseConfiguredBackend(argv)) await setupUserConfig();
2426
+ await cli(argv, {
2106
2427
  name: "editprompt",
2107
2428
  description: "A CLI tool that lets you write prompts for CLI tools using your favorite text editor",
2108
2429
  args: {},