@jhizzard/termdeck 1.10.0 → 1.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jhizzard/termdeck",
3
- "version": "1.10.0",
3
+ "version": "1.10.1",
4
4
  "description": "Browser-based terminal multiplexer with metadata overlays, panel flashback memory recall, and AI-aware session management",
5
5
  "bin": {
6
6
  "termdeck": "./packages/cli/src/index.js"
@@ -2350,7 +2350,7 @@ function createServer(config) {
2350
2350
  // Body: { text: string, source?: 'user' | 'reply' | 'ai', fromSessionId?: string }
2351
2351
  // Used by T1.3 reply button and any agent-to-agent routing.
2352
2352
  const inputRateLimit = new Map(); // sessionId -> { windowStart, count }
2353
- app.post('/api/sessions/:id/input', (req, res) => {
2353
+ app.post('/api/sessions/:id/input', async (req, res) => {
2354
2354
  const session = sessions.get(req.params.id);
2355
2355
  if (!session) return res.status(404).json({ error: 'Session not found' });
2356
2356
  // Sprint 72 T2 — web-chat panels have no PTY. Route the inject to the
@@ -2428,7 +2428,7 @@ function createServer(config) {
2428
2428
  });
2429
2429
  }
2430
2430
 
2431
- const { text, source, fromSessionId } = req.body || {};
2431
+ const { text, source, fromSessionId, submit } = req.body || {};
2432
2432
  if (typeof text !== 'string') {
2433
2433
  return res.status(400).json({ error: 'Missing text' });
2434
2434
  }
@@ -2449,11 +2449,65 @@ function createServer(config) {
2449
2449
  // CRLF normalize: zsh/readline want \r for Enter
2450
2450
  const normalized = text.replace(/\r\n?/g, '\r').replace(/\n/g, '\r');
2451
2451
 
2452
- try {
2453
- session.pty.write(normalized);
2454
- session.trackInput(normalized);
2455
- } catch (err) {
2456
- return res.status(500).json({ error: err.message });
2452
+ // Sprint 76.1 (Bug B — Brad's "POST /input returns 200 but never submits"):
2453
+ // optional server-sequenced submit. The documented two-stage inject (paste
2454
+ // body, ~400ms settle, then a lone `\r` as a SECOND POST) is a CALLER-side
2455
+ // race when the bracketed-paste close marker and the `\r` ride one PTY
2456
+ // write the foreground TUI absorbs the `\r` as paste content, so under
2457
+ // concurrent / mid-turn injects the submit is silently swallowed and the
2458
+ // text sits unsubmitted (a 200 here only ever meant "bytes written", not
2459
+ // "became a turn"). With `submit:true` the SERVER owns the ordering: write
2460
+ // the body, await the settle, then write a lone `\r` as its OWN PTY write —
2461
+ // the OS chunk-boundary race is impossible because the two writes are
2462
+ // distinct with a server-held gap between them. Mirrors the web-chat arm's
2463
+ // server-side assembly above. Absent/falsy `submit` ⇒ byte-identical to the
2464
+ // pre-76.1 pass-through (existing two-stage callers are untouched).
2465
+ let bytesWritten;
2466
+ let submitted;
2467
+ if (submit === true) {
2468
+ const rawSettle = process.env.TERMDECK_INPUT_SUBMIT_SETTLE_MS;
2469
+ const parsedSettle = Number(rawSettle);
2470
+ const settleMs = (rawSettle !== undefined && rawSettle !== ''
2471
+ && Number.isFinite(parsedSettle) && parsedSettle >= 0) ? parsedSettle : 400;
2472
+ // Strip any caller-supplied trailing CR so the body never self-submits;
2473
+ // the lone `\r` below is the one and only submit keystroke.
2474
+ const body = normalized.replace(/\r+$/, '');
2475
+ try {
2476
+ if (body) { session.pty.write(body); session.trackInput(body); }
2477
+ await new Promise((resolve) => setTimeout(resolve, settleMs));
2478
+ // The PTY can be torn down DURING the server-held settle (panel closed
2479
+ // mid-submit). Re-validate before the submit keystroke and return a
2480
+ // clean 410 (the route's exited-panel convention) instead of leaning on
2481
+ // the catch below to surface a generic 500 — so a caller can tell
2482
+ // "panel closed mid-submit, don't retry" from a real write error. The
2483
+ // try/catch remains the backstop for any unexpected write throw.
2484
+ if (session.meta.status === 'exited' || !session.pty) {
2485
+ const msg = `Panel ${req.params.id} exited during submit settle`;
2486
+ return res.status(410).json({
2487
+ ok: false, code: 'panel_exited', error: msg, message: msg,
2488
+ exitCode: session.meta.exitCode ?? null,
2489
+ exitedAt: session.meta.exitedAt || null,
2490
+ });
2491
+ }
2492
+ session.pty.write('\r');
2493
+ session.trackInput('\r');
2494
+ } catch (err) {
2495
+ return res.status(500).json({ error: err.message });
2496
+ }
2497
+ bytesWritten = body.length + 1;
2498
+ // The server completed the atomic submit sequence. This is the mechanical
2499
+ // guarantee that removes the caller-side `\r`-swallow race; `status` below
2500
+ // is the best-effort "did the TUI start the turn" signal (adapter-derived,
2501
+ // so it may lag a beat on a freshly-idle panel).
2502
+ submitted = true;
2503
+ } else {
2504
+ try {
2505
+ session.pty.write(normalized);
2506
+ session.trackInput(normalized);
2507
+ } catch (err) {
2508
+ return res.status(500).json({ error: err.message });
2509
+ }
2510
+ bytesWritten = normalized.length;
2457
2511
  }
2458
2512
 
2459
2513
  session.meta.replyCount = (session.meta.replyCount || 0) + 1;
@@ -2471,7 +2525,19 @@ function createServer(config) {
2471
2525
  }
2472
2526
  }
2473
2527
 
2474
- res.json({ ok: true, bytes: normalized.length, replyCount: session.meta.replyCount });
2528
+ // submit-confirm: callers (e.g. Brad's tg-poll re-inject) read
2529
+ // `status` / `inputBufferLength` to detect a stuck inject and retry
2530
+ // deterministically instead of separately polling GET /buffer. `submitted`
2531
+ // is present only when `submit:true` was requested.
2532
+ const responseBody = {
2533
+ ok: true,
2534
+ bytes: bytesWritten,
2535
+ replyCount: session.meta.replyCount,
2536
+ status: session.meta.status,
2537
+ inputBufferLength: (session._inputBuffer || '').length,
2538
+ };
2539
+ if (submit === true) responseBody.submitted = submitted;
2540
+ res.json(responseBody);
2475
2541
  });
2476
2542
 
2477
2543
  // POST /api/sessions/:id/upload?name=<filename> - File drop / clipboard image paste