@crouton-kit/humanloop 0.3.33 → 0.3.35

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 (46) hide show
  1. package/dist/api.d.ts +4 -8
  2. package/dist/api.js +6 -29
  3. package/dist/browser/server.d.ts +3 -3
  4. package/dist/browser/server.js +22 -25
  5. package/dist/cli.js +131 -1114
  6. package/dist/editor/feedback.d.ts +9 -0
  7. package/dist/editor/feedback.js +23 -0
  8. package/dist/editor/review.d.ts +12 -30
  9. package/dist/editor/review.js +109 -119
  10. package/dist/inbox/claim.d.ts +23 -0
  11. package/dist/inbox/claim.js +67 -0
  12. package/dist/inbox/completion.d.ts +4 -0
  13. package/dist/inbox/completion.js +107 -0
  14. package/dist/inbox/controller.d.ts +74 -0
  15. package/dist/inbox/controller.js +457 -0
  16. package/dist/inbox/convention.d.ts +15 -10
  17. package/dist/inbox/convention.js +149 -79
  18. package/dist/inbox/deck-adapter.d.ts +27 -0
  19. package/dist/inbox/deck-adapter.js +57 -0
  20. package/dist/inbox/deck-schema.d.ts +18 -14
  21. package/dist/inbox/deck-schema.js +59 -117
  22. package/dist/inbox/layout.d.ts +8 -0
  23. package/dist/inbox/layout.js +9 -0
  24. package/dist/inbox/registry.d.ts +29 -0
  25. package/dist/inbox/registry.js +97 -0
  26. package/dist/inbox/review-adapter.d.ts +24 -0
  27. package/dist/inbox/review-adapter.js +57 -0
  28. package/dist/inbox/scan.d.ts +3 -2
  29. package/dist/inbox/scan.js +71 -43
  30. package/dist/inbox/tickets.d.ts +63 -0
  31. package/dist/inbox/tickets.js +247 -0
  32. package/dist/inbox/tui.d.ts +3 -6
  33. package/dist/inbox/tui.js +24 -112
  34. package/dist/index.d.ts +12 -3
  35. package/dist/index.js +8 -2
  36. package/dist/render/termrender.d.ts +5 -0
  37. package/dist/render/termrender.js +63 -4
  38. package/dist/summary.d.ts +2 -3
  39. package/dist/summary.js +2 -3
  40. package/dist/surfaces/inbox-popup.d.ts +2 -0
  41. package/dist/surfaces/inbox-popup.js +32 -0
  42. package/dist/tui/app.js +13 -0
  43. package/dist/tui/tmux.d.ts +30 -7
  44. package/dist/tui/tmux.js +190 -66
  45. package/dist/types.d.ts +68 -7
  46. package/package.json +2 -2
package/dist/inbox/tui.js CHANGED
@@ -1,7 +1,4 @@
1
- import { setupTerminal, restoreTerminal, parseKeypress, getTerminalSize, } from '../tui/terminal.js';
2
- import { diffFrame } from '../tui/render.js';
3
1
  import { RESET, BOLD, DIM, ITALIC, CYAN, RED, GRAY, YELLOW, truncateRow } from '../tui/ansi.js';
4
- // ── ANSI helpers (local to this module) ──────────────────────────────────────
5
2
  function ansiColor(text, color) {
6
3
  switch (color) {
7
4
  case 'gray': return `${GRAY}${text}${RESET}`;
@@ -11,124 +8,39 @@ function ansiColor(text, color) {
11
8
  default: return text;
12
9
  }
13
10
  }
14
- // ── Row model (ported verbatim from sisyphus cross-session-inbox.ts:8-21) ────
15
- export const KIND_ICON = {
16
- notify: '✉',
17
- decision: '◆',
18
- context: '✎',
19
- error: '⚠',
20
- };
21
- export const KIND_COLOR = {
22
- notify: 'gray',
23
- decision: 'cyan',
24
- context: 'cyan',
25
- error: 'red',
26
- };
27
- // ── formatTimeAgo (ported from sisyphus src/tui/lib/format.ts:5-12) ──────────
11
+ export const KIND_ICON = { notify: '✉', decision: '◆', context: '✎', error: '⚠', review: '▤' };
12
+ export const KIND_COLOR = { notify: 'gray', decision: 'cyan', context: 'cyan', error: 'red', review: 'yellow' };
28
13
  export function formatTimeAgo(iso) {
29
- const diff = Date.now() - new Date(iso).getTime();
30
- const minutes = Math.floor(diff / 60000);
31
- const hours = Math.floor(minutes / 60);
32
- if (hours > 0)
33
- return `${hours}h ago`;
14
+ const minutes = Math.floor((Date.now() - new Date(iso).getTime()) / 60000);
15
+ if (minutes >= 60)
16
+ return `${Math.floor(minutes / 60)}h ago`;
34
17
  if (minutes > 0)
35
18
  return `${minutes}m ago`;
36
19
  return 'just now';
37
20
  }
38
- // ── buildInboxLines ───────────────────────────────────────────────────────────
21
+ /** Pure inbox rows; selection and terminal ownership belong to InboxController. */
39
22
  export function buildInboxLines(items, width, selectedIndex) {
40
- const lines = [];
41
- if (items.length === 0) {
42
- lines.push(` ${DIM}${ITALIC}No pending interactions${RESET}`);
43
- return lines;
44
- }
45
- lines.push(` ${BOLD}${items.length} pending${RESET}`);
46
- lines.push('');
23
+ if (items.length === 0)
24
+ return [` ${DIM}${ITALIC}No pending interactions${RESET}`];
25
+ const lines = [` ${BOLD}${items.length} pending${RESET}`, ''];
47
26
  const contentWidth = width - 4;
48
- for (let i = 0; i < items.length; i++) {
49
- const item = items[i];
50
- const kindKey = item.kind ?? '';
51
- const icon = kindKey in KIND_ICON ? KIND_ICON[kindKey] : '·';
52
- const iconColor = kindKey in KIND_COLOR ? KIND_COLOR[kindKey] : 'cyan';
53
- const sourceLabel = item.source?.sessionName ?? item.source?.askedBy ?? '';
54
- const titleText = item.title ?? `(${item.id.slice(0, 8)})`;
55
- const blocked = formatTimeAgo(item.blockedSince);
56
- const cursor = i === selectedIndex ? `${CYAN}▸${RESET} ` : ' ';
57
- const maxTitle = Math.max(10, contentWidth - sourceLabel.length - blocked.length - 8);
58
- let row = cursor;
59
- row += ansiColor(icon, iconColor);
60
- if (sourceLabel) {
61
- row += ` ${ansiColor(sourceLabel, 'yellow')}`;
62
- row += ` ${DIM}·${RESET} `;
63
- }
64
- else {
65
- row += ' ';
66
- }
67
- row += `${BOLD}${truncateRow(titleText, maxTitle)}${RESET}`;
68
- row += ` ${DIM}${blocked}${RESET}`;
27
+ for (let index = 0; index < items.length; index++) {
28
+ const item = items[index];
29
+ const kind = item.kind === 'deck' ? item.interactionKind ?? 'decision' : 'review';
30
+ const icon = KIND_ICON[kind] ?? '·';
31
+ const source = item.source.sessionName ?? item.source.askedBy ?? item.source.nodeId ?? '';
32
+ const age = formatTimeAgo(item.blockedSince);
33
+ const cursor = index === selectedIndex ? `${CYAN}▸${RESET} ` : ' ';
34
+ const titleWidth = Math.max(10, contentWidth - source.length - age.length - 8);
35
+ let row = `${cursor}${ansiColor(icon, KIND_COLOR[kind] ?? 'cyan')} `;
36
+ if (source)
37
+ row += `${ansiColor(source, 'yellow')} ${DIM}·${RESET} `;
38
+ row += `${BOLD}${truncateRow(item.title || `(${item.id.slice(0, 8)})`, titleWidth)}${RESET} ${DIM}${age}${RESET}`;
69
39
  lines.push(row);
70
- if (item.subtitle) {
40
+ if (item.claim)
41
+ lines.push(` ${DIM}claimed by ${item.claim.owner}${RESET}`);
42
+ else if (item.subtitle)
71
43
  lines.push(` ${DIM}${truncateRow(item.subtitle, contentWidth - 6)}${RESET}`);
72
- }
73
44
  }
74
45
  return lines;
75
46
  }
76
- // ── pickFromInbox ─────────────────────────────────────────────────────────────
77
- export function pickFromInbox(items, opts) {
78
- if (items.length === 0)
79
- return Promise.resolve(null);
80
- return new Promise((resolve) => {
81
- let selectedIndex = 0;
82
- let prevFrame = [];
83
- let onData;
84
- const flush = () => {
85
- const { cols: currentCols, rows: currentRows } = getTerminalSize();
86
- const lines = buildInboxLines(items, currentCols, selectedIndex);
87
- const { writes, nextPrevFrame } = diffFrame(prevFrame, lines, currentRows, currentCols);
88
- process.stdout.write('\x1b[?2026h');
89
- for (const w of writes)
90
- process.stdout.write(w);
91
- process.stdout.write('\x1b[?2026l');
92
- prevFrame = nextPrevFrame;
93
- };
94
- // Resize reflows/scrolls what's already on screen, invalidating the diff
95
- // model — clear and redraw from scratch at the new size.
96
- const onResize = () => {
97
- prevFrame = [];
98
- process.stdout.write('\x1b[2J\x1b[H');
99
- flush();
100
- };
101
- const done = (result) => {
102
- restoreTerminal();
103
- process.stdin.removeListener('data', onData);
104
- process.stdout.removeListener('resize', onResize);
105
- resolve(result);
106
- };
107
- setupTerminal();
108
- flush();
109
- onData = (data) => {
110
- const { input, key } = parseKeypress(data);
111
- if (key.downArrow || input === 'j') {
112
- selectedIndex = Math.min(selectedIndex + 1, items.length - 1);
113
- flush();
114
- return;
115
- }
116
- if (key.upArrow || input === 'k') {
117
- selectedIndex = Math.max(selectedIndex - 1, 0);
118
- flush();
119
- return;
120
- }
121
- if (key.return) {
122
- const selected = items[selectedIndex];
123
- done(selected ?? null);
124
- return;
125
- }
126
- if (key.escape || (key.ctrl && input === 'c') || input === 'q') {
127
- done(null);
128
- return;
129
- }
130
- };
131
- process.stdin.on('data', onData);
132
- process.stdout.on('resize', onResize);
133
- });
134
- }
package/dist/index.d.ts CHANGED
@@ -3,15 +3,24 @@ export { defaultGenerateVisual } from './visuals/generate.js';
3
3
  export { launchReview } from './editor/review.js';
4
4
  export { launchReview as review } from './editor/review.js';
5
5
  export type { ReviewOptions } from './editor/review.js';
6
- export { ask, notify, inbox } from './api.js';
6
+ export { ask, notify, openInbox } from './api.js';
7
+ export { openInboxPopup } from './surfaces/inbox-popup.js';
8
+ export { toggleInboxPopup, installInboxBinding, inspectInboxBinding, unbindInboxBinding, inboxPopupStyle } from './tui/tmux.js';
7
9
  export { display } from './surfaces/display.js';
8
10
  export { scanInbox } from './inbox/scan.js';
11
+ export { registerInboxRoot, unregisterInboxRoot, listInboxRoots, managedInboxRoot } from './inbox/registry.js';
12
+ export type { CompletionHandler, InboxRootRegistration, InboxRootStatus, RegisterInboxRootOptions } from './inbox/registry.js';
13
+ export { submitDeck, submitReview, readTicketResult, finalizeDeck, finalizeReview, completeDeck, completeReview, cancelTicket, cancelTicketResult, } from './inbox/tickets.js';
14
+ export type { SubmitDeckOptions, SubmitReviewOptions } from './inbox/tickets.js';
15
+ export { dispatchCompletion, reconcileCompletions } from './inbox/completion.js';
16
+ export { claimTicket, heartbeatClaim, releaseClaim, readTicketClaim } from './inbox/claim.js';
17
+ export type { TicketClaim, ClaimOptions } from './inbox/claim.js';
9
18
  export { renderMarkdown, checkMarkdown, ensureRenderer, isRendererReady, } from './render/termrender.js';
10
19
  export { parseDeck, validateDeck, deckSchema, resolveDeckBodyPaths } from './inbox/deck-schema.js';
11
20
  export { notifyDeck } from './inbox/deck-factories.js';
12
21
  export type { NotifyDeckOpts } from './inbox/deck-factories.js';
13
- export { deckPath, responsePath, progressPath, visualsDir, interactionState, isResolved, isClaimed, atomicWriteJson, readJson, writeResponse, writeProgress, clearProgress, } from './inbox/convention.js';
22
+ export { deckPath, reviewPath, responsePath, progressPath, visualsDir, interactionState, isResolved, isClaimed, atomicWriteJson, readJson, writeResponse, writeProgress, clearProgress, } from './inbox/convention.js';
14
23
  export type { InteractionState } from './inbox/convention.js';
15
- export type { Interaction, InteractionOption, InteractionResponse, InteractionKind, Deck, DeckSource, MountedPanel, MountedPanelOpts, GenerateVisual, VisualBlock, FeedbackComment, FeedbackResult, ResolutionEnvelope, InboxItem, DisplayOpts, } from './types.js';
24
+ export type { Interaction, InteractionOption, InteractionResponse, InteractionKind, Deck, DeckSource, MountedPanel, MountedPanelOpts, GenerateVisual, VisualBlock, FeedbackComment, FeedbackResult, ResolutionEnvelope, InboxItem, DisplayOpts, ClaimSummary, TicketSummary, DeckTicketSummary, ReviewTicketSummary, ReviewDescriptor, DeckTicketResult, ReviewTicketResult, CanceledTicketResult, TicketResult, InboxBindingState, CompletionEvent, } from './types.js';
16
25
  export type { Key } from './tui/terminal.js';
17
26
  export type { ConversationMessage } from './conversation/reader.js';
package/dist/index.js CHANGED
@@ -3,9 +3,15 @@ export { defaultGenerateVisual } from './visuals/generate.js';
3
3
  export { launchReview } from './editor/review.js';
4
4
  export { launchReview as review } from './editor/review.js';
5
5
  // Interaction-layer surface (SDK).
6
- export { ask, notify, inbox } from './api.js';
6
+ export { ask, notify, openInbox } from './api.js';
7
+ export { openInboxPopup } from './surfaces/inbox-popup.js';
8
+ export { toggleInboxPopup, installInboxBinding, inspectInboxBinding, unbindInboxBinding, inboxPopupStyle } from './tui/tmux.js';
7
9
  export { display } from './surfaces/display.js';
8
10
  export { scanInbox } from './inbox/scan.js';
11
+ export { registerInboxRoot, unregisterInboxRoot, listInboxRoots, managedInboxRoot } from './inbox/registry.js';
12
+ export { submitDeck, submitReview, readTicketResult, finalizeDeck, finalizeReview, completeDeck, completeReview, cancelTicket, cancelTicketResult, } from './inbox/tickets.js';
13
+ export { dispatchCompletion, reconcileCompletions } from './inbox/completion.js';
14
+ export { claimTicket, heartbeatClaim, releaseClaim, readTicketClaim } from './inbox/claim.js';
9
15
  // Renderer binding — the sole org-wide termrender caller. Consumers
10
16
  // (sisyphus md-render / ask-schema) route markdown through these.
11
17
  export { renderMarkdown, checkMarkdown, ensureRenderer, isRendererReady, } from './render/termrender.js';
@@ -15,4 +21,4 @@ export { parseDeck, validateDeck, deckSchema, resolveDeckBodyPaths } from './inb
15
21
  // who want validated Yes/No or notify decks without inline construction).
16
22
  export { notifyDeck } from './inbox/deck-factories.js';
17
23
  // Interaction-directory convention helpers (§B) — names humanloop owns.
18
- export { deckPath, responsePath, progressPath, visualsDir, interactionState, isResolved, isClaimed, atomicWriteJson, readJson, writeResponse, writeProgress, clearProgress, } from './inbox/convention.js';
24
+ export { deckPath, reviewPath, responsePath, progressPath, visualsDir, interactionState, isResolved, isClaimed, atomicWriteJson, readJson, writeResponse, writeProgress, clearProgress, } from './inbox/convention.js';
@@ -5,6 +5,11 @@
5
5
  * degradation path: `uv` absent → one stderr remediation line + plaintext
6
6
  * fallback. win32 → plaintext (no renderer).
7
7
  *
8
+ * Steady state is subprocess-free: a valid stamp is trusted outright. The
9
+ * spawn-based verification (`binaryOk` + `installedVersion`) runs only when the
10
+ * stamp is absent or stale — once, after which the venv is re-stamped — and the
11
+ * full `uv` reinstall runs only when the binary is actually missing or drifted.
12
+ *
8
13
  * Invoked at postinstall AND lazily on the first render/check/display call,
9
14
  * so `npm ci --ignore-scripts` consumers still self-heal on first use.
10
15
  */
@@ -1,5 +1,5 @@
1
1
  import { execFileSync, spawnSync } from 'node:child_process';
2
- import { existsSync } from 'node:fs';
2
+ import { existsSync, readFileSync, writeFileSync, statSync } from 'node:fs';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { dirname, join, resolve } from 'node:path';
5
5
  import stringWidth from 'string-width';
@@ -27,7 +27,48 @@ const PKG_ROOT = findPkgRoot();
27
27
  const VENV_DIR = resolve(PKG_ROOT, '.venv');
28
28
  const VENV_BIN = resolve(PKG_ROOT, '.venv/bin/termrender');
29
29
  const VENV_PYTHON = resolve(PKG_ROOT, '.venv/bin/python');
30
+ // Stamp written after a verified install: records the pin we provisioned and
31
+ // the binary's mtime at that moment. Steady-state validation is a stat + a
32
+ // tiny JSON read against this — no subprocess — so attach no longer pays the
33
+ // ~149ms `termrender -h` + `importlib.metadata` spawn tax on every launch.
34
+ const VENV_STAMP = resolve(PKG_ROOT, '.venv/.hl-termrender-stamp.json');
30
35
  let rendererState = 'unchecked';
36
+ function readStamp() {
37
+ try {
38
+ const parsed = JSON.parse(readFileSync(VENV_STAMP, 'utf8'));
39
+ if (typeof parsed.version === 'string' && typeof parsed.mtimeMs === 'number')
40
+ return parsed;
41
+ return null;
42
+ }
43
+ catch {
44
+ return null;
45
+ }
46
+ }
47
+ function writeStamp() {
48
+ try {
49
+ writeFileSync(VENV_STAMP, JSON.stringify({ version: TERMRENDER_VERSION, mtimeMs: statSync(VENV_BIN).mtimeMs }));
50
+ }
51
+ catch {
52
+ // Best-effort: an unwritable stamp just means the next process re-verifies
53
+ // the slow way once and re-stamps. Correctness is unaffected.
54
+ }
55
+ }
56
+ // Cheap steady-state trust: the pinned binary is present and unchanged since
57
+ // the verified install that wrote the stamp. A version bump or an out-of-band
58
+ // rewrite of the binary changes the pin or the mtime and forces re-provision.
59
+ function stampValid() {
60
+ if (!existsSync(VENV_BIN))
61
+ return false;
62
+ const stamp = readStamp();
63
+ if (!stamp || stamp.version !== TERMRENDER_VERSION)
64
+ return false;
65
+ try {
66
+ return statSync(VENV_BIN).mtimeMs === stamp.mtimeMs;
67
+ }
68
+ catch {
69
+ return false;
70
+ }
71
+ }
31
72
  function binaryOk() {
32
73
  if (!existsSync(VENV_BIN))
33
74
  return false;
@@ -76,6 +117,11 @@ function uvAvailable() {
76
117
  * degradation path: `uv` absent → one stderr remediation line + plaintext
77
118
  * fallback. win32 → plaintext (no renderer).
78
119
  *
120
+ * Steady state is subprocess-free: a valid stamp is trusted outright. The
121
+ * spawn-based verification (`binaryOk` + `installedVersion`) runs only when the
122
+ * stamp is absent or stale — once, after which the venv is re-stamped — and the
123
+ * full `uv` reinstall runs only when the binary is actually missing or drifted.
124
+ *
79
125
  * Invoked at postinstall AND lazily on the first render/check/display call,
80
126
  * so `npm ci --ignore-scripts` consumers still self-heal on first use.
81
127
  */
@@ -86,7 +132,16 @@ export function ensureRenderer() {
86
132
  rendererState = 'unavailable';
87
133
  return;
88
134
  }
135
+ // Steady state: trust the stamp — zero subprocess spawns.
136
+ if (stampValid()) {
137
+ rendererState = 'ready';
138
+ return;
139
+ }
140
+ // No/stale stamp but the correct binary is already present (a venv from a
141
+ // pre-stamp humanloop, or an interrupted stamp write): verify once the slow
142
+ // way, stamp it, and skip the reinstall.
89
143
  if (binaryOk() && installedVersion() === TERMRENDER_VERSION) {
144
+ writeStamp();
90
145
  rendererState = 'ready';
91
146
  return;
92
147
  }
@@ -113,8 +168,12 @@ export function ensureRenderer() {
113
168
  rendererState = 'unavailable';
114
169
  return;
115
170
  }
116
- rendererState = (binaryOk() && installedVersion() === TERMRENDER_VERSION) ? 'ready' : 'unavailable';
117
- if (rendererState === 'unavailable') {
171
+ if (binaryOk() && installedVersion() === TERMRENDER_VERSION) {
172
+ writeStamp();
173
+ rendererState = 'ready';
174
+ }
175
+ else {
176
+ rendererState = 'unavailable';
118
177
  process.stderr.write('[hl] termrender install completed but health check failed; using plaintext fallback\n');
119
178
  }
120
179
  }
@@ -124,7 +183,7 @@ export function isRendererReady() {
124
183
  return true;
125
184
  if (rendererState === 'unavailable')
126
185
  return false;
127
- return process.platform !== 'win32' && binaryOk();
186
+ return process.platform !== 'win32' && (stampValid() || binaryOk());
128
187
  }
129
188
  // ── Plaintext fallback helpers (kept here so this is the only termrender site) ─
130
189
  const CONTROL_CHARS_RE = /\x1b\[[0-9;?]*[a-zA-Z]|\x1b[@-_]|[\x00-\x08\x0B\x0E-\x1F\x7F-\x9F]/g;
package/dist/summary.d.ts CHANGED
@@ -2,8 +2,7 @@ import type { Deck, InteractionResponse } from './types.js';
2
2
  /**
3
3
  * Deterministic, no-LLM resolution summary — one line per answered
4
4
  * interaction: `"<title>: <option label>[ — <freetext>]"`. Shared by `ask()`
5
- * (envelope summary), `writeResponse` (persisted into response.json at write
6
- * time), and `hl job result` (fallback rebuild for legacy response.json files
7
- * written before the summary field existed).
5
+ * (envelope summary) and `writeResponse` (persisted into response.json at write
6
+ * time).
8
7
  */
9
8
  export declare function buildSummary(deck: Deck, responses: InteractionResponse[]): string;
package/dist/summary.js CHANGED
@@ -1,9 +1,8 @@
1
1
  /**
2
2
  * Deterministic, no-LLM resolution summary — one line per answered
3
3
  * interaction: `"<title>: <option label>[ — <freetext>]"`. Shared by `ask()`
4
- * (envelope summary), `writeResponse` (persisted into response.json at write
5
- * time), and `hl job result` (fallback rebuild for legacy response.json files
6
- * written before the summary field existed).
4
+ * (envelope summary) and `writeResponse` (persisted into response.json at write
5
+ * time).
7
6
  */
8
7
  export function buildSummary(deck, responses) {
9
8
  const byId = new Map(responses.map((r) => [r.id, r]));
@@ -0,0 +1,2 @@
1
+ /** Run the inbox controller in a popup-owned TTY and accept graceful close requests. */
2
+ export declare function openInboxPopup(controlSocket?: string, roots?: string[]): Promise<void>;
@@ -0,0 +1,32 @@
1
+ import { createServer } from 'node:net';
2
+ import { rmSync } from 'node:fs';
3
+ import { InboxController } from '../inbox/controller.js';
4
+ /** Run the inbox controller in a popup-owned TTY and accept graceful close requests. */
5
+ export async function openInboxPopup(controlSocket, roots) {
6
+ const controller = new InboxController({ roots });
7
+ const server = controlSocket === undefined ? undefined : createServer((connection) => {
8
+ connection.once('data', (data) => {
9
+ if (data.toString('utf8').trim() === 'close')
10
+ controller.close();
11
+ connection.end();
12
+ });
13
+ });
14
+ if (server !== undefined && controlSocket !== undefined) {
15
+ try {
16
+ rmSync(controlSocket, { force: true });
17
+ }
18
+ catch { /* socket ownership is established by listen */ }
19
+ await new Promise((resolve, reject) => {
20
+ server.once('error', reject);
21
+ server.listen(controlSocket, resolve);
22
+ });
23
+ }
24
+ try {
25
+ await controller.run();
26
+ }
27
+ finally {
28
+ await new Promise((resolve) => server?.close(() => resolve()) ?? resolve());
29
+ if (controlSocket !== undefined)
30
+ rmSync(controlSocket, { force: true });
31
+ }
32
+ }
package/dist/tui/app.js CHANGED
@@ -203,6 +203,7 @@ export function mountPanel(opts) {
203
203
  loadDeck(deck, loadOpts) {
204
204
  if (!internals.mounted)
205
205
  return;
206
+ const prior = collectResponses(internals.state);
206
207
  internals.state = buildInitialState(deck);
207
208
  if (loadOpts !== undefined && loadOpts.progressPath !== undefined) {
208
209
  internals.progressPath = loadOpts.progressPath;
@@ -212,6 +213,18 @@ export function mountPanel(opts) {
212
213
  if (internals.progressPath !== undefined) {
213
214
  tryResume(internals.state, internals.progressPath, deck.interactions);
214
215
  }
216
+ // A live request replacement is allowed to add/remove questions but not
217
+ // discard answers the human has already made to surviving ids.
218
+ const validIds = new Set(deck.interactions.map((interaction) => interaction.id));
219
+ for (const response of prior) {
220
+ if (validIds.has(response.id))
221
+ internals.state.responses.set(response.id, response);
222
+ }
223
+ const currentId = internals.state.interactions[internals.state.currentIndex]?.id;
224
+ if (currentId === undefined || !validIds.has(currentId)) {
225
+ const firstUnanswered = internals.state.interactions.findIndex((interaction) => !internals.state.responses.has(interaction.id));
226
+ internals.state.currentIndex = firstUnanswered >= 0 ? firstUnanswered : 0;
227
+ }
215
228
  fireVisuals(internals, deck.interactions);
216
229
  },
217
230
  canAcceptHostKeys() {
@@ -1,8 +1,31 @@
1
- import type { ResolutionEnvelope } from '../types.js';
2
- export interface TmuxDispatchOpts {
3
- sessionId?: string;
4
- visuals: boolean;
5
- /** Interaction dir forwarded to the child so response.json lands there. */
6
- dir: string;
1
+ import type { InboxBindingState } from '../types.js';
2
+ export interface TmuxPopupTarget {
3
+ socket: string;
4
+ client: string;
5
+ targetPane?: string;
7
6
  }
8
- export declare function dispatchToTmuxPane(file: string, opts: TmuxDispatchOpts): Promise<ResolutionEnvelope>;
7
+ export type ToggleInboxPopupResult = 'opened' | 'closed' | 'other_popup' | 'ambiguous_client' | 'not_in_tmux' | 'failed';
8
+ export declare function popupPaths(target: TmuxPopupTarget): {
9
+ controlSocket: string;
10
+ startupLock: string;
11
+ };
12
+ export declare function inspectInboxBinding(socket?: string | undefined): InboxBindingState;
13
+ export declare function installInboxBinding(opts?: {
14
+ socket?: string;
15
+ key?: string;
16
+ }): InboxBindingState;
17
+ export declare function unbindInboxBinding(socket?: string | undefined): InboxBindingState;
18
+ export declare function tmuxSocketFromEnvironment(): string | undefined;
19
+ export declare function inferTmuxClient(socket: string, pane?: string | undefined): string | undefined | 'ambiguous';
20
+ export declare function toggleInboxPopup(target?: Partial<TmuxPopupTarget>): Promise<ToggleInboxPopupResult>;
21
+ /** The static tmux `display-popup` geometry and style flags; the client and target pane are added per-invocation. */
22
+ export declare function inboxPopupFlags(): string[];
23
+ export declare const inboxPopupStyle: {
24
+ readonly width: "90%";
25
+ readonly height: "90%";
26
+ readonly border: "rounded";
27
+ readonly title: "humanloop · inbox";
28
+ readonly background: "#20242d";
29
+ readonly chrome: "#2b3245";
30
+ readonly borderColor: "#5c6370";
31
+ };