@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.
Files changed (44) 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 +126 -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 +8 -30
  9. package/dist/editor/review.js +79 -118
  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 +100 -0
  14. package/dist/inbox/controller.d.ts +57 -0
  15. package/dist/inbox/controller.js +311 -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 +22 -0
  27. package/dist/inbox/review-adapter.js +56 -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/summary.d.ts +2 -3
  37. package/dist/summary.js +2 -3
  38. package/dist/surfaces/inbox-popup.d.ts +2 -0
  39. package/dist/surfaces/inbox-popup.js +32 -0
  40. package/dist/tui/app.js +13 -0
  41. package/dist/tui/tmux.d.ts +30 -7
  42. package/dist/tui/tmux.js +179 -66
  43. package/dist/types.d.ts +68 -7
  44. 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, 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';
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
+ };
package/dist/tui/tmux.js CHANGED
@@ -1,74 +1,187 @@
1
- import { execFileSync } from 'child_process';
2
- import { existsSync, mkdtempSync, readFileSync, rmdirSync, unlinkSync } from 'fs';
3
- import { tmpdir } from 'os';
4
- import { join } from 'path';
5
- function shellQuote(s) {
6
- if (s.length > 0 && /^[a-zA-Z0-9_\-./:@%+=]+$/.test(s))
7
- return s;
8
- return "'" + s.replace(/'/g, `'\\''`) + "'";
9
- }
10
- function buildChildCmd(file, resultPath, opts) {
11
- const scriptPath = process.argv[1];
12
- if (!scriptPath) {
13
- throw new Error('Cannot determine hl script path from process.argv[1]');
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
- const parts = [
16
- shellQuote(process.execPath),
17
- shellQuote(scriptPath),
18
- 'ask',
19
- shellQuote(file),
20
- '--dir',
21
- shellQuote(opts.dir),
22
- '--write-to',
23
- shellQuote(resultPath),
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
- if (!opts.visuals) {
29
- parts.push('--no-visuals');
49
+ catch {
50
+ return undefined;
30
51
  }
31
- return parts.join(' ');
32
- }
33
- export async function dispatchToTmuxPane(file, opts) {
34
- const parentTmp = mkdtempSync(join(tmpdir(), 'hl-'));
35
- const resultPath = join(parentTmp, 'result.json');
36
- const cmd = buildChildCmd(file, resultPath, opts);
37
- // Capture the spawned pane id so we can detect if the user closes it
38
- // without finishing — otherwise the parent would poll forever.
39
- const paneId = execFileSync('tmux', ['split-window', '-P', '-F', '#{pane_id}', '-h', '-d', cmd], { encoding: 'utf8' }).trim();
40
- await new Promise((resolve, reject) => {
41
- const poll = setInterval(() => {
42
- if (existsSync(resultPath)) {
43
- clearInterval(poll);
44
- resolve();
45
- return;
46
- }
47
- // Check the pane is still alive. If it's gone and there's still no
48
- // result file, the child died (closed pane, crash, etc).
49
- try {
50
- const panes = execFileSync('tmux', ['list-panes', '-a', '-F', '#{pane_id}'], {
51
- encoding: 'utf8',
52
- });
53
- if (!panes.split('\n').map((s) => s.trim()).includes(paneId)) {
54
- clearInterval(poll);
55
- reject(new Error(`tmux pane ${paneId} closed before writing a result`));
56
- }
57
- }
58
- catch (err) {
59
- clearInterval(poll);
60
- reject(new Error(`tmux list-panes failed: ${err instanceof Error ? err.message : String(err)}`));
61
- }
62
- }, 150);
63
- });
64
- const json = readFileSync(resultPath, 'utf8');
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
- unlinkSync(resultPath);
103
+ mkdirSync(path, { mode: 0o700 });
104
+ return true;
105
+ }
106
+ catch {
107
+ return false;
67
108
  }
68
- catch { /* ignore */ }
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
- rmdirSync(parentTmp);
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
- catch { /* ignore */ }
73
- return JSON.parse(json);
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. `hl schema
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 InboxItem {
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?: string;
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?: DeckSource;
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.33",
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",