@volter/supercode-terminal 0.2.46 → 0.2.48

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 (2) hide show
  1. package/index.mjs +37 -6
  2. package/package.json +1 -1
package/index.mjs CHANGED
@@ -1545,9 +1545,12 @@ export class SupercodeTerminalController {
1545
1545
  const abort = new AbortController();
1546
1546
  const task = (async () => {
1547
1547
  const deadline = Date.now() + 5 * 60_000;
1548
+ let readySince = null;
1548
1549
  while (!abort.signal.aborted && Date.now() < deadline) {
1549
1550
  const content = await this.#host.capture(sessionId, { preserveEscapes: true });
1550
- if (composerReadyForInput(harness, content) === true) {
1551
+ const ready = composerReadyForInput(harness, content) === true;
1552
+ readySince = ready ? (readySince ?? Date.now()) : null;
1553
+ if (ready && Date.now() - readySince >= 1_000) {
1551
1554
  if (harness !== 'claude-code' && harness !== 'codex') {
1552
1555
  await this.#host.sendInput(sessionId, input, { submitMode: 'single', deadlineMs: Date.now() + 10_000 });
1553
1556
  return;
@@ -1736,14 +1739,18 @@ export function composerText(kind, screen) {
1736
1739
  const lines = String(screen ?? '').split('\n').map(styledCells);
1737
1740
  const plain = lines.map((cells) => cells.map((cell) => cell.char).join(''));
1738
1741
  const rule = /^\s*[─━]{4,}\s*$/u;
1742
+ const footer = /^\s*(?:\? for shortcuts\b|[^\n]+ · (?:~[\\/]|[\\/]|[A-Z]:[\\/])[^\n]+$|.*← for agents\s*$|.*\bcontext(?:\s+\d+%)?\s+left\b)/iu;
1739
1743
  const caret = kind === 'codex' ? /^\s*›(?:\s|$)/u : /^\s*[❯>](?:\s|$)/u;
1740
1744
  for (let row = plain.length - 1; row >= 0; row -= 1) {
1741
1745
  if (!caret.test(plain[row])) continue;
1742
1746
  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;
1747
+ while (end < plain.length && !(kind === 'codex' ? footer.test(plain[end]) : rule.test(plain[end]))) end += 1;
1745
1748
  // Claude encloses the editor in rules; Codex ends it at its shortcut/context footer.
1746
1749
  if (end === plain.length) return null;
1750
+ // Startup can draw a caret before loading finishes or a modal opens. A rule
1751
+ // alone is not Codex's ready editor; require its footer at the live bottom.
1752
+ if (kind === 'codex' && (plain.slice(end + 1).some((line) => line.trim()) ||
1753
+ plain.some((line) => /\bmodel:\s+loading\b/iu.test(line)))) return null;
1747
1754
  if (kind === 'claude-code' && (row === 0 || !rule.test(plain[row - 1]))) return null;
1748
1755
  const first = plain[row].match(caret)[0].length;
1749
1756
  const value = lines.slice(row, end).map((cells, index) => cells
@@ -1754,6 +1761,30 @@ export function composerText(kind, screen) {
1754
1761
  return null;
1755
1762
  }
1756
1763
 
1764
+ /** Match screen rows against the original text. A visual row boundary can be
1765
+ * a hard newline, a word wrap (one space), or a wrap inside a word. Never erase
1766
+ * whitespace within rows: a changed or partially pasted draft stays unconfirmed.
1767
+ */
1768
+ function composerMatches(value, wanted) {
1769
+ if (value === null) return false;
1770
+ let offsets = new Set([0]);
1771
+ const rows = value.split('\n');
1772
+ for (let row = 0; row < rows.length; row += 1) {
1773
+ const next = new Set();
1774
+ for (const offset of offsets) {
1775
+ if (!wanted.startsWith(rows[row], offset)) continue;
1776
+ const end = offset + rows[row].length;
1777
+ if (row === rows.length - 1) next.add(end);
1778
+ else {
1779
+ next.add(end);
1780
+ if (wanted[end] === ' ' || wanted[end] === '\n') next.add(end + 1);
1781
+ }
1782
+ }
1783
+ offsets = next;
1784
+ }
1785
+ return offsets.has(wanted.length);
1786
+ }
1787
+
1757
1788
  /** Paste once, then submit only text read back from the live composer.
1758
1789
  * False means unconfirmed, and can leave a draft: callers must not blindly resend.
1759
1790
  */
@@ -1773,14 +1804,14 @@ export async function deliverComposerInput(host, sessionId, kind, text, { deadli
1773
1804
  for (let attempt = 0; attempt < 10 && !expired(); attempt += 1) {
1774
1805
  await sleep(100);
1775
1806
  const current = await read();
1776
- if (current === wanted) { pasted = true; break; }
1807
+ if (composerMatches(current, wanted)) { pasted = true; break; }
1777
1808
  if (current !== '') return failed('paste-unconfirmed');
1778
1809
  }
1779
1810
  if (!pasted) return failed('paste-unconfirmed');
1780
1811
  for (let attempt = 0; attempt < 3; attempt += 1) {
1781
1812
  // Re-read immediately before each Enter, including the first one.
1782
1813
  if (expired()) return failed('deadline');
1783
- if (await read() !== wanted) return failed('composer-changed');
1814
+ if (!composerMatches(await read(), wanted)) return failed('composer-changed');
1784
1815
  if (expired()) return failed('deadline');
1785
1816
  // A keystream Enter has no hidden delay or retry between the read and the write.
1786
1817
  await host.sendInput(sessionId, '\r', { submitMode: 'single', deadlineMs });
@@ -1788,7 +1819,7 @@ export async function deliverComposerInput(host, sessionId, kind, text, { deadli
1788
1819
  await sleep(100);
1789
1820
  const current = await read();
1790
1821
  if (current === '') return { delivered: true, delivery };
1791
- if (current !== wanted) return failed('submission-unconfirmed');
1822
+ if (!composerMatches(current, wanted)) return failed('submission-unconfirmed');
1792
1823
  }
1793
1824
  }
1794
1825
  return failed('submit-unconfirmed');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-terminal",
3
- "version": "0.2.46",
3
+ "version": "0.2.48",
4
4
  "type": "module",
5
5
  "description": "Optional leak-safe terminal and tmux host adapter for Volter Harness frontends",
6
6
  "exports": {