@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.
- package/dist/api.d.ts +4 -8
- package/dist/api.js +6 -29
- package/dist/browser/server.d.ts +3 -3
- package/dist/browser/server.js +22 -25
- package/dist/cli.js +131 -1114
- package/dist/editor/feedback.d.ts +9 -0
- package/dist/editor/feedback.js +23 -0
- package/dist/editor/review.d.ts +12 -30
- package/dist/editor/review.js +109 -119
- package/dist/inbox/claim.d.ts +23 -0
- package/dist/inbox/claim.js +67 -0
- package/dist/inbox/completion.d.ts +4 -0
- package/dist/inbox/completion.js +107 -0
- package/dist/inbox/controller.d.ts +74 -0
- package/dist/inbox/controller.js +457 -0
- package/dist/inbox/convention.d.ts +15 -10
- package/dist/inbox/convention.js +149 -79
- package/dist/inbox/deck-adapter.d.ts +27 -0
- package/dist/inbox/deck-adapter.js +57 -0
- package/dist/inbox/deck-schema.d.ts +18 -14
- package/dist/inbox/deck-schema.js +59 -117
- package/dist/inbox/layout.d.ts +8 -0
- package/dist/inbox/layout.js +9 -0
- package/dist/inbox/registry.d.ts +29 -0
- package/dist/inbox/registry.js +97 -0
- package/dist/inbox/review-adapter.d.ts +24 -0
- package/dist/inbox/review-adapter.js +57 -0
- package/dist/inbox/scan.d.ts +3 -2
- package/dist/inbox/scan.js +71 -43
- package/dist/inbox/tickets.d.ts +63 -0
- package/dist/inbox/tickets.js +247 -0
- package/dist/inbox/tui.d.ts +3 -6
- package/dist/inbox/tui.js +24 -112
- package/dist/index.d.ts +12 -3
- package/dist/index.js +8 -2
- package/dist/render/termrender.d.ts +5 -0
- package/dist/render/termrender.js +63 -4
- package/dist/summary.d.ts +2 -3
- package/dist/summary.js +2 -3
- package/dist/surfaces/inbox-popup.d.ts +2 -0
- package/dist/surfaces/inbox-popup.js +32 -0
- package/dist/tui/app.js +13 -0
- package/dist/tui/tmux.d.ts +30 -7
- package/dist/tui/tmux.js +190 -66
- package/dist/types.d.ts +68 -7
- 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
|
-
|
|
15
|
-
export const
|
|
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
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
21
|
+
/** Pure inbox rows; selection and terminal ownership belong to InboxController. */
|
|
39
22
|
export function buildInboxLines(items, width, selectedIndex) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
49
|
-
const item = items[
|
|
50
|
-
const
|
|
51
|
-
const icon =
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
row +=
|
|
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.
|
|
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,
|
|
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,
|
|
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
|
-
|
|
117
|
-
|
|
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)
|
|
6
|
-
* time)
|
|
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)
|
|
5
|
-
* time)
|
|
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,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() {
|
package/dist/tui/tmux.d.ts
CHANGED
|
@@ -1,8 +1,31 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export interface
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
|
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
|
+
};
|