@volter/supercode-terminal 0.2.45 → 0.2.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.d.ts +8 -3
- package/index.mjs +81 -47
- package/package.json +1 -1
- package/substrate/tmux.d.ts +2 -1
- package/substrate/tmux.js +7 -10
package/index.d.ts
CHANGED
|
@@ -56,7 +56,7 @@ export class TmuxTerminalHost {
|
|
|
56
56
|
sendInput(
|
|
57
57
|
sessionId: string,
|
|
58
58
|
data: string,
|
|
59
|
-
options?: { deadlineMs?: number; submitMode?: 'retry' | 'single' },
|
|
59
|
+
options?: { deadlineMs?: number; submitMode?: 'retry' | 'single' | 'none' },
|
|
60
60
|
): Promise<TerminalInputDelivery>;
|
|
61
61
|
/** Persist an opaque application context association on a session owned by this host. */
|
|
62
62
|
bindContext(sessionId: string, contextKey: string): Promise<TerminalSession>;
|
|
@@ -81,7 +81,7 @@ export class PaneServerHost {
|
|
|
81
81
|
listSessions(): Promise<TerminalSession[]>;
|
|
82
82
|
createSession(input: { command: TmuxCommand; contextKey?: string; cwd?: string; env?: Record<string, string>; name?: string }): Promise<TerminalSession>;
|
|
83
83
|
capture(sessionId: string, options?: { lines?: number; preserveEscapes?: boolean }): Promise<string>;
|
|
84
|
-
sendInput(sessionId: string, data: string, options?: { deadlineMs?: number; submitMode?: 'retry' | 'single' }): Promise<TerminalInputDelivery>;
|
|
84
|
+
sendInput(sessionId: string, data: string, options?: { deadlineMs?: number; submitMode?: 'retry' | 'single' | 'none' }): Promise<TerminalInputDelivery>;
|
|
85
85
|
bindContext(sessionId: string, contextKey: string): Promise<TerminalSession>;
|
|
86
86
|
renameWindow(sessionId: string, title?: string | null): Promise<string>;
|
|
87
87
|
issueAttachment(sessionId: string, options?: { mode?: 'observe' | 'control' }): Promise<TerminalAttachmentGrant>;
|
|
@@ -111,7 +111,7 @@ export class PtyTerminalHost {
|
|
|
111
111
|
sendInput(
|
|
112
112
|
sessionId: string,
|
|
113
113
|
data: string,
|
|
114
|
-
options?: { deadlineMs?: number; submitMode?: 'retry' | 'single' },
|
|
114
|
+
options?: { deadlineMs?: number; submitMode?: 'retry' | 'single' | 'none' },
|
|
115
115
|
): Promise<TerminalInputDelivery>;
|
|
116
116
|
bindContext(sessionId: string, contextKey: string): Promise<TerminalSession>;
|
|
117
117
|
/** A ConPTY pane has no window to rename; returns its label. */
|
|
@@ -250,3 +250,8 @@ export type {
|
|
|
250
250
|
} from './substrate/session-lifecycle.js';
|
|
251
251
|
|
|
252
252
|
export type { TerminalInputDelivery, TmuxCommand, TmuxWindowSize };
|
|
253
|
+
|
|
254
|
+
/** The bounded live composer text, or null for an unrecognized screen. */
|
|
255
|
+
export function composerText(kind: string, screen: string | undefined): string | null;
|
|
256
|
+
/** False means unconfirmed and may leave a draft; never blindly resend. */
|
|
257
|
+
export function deliverComposerInput(host: TmuxTerminalHost | PtyTerminalHost | PaneServerHost, sessionId: string, kind: string, text: string, options?: { deadlineMs?: number; signal?: AbortSignal }): Promise<{ delivered: boolean; reason?: string; delivery?: TerminalInputDelivery }>;
|
package/index.mjs
CHANGED
|
@@ -437,9 +437,8 @@ export class TmuxTerminalHost {
|
|
|
437
437
|
}
|
|
438
438
|
}
|
|
439
439
|
|
|
440
|
-
//
|
|
440
|
+
// Raw terminal writes submit once. Only a composer readback can authorize a retry.
|
|
441
441
|
const SINGLE_SUBMIT_DELAY_MS = 500;
|
|
442
|
-
const SUBMITTED_LINE_DELAYS_MS = [500, 1500, 3500];
|
|
443
442
|
const PTY_DEFAULT_SIZE = Object.freeze({ cols: 80, rows: 24 });
|
|
444
443
|
const PTY_SCROLLBACK = 5_000;
|
|
445
444
|
// How long one read of the Windows process table answers every pane's activeCommand: the read costs a
|
|
@@ -650,7 +649,7 @@ export class PtyTerminalHost {
|
|
|
650
649
|
this.#live(entry.id).child.write(bytes);
|
|
651
650
|
};
|
|
652
651
|
const submit = async () => {
|
|
653
|
-
for (const delayMs of options.submitMode === '
|
|
652
|
+
for (const delayMs of options.submitMode === 'none' ? [] : [SINGLE_SUBMIT_DELAY_MS]) {
|
|
654
653
|
await sleep(delayMs);
|
|
655
654
|
write('\r');
|
|
656
655
|
}
|
|
@@ -663,7 +662,7 @@ export class PtyTerminalHost {
|
|
|
663
662
|
await submit();
|
|
664
663
|
return 'submitted-paste';
|
|
665
664
|
}
|
|
666
|
-
|
|
665
|
+
if (options.submitMode !== 'none') write('\r');
|
|
667
666
|
return 'submitted-line';
|
|
668
667
|
}
|
|
669
668
|
// tmux sends a newline in a keystream as Enter.
|
|
@@ -1547,16 +1546,15 @@ export class SupercodeTerminalController {
|
|
|
1547
1546
|
const task = (async () => {
|
|
1548
1547
|
const deadline = Date.now() + 5 * 60_000;
|
|
1549
1548
|
while (!abort.signal.aborted && Date.now() < deadline) {
|
|
1550
|
-
const content = this.#host.capture(sessionId, {
|
|
1549
|
+
const content = await this.#host.capture(sessionId, { preserveEscapes: true });
|
|
1551
1550
|
if (composerReadyForInput(harness, content) === true) {
|
|
1552
|
-
if (harness
|
|
1553
|
-
await this.#
|
|
1554
|
-
|
|
1555
|
-
await this.#host.sendInput(sessionId, input, {
|
|
1556
|
-
deadlineMs: Date.now() + 15_000,
|
|
1557
|
-
submitMode: 'retry',
|
|
1558
|
-
});
|
|
1551
|
+
if (harness !== 'claude-code' && harness !== 'codex') {
|
|
1552
|
+
await this.#host.sendInput(sessionId, input, { submitMode: 'single', deadlineMs: Date.now() + 10_000 });
|
|
1553
|
+
return;
|
|
1559
1554
|
}
|
|
1555
|
+
const result = await deliverComposerInput(this.#host, sessionId, harness,
|
|
1556
|
+
input.replace(/(?:\r\n|\n|\r)$/u, ''), { signal: abort.signal });
|
|
1557
|
+
if (!result.delivered) throw new Error(result.reason);
|
|
1560
1558
|
return;
|
|
1561
1559
|
}
|
|
1562
1560
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
@@ -1574,28 +1572,6 @@ export class SupercodeTerminalController {
|
|
|
1574
1572
|
this.#pendingInitialInputs.set(sessionId, { abort, task });
|
|
1575
1573
|
}
|
|
1576
1574
|
|
|
1577
|
-
async #submitCodexInitialInput(sessionId, input, signal) {
|
|
1578
|
-
await this.#host.sendInput(sessionId, input, {
|
|
1579
|
-
deadlineMs: Date.now() + 15_000,
|
|
1580
|
-
submitMode: 'single',
|
|
1581
|
-
});
|
|
1582
|
-
const deadline = Date.now() + 60_000;
|
|
1583
|
-
let sawPrompt = false;
|
|
1584
|
-
while (!signal.aborted && Date.now() < deadline) {
|
|
1585
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1586
|
-
const state = codexInitialInputState(this.#host.capture(sessionId, { lines: 100 }), input);
|
|
1587
|
-
sawPrompt ||= state.promptVisible;
|
|
1588
|
-
if (state.turnStarted || (sawPrompt && state.emptyComposerVisible)) return;
|
|
1589
|
-
await this.#host.sendInput(sessionId, '\n', {
|
|
1590
|
-
deadlineMs: Date.now() + 15_000,
|
|
1591
|
-
submitMode: 'retry',
|
|
1592
|
-
});
|
|
1593
|
-
}
|
|
1594
|
-
if (!signal.aborted) {
|
|
1595
|
-
throw new Error('codex accepted the initial terminal text but did not start the turn');
|
|
1596
|
-
}
|
|
1597
|
-
}
|
|
1598
|
-
|
|
1599
1575
|
#sessionName(harness) {
|
|
1600
1576
|
const label = harness === 'claude-code' ? 'claude' : harness;
|
|
1601
1577
|
return `${this.#sessionNamePrefix}-${label}-${randomUUID().slice(0, 8)}`;
|
|
@@ -1708,6 +1684,7 @@ function composerLine(screen, caret) {
|
|
|
1708
1684
|
*/
|
|
1709
1685
|
export function composerReadyForInput(kind, screen) {
|
|
1710
1686
|
if (!Object.hasOwn(COMPOSER_READY, kind)) return null;
|
|
1687
|
+
if (kind === 'claude-code' || kind === 'codex') return composerText(kind, screen) === '';
|
|
1711
1688
|
return COMPOSER_READY[kind](String(screen ?? ''));
|
|
1712
1689
|
}
|
|
1713
1690
|
|
|
@@ -1724,6 +1701,10 @@ export function composerReadyForInput(kind, screen) {
|
|
|
1724
1701
|
* The caret is the kind's own, the one `composerReadyForInput` reads.
|
|
1725
1702
|
*/
|
|
1726
1703
|
export function composerHoldsDraft(kind, screen) {
|
|
1704
|
+
if (kind === 'claude-code' || kind === 'codex') {
|
|
1705
|
+
const text = composerText(kind, screen);
|
|
1706
|
+
return text === null ? null : text.length > 0;
|
|
1707
|
+
}
|
|
1727
1708
|
const carets = COMPOSER_CARETS[kind];
|
|
1728
1709
|
if (!carets) return null;
|
|
1729
1710
|
const lines = String(screen ?? '').split('\n');
|
|
@@ -1747,6 +1728,72 @@ export function composerHoldsDraft(kind, screen) {
|
|
|
1747
1728
|
return null;
|
|
1748
1729
|
}
|
|
1749
1730
|
|
|
1731
|
+
/** Read only a bounded, live Claude/Codex composer, never a prompt in scrollback.
|
|
1732
|
+
* Unknown layouts and collapsed pastes are deliberately unconfirmed.
|
|
1733
|
+
*/
|
|
1734
|
+
export function composerText(kind, screen) {
|
|
1735
|
+
if (kind !== 'claude-code' && kind !== 'codex') return null;
|
|
1736
|
+
const lines = String(screen ?? '').split('\n').map(styledCells);
|
|
1737
|
+
const plain = lines.map((cells) => cells.map((cell) => cell.char).join(''));
|
|
1738
|
+
const rule = /^\s*[─━]{4,}\s*$/u;
|
|
1739
|
+
const caret = kind === 'codex' ? /^\s*›(?:\s|$)/u : /^\s*[❯>](?:\s|$)/u;
|
|
1740
|
+
for (let row = plain.length - 1; row >= 0; row -= 1) {
|
|
1741
|
+
if (!caret.test(plain[row])) continue;
|
|
1742
|
+
let end = row + 1;
|
|
1743
|
+
while (end < plain.length && !rule.test(plain[end]) &&
|
|
1744
|
+
!/^\s*(?:\? for shortcuts|.*\bcontext left\b)/u.test(plain[end])) end += 1;
|
|
1745
|
+
// Claude encloses the editor in rules; Codex ends it at its shortcut/context footer.
|
|
1746
|
+
if (end === plain.length) return null;
|
|
1747
|
+
if (kind === 'claude-code' && (row === 0 || !rule.test(plain[row - 1]))) return null;
|
|
1748
|
+
const first = plain[row].match(caret)[0].length;
|
|
1749
|
+
const value = lines.slice(row, end).map((cells, index) => cells
|
|
1750
|
+
.slice(index === 0 || /^\s*$/u.test(plain[row + index].slice(0, first)) ? first : 0).filter((cell) => !cell.placeholder)
|
|
1751
|
+
.map((cell) => cell.char).join('').trimEnd()).join('\n').trim();
|
|
1752
|
+
return value;
|
|
1753
|
+
}
|
|
1754
|
+
return null;
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
/** Paste once, then submit only text read back from the live composer.
|
|
1758
|
+
* False means unconfirmed, and can leave a draft: callers must not blindly resend.
|
|
1759
|
+
*/
|
|
1760
|
+
export async function deliverComposerInput(host, sessionId, kind, text, { deadlineMs = Date.now() + 10_000, signal } = {}) {
|
|
1761
|
+
const read = async () => composerText(kind, await host.capture(sessionId, { preserveEscapes: true }));
|
|
1762
|
+
const expired = () => signal?.aborted || Date.now() >= deadlineMs;
|
|
1763
|
+
const failed = (reason) => ({ delivered: false, reason });
|
|
1764
|
+
if (expired()) return failed('deadline');
|
|
1765
|
+
const before = await read();
|
|
1766
|
+
if (before !== '') return failed(before === null ? 'no-composer' : 'draft');
|
|
1767
|
+
const wanted = String(text).trim();
|
|
1768
|
+
if (!wanted || /[\x00-\x08\x0b-\x1f\x7f]/u.test(text)) return failed('unsupported-text');
|
|
1769
|
+
if (expired()) return failed('deadline');
|
|
1770
|
+
const delivery = await host.sendInput(sessionId, `${text}\n`, { submitMode: 'none', deadlineMs });
|
|
1771
|
+
// A dropped paste must never be followed by Enter. Give the editor time to render it.
|
|
1772
|
+
let pasted = false;
|
|
1773
|
+
for (let attempt = 0; attempt < 10 && !expired(); attempt += 1) {
|
|
1774
|
+
await sleep(100);
|
|
1775
|
+
const current = await read();
|
|
1776
|
+
if (current === wanted) { pasted = true; break; }
|
|
1777
|
+
if (current !== '') return failed('paste-unconfirmed');
|
|
1778
|
+
}
|
|
1779
|
+
if (!pasted) return failed('paste-unconfirmed');
|
|
1780
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
1781
|
+
// Re-read immediately before each Enter, including the first one.
|
|
1782
|
+
if (expired()) return failed('deadline');
|
|
1783
|
+
if (await read() !== wanted) return failed('composer-changed');
|
|
1784
|
+
if (expired()) return failed('deadline');
|
|
1785
|
+
// A keystream Enter has no hidden delay or retry between the read and the write.
|
|
1786
|
+
await host.sendInput(sessionId, '\r', { submitMode: 'single', deadlineMs });
|
|
1787
|
+
for (let poll = 0; poll < 10 && !expired(); poll += 1) {
|
|
1788
|
+
await sleep(100);
|
|
1789
|
+
const current = await read();
|
|
1790
|
+
if (current === '') return { delivered: true, delivery };
|
|
1791
|
+
if (current !== wanted) return failed('submission-unconfirmed');
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
return failed('submit-unconfirmed');
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1750
1797
|
/** The carets each kind draws at the start of its composer line (Claude on Windows draws `>`). */
|
|
1751
1798
|
const COMPOSER_CARETS = Object.freeze({ 'claude-code': ['❯', '>'], codex: ['›'], gemini: ['>'], grok: ['❯'] });
|
|
1752
1799
|
|
|
@@ -1782,19 +1829,6 @@ function styledCells(line) {
|
|
|
1782
1829
|
return cells;
|
|
1783
1830
|
}
|
|
1784
1831
|
|
|
1785
|
-
function codexInitialInputState(content, input) {
|
|
1786
|
-
const firstLine = input.replace(/(?:\r\n|\n|\r)$/u, '').split(/\r?\n/u, 1)[0] ?? '';
|
|
1787
|
-
const marker = `› ${firstLine.slice(0, 24)}`;
|
|
1788
|
-
const promptAt = marker.length > 2 ? content.lastIndexOf(marker) : -1;
|
|
1789
|
-
const emptyComposerAt = content.lastIndexOf('› Ask Codex to do anything');
|
|
1790
|
-
const tail = promptAt >= 0 ? content.slice(promptAt + marker.length) : '';
|
|
1791
|
-
return {
|
|
1792
|
-
emptyComposerVisible: emptyComposerAt > promptAt,
|
|
1793
|
-
promptVisible: promptAt >= 0,
|
|
1794
|
-
turnStarted: promptAt >= 0 && /(?:^|\n)\s*[•◦✦⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]\s/u.test(tail),
|
|
1795
|
-
};
|
|
1796
|
-
}
|
|
1797
|
-
|
|
1798
1832
|
function validSessionPrefix(value) {
|
|
1799
1833
|
if (!/^[A-Za-z0-9_-]{1,32}$/.test(value)) {
|
|
1800
1834
|
throw new TerminalHostError(
|
package/package.json
CHANGED
package/substrate/tmux.d.ts
CHANGED
|
@@ -128,11 +128,12 @@ export declare function sendInterruptAsync({ socket, target }: {
|
|
|
128
128
|
target: string;
|
|
129
129
|
socket?: string;
|
|
130
130
|
}): Promise<void>;
|
|
131
|
+
/** `none` pastes without Enter. Legacy `retry` submits once; retries require composer evidence. */
|
|
131
132
|
export declare function sendInputAsync({ data, deadlineMs, socket, submitMode, target }: {
|
|
132
133
|
target: string;
|
|
133
134
|
data: string;
|
|
134
135
|
deadlineMs?: number;
|
|
135
|
-
submitMode?: "retry" | "single";
|
|
136
|
+
submitMode?: "retry" | "single" | "none";
|
|
136
137
|
socket?: string;
|
|
137
138
|
}): Promise<TerminalInputDelivery>;
|
|
138
139
|
export declare function attachSession({ name, socket, window }: {
|
package/substrate/tmux.js
CHANGED
|
@@ -748,6 +748,7 @@ export async function sendInterruptAsync({ socket, target }) {
|
|
|
748
748
|
}
|
|
749
749
|
await runAsync("tmux", tmuxArgs(socket, ["send-keys", "-t", target, "Escape"]));
|
|
750
750
|
}
|
|
751
|
+
/** `none` pastes without Enter. Legacy `retry` submits once; retries require composer evidence. */
|
|
751
752
|
export async function sendInputAsync({ data, deadlineMs, socket, submitMode = "retry", target }) {
|
|
752
753
|
assertTmux();
|
|
753
754
|
if (!target) {
|
|
@@ -768,7 +769,8 @@ export async function sendInputAsync({ data, deadlineMs, socket, submitMode = "r
|
|
|
768
769
|
await sendSubmittedPasteAsync({ deadlineMs, socket, submitMode, target, text: submittedText });
|
|
769
770
|
return "submitted-paste";
|
|
770
771
|
}
|
|
771
|
-
|
|
772
|
+
if (submitMode !== "none")
|
|
773
|
+
await runAsync("tmux", tmuxArgs(socket, ["send-keys", "-t", target, "Enter"]), terminalInputRunOptions(deadlineMs));
|
|
772
774
|
return "submitted-line";
|
|
773
775
|
}
|
|
774
776
|
for (const token of tokenizeTerminalInput(data)) {
|
|
@@ -790,7 +792,6 @@ export function attachSession({ name, socket, window }) {
|
|
|
790
792
|
spawnInherited("tmux", tmuxArgs(socket, ["attach-session", "-t", target]));
|
|
791
793
|
}
|
|
792
794
|
const SINGLE_SUBMIT_DELAY_MS = 500;
|
|
793
|
-
const SUBMITTED_LINE_DELAYS_MS = [500, 1500, 3500];
|
|
794
795
|
async function sleepAsync(ms) {
|
|
795
796
|
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
796
797
|
}
|
|
@@ -834,15 +835,11 @@ async function sendSubmittedPasteAsync({ deadlineMs, socket, submitMode, target,
|
|
|
834
835
|
await sendSubmitKeysAsync({ deadlineMs, socket, submitMode, target });
|
|
835
836
|
}
|
|
836
837
|
async function sendSubmitKeysAsync({ deadlineMs, socket, submitMode, target }) {
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
await runAsync("tmux", tmuxArgs(socket, ["send-keys", "-t", target, "Enter"]), terminalInputRunOptions(deadlineMs));
|
|
838
|
+
// Retrying Enter requires a composer readback; raw terminal writes cannot do that.
|
|
839
|
+
if (submitMode === "none")
|
|
840
840
|
return;
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
await sleepAsync(delayMs);
|
|
844
|
-
await runAsync("tmux", tmuxArgs(socket, ["send-keys", "-t", target, "Enter"]), terminalInputRunOptions(deadlineMs));
|
|
845
|
-
}
|
|
841
|
+
await sleepAsync(SINGLE_SUBMIT_DELAY_MS);
|
|
842
|
+
await runAsync("tmux", tmuxArgs(socket, ["send-keys", "-t", target, "Enter"]), terminalInputRunOptions(deadlineMs));
|
|
846
843
|
}
|
|
847
844
|
function terminalInputRunOptions(deadlineMs) {
|
|
848
845
|
if (deadlineMs === undefined)
|