@crouton-kit/humanloop 0.4.0 → 0.4.2
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/cli.js +6 -2
- package/dist/inbox/controller.d.ts +9 -8
- package/dist/inbox/controller.js +61 -129
- package/dist/inbox/deck-factories.js +1 -0
- package/dist/inbox/deck-schema.d.ts +1 -1
- package/dist/inbox/deck-schema.js +2 -2
- package/dist/inbox/maintenance.d.ts +4 -0
- package/dist/inbox/maintenance.js +145 -0
- package/dist/inbox/registry.d.ts +6 -0
- package/dist/inbox/registry.js +14 -3
- package/dist/inbox/scan.js +1 -1
- package/dist/inbox/tickets.js +17 -5
- package/dist/index.d.ts +1 -1
- package/dist/surfaces/inbox-popup.d.ts +1 -1
- package/dist/surfaces/inbox-popup.js +2 -2
- package/dist/tui/tmux.js +2 -1
- package/dist/types.d.ts +12 -1
- package/dist/web/assets/abnfDiagram-VRR7QNED-B71KRG4-.js +1 -0
- package/dist/web/assets/arc-BzH4jnTf.js +1 -0
- package/dist/web/assets/architecture-TIHT7OUA-CZWJOUVd.js +1 -0
- package/dist/web/assets/architectureDiagram-ZJ3FMSHR-D8ZCrOlr.js +36 -0
- package/dist/web/assets/array-BifhSqXX.js +1 -0
- package/dist/web/assets/blockDiagram-677ZJIJ3-DJrvEHzG.js +132 -0
- package/dist/web/assets/c4Diagram-LMCZKHZV-BhpT2Gsz.js +10 -0
- package/dist/web/assets/channel-C_TWBgFl.js +1 -0
- package/dist/web/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
- package/dist/web/assets/chunk-32BRIVSS-ECC6qGeB.js +1 -0
- package/dist/web/assets/chunk-52WLFC77-DavNmYy5.js +10 -0
- package/dist/web/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
- package/dist/web/assets/chunk-7BUUIJ7U-Bb538aSH.js +1 -0
- package/dist/web/assets/chunk-C7G6YPKG-BAknDHkJ.js +1 -0
- package/dist/web/assets/chunk-EX3LRPZG-D8FzKvzP.js +231 -0
- package/dist/web/assets/chunk-FWX5IMBZ-DmneLaBP.js +2 -0
- package/dist/web/assets/chunk-HOUHSVGY-DfL8sRH8.js +1 -0
- package/dist/web/assets/chunk-ICXQ74PX-BDsAibh6.js +2 -0
- package/dist/web/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
- package/dist/web/assets/chunk-KEIR6QF5-BfrZ3jm6.js +161 -0
- package/dist/web/assets/chunk-MOJQB5TN-CNuZ6pa2.js +88 -0
- package/dist/web/assets/chunk-OGEWGWER-BzCEWO5x.js +1 -0
- package/dist/web/assets/chunk-PUDLZKDR-C9EdjbBR.js +156 -0
- package/dist/web/assets/chunk-Q4XR5HBZ-BXjEOFZL.js +70 -0
- package/dist/web/assets/chunk-RYQCIY6F-Bjx13MLc.js +1 -0
- package/dist/web/assets/chunk-V7JOEXUC-Tvu7hSiY.js +206 -0
- package/dist/web/assets/chunk-VAUOI2AC-CtE8-0Em.js +1 -0
- package/dist/web/assets/chunk-VR4S4FIN-BxDOvumS.js +1 -0
- package/dist/web/assets/chunk-WYO6CB5R-NSErcIKL.js +127 -0
- package/dist/web/assets/chunk-XXDRQBXY-CvnzTWOn.js +1 -0
- package/dist/web/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
- package/dist/web/assets/chunk-ZGVPDNZ5-c3Lgoc6U.js +62 -0
- package/dist/web/assets/chunk-ZIRB5QZD-C6fEPe3t.js +32 -0
- package/dist/web/assets/classDiagram-OUVF2IWQ-BraUhK-5.js +1 -0
- package/dist/web/assets/classDiagram-v2-EOCWNBFH-BraUhK-5.js +1 -0
- package/dist/web/assets/cose-bilkent-JH36ORCC-FucxEJcd.js +1 -0
- package/dist/web/assets/cynefin-VYW2F7L2-Dao-QhOj.js +1 -0
- package/dist/web/assets/cynefinDiagram-TSTJHNR4-CTzeNNi6.js +62 -0
- package/dist/web/assets/cytoscape.esm-B3I8pqwA.js +321 -0
- package/dist/web/assets/dagre-CXRCoUWR.js +1 -0
- package/dist/web/assets/dagre-VKFMJZFB-DHJBohq9.js +4 -0
- package/dist/web/assets/defaultLocale-C8Fc0cco.js +1 -0
- package/dist/web/assets/diagram-FQU43EPY-BJD429Ew.js +3 -0
- package/dist/web/assets/diagram-G47NLZAW-Rr7qWaPB.js +24 -0
- package/dist/web/assets/diagram-NH7WQ7WH-DWt2lASp.js +24 -0
- package/dist/web/assets/diagram-OA4YK3LP-BEffXGoK.js +30 -0
- package/dist/web/assets/diagram-WEI45ONY-DmptqIxd.js +41 -0
- package/dist/web/assets/dist-RLVSx5kB.js +1 -0
- package/dist/web/assets/ebnfDiagram-CCIWWBDH-CJAdBcoT.js +1 -0
- package/dist/web/assets/erDiagram-Q63AITRT-NlmP932t.js +85 -0
- package/dist/web/assets/eventmodeling-45OFAUF4-BKX8_y74.js +1 -0
- package/dist/web/assets/flowDiagram-23GEKE2U-CBN8yaFm.js +1 -0
- package/dist/web/assets/ganttDiagram-NO4QXBWP-DhaQF0vK.js +292 -0
- package/dist/web/assets/gitGraph-TEB2WS4Q-jJerQW_A.js +1 -0
- package/dist/web/assets/gitGraphDiagram-IHSO6WYX-B7ujXqgi.js +106 -0
- package/dist/web/assets/graphlib-B8gBHxth.js +1 -0
- package/dist/web/assets/{index-YFwgZZMg.js → index-BMZ4RcHs.js} +5 -4
- package/dist/web/assets/{index-BxJZgs8l.css → index-DV35aYD7.css} +1 -1
- package/dist/web/assets/info-DKCQHKI2-9B9vE5Rs.js +1 -0
- package/dist/web/assets/infoDiagram-FWYZ7A6U-QVQfmq5Q.js +2 -0
- package/dist/web/assets/init-D6jRqBbL.js +1 -0
- package/dist/web/assets/ishikawaDiagram-FXEZZL3T-DINx8lcq.js +70 -0
- package/dist/web/assets/journeyDiagram-5HDEW3XC-CCimY8Ca.js +139 -0
- package/dist/web/assets/kanban-definition-HUTT4EX6-CiyVpYz7.js +89 -0
- package/dist/web/assets/katex-CddkPoXu.js +257 -0
- package/dist/web/assets/line-BS6nDp3b.js +1 -0
- package/dist/web/assets/linear-Djl12XgD.js +1 -0
- package/dist/web/assets/map-DsCK-0Cs.js +1 -0
- package/dist/web/assets/mermaid-parser.core-BbWtntfd.js +7 -0
- package/dist/web/assets/mermaid.core-BxoR3aGT.js +11 -0
- package/dist/web/assets/mindmap-definition-LN4V7U3C-CP5EmYtx.js +96 -0
- package/dist/web/assets/ordinal-hYBb2elL.js +1 -0
- package/dist/web/assets/packet-7NZHBO7P-BZxF_2Fs.js +1 -0
- package/dist/web/assets/path-BWPyau1x.js +1 -0
- package/dist/web/assets/pegDiagram-2B236MQR-qzgVPCkt.js +1 -0
- package/dist/web/assets/pie-RZYD4A2V-BBSGE3OQ.js +1 -0
- package/dist/web/assets/pieDiagram-ENE6RG2P-C-IRFSgk.js +39 -0
- package/dist/web/assets/quadrantDiagram-ABIIQ3AL-T5ncf2ka.js +7 -0
- package/dist/web/assets/radar-I7S5WNFK-BEbxioN2.js +1 -0
- package/dist/web/assets/railroad-3IZDKUUU-CylJQRva.js +1 -0
- package/dist/web/assets/railroad-abnf-AHOZXSZD-CFDo5gvn.js +1 -0
- package/dist/web/assets/railroad-ebnf-EBAXGLYW-Cg-yyAK9.js +1 -0
- package/dist/web/assets/railroad-peg-LSFZ7HO6-DcopMOfK.js +1 -0
- package/dist/web/assets/railroadDiagram-RFXS5EU6-CBYIviLP.js +1 -0
- package/dist/web/assets/requirementDiagram-TGXJPOKE-DnUCDDfN.js +84 -0
- package/dist/web/assets/rough.esm-CSKSodPl.js +1 -0
- package/dist/web/assets/sankeyDiagram-HTMAVEWB-BPcUd5rv.js +40 -0
- package/dist/web/assets/sequenceDiagram-DBY2YBRQ-B_KozvYN.js +162 -0
- package/dist/web/assets/sizeCapture-X5ZJPWSS-B0uUizjq.js +1 -0
- package/dist/web/assets/src-COFaHe7g.js +1 -0
- package/dist/web/assets/stateDiagram-2N3HPSRC-CU0lc0nI.js +1 -0
- package/dist/web/assets/stateDiagram-v2-6OUMAXLB-CSRqotH1.js +1 -0
- package/dist/web/assets/swimlanes-5IMT3BWC-ChpfV6mg.js +2 -0
- package/dist/web/assets/swimlanesDiagram-G3AALYLV-Cob34HwG.js +8 -0
- package/dist/web/assets/timeline-definition-FHXFAJF6-D6Uo2p7h.js +120 -0
- package/dist/web/assets/treeView-QDETBFTQ-BqyWL8XS.js +1 -0
- package/dist/web/assets/treemap-6X3UGDF4-DZt75S3Q.js +1 -0
- package/dist/web/assets/vennDiagram-L72KCM5P-Bv7XNrPm.js +34 -0
- package/dist/web/assets/wardley-OPB4EBWU-CeEs_UEB.js +1 -0
- package/dist/web/assets/wardleyDiagram-EHGQE667-C4cUF7Ft.js +78 -0
- package/dist/web/assets/xychartDiagram-FW5EYKEG-BntLSyoW.js +7 -0
- package/dist/web/index.html +2 -2
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -7,6 +7,7 @@ import { validateDeck } from './inbox/deck-schema.js';
|
|
|
7
7
|
import { managedInboxRoot, registerInboxRoot, registeredInboxRoot, unregisterInboxRoot, listInboxRoots } from './inbox/registry.js';
|
|
8
8
|
import { submitDeck, submitReview } from './inbox/tickets.js';
|
|
9
9
|
import { scanInbox } from './inbox/scan.js';
|
|
10
|
+
import { runInboxMaintenance } from './inbox/maintenance.js';
|
|
10
11
|
import { openInboxPopup } from './surfaces/inbox-popup.js';
|
|
11
12
|
import { toggleInboxPopup } from './tui/tmux.js';
|
|
12
13
|
import { ask } from './api.js';
|
|
@@ -47,11 +48,11 @@ function requireRegisteredRoot(root) {
|
|
|
47
48
|
const program = new Command();
|
|
48
49
|
program.name('hl').description('Humanloop durable inbox and review surface.').helpOption('-h, --help');
|
|
49
50
|
const inbox = program.command('inbox').description('Open, inspect, and configure the centralized human inbox.');
|
|
50
|
-
inbox.command('open').description('Open the inbox controller in this human TTY.').option('--root <path>', 'filter to a registered root', (value, prior) => [...prior, value], []).option('--control-socket <path>', 'popup control socket').action(async (options) => {
|
|
51
|
+
inbox.command('open').description('Open the inbox controller in this human TTY.').option('--root <path>', 'filter to a registered root', (value, prior) => [...prior, value], []).option('--control-socket <path>', 'popup control socket').option('--target-pane <pane>', 'tmux pane underneath the popup').action(async (options) => {
|
|
51
52
|
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
52
53
|
fail('hl inbox open requires an interactive TTY; use hl inbox list for read-only output');
|
|
53
54
|
try {
|
|
54
|
-
await openInboxPopup(options.controlSocket, roots(options.root));
|
|
55
|
+
await openInboxPopup(options.controlSocket, roots(options.root), options.targetPane);
|
|
55
56
|
}
|
|
56
57
|
catch (error) {
|
|
57
58
|
fail(error);
|
|
@@ -73,6 +74,9 @@ inbox.command('toggle').description('Toggle the inbox popup for one tmux client.
|
|
|
73
74
|
process.exit(result === 'failed' || result === 'ambiguous_client' ? 1 : 0);
|
|
74
75
|
});
|
|
75
76
|
inbox.command('list').description('Print pending tickets newest first as JSON.').option('--root <path>', 'filter to a root', (value, prior) => [...prior, value], []).action((options) => emit(scanInbox(roots(options.root))));
|
|
77
|
+
inbox.command('_maintain', { hidden: true }).requiredOption('--lease <path>').action(async (options) => {
|
|
78
|
+
await runInboxMaintenance(resolve(options.lease));
|
|
79
|
+
});
|
|
76
80
|
const root = inbox.command('roots').description('Manage durable interaction roots.');
|
|
77
81
|
root.command('register').description('Register a root owned by a host.').requiredOption('--root <path>').requiredOption('--owner <owner>').option('--handler-command <path>').option('--handler-arg <arg>', 'repeatable direct-exec handler argument', (value, prior) => [...prior, value], []).action((options) => {
|
|
78
82
|
if ((options.handlerCommand === undefined) !== (options.handlerArg.length === 0))
|
|
@@ -10,6 +10,9 @@ export interface InboxControllerOptions {
|
|
|
10
10
|
startDeckBrowser?: typeof startWebServer;
|
|
11
11
|
openBrowser?: (url: string) => void;
|
|
12
12
|
visualProvider?: VisualProvider;
|
|
13
|
+
/** Pane underneath a tmux popup. Enables a registered focus handler to
|
|
14
|
+
* reveal the host surface that created the selected ticket. */
|
|
15
|
+
targetPane?: string;
|
|
13
16
|
}
|
|
14
17
|
type Screen = 'list' | 'detail';
|
|
15
18
|
/** One terminal owner for scanning, stable selection, claims, and frame diffs. */
|
|
@@ -33,12 +36,10 @@ export declare class InboxController {
|
|
|
33
36
|
private deckBrowserFinalizing;
|
|
34
37
|
private deckBrowserStarting;
|
|
35
38
|
private deckBrowserStartingGeneration;
|
|
39
|
+
private focusingSource;
|
|
36
40
|
/** Invalidates an in-flight asynchronous browser start when ownership ends. */
|
|
37
41
|
private deckBrowserGeneration;
|
|
38
42
|
private claim;
|
|
39
|
-
private reconciling;
|
|
40
|
-
private visualReconciling;
|
|
41
|
-
private readonly visualCleanupExecutor;
|
|
42
43
|
private suspended;
|
|
43
44
|
private submittingDir;
|
|
44
45
|
private stdinListener;
|
|
@@ -91,11 +92,6 @@ export declare class InboxController {
|
|
|
91
92
|
/** Retake the TTY after the child exits and force a full repaint. */
|
|
92
93
|
private resumeAfterChild;
|
|
93
94
|
private resolvedRoots;
|
|
94
|
-
/** Owner-boundary reconciliation for resolved results still lacking an ack.
|
|
95
|
-
* Guarded so overlapping fs events cannot stack concurrent scans. */
|
|
96
|
-
private reconcileRoots;
|
|
97
|
-
/** Reconciliation owns stale-claim retirement and durable cleanup, never a Visual start. */
|
|
98
|
-
private reconcileVisualWork;
|
|
99
95
|
/** Automatic ticket capability is deliberately marker + current-root-handler only. */
|
|
100
96
|
private visualProviderFor;
|
|
101
97
|
private startTicketVisual;
|
|
@@ -104,6 +100,11 @@ export declare class InboxController {
|
|
|
104
100
|
private scrollPreview;
|
|
105
101
|
private detailSize;
|
|
106
102
|
private detailLines;
|
|
103
|
+
private focusAvailable;
|
|
104
|
+
/** Ask the registered host to reveal this ticket's source beside the pane
|
|
105
|
+
* underneath the popup. The popup closes only after the host acknowledges
|
|
106
|
+
* that focus succeeded, so a failure remains visible as status. */
|
|
107
|
+
private focusSelectedSource;
|
|
107
108
|
private passiveDetailLines;
|
|
108
109
|
private previewViewport;
|
|
109
110
|
private withStatus;
|
package/dist/inbox/controller.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFileSync, watch } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
2
|
+
import { basename, join } from 'node:path';
|
|
3
3
|
import { getTerminalSize, parseKeypress, restoreTerminal, setupTerminal } from '../tui/terminal.js';
|
|
4
4
|
import { diffFrame } from '../tui/render.js';
|
|
5
5
|
import { renderMarkdown } from '../render/termrender.js';
|
|
@@ -10,72 +10,17 @@ import { BOLD, CYAN, DIM, GRAY, RESET, YELLOW, clipLine } from '../tui/ansi.js';
|
|
|
10
10
|
import { buildInboxLines } from './tui.js';
|
|
11
11
|
import { inboxLayout } from './layout.js';
|
|
12
12
|
import { scanInbox } from './scan.js';
|
|
13
|
-
import { inboxRootsDirectory, listInboxRoots, registeredInboxRoot } from './registry.js';
|
|
13
|
+
import { inboxActivityPath, inboxRootsDirectory, inboxStateDirectory, listInboxRoots, registeredInboxRoot } from './registry.js';
|
|
14
14
|
import { claimTicket, heartbeatClaim, releaseClaim } from './claim.js';
|
|
15
15
|
import { completeDeck, readTicketResult, ticketRoot } from './tickets.js';
|
|
16
|
-
import {
|
|
17
|
-
import { clearProgress, deckPath, progressPath, readJson, responsePath, reviewPath, visualsDir } from './convention.js';
|
|
16
|
+
import { clearProgress, deckPath, progressPath, readJson, responsePath, reviewPath, runHandler, visualsDir } from './convention.js';
|
|
18
17
|
import { DeckAdapter } from './deck-adapter.js';
|
|
19
18
|
import { validateDeck } from './deck-schema.js';
|
|
20
19
|
import { ReviewAdapter } from './review-adapter.js';
|
|
21
20
|
import { editBufferInEditor } from '../editor/roundtrip.js';
|
|
22
21
|
import { cancelFollowUp, readFollowUp, requestFollowUp } from './followup.js';
|
|
23
|
-
import { cancelVisualRequest,
|
|
24
|
-
|
|
25
|
-
class VisualCleanupExecutor {
|
|
26
|
-
roots;
|
|
27
|
-
timer;
|
|
28
|
-
dispatching = new Set();
|
|
29
|
-
stopped = false;
|
|
30
|
-
constructor(roots) {
|
|
31
|
-
this.roots = roots;
|
|
32
|
-
}
|
|
33
|
-
reconcile() {
|
|
34
|
-
if (this.stopped)
|
|
35
|
-
return;
|
|
36
|
-
let tasks = [];
|
|
37
|
-
for (const root of this.roots()) {
|
|
38
|
-
try {
|
|
39
|
-
tasks.push(...listVisualCleanupObligationsForRoot(root));
|
|
40
|
-
}
|
|
41
|
-
catch { /* the next root reconciliation retries durable work */ }
|
|
42
|
-
}
|
|
43
|
-
tasks = tasks.filter((task) => !this.dispatching.has(this.key(task)));
|
|
44
|
-
const now = Date.now();
|
|
45
|
-
for (const task of tasks.filter((candidate) => Date.parse(candidate.nextAttemptAt) <= now))
|
|
46
|
-
this.dispatch(task);
|
|
47
|
-
this.arm(tasks.filter((task) => !this.dispatching.has(this.key(task))));
|
|
48
|
-
}
|
|
49
|
-
stop() {
|
|
50
|
-
this.stopped = true;
|
|
51
|
-
if (this.timer !== undefined)
|
|
52
|
-
clearTimeout(this.timer);
|
|
53
|
-
this.timer = undefined;
|
|
54
|
-
}
|
|
55
|
-
dispatch(task) {
|
|
56
|
-
const key = this.key(task);
|
|
57
|
-
if (this.dispatching.has(key))
|
|
58
|
-
return;
|
|
59
|
-
this.dispatching.add(key);
|
|
60
|
-
void dispatchVisualCleanup(task.root, task.dir, task.requestId).catch(() => undefined).finally(() => {
|
|
61
|
-
this.dispatching.delete(key);
|
|
62
|
-
this.reconcile();
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
arm(tasks) {
|
|
66
|
-
if (this.timer !== undefined)
|
|
67
|
-
clearTimeout(this.timer);
|
|
68
|
-
this.timer = undefined;
|
|
69
|
-
const earliest = tasks.reduce((next, task) => {
|
|
70
|
-
const due = Date.parse(task.nextAttemptAt);
|
|
71
|
-
return Number.isFinite(due) && (next === undefined || due < next) ? due : next;
|
|
72
|
-
}, undefined);
|
|
73
|
-
if (earliest === undefined)
|
|
74
|
-
return;
|
|
75
|
-
this.timer = setTimeout(() => { this.timer = undefined; this.reconcile(); }, Math.max(0, earliest - Date.now()));
|
|
76
|
-
}
|
|
77
|
-
key(task) { return `${task.root}\u0000${task.dir}\u0000${task.requestId}`; }
|
|
78
|
-
}
|
|
22
|
+
import { cancelVisualRequest, readVisualResult, reconcileVisualRequestsForTicket, startVisualRequest, VISUAL_CAPABILITY, } from './visual.js';
|
|
23
|
+
import { kickInboxMaintenance } from './maintenance.js';
|
|
79
24
|
/** One terminal owner for scanning, stable selection, claims, and frame diffs. */
|
|
80
25
|
export class InboxController {
|
|
81
26
|
options;
|
|
@@ -97,12 +42,10 @@ export class InboxController {
|
|
|
97
42
|
deckBrowserFinalizing;
|
|
98
43
|
deckBrowserStarting = false;
|
|
99
44
|
deckBrowserStartingGeneration;
|
|
45
|
+
focusingSource = false;
|
|
100
46
|
/** Invalidates an in-flight asynchronous browser start when ownership ends. */
|
|
101
47
|
deckBrowserGeneration = 0;
|
|
102
48
|
claim;
|
|
103
|
-
reconciling = false;
|
|
104
|
-
visualReconciling = false;
|
|
105
|
-
visualCleanupExecutor;
|
|
106
49
|
suspended = false;
|
|
107
50
|
submittingDir;
|
|
108
51
|
stdinListener;
|
|
@@ -122,9 +65,7 @@ export class InboxController {
|
|
|
122
65
|
const size = getTerminalSize();
|
|
123
66
|
this.cols = options.cols ?? size.cols;
|
|
124
67
|
this.rows = options.rows ?? size.rows;
|
|
125
|
-
this.visualCleanupExecutor = new VisualCleanupExecutor(() => this.resolvedRoots());
|
|
126
68
|
this.rescan();
|
|
127
|
-
this.reconcileVisualWork();
|
|
128
69
|
}
|
|
129
70
|
snapshot() {
|
|
130
71
|
return { items: [...this.items], selectedDir: this.selectedDir, screen: this.screen, inputBuffer: this.adapter?.inputBuffer() };
|
|
@@ -213,6 +154,10 @@ export class InboxController {
|
|
|
213
154
|
return;
|
|
214
155
|
}
|
|
215
156
|
if (this.screen === 'detail' && this.adapter !== undefined) {
|
|
157
|
+
if ((input === 'g' || input === 'G') && this.adapter.canAcceptHostKeys()) {
|
|
158
|
+
void this.focusSelectedSource();
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
216
161
|
if ((input === 'w' || input === 'W') && this.adapter.canAcceptHostKeys()) {
|
|
217
162
|
void this.openDeckBrowser();
|
|
218
163
|
return;
|
|
@@ -236,6 +181,8 @@ export class InboxController {
|
|
|
236
181
|
this.select(this.selectedIndex + 1);
|
|
237
182
|
else if (input === 'k' || key.upArrow)
|
|
238
183
|
this.select(this.selectedIndex - 1);
|
|
184
|
+
else if (input === 'g' || input === 'G')
|
|
185
|
+
void this.focusSelectedSource();
|
|
239
186
|
else if (key.return || input === 'a')
|
|
240
187
|
this.activate();
|
|
241
188
|
this.repaint();
|
|
@@ -268,7 +215,7 @@ export class InboxController {
|
|
|
268
215
|
// A newly acquired claim makes every older running generation stale
|
|
269
216
|
// before the panel can mint its own work. This only dispatches cleanup.
|
|
270
217
|
const retirement = reconcileVisualRequestsForTicket(root, item.dir, claim.token);
|
|
271
|
-
void retirement.delivery.finally(
|
|
218
|
+
void retirement.delivery.finally(kickInboxMaintenance);
|
|
272
219
|
}
|
|
273
220
|
// Notifications use the same canonical deck panel as every other deck:
|
|
274
221
|
// opening is not acknowledgement; panel completion is.
|
|
@@ -331,7 +278,6 @@ export class InboxController {
|
|
|
331
278
|
else {
|
|
332
279
|
this.resumeAfterChild();
|
|
333
280
|
this.rescan();
|
|
334
|
-
this.reconcileRoots();
|
|
335
281
|
this.repaint(true);
|
|
336
282
|
}
|
|
337
283
|
}
|
|
@@ -486,7 +432,6 @@ export class InboxController {
|
|
|
486
432
|
if (this.claim?.dir === dir)
|
|
487
433
|
this.leaveDetail();
|
|
488
434
|
this.rescan();
|
|
489
|
-
this.reconcileRoots();
|
|
490
435
|
this.repaint();
|
|
491
436
|
}
|
|
492
437
|
/** Return browser authority to the terminal deck without changing its draft. */
|
|
@@ -530,7 +475,6 @@ export class InboxController {
|
|
|
530
475
|
this.deckBrowser = undefined;
|
|
531
476
|
void browser?.stop();
|
|
532
477
|
this.leaveDetail();
|
|
533
|
-
this.visualCleanupExecutor.stop();
|
|
534
478
|
for (const watcher of this.watchers)
|
|
535
479
|
watcher.close();
|
|
536
480
|
this.watchers = [];
|
|
@@ -542,10 +486,9 @@ export class InboxController {
|
|
|
542
486
|
setupTerminal();
|
|
543
487
|
this.running = true;
|
|
544
488
|
this.watchRoots();
|
|
545
|
-
// Close the crash window between an earlier result publication and its
|
|
546
|
-
// handler launch: dispatch every resolved-but-undelivered ticket now.
|
|
547
|
-
this.reconcileRoots();
|
|
548
489
|
this.repaint(true);
|
|
490
|
+
// Crash repair is durable but never part of a human keystroke.
|
|
491
|
+
kickInboxMaintenance();
|
|
549
492
|
const heartbeat = setInterval(() => {
|
|
550
493
|
if (this.claim !== undefined)
|
|
551
494
|
heartbeatClaim(this.claim.dir, this.claim.token);
|
|
@@ -594,7 +537,6 @@ export class InboxController {
|
|
|
594
537
|
this.submittingDir = undefined;
|
|
595
538
|
this.leaveDetail(false);
|
|
596
539
|
this.rescan();
|
|
597
|
-
this.reconcileRoots();
|
|
598
540
|
this.repaint();
|
|
599
541
|
}
|
|
600
542
|
/** The controller owns the terminal handoff and repaint around $EDITOR. */
|
|
@@ -636,52 +578,6 @@ export class InboxController {
|
|
|
636
578
|
resolvedRoots() {
|
|
637
579
|
return this.options.roots ?? listInboxRoots().filter((root) => root.available).map((root) => root.root);
|
|
638
580
|
}
|
|
639
|
-
/** Owner-boundary reconciliation for resolved results still lacking an ack.
|
|
640
|
-
* Guarded so overlapping fs events cannot stack concurrent scans. */
|
|
641
|
-
reconcileRoots() {
|
|
642
|
-
if (this.reconciling)
|
|
643
|
-
return;
|
|
644
|
-
this.reconciling = true;
|
|
645
|
-
const roots = this.resolvedRoots();
|
|
646
|
-
void (async () => {
|
|
647
|
-
try {
|
|
648
|
-
for (const root of roots) {
|
|
649
|
-
try {
|
|
650
|
-
await reconcileCompletions(root);
|
|
651
|
-
}
|
|
652
|
-
catch { /* undelivered stays for the next pass */ }
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
finally {
|
|
656
|
-
this.reconciling = false;
|
|
657
|
-
}
|
|
658
|
-
})();
|
|
659
|
-
this.reconcileVisualWork();
|
|
660
|
-
}
|
|
661
|
-
/** Reconciliation owns stale-claim retirement and durable cleanup, never a Visual start. */
|
|
662
|
-
reconcileVisualWork() {
|
|
663
|
-
if (this.visualReconciling)
|
|
664
|
-
return;
|
|
665
|
-
this.visualReconciling = true;
|
|
666
|
-
const roots = this.resolvedRoots();
|
|
667
|
-
void (async () => {
|
|
668
|
-
try {
|
|
669
|
-
const deliveries = roots.flatMap((root) => {
|
|
670
|
-
try {
|
|
671
|
-
return reconcileStaleVisualRequestsForRoot(root).map((entry) => entry.delivery);
|
|
672
|
-
}
|
|
673
|
-
catch {
|
|
674
|
-
return [];
|
|
675
|
-
}
|
|
676
|
-
});
|
|
677
|
-
await Promise.all(deliveries);
|
|
678
|
-
}
|
|
679
|
-
finally {
|
|
680
|
-
this.visualReconciling = false;
|
|
681
|
-
this.visualCleanupExecutor.reconcile();
|
|
682
|
-
}
|
|
683
|
-
})();
|
|
684
|
-
}
|
|
685
581
|
/** Automatic ticket capability is deliberately marker + current-root-handler only. */
|
|
686
582
|
visualProviderFor(dir, deck, claimToken) {
|
|
687
583
|
// An inline provider is intentional host injection, independent of ticket capability.
|
|
@@ -744,7 +640,7 @@ export class InboxController {
|
|
|
744
640
|
cancel: () => {
|
|
745
641
|
watcher?.close();
|
|
746
642
|
watcher = undefined;
|
|
747
|
-
void cancelVisualRequest(root, dir, request.requestId).finally(
|
|
643
|
+
void cancelVisualRequest(root, dir, request.requestId).finally(kickInboxMaintenance);
|
|
748
644
|
},
|
|
749
645
|
};
|
|
750
646
|
}
|
|
@@ -755,7 +651,6 @@ export class InboxController {
|
|
|
755
651
|
this.deckBrowserStarting = false;
|
|
756
652
|
this.adapter?.close();
|
|
757
653
|
this.adapter = undefined;
|
|
758
|
-
this.visualCleanupExecutor.reconcile();
|
|
759
654
|
this.activeDeck = undefined;
|
|
760
655
|
this.selectedWatcher?.close();
|
|
761
656
|
this.selectedWatcher = undefined;
|
|
@@ -790,11 +685,42 @@ export class InboxController {
|
|
|
790
685
|
const selected = this.items[this.selectedIndex];
|
|
791
686
|
if (selected === undefined)
|
|
792
687
|
return this.previewViewport(this.passiveDetailLines(width), width, rows);
|
|
688
|
+
const focusHint = this.focusAvailable(selected.dir) ? ` ${DIM}g${RESET} chat` : '';
|
|
793
689
|
const footer = selected.kind === 'deck'
|
|
794
|
-
? [` ${DIM}Enter${RESET} opens the full ticket ${DIM}u/d${RESET} scroll ${DIM}j/k${RESET} select`, ` ${DIM}Active ask:${RESET} c comment u/d scroll w browser ${DIM}q${RESET} close`]
|
|
795
|
-
: [` ${DIM}Enter${RESET} opens the full review ${DIM}u/d${RESET} scroll ${DIM}j/k${RESET} select`, ` ${DIM}q${RESET} close`];
|
|
690
|
+
? [` ${DIM}Enter${RESET} opens the full ticket ${DIM}u/d${RESET} scroll ${DIM}j/k${RESET} select`, ` ${DIM}Active ask:${RESET} c comment u/d scroll w browser${focusHint} ${DIM}q${RESET} close`]
|
|
691
|
+
: [` ${DIM}Enter${RESET} opens the full review ${DIM}u/d${RESET} scroll ${DIM}j/k${RESET} select`, ` ${focusHint} ${DIM}q${RESET} close`];
|
|
796
692
|
return [...this.previewViewport(this.passiveDetailLines(width), width, Math.max(0, rows - footer.length)), ...footer.map((line) => clipLine(line, width))];
|
|
797
693
|
}
|
|
694
|
+
focusAvailable(dir) {
|
|
695
|
+
const root = ticketRoot(dir);
|
|
696
|
+
return this.options.targetPane !== undefined && root !== null && registeredInboxRoot(root)?.focusHandler !== undefined;
|
|
697
|
+
}
|
|
698
|
+
/** Ask the registered host to reveal this ticket's source beside the pane
|
|
699
|
+
* underneath the popup. The popup closes only after the host acknowledges
|
|
700
|
+
* that focus succeeded, so a failure remains visible as status. */
|
|
701
|
+
async focusSelectedSource() {
|
|
702
|
+
const item = this.items[this.selectedIndex];
|
|
703
|
+
const targetPane = this.options.targetPane;
|
|
704
|
+
if (item === undefined || targetPane === undefined || this.focusingSource)
|
|
705
|
+
return;
|
|
706
|
+
const root = ticketRoot(item.dir);
|
|
707
|
+
const registration = root === null ? null : registeredInboxRoot(root);
|
|
708
|
+
if (root === null || registration?.focusHandler === undefined)
|
|
709
|
+
return;
|
|
710
|
+
const event = { schema: 'humanloop.focus/v1', root, dir: item.dir, ticketId: item.id, targetPane };
|
|
711
|
+
this.focusingSource = true;
|
|
712
|
+
try {
|
|
713
|
+
await runHandler(registration.focusHandler.command, registration.focusHandler.args, event);
|
|
714
|
+
this.close();
|
|
715
|
+
}
|
|
716
|
+
catch (error) {
|
|
717
|
+
this.status = error instanceof Error ? error.message : String(error);
|
|
718
|
+
this.repaint();
|
|
719
|
+
}
|
|
720
|
+
finally {
|
|
721
|
+
this.focusingSource = false;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
798
724
|
passiveDetailLines(width) {
|
|
799
725
|
const selected = this.items[this.selectedIndex];
|
|
800
726
|
if (selected === undefined)
|
|
@@ -888,15 +814,21 @@ export class InboxController {
|
|
|
888
814
|
this.frame = diff.nextPrevFrame;
|
|
889
815
|
}
|
|
890
816
|
watchRoots() {
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
817
|
+
// A single state-level activity marker replaces one watcher per historical
|
|
818
|
+
// root. Closing hundreds of fs watchers was itself a multi-second UI path.
|
|
819
|
+
const activity = basename(inboxActivityPath());
|
|
820
|
+
try {
|
|
821
|
+
this.watchers.push(watch(inboxStateDirectory(), (_event, file) => {
|
|
822
|
+
if (String(file) === activity) {
|
|
823
|
+
this.invalidate();
|
|
824
|
+
kickInboxMaintenance();
|
|
825
|
+
}
|
|
826
|
+
}));
|
|
896
827
|
}
|
|
828
|
+
catch { /* the state directory appears when the next ticket is submitted */ }
|
|
897
829
|
if (this.options.roots === undefined) {
|
|
898
830
|
try {
|
|
899
|
-
this.watchers.push(watch(inboxRootsDirectory(), () => this.invalidate()));
|
|
831
|
+
this.watchers.push(watch(inboxRootsDirectory(), () => { this.invalidate(); kickInboxMaintenance(); }));
|
|
900
832
|
}
|
|
901
833
|
catch { /* registry appears after the next explicit open */ }
|
|
902
834
|
}
|
|
@@ -12,7 +12,7 @@ export declare const preAnswerSchema: z.ZodObject<{
|
|
|
12
12
|
label: z.ZodOptional<z.ZodString>;
|
|
13
13
|
}, z.core.$strip>;
|
|
14
14
|
export declare const deckSchema: z.ZodObject<{
|
|
15
|
-
title: z.
|
|
15
|
+
title: z.ZodString;
|
|
16
16
|
source: z.ZodOptional<z.ZodObject<{
|
|
17
17
|
sessionName: z.ZodOptional<z.ZodString>;
|
|
18
18
|
askedBy: z.ZodOptional<z.ZodString>;
|
|
@@ -8,11 +8,11 @@ export const interactionOptionSchema = z.object({ id: z.string().min(1), label:
|
|
|
8
8
|
export const preAnswerSchema = z.object({ selectedOptionId: z.string().optional(), selectedOptionIds: z.array(z.string()).optional(), freetext: z.string().optional(), label: z.string().optional() });
|
|
9
9
|
const interactionSchema = z.object({
|
|
10
10
|
id: z.string().regex(/^[A-Za-z0-9_-]+$/, { error: 'interaction id must match /^[A-Za-z0-9_-]+$/' }).min(1).max(64),
|
|
11
|
-
title: z.string().min(1), subtitle: z.string().min(1).optional(), body: z.string().optional(), bodyPath: z.string().optional(),
|
|
11
|
+
title: z.string().trim().min(1), subtitle: z.string().min(1).optional(), body: z.string().optional(), bodyPath: z.string().optional(),
|
|
12
12
|
options: z.array(interactionOptionSchema), multiSelect: z.boolean().optional(), allowFreetext: z.boolean().optional(), freetextLabel: z.string().optional(), kind: z.enum(INTERACTION_KINDS).optional(), preAnswered: preAnswerSchema.optional(),
|
|
13
13
|
});
|
|
14
14
|
const deckSourceSchema = z.object({ sessionName: z.string().optional(), askedBy: z.string().optional(), blockedSince: z.string().optional(), nodeId: z.string().optional(), visual: z.literal('humanloop.visual/v1').optional() });
|
|
15
|
-
export const deckSchema = z.object({ title: z.string().
|
|
15
|
+
export const deckSchema = z.object({ title: z.string().trim().min(1), source: deckSourceSchema.optional(), interactions: z.array(interactionSchema).min(1) }).superRefine((input, ctx) => {
|
|
16
16
|
const seen = new Set();
|
|
17
17
|
input.interactions.forEach((interaction, index) => {
|
|
18
18
|
if (interaction.body !== undefined && interaction.bodyPath !== undefined)
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Start one detached repair pass. The lease makes repeated UI gestures free. */
|
|
2
|
+
export declare function kickInboxMaintenance(): void;
|
|
3
|
+
/** Run repair and stay alive only until the durable cleanup retry queue is empty. */
|
|
4
|
+
export declare function runInboxMaintenance(lease: string): Promise<void>;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { inboxRootsDirectory, listInboxRoots } from './registry.js';
|
|
6
|
+
import { reconcileCompletions } from './completion.js';
|
|
7
|
+
import { dispatchVisualCleanup, listVisualCleanupObligationsForRoot, reconcileStaleVisualRequestsForRoot } from './visual.js';
|
|
8
|
+
const LEASE_STALE_MS = 300_000;
|
|
9
|
+
function leasePath() { return join(dirname(inboxRootsDirectory()), 'maintenance.lock'); }
|
|
10
|
+
function roots() { return listInboxRoots().filter((root) => root.available).map((root) => root.root); }
|
|
11
|
+
function leaseOwner(path) {
|
|
12
|
+
try {
|
|
13
|
+
const parsed = JSON.parse(readFileSync(join(path, 'owner.json'), 'utf8'));
|
|
14
|
+
return typeof parsed.pid === 'number' && Number.isInteger(parsed.pid) ? parsed.pid : undefined;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function processIsAlive(pid) {
|
|
21
|
+
try {
|
|
22
|
+
process.kill(pid, 0);
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function staleLease(path) {
|
|
30
|
+
const owner = leaseOwner(path);
|
|
31
|
+
if (owner !== undefined)
|
|
32
|
+
return !processIsAlive(owner);
|
|
33
|
+
try {
|
|
34
|
+
return Date.now() - statSync(path).mtimeMs > LEASE_STALE_MS;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** Start one detached repair pass. The lease makes repeated UI gestures free. */
|
|
41
|
+
export function kickInboxMaintenance() {
|
|
42
|
+
const lease = leasePath();
|
|
43
|
+
try {
|
|
44
|
+
mkdirSync(dirname(lease), { recursive: true, mode: 0o700 });
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
mkdirSync(lease, { mode: 0o700 });
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
if (!staleLease(lease))
|
|
54
|
+
return;
|
|
55
|
+
try {
|
|
56
|
+
rmSync(lease, { recursive: true, force: true });
|
|
57
|
+
mkdirSync(lease, { mode: 0o700 });
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const builtEntry = fileURLToPath(new URL('../cli.js', import.meta.url));
|
|
64
|
+
const sourceEntry = fileURLToPath(new URL('../cli.ts', import.meta.url));
|
|
65
|
+
const entry = existsSync(builtEntry) ? builtEntry : sourceEntry;
|
|
66
|
+
if (!existsSync(entry)) {
|
|
67
|
+
rmSync(lease, { recursive: true, force: true });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
// A built CLI needs no parent flags. Source-mode launches retain their tsx
|
|
71
|
+
// loader, but always execute Humanloop's CLI rather than the caller's
|
|
72
|
+
// entrypoint (which may be a test or a consuming application).
|
|
73
|
+
const runtimeArgs = entry.endsWith('.ts') ? process.execArgv : [];
|
|
74
|
+
const child = spawn(process.execPath, [...runtimeArgs, entry, 'inbox', '_maintain', '--lease', lease], {
|
|
75
|
+
detached: true,
|
|
76
|
+
stdio: 'ignore',
|
|
77
|
+
});
|
|
78
|
+
if (child.pid === undefined) {
|
|
79
|
+
rmSync(lease, { recursive: true, force: true });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
writeFileSync(join(lease, 'owner.json'), JSON.stringify({ pid: child.pid }));
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
child.kill();
|
|
87
|
+
rmSync(lease, { recursive: true, force: true });
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
child.unref();
|
|
91
|
+
}
|
|
92
|
+
function dueTasks(allRoots) {
|
|
93
|
+
const tasks = [];
|
|
94
|
+
for (const root of allRoots) {
|
|
95
|
+
try {
|
|
96
|
+
tasks.push(...listVisualCleanupObligationsForRoot(root));
|
|
97
|
+
}
|
|
98
|
+
catch { /* malformed historical state is isolated to its root */ }
|
|
99
|
+
}
|
|
100
|
+
return tasks;
|
|
101
|
+
}
|
|
102
|
+
async function repairOnce() {
|
|
103
|
+
const allRoots = roots();
|
|
104
|
+
for (const root of allRoots) {
|
|
105
|
+
try {
|
|
106
|
+
await reconcileCompletions(root);
|
|
107
|
+
}
|
|
108
|
+
catch { /* receipt remains durable for the next pass */ }
|
|
109
|
+
}
|
|
110
|
+
const retirements = allRoots.flatMap((root) => {
|
|
111
|
+
try {
|
|
112
|
+
return reconcileStaleVisualRequestsForRoot(root).map((entry) => entry.delivery);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
await Promise.all(retirements);
|
|
119
|
+
const tasks = dueTasks(allRoots);
|
|
120
|
+
await Promise.all(tasks.filter((task) => Date.parse(task.nextAttemptAt) <= Date.now()).map((task) => dispatchVisualCleanup(task.root, task.dir, task.requestId)));
|
|
121
|
+
return dueTasks(roots());
|
|
122
|
+
}
|
|
123
|
+
function releaseLease(lease) {
|
|
124
|
+
if (leaseOwner(lease) !== process.pid)
|
|
125
|
+
return;
|
|
126
|
+
rmSync(lease, { recursive: true, force: true });
|
|
127
|
+
}
|
|
128
|
+
/** Run repair and stay alive only until the durable cleanup retry queue is empty. */
|
|
129
|
+
export async function runInboxMaintenance(lease) {
|
|
130
|
+
try {
|
|
131
|
+
while (true) {
|
|
132
|
+
const tasks = await repairOnce();
|
|
133
|
+
const next = tasks.reduce((earliest, task) => {
|
|
134
|
+
const due = Date.parse(task.nextAttemptAt);
|
|
135
|
+
return Number.isFinite(due) && (earliest === undefined || due < earliest) ? due : earliest;
|
|
136
|
+
}, undefined);
|
|
137
|
+
if (next === undefined)
|
|
138
|
+
return;
|
|
139
|
+
await new Promise((resolve) => setTimeout(resolve, Math.max(0, next - Date.now())));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
releaseLease(lease);
|
|
144
|
+
}
|
|
145
|
+
}
|
package/dist/inbox/registry.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export interface InboxRootRegistration {
|
|
|
10
10
|
handler?: CompletionHandler;
|
|
11
11
|
followUpHandler?: CompletionHandler;
|
|
12
12
|
visualHandler?: CompletionHandler;
|
|
13
|
+
focusHandler?: CompletionHandler;
|
|
13
14
|
}
|
|
14
15
|
export interface InboxRootStatus extends InboxRootRegistration {
|
|
15
16
|
available: boolean;
|
|
@@ -20,8 +21,13 @@ export interface RegisterInboxRootOptions {
|
|
|
20
21
|
handler?: CompletionHandler;
|
|
21
22
|
followUpHandler?: CompletionHandler;
|
|
22
23
|
visualHandler?: CompletionHandler;
|
|
24
|
+
focusHandler?: CompletionHandler;
|
|
23
25
|
}
|
|
26
|
+
export declare function inboxStateDirectory(): string;
|
|
24
27
|
export declare function inboxRootsDirectory(): string;
|
|
28
|
+
export declare function inboxActivityPath(): string;
|
|
29
|
+
/** Signal one durable ticket mutation to every open inbox without watching every root. */
|
|
30
|
+
export declare function signalInboxActivity(): void;
|
|
25
31
|
/** Create/canonicalize a root and claim its user-scoped registration. */
|
|
26
32
|
export declare function registerInboxRoot(opts: RegisterInboxRootOptions): InboxRootRegistration;
|
|
27
33
|
/** Removes a matching available root through its real path, or an unavailable record by its stored canonical path. */
|
package/dist/inbox/registry.js
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
|
-
import { chmodSync, existsSync, mkdirSync, readdirSync, realpathSync, unlinkSync } from 'node:fs';
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readdirSync, realpathSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { join, resolve } from 'node:path';
|
|
5
5
|
import { atomicWriteJson, readJson, withExclusiveDirectoryLock } from './convention.js';
|
|
6
6
|
function stateHome() { return process.env['XDG_STATE_HOME'] || join(homedir(), '.local', 'state'); }
|
|
7
|
-
export function
|
|
7
|
+
export function inboxStateDirectory() { return join(stateHome(), 'humanloop'); }
|
|
8
|
+
export function inboxRootsDirectory() { return join(inboxStateDirectory(), 'inbox-roots'); }
|
|
9
|
+
export function inboxActivityPath() { return join(inboxStateDirectory(), 'inbox-activity'); }
|
|
10
|
+
/** Signal one durable ticket mutation to every open inbox without watching every root. */
|
|
11
|
+
export function signalInboxActivity() {
|
|
12
|
+
try {
|
|
13
|
+
mkdirSync(inboxStateDirectory(), { recursive: true, mode: 0o700 });
|
|
14
|
+
writeFileSync(inboxActivityPath(), `${Date.now()}\n`, { mode: 0o600 });
|
|
15
|
+
}
|
|
16
|
+
catch { /* a later popup scan is still authoritative */ }
|
|
17
|
+
}
|
|
8
18
|
function recordPath(root) { return join(inboxRootsDirectory(), createHash('sha256').update(root).digest('hex')); }
|
|
9
19
|
function canonicalRoot(root) { try {
|
|
10
20
|
return realpathSync(root);
|
|
@@ -31,6 +41,7 @@ function validateRegistration(raw) {
|
|
|
31
41
|
handler: value.handler === undefined ? undefined : validateHandler(value.handler),
|
|
32
42
|
followUpHandler: value.followUpHandler === undefined ? undefined : validateHandler(value.followUpHandler),
|
|
33
43
|
visualHandler: value.visualHandler === undefined ? undefined : validateHandler(value.visualHandler),
|
|
44
|
+
focusHandler: value.focusHandler === undefined ? undefined : validateHandler(value.focusHandler),
|
|
34
45
|
};
|
|
35
46
|
}
|
|
36
47
|
catch {
|
|
@@ -51,7 +62,7 @@ export function registerInboxRoot(opts) {
|
|
|
51
62
|
throw new Error(`inbox root is already owned by ${existing.owner}`);
|
|
52
63
|
if (existing !== null && existing.root !== root)
|
|
53
64
|
throw new Error('inbox root registry hash collision');
|
|
54
|
-
const registration = { schema: 'humanloop.inbox-root/v1', root, owner: opts.owner, handler: validateHandler(opts.handler), followUpHandler: validateHandler(opts.followUpHandler), visualHandler: validateHandler(opts.visualHandler) };
|
|
65
|
+
const registration = { schema: 'humanloop.inbox-root/v1', root, owner: opts.owner, handler: validateHandler(opts.handler), followUpHandler: validateHandler(opts.followUpHandler), visualHandler: validateHandler(opts.visualHandler), focusHandler: validateHandler(opts.focusHandler) };
|
|
55
66
|
atomicWriteJson(path, registration);
|
|
56
67
|
chmodSync(path, 0o600);
|
|
57
68
|
return registration;
|
package/dist/inbox/scan.js
CHANGED
|
@@ -32,7 +32,7 @@ function deckSummary(dir, id) {
|
|
|
32
32
|
return null;
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
-
return { dir, id, kind: 'deck', title: deck.title
|
|
35
|
+
return { dir, id, kind: 'deck', title: deck.title, subtitle: first.subtitle, interactionKind: first.kind, source: deck.source ?? {}, blockedSince, claim: claimSummary(dir) };
|
|
36
36
|
}
|
|
37
37
|
function reviewSummary(dir, id) {
|
|
38
38
|
const raw = readJson(reviewPath(dir));
|