@crouton-kit/humanloop 0.3.33 → 0.3.34
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 +126 -1114
- package/dist/editor/feedback.d.ts +9 -0
- package/dist/editor/feedback.js +23 -0
- package/dist/editor/review.d.ts +8 -30
- package/dist/editor/review.js +79 -118
- 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 +100 -0
- package/dist/inbox/controller.d.ts +57 -0
- package/dist/inbox/controller.js +311 -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 +22 -0
- package/dist/inbox/review-adapter.js +56 -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/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 +179 -66
- package/dist/types.d.ts +68 -7
- package/package.json +2 -2
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';
|
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
|
+
};
|
package/dist/tui/tmux.js
CHANGED
|
@@ -1,74 +1,187 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { execFileSync, spawn } from 'node:child_process';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { homedir, tmpdir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
const DEFAULT_KEY = 'M-i';
|
|
8
|
+
const POPUP_TITLE = 'humanloop · inbox';
|
|
9
|
+
const POPUP_STYLE = 'bg=#20242d';
|
|
10
|
+
const POPUP_BORDER_STYLE = 'fg=#5c6370';
|
|
11
|
+
function runtimeDirectory() {
|
|
12
|
+
const base = process.env['XDG_RUNTIME_DIR'] || process.env['TMPDIR'] || tmpdir();
|
|
13
|
+
return join(base, `humanloop-${process.getuid?.() ?? process.env['UID'] ?? 'user'}`);
|
|
14
|
+
}
|
|
15
|
+
export function popupPaths(target) {
|
|
16
|
+
const identity = createHash('sha256').update(`${target.socket}\0${target.client}`).digest('hex').slice(0, 16);
|
|
17
|
+
const base = join(runtimeDirectory(), 'inbox');
|
|
18
|
+
return { controlSocket: join(base, `${identity}.sock`), startupLock: join(base, `${identity}.lock`) };
|
|
19
|
+
}
|
|
20
|
+
function tmux(socket, args) {
|
|
21
|
+
return execFileSync('tmux', ['-S', socket, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
|
|
22
|
+
}
|
|
23
|
+
function quote(value) { return `'${value.replace(/'/g, `'\\''`)}'`; }
|
|
24
|
+
function bindingCommand() {
|
|
25
|
+
return 'hl inbox toggle --tmux-socket "#{socket_path}" --tmux-client "#{client_name}" --target-pane "#{pane_id}"';
|
|
26
|
+
}
|
|
27
|
+
function configuredKeyPath() {
|
|
28
|
+
const state = process.env['XDG_STATE_HOME'] || join(homedir(), '.local', 'state');
|
|
29
|
+
return join(state, 'humanloop', 'inbox-key');
|
|
30
|
+
}
|
|
31
|
+
function configuredKey() {
|
|
32
|
+
try {
|
|
33
|
+
return readFileSync(configuredKeyPath(), 'utf8').trim() || DEFAULT_KEY;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return DEFAULT_KEY;
|
|
14
37
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
'
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (opts.sessionId) {
|
|
26
|
-
parts.push('--session-id', shellQuote(opts.sessionId));
|
|
38
|
+
}
|
|
39
|
+
function writeConfiguredKey(key) {
|
|
40
|
+
mkdirSync(join(configuredKeyPath(), '..'), { recursive: true, mode: 0o700 });
|
|
41
|
+
writeFileSync(configuredKeyPath(), `${key}\n`, { mode: 0o600 });
|
|
42
|
+
}
|
|
43
|
+
function rootBinding(socket, key) {
|
|
44
|
+
try {
|
|
45
|
+
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
46
|
+
const match = tmux(socket, ['list-keys', '-T', 'root']).split('\n').map((row) => new RegExp(`^bind-key\\s+-T root\\s+${escaped}\\s+(.*)$`).exec(row)).find((entry) => entry !== null);
|
|
47
|
+
return match?.[1];
|
|
27
48
|
}
|
|
28
|
-
|
|
29
|
-
|
|
49
|
+
catch {
|
|
50
|
+
return undefined;
|
|
30
51
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const
|
|
52
|
+
}
|
|
53
|
+
function isCanonical(command) {
|
|
54
|
+
const normalized = command?.replace(/\\"/g, '"');
|
|
55
|
+
return normalized !== undefined && normalized.includes('hl inbox toggle') && normalized.includes('--tmux-socket "#{socket_path}"') && normalized.includes('--tmux-client "#{client_name}"') && normalized.includes('--target-pane "#{pane_id}"');
|
|
56
|
+
}
|
|
57
|
+
export function inspectInboxBinding(socket = tmuxSocketFromEnvironment()) {
|
|
58
|
+
const key = configuredKey();
|
|
59
|
+
if (socket === undefined)
|
|
60
|
+
return { state: 'unbound', key, isDefault: key === DEFAULT_KEY };
|
|
61
|
+
const command = rootBinding(socket, key);
|
|
62
|
+
return { state: command === undefined ? 'unbound' : isCanonical(command) ? 'installed' : 'collision', key, isDefault: key === DEFAULT_KEY };
|
|
63
|
+
}
|
|
64
|
+
export function installInboxBinding(opts = {}) {
|
|
65
|
+
const socket = opts.socket ?? tmuxSocketFromEnvironment();
|
|
66
|
+
const key = opts.key ?? configuredKey();
|
|
67
|
+
if (socket === undefined)
|
|
68
|
+
return { state: 'unbound', key, isDefault: key === DEFAULT_KEY };
|
|
69
|
+
const existing = rootBinding(socket, key);
|
|
70
|
+
if (existing !== undefined && !isCanonical(existing))
|
|
71
|
+
return { state: 'collision', key, isDefault: key === DEFAULT_KEY };
|
|
72
|
+
if (existing === undefined)
|
|
73
|
+
tmux(socket, ['bind-key', '-T', 'root', key, 'run-shell', '-b', bindingCommand()]);
|
|
74
|
+
if (opts.key !== undefined) {
|
|
75
|
+
// Switching to a new key: drop the previous configured key iff it still holds the
|
|
76
|
+
// canonical toggle, so bindings don't accrete and inspect/unbind track a single live key.
|
|
77
|
+
const previous = configuredKey();
|
|
78
|
+
if (previous !== key && isCanonical(rootBinding(socket, previous)))
|
|
79
|
+
tmux(socket, ['unbind-key', '-T', 'root', previous]);
|
|
80
|
+
writeConfiguredKey(key);
|
|
81
|
+
}
|
|
82
|
+
return { state: 'installed', key, isDefault: key === DEFAULT_KEY };
|
|
83
|
+
}
|
|
84
|
+
export function unbindInboxBinding(socket = tmuxSocketFromEnvironment()) {
|
|
85
|
+
const key = configuredKey();
|
|
86
|
+
if (socket !== undefined && isCanonical(rootBinding(socket, key)))
|
|
87
|
+
tmux(socket, ['unbind-key', '-T', 'root', key]);
|
|
88
|
+
return inspectInboxBinding(socket);
|
|
89
|
+
}
|
|
90
|
+
export function tmuxSocketFromEnvironment() {
|
|
91
|
+
const value = process.env['TMUX'];
|
|
92
|
+
return value?.split(',')[0] || undefined;
|
|
93
|
+
}
|
|
94
|
+
export function inferTmuxClient(socket, pane = process.env['TMUX_PANE']) {
|
|
95
|
+
if (pane === undefined)
|
|
96
|
+
return undefined;
|
|
97
|
+
const matches = tmux(socket, ['list-clients', '-F', '#{client_name}\t#{pane_id}']).split('\n')
|
|
98
|
+
.map((line) => line.split('\t')).filter((parts) => parts[1] === pane).map((parts) => parts[0]);
|
|
99
|
+
return matches.length === 1 ? matches[0] : matches.length > 1 ? 'ambiguous' : undefined;
|
|
100
|
+
}
|
|
101
|
+
function acquireStartupLock(path) {
|
|
65
102
|
try {
|
|
66
|
-
|
|
103
|
+
mkdirSync(path, { mode: 0o700 });
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return false;
|
|
67
108
|
}
|
|
68
|
-
|
|
109
|
+
}
|
|
110
|
+
export async function toggleInboxPopup(target) {
|
|
111
|
+
const socket = target?.socket ?? tmuxSocketFromEnvironment();
|
|
112
|
+
if (socket === undefined)
|
|
113
|
+
return 'not_in_tmux';
|
|
114
|
+
const inferred = target?.client ?? inferTmuxClient(socket, target?.targetPane);
|
|
115
|
+
if (inferred === 'ambiguous')
|
|
116
|
+
return 'ambiguous_client';
|
|
117
|
+
if (inferred === undefined)
|
|
118
|
+
return 'ambiguous_client';
|
|
119
|
+
const resolved = { socket, client: inferred, targetPane: target?.targetPane };
|
|
120
|
+
const paths = popupPaths(resolved);
|
|
121
|
+
mkdirSync(join(paths.controlSocket, '..'), { recursive: true, mode: 0o700 });
|
|
122
|
+
if (await requestPopupClose(paths.controlSocket))
|
|
123
|
+
return 'closed';
|
|
124
|
+
if (!acquireStartupLock(paths.startupLock))
|
|
125
|
+
return 'failed';
|
|
69
126
|
try {
|
|
70
|
-
|
|
127
|
+
// Re-probe under the lock: between the first probe and acquiring the lock a concurrent
|
|
128
|
+
// toggle may have brought a live popup up. Deleting its control socket now would orphan
|
|
129
|
+
// that popup, so close it instead. The finally below releases the lock on every path.
|
|
130
|
+
if (await requestPopupClose(paths.controlSocket))
|
|
131
|
+
return 'closed';
|
|
132
|
+
if (existsSync(paths.controlSocket))
|
|
133
|
+
rmSync(paths.controlSocket, { force: true });
|
|
134
|
+
const command = `${quote(process.execPath)} ${quote(fileURLToPath(new URL('../cli.js', import.meta.url)))} inbox open --control-socket ${quote(paths.controlSocket)}`;
|
|
135
|
+
return await launchPopup(socket, resolved, paths.controlSocket, command);
|
|
71
136
|
}
|
|
72
|
-
|
|
73
|
-
|
|
137
|
+
finally {
|
|
138
|
+
rmSync(paths.startupLock, { recursive: true, force: true });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/** Connect to the control socket and, if a live popup owns it, ask it to close. Resolves true when a popup answered. */
|
|
142
|
+
async function requestPopupClose(controlSocket) {
|
|
143
|
+
const { Socket } = await import('node:net');
|
|
144
|
+
return new Promise((resolve) => {
|
|
145
|
+
const client = new Socket();
|
|
146
|
+
const done = (value) => { client.destroy(); resolve(value); };
|
|
147
|
+
client.setTimeout(300, () => done(false));
|
|
148
|
+
client.once('error', () => done(false));
|
|
149
|
+
client.connect(controlSocket, () => { client.end('close\n'); done(true); });
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
/** Launch one popup and report `opened` only once its controller owns the control socket. */
|
|
153
|
+
async function launchPopup(socket, target, controlSocket, command) {
|
|
154
|
+
const args = ['-S', socket, 'display-popup', '-E', '-c', target.client, ...inboxPopupFlags(), ...(target.targetPane === undefined ? [] : ['-t', target.targetPane]), command];
|
|
155
|
+
const child = spawn('tmux', args, { stdio: ['ignore', 'ignore', 'pipe'], detached: true });
|
|
156
|
+
let stderr = '';
|
|
157
|
+
child.stderr.on('data', (chunk) => { stderr += chunk.toString('utf8'); });
|
|
158
|
+
const exited = new Promise((resolve) => {
|
|
159
|
+
child.once('exit', (code) => resolve(code === 0 ? 'closed' : /popup/i.test(stderr) ? 'other_popup' : 'failed'));
|
|
160
|
+
child.once('error', () => resolve('failed'));
|
|
161
|
+
});
|
|
162
|
+
const { Socket } = await import('node:net');
|
|
163
|
+
for (let attempt = 0; attempt < 50; attempt++) {
|
|
164
|
+
const outcome = await Promise.race([
|
|
165
|
+
exited,
|
|
166
|
+
new Promise((resolve) => {
|
|
167
|
+
const probe = new Socket();
|
|
168
|
+
const done = (value) => { probe.destroy(); setTimeout(() => resolve(value), value === 'pending' ? 100 : 0); };
|
|
169
|
+
probe.setTimeout(200, () => done('pending'));
|
|
170
|
+
probe.once('error', () => done('pending'));
|
|
171
|
+
probe.connect(controlSocket, () => done('opened'));
|
|
172
|
+
}),
|
|
173
|
+
]);
|
|
174
|
+
if (outcome === 'opened') {
|
|
175
|
+
child.unref();
|
|
176
|
+
return 'opened';
|
|
177
|
+
}
|
|
178
|
+
if (outcome !== 'pending')
|
|
179
|
+
return outcome === 'closed' ? 'failed' : outcome;
|
|
180
|
+
}
|
|
181
|
+
return 'failed';
|
|
182
|
+
}
|
|
183
|
+
/** The static tmux `display-popup` geometry and style flags; the client and target pane are added per-invocation. */
|
|
184
|
+
export function inboxPopupFlags() {
|
|
185
|
+
return ['-w', '90%', '-h', '90%', '-b', 'rounded', '-T', POPUP_TITLE, '-s', POPUP_STYLE, '-S', POPUP_BORDER_STYLE];
|
|
74
186
|
}
|
|
187
|
+
export const inboxPopupStyle = { width: '90%', height: '90%', border: 'rounded', title: POPUP_TITLE, background: '#20242d', chrome: '#2b3245', borderColor: '#5c6370' };
|
package/dist/types.d.ts
CHANGED
|
@@ -137,8 +137,7 @@ export interface TuiState {
|
|
|
137
137
|
}
|
|
138
138
|
/**
|
|
139
139
|
* Resolution contract returned by `ask`/`inbox`. On-disk `response.json` stays
|
|
140
|
-
* `{ responses, completedAt }`; `responsePath` points at it.
|
|
141
|
-
* response` returns the JSON Schema this `schema` id names.
|
|
140
|
+
* `{ responses, completedAt }`; `responsePath` points at it.
|
|
142
141
|
*/
|
|
143
142
|
export interface ResolutionEnvelope {
|
|
144
143
|
/** 1 line/interaction "<title>: <option label>[ — <freetext>]"; deterministic, no LLM. */
|
|
@@ -155,15 +154,76 @@ export interface ResolutionEnvelope {
|
|
|
155
154
|
* One pending interaction discovered by `scanInbox`. Read from the
|
|
156
155
|
* `deck.json` header only — never the full deck.
|
|
157
156
|
*/
|
|
158
|
-
export interface
|
|
157
|
+
export interface ClaimSummary {
|
|
158
|
+
owner: string;
|
|
159
|
+
claimedAt: string;
|
|
160
|
+
heartbeatAt: string;
|
|
161
|
+
}
|
|
162
|
+
interface TicketSummaryBase {
|
|
159
163
|
dir: string;
|
|
160
164
|
id: string;
|
|
161
|
-
title
|
|
165
|
+
title: string;
|
|
162
166
|
subtitle?: string;
|
|
163
|
-
kind?: InteractionKind;
|
|
164
|
-
/** `deck.source.blockedSince` ?? `statSync(deck.json).mtime` (ISO). */
|
|
165
167
|
blockedSince: string;
|
|
166
|
-
source
|
|
168
|
+
source: DeckSource;
|
|
169
|
+
claim?: ClaimSummary;
|
|
170
|
+
}
|
|
171
|
+
export interface DeckTicketSummary extends TicketSummaryBase {
|
|
172
|
+
kind: 'deck';
|
|
173
|
+
interactionKind?: InteractionKind;
|
|
174
|
+
}
|
|
175
|
+
export interface ReviewTicketSummary extends TicketSummaryBase {
|
|
176
|
+
kind: 'review';
|
|
177
|
+
file: string;
|
|
178
|
+
output: string;
|
|
179
|
+
}
|
|
180
|
+
/** The only pending-ticket shape scanners expose. */
|
|
181
|
+
export type TicketSummary = DeckTicketSummary | ReviewTicketSummary;
|
|
182
|
+
/** Compatibility name retained only while the list adapter is moved in H2. */
|
|
183
|
+
export type InboxItem = TicketSummary;
|
|
184
|
+
export interface ReviewDescriptor {
|
|
185
|
+
schema: 'humanloop.review/v1';
|
|
186
|
+
file: string;
|
|
187
|
+
output: string;
|
|
188
|
+
title: string;
|
|
189
|
+
source: DeckSource;
|
|
190
|
+
blockedSince: string;
|
|
191
|
+
}
|
|
192
|
+
export interface DeckTicketResult {
|
|
193
|
+
schema: 'humanloop.response/v2';
|
|
194
|
+
kind: 'deck';
|
|
195
|
+
responses: InteractionResponse[];
|
|
196
|
+
summary: string;
|
|
197
|
+
completedAt: string;
|
|
198
|
+
}
|
|
199
|
+
export interface ReviewTicketResult {
|
|
200
|
+
schema: 'humanloop.review-response/v1';
|
|
201
|
+
kind: 'review';
|
|
202
|
+
result: FeedbackResult;
|
|
203
|
+
completedAt: string;
|
|
204
|
+
}
|
|
205
|
+
export interface CanceledTicketResult {
|
|
206
|
+
schema: 'humanloop.cancel/v1';
|
|
207
|
+
kind: 'canceled';
|
|
208
|
+
canceledAt: string;
|
|
209
|
+
reason?: string;
|
|
210
|
+
actor?: string;
|
|
211
|
+
}
|
|
212
|
+
/** The sole canonical response.json union. */
|
|
213
|
+
export type TicketResult = DeckTicketResult | ReviewTicketResult | CanceledTicketResult;
|
|
214
|
+
export interface InboxBindingState {
|
|
215
|
+
state: 'installed' | 'collision' | 'unbound';
|
|
216
|
+
key: string;
|
|
217
|
+
isDefault: boolean;
|
|
218
|
+
}
|
|
219
|
+
export interface CompletionEvent {
|
|
220
|
+
schema: 'humanloop.completion/v1';
|
|
221
|
+
root: string;
|
|
222
|
+
dir: string;
|
|
223
|
+
ticketId: string;
|
|
224
|
+
kind: 'deck' | 'review' | 'canceled';
|
|
225
|
+
outcome: 'resolved' | 'canceled';
|
|
226
|
+
responsePath: string;
|
|
167
227
|
}
|
|
168
228
|
/** Options for `display()` — the live-watch tmux pane surface. The pane always
|
|
169
229
|
* watches the file and live-updates on edits; there is no non-watched mode. */
|
|
@@ -223,3 +283,4 @@ export interface MountedPanel {
|
|
|
223
283
|
*/
|
|
224
284
|
setInputBuffer(text: string): void;
|
|
225
285
|
}
|
|
286
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crouton-kit/humanloop",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.34",
|
|
4
4
|
"description": "Human-in-the-loop decision TUI — agents write questions, humans answer them",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"dev": "tsx src/cli.ts",
|
|
30
30
|
"link": "npm link",
|
|
31
31
|
"postinstall": "node dist/scripts/install-renderer.js || true",
|
|
32
|
-
"test": "tsx src/__tests__/mount-panel.test.ts && tsx src/__tests__/feedback.test.ts && tsx src/__tests__/browser-server.test.ts && tsx web/src/__tests__/review-sourcemap.test.ts && tsx web/src/__tests__/review-reducer.test.ts && tsx web/src/__tests__/review-keymap.test.ts && tsx web/src/__tests__/review-markdown-instrumentation.test.ts && tsx --tsconfig web/tsconfig.json web/src/__tests__/app-deck-regression.test.ts && tsx --tsconfig web/tsconfig.json web/src/__tests__/review-surface-conflict.test.ts && tsx --tsconfig web/tsconfig.json web/src/__tests__/app-ws-close.test.ts && tsx --tsconfig web/tsconfig.json web/src/__tests__/review-surface-takeback.test.ts && tsx --tsconfig web/tsconfig.json web/src/__tests__/review-surface-submit-race.test.ts && tsx --tsconfig web/tsconfig.json web/src/__tests__/review-surface-stale-submit-failure.test.ts"
|
|
32
|
+
"test": "tsx src/__tests__/inbox-core.test.ts && tsx src/__tests__/mount-panel.test.ts && tsx src/__tests__/feedback.test.ts && tsx src/__tests__/browser-server.test.ts && tsx web/src/__tests__/review-sourcemap.test.ts && tsx web/src/__tests__/review-reducer.test.ts && tsx web/src/__tests__/review-keymap.test.ts && tsx web/src/__tests__/review-markdown-instrumentation.test.ts && tsx --tsconfig web/tsconfig.json web/src/__tests__/app-deck-regression.test.ts && tsx --tsconfig web/tsconfig.json web/src/__tests__/review-surface-conflict.test.ts && tsx --tsconfig web/tsconfig.json web/src/__tests__/app-ws-close.test.ts && tsx --tsconfig web/tsconfig.json web/src/__tests__/review-surface-takeback.test.ts && tsx --tsconfig web/tsconfig.json web/src/__tests__/review-surface-submit-race.test.ts && tsx --tsconfig web/tsconfig.json web/src/__tests__/review-surface-stale-submit-failure.test.ts && tsx src/__tests__/inbox-popup.test.ts"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@r-cli/sdk": "^1.3.0",
|