@crouton-kit/humanloop 0.4.8 → 0.4.10
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/editor/terminal-review.d.ts +21 -1
- package/dist/editor/terminal-review.js +88 -19
- package/dist/inbox/controller.d.ts +2 -0
- package/dist/inbox/controller.js +75 -23
- package/dist/inbox/maintenance.js +1 -10
- package/dist/inbox/tui.js +4 -11
- package/dist/inbox/visual.d.ts +3 -11
- package/dist/inbox/visual.js +12 -56
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/render/termrender.d.ts +18 -1
- package/dist/render/termrender.js +66 -5
- package/dist/render/version.d.ts +1 -1
- package/dist/render/version.js +1 -1
- package/dist/tui/ansi.d.ts +29 -5
- package/dist/tui/ansi.js +123 -27
- package/dist/tui/app.js +24 -29
- package/dist/tui/input.js +13 -0
- package/dist/tui/render.d.ts +3 -0
- package/dist/tui/render.js +71 -29
- package/dist/types.d.ts +9 -0
- package/dist/web/assets/{abnfDiagram-VRR7QNED-CeahG6AG.js → abnfDiagram-VRR7QNED-By5eg834.js} +1 -1
- package/dist/web/assets/{arc-CDoEbwHt.js → arc-vdkyOWe2.js} +1 -1
- package/dist/web/assets/architecture-TIHT7OUA-BjaTawyn.js +1 -0
- package/dist/web/assets/{architectureDiagram-ZJ3FMSHR-BPjR0pOT.js → architectureDiagram-ZJ3FMSHR-CKy_ZE5N.js} +1 -1
- package/dist/web/assets/{blockDiagram-677ZJIJ3-COPZ3wOy.js → blockDiagram-677ZJIJ3-5LiBO7Io.js} +1 -1
- package/dist/web/assets/{c4Diagram-LMCZKHZV-BDlHqm5m.js → c4Diagram-LMCZKHZV-D79itWPb.js} +1 -1
- package/dist/web/assets/channel-HnF1q1Mh.js +1 -0
- package/dist/web/assets/{chunk-32BRIVSS-ChE1ajNa.js → chunk-32BRIVSS-4LnkfHUo.js} +1 -1
- package/dist/web/assets/{chunk-52WLFC77-DN_FDyhc.js → chunk-52WLFC77-2raU4D63.js} +1 -1
- package/dist/web/assets/{chunk-C7G6YPKG-B3RJ96Zq.js → chunk-C7G6YPKG-Cq1co5_N.js} +1 -1
- package/dist/web/assets/{chunk-EX3LRPZG-DgUeuz17.js → chunk-EX3LRPZG-5pgwNnOC.js} +1 -1
- package/dist/web/assets/{chunk-FWX5IMBZ-BLUVTbLv.js → chunk-FWX5IMBZ-DLBDSAUG.js} +2 -2
- package/dist/web/assets/{chunk-HOUHSVGY-Be0i0s6_.js → chunk-HOUHSVGY-7q8piy7F.js} +1 -1
- package/dist/web/assets/{chunk-ICXQ74PX-3WXYMfu9.js → chunk-ICXQ74PX-B8V7Hf73.js} +1 -1
- package/dist/web/assets/{chunk-MOJQB5TN-D-pOUBYm.js → chunk-MOJQB5TN-CvpfWs-s.js} +1 -1
- package/dist/web/assets/{chunk-OGEWGWER-B-il3nZ9.js → chunk-OGEWGWER-BIcVy_-w.js} +1 -1
- package/dist/web/assets/{chunk-PUDLZKDR-D6N4Hutq.js → chunk-PUDLZKDR-HzobduhR.js} +1 -1
- package/dist/web/assets/{chunk-Q4XR5HBZ-9cq52Pkk.js → chunk-Q4XR5HBZ-BVzkZDMe.js} +1 -1
- package/dist/web/assets/{chunk-RYQCIY6F-BVYSEout.js → chunk-RYQCIY6F-BXvuzyNS.js} +1 -1
- package/dist/web/assets/{chunk-V7JOEXUC-BuJn3hRD.js → chunk-V7JOEXUC-BGmVQ8Gl.js} +1 -1
- package/dist/web/assets/{chunk-VAUOI2AC-C6vQq47C.js → chunk-VAUOI2AC-Dr7gTfE6.js} +1 -1
- package/dist/web/assets/{chunk-VR4S4FIN-DqY4BkmV.js → chunk-VR4S4FIN-T9cDy9xC.js} +1 -1
- package/dist/web/assets/{chunk-WYO6CB5R-DgJ7qTeI.js → chunk-WYO6CB5R-DCmNxnM8.js} +1 -1
- package/dist/web/assets/{chunk-XXDRQBXY-BA7v7GnL.js → chunk-XXDRQBXY-D3CBQrix.js} +1 -1
- package/dist/web/assets/{chunk-ZGVPDNZ5-BbkNKCvq.js → chunk-ZGVPDNZ5-az9vUYuT.js} +1 -1
- package/dist/web/assets/classDiagram-OUVF2IWQ-eVXwjNrO.js +1 -0
- package/dist/web/assets/classDiagram-v2-EOCWNBFH-eVXwjNrO.js +1 -0
- package/dist/web/assets/{cose-bilkent-JH36ORCC-CXK095PF.js → cose-bilkent-JH36ORCC-BA_JeicW.js} +1 -1
- package/dist/web/assets/{cynefin-VYW2F7L2-C_VXlLiV.js → cynefin-VYW2F7L2-CctiGrrW.js} +1 -1
- package/dist/web/assets/{cynefinDiagram-TSTJHNR4-C1xlbL_1.js → cynefinDiagram-TSTJHNR4-CUy0zmIT.js} +1 -1
- package/dist/web/assets/{dagre-VKFMJZFB-CCTbik65.js → dagre-VKFMJZFB-Ds3jP1MK.js} +1 -1
- package/dist/web/assets/{diagram-FQU43EPY-DsT-Ov2v.js → diagram-FQU43EPY-EwA9l88S.js} +1 -1
- package/dist/web/assets/{diagram-G47NLZAW-DtoIPU9F.js → diagram-G47NLZAW-DABQWhEl.js} +1 -1
- package/dist/web/assets/{diagram-NH7WQ7WH-OFu5gDhD.js → diagram-NH7WQ7WH-ghhVWX8i.js} +1 -1
- package/dist/web/assets/{diagram-OA4YK3LP-Dd7dF-gL.js → diagram-OA4YK3LP-DQqZ5_Hn.js} +1 -1
- package/dist/web/assets/{diagram-WEI45ONY-Mux4SvIA.js → diagram-WEI45ONY-B7MqXpzX.js} +1 -1
- package/dist/web/assets/{dist-9QOA8OIw.js → dist-CccYnoy2.js} +1 -1
- package/dist/web/assets/{ebnfDiagram-CCIWWBDH-BrKI9mhn.js → ebnfDiagram-CCIWWBDH-BNbGnz6Y.js} +1 -1
- package/dist/web/assets/{erDiagram-Q63AITRT-ByTXMLhH.js → erDiagram-Q63AITRT-C9FlumCQ.js} +1 -1
- package/dist/web/assets/eventmodeling-45OFAUF4-BIuBn_KP.js +1 -0
- package/dist/web/assets/flowDiagram-23GEKE2U-D38IdS7U.js +1 -0
- package/dist/web/assets/{ganttDiagram-NO4QXBWP-DrERNlg-.js → ganttDiagram-NO4QXBWP-CH9MCvIC.js} +1 -1
- package/dist/web/assets/{gitGraph-TEB2WS4Q-DD6JbRZi.js → gitGraph-TEB2WS4Q-78WIeQv4.js} +1 -1
- package/dist/web/assets/{gitGraphDiagram-IHSO6WYX-Btg9UWYR.js → gitGraphDiagram-IHSO6WYX-DhpBgjPK.js} +1 -1
- package/dist/web/assets/{index-CkGfXbfm.js → index-CubZRCZd.js} +3 -3
- package/dist/web/assets/index-DOlqkLKF.css +2 -0
- package/dist/web/assets/{info-DKCQHKI2-e7fTIPJt.js → info-DKCQHKI2-C92zll-G.js} +1 -1
- package/dist/web/assets/{infoDiagram-FWYZ7A6U-D8qwevDR.js → infoDiagram-FWYZ7A6U-SJzTsCEQ.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-FXEZZL3T-DVk__o1X.js → ishikawaDiagram-FXEZZL3T-uPFBPNUE.js} +1 -1
- package/dist/web/assets/{journeyDiagram-5HDEW3XC-BAqtMRPh.js → journeyDiagram-5HDEW3XC-Yp2LdPKb.js} +1 -1
- package/dist/web/assets/{kanban-definition-HUTT4EX6-BSx4RvJG.js → kanban-definition-HUTT4EX6-CkkJLiTZ.js} +1 -1
- package/dist/web/assets/{line-U7gCrM1H.js → line-UR1QpiGL.js} +1 -1
- package/dist/web/assets/{linear-LChDqo68.js → linear-Dv1BPDYH.js} +1 -1
- package/dist/web/assets/{mermaid-parser.core-DN-im0ZM.js → mermaid-parser.core-CZSrtYeZ.js} +3 -3
- package/dist/web/assets/{mermaid.core-BtXRl0Mb.js → mermaid.core-3IhLnwHU.js} +3 -3
- package/dist/web/assets/{mindmap-definition-LN4V7U3C-CyKxTVZ0.js → mindmap-definition-LN4V7U3C-D1TtVuKI.js} +1 -1
- package/dist/web/assets/{packet-7NZHBO7P-DqwDWu78.js → packet-7NZHBO7P-CJ2IWX3g.js} +1 -1
- package/dist/web/assets/{pegDiagram-2B236MQR-BtsNWFkr.js → pegDiagram-2B236MQR-BHoRQXZg.js} +1 -1
- package/dist/web/assets/{pie-RZYD4A2V-BHZhIJNY.js → pie-RZYD4A2V-oA5BEo9n.js} +1 -1
- package/dist/web/assets/{pieDiagram-ENE6RG2P-Bg2vN06Z.js → pieDiagram-ENE6RG2P-CQHUvgRQ.js} +1 -1
- package/dist/web/assets/{quadrantDiagram-ABIIQ3AL-Cy3idFFa.js → quadrantDiagram-ABIIQ3AL-Cns6Us8l.js} +1 -1
- package/dist/web/assets/{radar-I7S5WNFK-YJjo6vJv.js → radar-I7S5WNFK-D5BbXMdK.js} +1 -1
- package/dist/web/assets/{railroad-3IZDKUUU-CmVtviO7.js → railroad-3IZDKUUU-DFBa68pp.js} +1 -1
- package/dist/web/assets/railroad-abnf-AHOZXSZD-BZJp7hTI.js +1 -0
- package/dist/web/assets/railroad-ebnf-EBAXGLYW-B7zfPT86.js +1 -0
- package/dist/web/assets/railroad-peg-LSFZ7HO6-DGmY03r8.js +1 -0
- package/dist/web/assets/{railroadDiagram-RFXS5EU6-H_2GxnX4.js → railroadDiagram-RFXS5EU6-BhjRkGS_.js} +1 -1
- package/dist/web/assets/{requirementDiagram-TGXJPOKE-DapbEtLD.js → requirementDiagram-TGXJPOKE-B3Vrz4dg.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-HTMAVEWB-CCQHHs1d.js → sankeyDiagram-HTMAVEWB-IguzjAYn.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-DBY2YBRQ-CgVVV-1t.js → sequenceDiagram-DBY2YBRQ-uvBF9oJI.js} +1 -1
- package/dist/web/assets/{src-BfumX_U8.js → src-C1GaCc7i.js} +1 -1
- package/dist/web/assets/{stateDiagram-2N3HPSRC-BBCtLj7x.js → stateDiagram-2N3HPSRC-D55Ia0GX.js} +1 -1
- package/dist/web/assets/stateDiagram-v2-6OUMAXLB-D9CZKnmk.js +1 -0
- package/dist/web/assets/{swimlanes-5IMT3BWC-DO2t3ahL.js → swimlanes-5IMT3BWC-CxH58sKc.js} +1 -1
- package/dist/web/assets/swimlanesDiagram-G3AALYLV-B7yosACx.js +8 -0
- package/dist/web/assets/{timeline-definition-FHXFAJF6-lUeG8UJD.js → timeline-definition-FHXFAJF6-BOpGS8lX.js} +1 -1
- package/dist/web/assets/{treeView-QDETBFTQ-Btl8LjLd.js → treeView-QDETBFTQ-BLLCYqFV.js} +1 -1
- package/dist/web/assets/{treemap-6X3UGDF4-DrCUe046.js → treemap-6X3UGDF4-Cs2rTjAs.js} +1 -1
- package/dist/web/assets/{vennDiagram-L72KCM5P-DC1qneaM.js → vennDiagram-L72KCM5P-JEEkwytf.js} +1 -1
- package/dist/web/assets/{wardley-OPB4EBWU-BuUed_9_.js → wardley-OPB4EBWU-CCQlq4PR.js} +1 -1
- package/dist/web/assets/{wardleyDiagram-EHGQE667-CaHmI1FN.js → wardleyDiagram-EHGQE667-BiZiwurx.js} +1 -1
- package/dist/web/assets/{xychartDiagram-FW5EYKEG-ZhKRrmhh.js → xychartDiagram-FW5EYKEG-DKg63gko.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/architecture-TIHT7OUA-dmDc2lc6.js +0 -1
- package/dist/web/assets/channel-Cz6ImOzU.js +0 -1
- package/dist/web/assets/classDiagram-OUVF2IWQ-CDIgAUln.js +0 -1
- package/dist/web/assets/classDiagram-v2-EOCWNBFH-CDIgAUln.js +0 -1
- package/dist/web/assets/eventmodeling-45OFAUF4-DODcRvgw.js +0 -1
- package/dist/web/assets/flowDiagram-23GEKE2U-BOjPmrMg.js +0 -1
- package/dist/web/assets/index-BT22-qAj.css +0 -2
- package/dist/web/assets/railroad-abnf-AHOZXSZD-BwPWzzAs.js +0 -1
- package/dist/web/assets/railroad-ebnf-EBAXGLYW-Cc8VfiVd.js +0 -1
- package/dist/web/assets/railroad-peg-LSFZ7HO6-DivliSK5.js +0 -1
- package/dist/web/assets/stateDiagram-v2-6OUMAXLB-CBzJzUEN.js +0 -1
- package/dist/web/assets/swimlanesDiagram-G3AALYLV-CNLJY6SU.js +0 -8
|
@@ -50,6 +50,9 @@ export interface ReviewState {
|
|
|
50
50
|
selectionAnchor: number | null;
|
|
51
51
|
/** Scroll offset into the rendered (termrender) lines. */
|
|
52
52
|
scroll: number;
|
|
53
|
+
/** Horizontal pan offset, in display cells, for rows wider than the body
|
|
54
|
+
* rectangle (diagrams). Rows that fit never move. */
|
|
55
|
+
hscroll: number;
|
|
53
56
|
mode: 'view' | 'list' | 'compose' | 'help';
|
|
54
57
|
compose: ComposeState | null;
|
|
55
58
|
listIndex: number;
|
|
@@ -130,9 +133,26 @@ export declare function textDelete(buffer: string, cursor: number): {
|
|
|
130
133
|
export declare function textHome(buffer: string, cursor: number): number;
|
|
131
134
|
export declare function textEnd(buffer: string, cursor: number): number;
|
|
132
135
|
export declare function textVertical(buffer: string, cursor: number, delta: number): number;
|
|
133
|
-
/** Width
|
|
136
|
+
/** Width prose is rendered at for a given terminal width: 2-col left
|
|
134
137
|
* margin + 2-col gutter before the content, capped for readability. */
|
|
135
138
|
export declare function renderWidthForCols(cols: number): number;
|
|
139
|
+
/** The body rectangle a rendered row is painted into: the terminal minus the
|
|
140
|
+
* 4-col gutter prefix. Diagram blocks render at this width (no prose cap),
|
|
141
|
+
* and every emitted body row is contained within it. */
|
|
142
|
+
export declare function contentWidthForCols(cols: number): number;
|
|
143
|
+
/** The mapped render this surface paints: prose at the readability cap,
|
|
144
|
+
* diagrams at the full body width. */
|
|
145
|
+
export declare function renderReviewDoc(content: string, cols: number): RenderedDoc;
|
|
146
|
+
/** Widest painted row, in display cells, for a body window. */
|
|
147
|
+
export declare function widestVisibleRow(doc: RenderedDoc, scroll: number, bodyHeight: number): number;
|
|
148
|
+
/** How far the body can pan right, given what is currently on screen. Zero
|
|
149
|
+
* when nothing visible overflows — which is also what makes h/l contextual:
|
|
150
|
+
* the keys only pan while an overflowing row is in view. */
|
|
151
|
+
export declare function maxHScroll(state: ReviewState, doc: RenderedDoc, cols: number, rows: number): number;
|
|
152
|
+
/** Pan the body horizontally, clamped to what is currently reachable. The
|
|
153
|
+
* stored offset is only ever clamped against the live window, so vertical
|
|
154
|
+
* scrolling and resize can never leave it pointing past the content. */
|
|
155
|
+
export declare function panBy(state: ReviewState, delta: number, doc: RenderedDoc, cols: number, rows: number): ReviewState;
|
|
136
156
|
/** Reserved (non-body) rows for each mode's fixed header/footer chrome. Kept in
|
|
137
157
|
* sync with renderReviewFrame so scroll math can clamp before a repaint — the
|
|
138
158
|
* compose case counts the same hard-wrapped input lines the frame renders. */
|
|
@@ -7,10 +7,10 @@ import { waitForParkedReviewSubmit } from './parked.js';
|
|
|
7
7
|
import { openBrowser } from '../browser/open.js';
|
|
8
8
|
import { startReviewWebServer } from '../browser/server.js';
|
|
9
9
|
import { buildDraftFeedbackResult, buildFinalFeedbackResult, readReviewDraft, writeReviewDraft } from './feedback.js';
|
|
10
|
-
import {
|
|
10
|
+
import { renderMarkdownBlockAware } from '../render/termrender.js';
|
|
11
11
|
import { setupTerminal, restoreTerminal, getTerminalSize, parseKeypress } from '../tui/terminal.js';
|
|
12
12
|
import { diffFrame, renderInputBuffer } from '../tui/render.js';
|
|
13
|
-
import { BOLD, CYAN, DIM, ESC, RESET, YELLOW, hline, sanitize, truncate } from '../tui/ansi.js';
|
|
13
|
+
import { BOLD, CYAN, DIM, ESC, RESET, YELLOW, clipLine, hline, panLine, sanitize, truncate, visibleWidth } from '../tui/ansi.js';
|
|
14
14
|
/** Derive leaf anchor units from a mapped render: dedupe runs of consecutive
|
|
15
15
|
* rows carrying the identical source span. A mapped row without a leaf span
|
|
16
16
|
* (an unmapped block) anchors at its whole block's range, so every content
|
|
@@ -113,6 +113,7 @@ export function initReviewState(sourceLines, anchor, comments, version) {
|
|
|
113
113
|
activeUnit: comments.length > 0 ? unitIndexForLine(safe.units, comments[0].line) : 0,
|
|
114
114
|
selectionAnchor: null,
|
|
115
115
|
scroll: 0,
|
|
116
|
+
hscroll: 0,
|
|
116
117
|
mode: 'view',
|
|
117
118
|
compose: null,
|
|
118
119
|
listIndex: 0,
|
|
@@ -342,11 +343,64 @@ export function textVertical(buffer, cursor, delta) {
|
|
|
342
343
|
return Math.min(nextEnd, nextStart + col);
|
|
343
344
|
}
|
|
344
345
|
// ── Rendering (pure) ─────────────────────────────────────────────────────────
|
|
345
|
-
/** Width
|
|
346
|
+
/** Width prose is rendered at for a given terminal width: 2-col left
|
|
346
347
|
* margin + 2-col gutter before the content, capped for readability. */
|
|
347
348
|
export function renderWidthForCols(cols) {
|
|
348
349
|
return Math.min(Math.max(20, cols - 6), 120);
|
|
349
350
|
}
|
|
351
|
+
/** The body rectangle a rendered row is painted into: the terminal minus the
|
|
352
|
+
* 4-col gutter prefix. Diagram blocks render at this width (no prose cap),
|
|
353
|
+
* and every emitted body row is contained within it. */
|
|
354
|
+
export function contentWidthForCols(cols) {
|
|
355
|
+
return Math.max(20, cols - 4);
|
|
356
|
+
}
|
|
357
|
+
/** The mapped render this surface paints: prose at the readability cap,
|
|
358
|
+
* diagrams at the full body width. */
|
|
359
|
+
export function renderReviewDoc(content, cols) {
|
|
360
|
+
return renderMarkdownBlockAware(content, renderWidthForCols(cols), contentWidthForCols(cols));
|
|
361
|
+
}
|
|
362
|
+
/** Body rows as they are actually painted. Vertical-scroll indicators replace
|
|
363
|
+
* their endpoint rows, so content behind one cannot advertise or claim h/l. */
|
|
364
|
+
function paintedBodyRows(doc, scroll, bodyHeight) {
|
|
365
|
+
const rows = [];
|
|
366
|
+
for (let i = 0; i < bodyHeight; i++) {
|
|
367
|
+
const abs = scroll + i;
|
|
368
|
+
rows.push(abs < doc.lines.length ? { abs, line: doc.lines[abs] } : { abs: null, line: '' });
|
|
369
|
+
}
|
|
370
|
+
if (scroll > 0 && rows.length > 0)
|
|
371
|
+
rows[0] = { abs: null, line: ` ${DIM}↑ ${scroll} more above${RESET}` };
|
|
372
|
+
const remaining = doc.lines.length - (scroll + bodyHeight);
|
|
373
|
+
if (remaining > 0 && rows.length > 0)
|
|
374
|
+
rows[rows.length - 1] = { abs: null, line: ` ${DIM}↓ ${remaining} more below${RESET}` };
|
|
375
|
+
return rows;
|
|
376
|
+
}
|
|
377
|
+
/** Widest painted row, in display cells, for a body window. */
|
|
378
|
+
export function widestVisibleRow(doc, scroll, bodyHeight) {
|
|
379
|
+
let widest = 0;
|
|
380
|
+
for (const row of paintedBodyRows(doc, scroll, bodyHeight))
|
|
381
|
+
widest = Math.max(widest, visibleWidth(row.line));
|
|
382
|
+
return widest;
|
|
383
|
+
}
|
|
384
|
+
/** How far the body can pan right, given what is currently on screen. Zero
|
|
385
|
+
* when nothing visible overflows — which is also what makes h/l contextual:
|
|
386
|
+
* the keys only pan while an overflowing row is in view. */
|
|
387
|
+
export function maxHScroll(state, doc, cols, rows) {
|
|
388
|
+
const bodyHeight = Math.max(1, rows - reservedRows(state, cols));
|
|
389
|
+
const maxScroll = Math.max(0, doc.lines.length - bodyHeight);
|
|
390
|
+
const scroll = Math.max(0, Math.min(state.scroll, maxScroll));
|
|
391
|
+
return Math.max(0, widestVisibleRow(doc, scroll, bodyHeight) - contentWidthForCols(cols));
|
|
392
|
+
}
|
|
393
|
+
/** Pan the body horizontally, clamped to what is currently reachable. The
|
|
394
|
+
* stored offset is only ever clamped against the live window, so vertical
|
|
395
|
+
* scrolling and resize can never leave it pointing past the content. */
|
|
396
|
+
export function panBy(state, delta, doc, cols, rows) {
|
|
397
|
+
const max = maxHScroll(state, doc, cols, rows);
|
|
398
|
+
const next = Math.max(0, Math.min(state.hscroll + delta, max));
|
|
399
|
+
return next === state.hscroll ? state : { ...state, hscroll: next };
|
|
400
|
+
}
|
|
401
|
+
/** Cells one h/l press pans by — a chunk large enough to cross a diagram in a
|
|
402
|
+
* few presses, small enough to keep orientation. */
|
|
403
|
+
const PAN_STEP = 8;
|
|
350
404
|
// Single-hue anchor tint (a dark neutral step below the CYAN gutter bar).
|
|
351
405
|
// termrender's only reset is `\x1b[0m`, so re-arming the background after each
|
|
352
406
|
// reset keeps the tint alive across inner styling. Heading rows carry their
|
|
@@ -409,7 +463,7 @@ export function renderReviewFrame(state, fileLabel, doc, cols, rows) {
|
|
|
409
463
|
else if (state.mode === 'help') {
|
|
410
464
|
footer.push(' Keys:');
|
|
411
465
|
footer.push(` ${DIM}j/k${RESET} move anchor ${DIM}shift+j/k${RESET} extend selection`);
|
|
412
|
-
footer.push(` ${DIM}u/d, pgup/pgdn${RESET} scroll document`);
|
|
466
|
+
footer.push(` ${DIM}u/d, pgup/pgdn${RESET} scroll document ${DIM}h/l${RESET} pan visible wide diagrams`);
|
|
413
467
|
footer.push(` ${DIM}space c${RESET} compose comment ${DIM}space l${RESET} list comments`);
|
|
414
468
|
footer.push(` ${DIM}space u${RESET} undo last comment ${DIM}space s${RESET} submit review`);
|
|
415
469
|
footer.push(` ${DIM}space w${RESET} hand off to browser`);
|
|
@@ -425,35 +479,46 @@ export function renderReviewFrame(state, fileLabel, doc, cols, rows) {
|
|
|
425
479
|
const bodyHeight = Math.max(1, rows - reserved);
|
|
426
480
|
const maxScroll = Math.max(0, doc.lines.length - bodyHeight);
|
|
427
481
|
const scroll = Math.max(0, Math.min(state.scroll, maxScroll));
|
|
482
|
+
const contentW = contentWidthForCols(cols);
|
|
483
|
+
// Effective pan: the stored offset, clamped to what this window can reach.
|
|
484
|
+
// Scrolling into narrow content parks the diagram offset rather than losing
|
|
485
|
+
// it, so scrolling back restores the same horizontal position.
|
|
486
|
+
const overflow = Math.max(0, widestVisibleRow(doc, scroll, bodyHeight) - contentW);
|
|
487
|
+
const hscroll = Math.max(0, Math.min(state.hscroll, overflow));
|
|
428
488
|
const { lo, hi } = selectedUnitBounds(state);
|
|
429
489
|
const commented = commentedUnitSet(state);
|
|
430
490
|
const body = [];
|
|
431
|
-
for (
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
body.push('');
|
|
491
|
+
for (const source of paintedBodyRows(doc, scroll, bodyHeight)) {
|
|
492
|
+
if (source.abs === null) {
|
|
493
|
+
body.push(source.line);
|
|
435
494
|
continue;
|
|
436
495
|
}
|
|
437
|
-
|
|
438
|
-
|
|
496
|
+
// Every body row is windowed into the body rectangle: a diagram wider than
|
|
497
|
+
// the pane pans instead of spilling past the right edge.
|
|
498
|
+
const row = panLine(source.line, hscroll, contentW);
|
|
499
|
+
const u = state.rowUnits[source.abs] ?? null;
|
|
500
|
+
// Tint the prose column at minimum, and a wide row across what it occupies.
|
|
501
|
+
const tintW = Math.min(contentW, Math.max(docW, visibleWidth(row)));
|
|
439
502
|
if (u !== null && u >= lo && u <= hi)
|
|
440
|
-
body.push(` ${CYAN}▌${RESET} ${tintRow(row,
|
|
503
|
+
body.push(` ${CYAN}▌${RESET} ${tintRow(row, tintW)}`);
|
|
441
504
|
else if (u !== null && commented.has(u))
|
|
442
505
|
body.push(` ${YELLOW}▎${RESET} ${row}`);
|
|
443
506
|
else
|
|
444
507
|
body.push(` ${row}`);
|
|
445
508
|
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
509
|
+
// Contextual pan hint: only while something on screen actually overflows.
|
|
510
|
+
// Appends to the existing footer line, so the reserved row count is unchanged.
|
|
511
|
+
if (overflow > 0 && state.mode === 'view' && footer.length > 0) {
|
|
512
|
+
footer[0] = `${footer[0]} ${DIM}h/l${RESET} pan`;
|
|
513
|
+
}
|
|
451
514
|
while (body.length < bodyHeight)
|
|
452
515
|
body.push('');
|
|
453
516
|
const lines = [...header, ...body, ...footer];
|
|
454
517
|
while (lines.length < rows)
|
|
455
518
|
lines.push('');
|
|
456
|
-
|
|
519
|
+
// The frame is contained by construction: no row may exceed the terminal, or
|
|
520
|
+
// it wraps and breaks the differ's one-line-per-row model.
|
|
521
|
+
return lines.slice(0, rows).map((line) => clipLine(line, cols));
|
|
457
522
|
}
|
|
458
523
|
function diskDraftResult(absFile, outPath) {
|
|
459
524
|
const draft = readReviewDraft(outPath);
|
|
@@ -537,7 +602,7 @@ function runTerminalReviewSession(absFile, content, outPath, fileLabel, opts) {
|
|
|
537
602
|
const draft = readReviewDraft(outPath);
|
|
538
603
|
setupTerminal();
|
|
539
604
|
let { cols, rows } = getTerminalSize();
|
|
540
|
-
let doc =
|
|
605
|
+
let doc = renderReviewDoc(content, cols);
|
|
541
606
|
let state = initReviewState(sourceLines, deriveAnchorUnits(doc), draft?.comments ?? [], draft?.version ?? 0);
|
|
542
607
|
let prevFrame = [];
|
|
543
608
|
let spaceArmed = false;
|
|
@@ -575,7 +640,7 @@ function runTerminalReviewSession(absFile, content, outPath, fileLabel, opts) {
|
|
|
575
640
|
const finish = (result) => finishWith({ type: 'done', result });
|
|
576
641
|
const onResize = () => {
|
|
577
642
|
({ cols, rows } = getTerminalSize());
|
|
578
|
-
doc =
|
|
643
|
+
doc = renderReviewDoc(content, cols);
|
|
579
644
|
// Unit SOURCE ranges are width-independent under the same renderer (only
|
|
580
645
|
// their rendered-row bounds move), but a mid-session renderer→fallback
|
|
581
646
|
// transition can reshape the list — so remap the anchor by its source
|
|
@@ -775,6 +840,10 @@ function runTerminalReviewSession(absFile, content, outPath, fileLabel, opts) {
|
|
|
775
840
|
state = scrollBy(state, -bodyH(), bodyH(), doc.lines.length);
|
|
776
841
|
else if (input === 'd' || key.pageDown)
|
|
777
842
|
state = scrollBy(state, bodyH(), bodyH(), doc.lines.length);
|
|
843
|
+
else if ((input === 'h' || key.leftArrow) && maxHScroll(state, doc, cols, rows) > 0)
|
|
844
|
+
state = panBy(state, -PAN_STEP, doc, cols, rows);
|
|
845
|
+
else if ((input === 'l' || key.rightArrow) && maxHScroll(state, doc, cols, rows) > 0)
|
|
846
|
+
state = panBy(state, PAN_STEP, doc, cols, rows);
|
|
778
847
|
else if (input === '?')
|
|
779
848
|
state = openHelp(state);
|
|
780
849
|
else if (key.escape) {
|
|
@@ -42,6 +42,7 @@ export declare class InboxController {
|
|
|
42
42
|
private claim;
|
|
43
43
|
private suspended;
|
|
44
44
|
private submittingDir;
|
|
45
|
+
private cancelConfirmation;
|
|
45
46
|
private stdinListener;
|
|
46
47
|
private cols;
|
|
47
48
|
private rows;
|
|
@@ -64,6 +65,7 @@ export declare class InboxController {
|
|
|
64
65
|
resize(cols?: number, rows?: number): void;
|
|
65
66
|
render(): string[];
|
|
66
67
|
handleKey(input: string, key: Key): void;
|
|
68
|
+
private cancelFromInbox;
|
|
67
69
|
activate(): void;
|
|
68
70
|
/** Claim the review, hand the whole popup TTY to the native editor, then
|
|
69
71
|
* converge to the on-disk outcome when it exits. Draft/final ownership stays
|
package/dist/inbox/controller.js
CHANGED
|
@@ -12,14 +12,14 @@ import { inboxLayout } from './layout.js';
|
|
|
12
12
|
import { scanInbox } from './scan.js';
|
|
13
13
|
import { inboxActivityPath, inboxRootsDirectory, inboxStateDirectory, listInboxRoots, registeredInboxRoot } from './registry.js';
|
|
14
14
|
import { claimTicket, heartbeatClaim, releaseClaim } from './claim.js';
|
|
15
|
-
import { completeDeck, readTicketResult, ticketRoot } from './tickets.js';
|
|
15
|
+
import { cancelTicket, completeDeck, readTicketResult, ticketRoot } from './tickets.js';
|
|
16
16
|
import { clearProgress, deckPath, progressPath, readJson, responsePath, reviewPath, runHandler, visualsDir } from './convention.js';
|
|
17
17
|
import { DeckAdapter } from './deck-adapter.js';
|
|
18
18
|
import { validateDeck } from './deck-schema.js';
|
|
19
19
|
import { ReviewAdapter } from './review-adapter.js';
|
|
20
20
|
import { editBufferInEditor } from '../editor/roundtrip.js';
|
|
21
21
|
import { cancelFollowUp, readFollowUp, requestFollowUp } from './followup.js';
|
|
22
|
-
import { cancelVisualRequest,
|
|
22
|
+
import { cancelVisualRequest, latestVisualRequestForInteraction, readVisualResult, startVisualRequest, VISUAL_CAPABILITY, } from './visual.js';
|
|
23
23
|
import { kickInboxMaintenance } from './maintenance.js';
|
|
24
24
|
/** One terminal owner for scanning, stable selection, claims, and frame diffs. */
|
|
25
25
|
export class InboxController {
|
|
@@ -48,6 +48,7 @@ export class InboxController {
|
|
|
48
48
|
claim;
|
|
49
49
|
suspended = false;
|
|
50
50
|
submittingDir;
|
|
51
|
+
cancelConfirmation;
|
|
51
52
|
stdinListener;
|
|
52
53
|
cols;
|
|
53
54
|
rows;
|
|
@@ -128,7 +129,9 @@ export class InboxController {
|
|
|
128
129
|
// Hard-clip the list line to its column: an overflowing row would eat
|
|
129
130
|
// the pad and push the divider out of alignment for that row alone.
|
|
130
131
|
const left = clipLine(list[i] ?? '', geometry.listWidth);
|
|
131
|
-
|
|
132
|
+
// Clip the detail column too: a deck/preview row is only ever entitled to
|
|
133
|
+
// its own panel, so an over-wide row cannot run past the pane's edge.
|
|
134
|
+
const right = clipLine(detail[i] ?? '', geometry.detailWidth);
|
|
132
135
|
const pad = ' '.repeat(Math.max(0, geometry.listWidth - visibleWidth(left)));
|
|
133
136
|
lines.push(`${left}${pad}${divider}${right}`);
|
|
134
137
|
}
|
|
@@ -166,6 +169,29 @@ export class InboxController {
|
|
|
166
169
|
this.repaint();
|
|
167
170
|
return;
|
|
168
171
|
}
|
|
172
|
+
if (this.cancelConfirmation !== undefined) {
|
|
173
|
+
if (input === 'y' || input === 'Y') {
|
|
174
|
+
const dir = this.cancelConfirmation.dir;
|
|
175
|
+
this.cancelConfirmation = undefined;
|
|
176
|
+
this.status = 'canceling…';
|
|
177
|
+
void this.cancelFromInbox(dir);
|
|
178
|
+
}
|
|
179
|
+
else if (input === 'n' || input === 'N' || key.escape) {
|
|
180
|
+
this.cancelConfirmation = undefined;
|
|
181
|
+
this.status = undefined;
|
|
182
|
+
}
|
|
183
|
+
this.repaint();
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (input === 'x' || input === 'X') {
|
|
187
|
+
const selected = this.items[this.selectedIndex];
|
|
188
|
+
if (selected !== undefined) {
|
|
189
|
+
this.cancelConfirmation = { dir: selected.dir, title: selected.title };
|
|
190
|
+
this.status = undefined;
|
|
191
|
+
}
|
|
192
|
+
this.repaint();
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
169
195
|
if (key.escape || input === 'q') {
|
|
170
196
|
this.close();
|
|
171
197
|
return;
|
|
@@ -187,6 +213,21 @@ export class InboxController {
|
|
|
187
213
|
this.activate();
|
|
188
214
|
this.repaint();
|
|
189
215
|
}
|
|
216
|
+
async cancelFromInbox(dir) {
|
|
217
|
+
this.submittingDir = dir;
|
|
218
|
+
try {
|
|
219
|
+
const result = await cancelTicket(dir, { reason: 'Canceled from the inbox.', actor: 'human' });
|
|
220
|
+
this.status = result.status === 'canceled' ? 'canceled' : 'ticket already resolved';
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
this.status = error instanceof Error ? error.message : String(error);
|
|
224
|
+
}
|
|
225
|
+
finally {
|
|
226
|
+
this.submittingDir = undefined;
|
|
227
|
+
this.rescan();
|
|
228
|
+
this.repaint();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
190
231
|
activate() {
|
|
191
232
|
const item = this.items[this.selectedIndex];
|
|
192
233
|
if (item === undefined)
|
|
@@ -210,13 +251,6 @@ export class InboxController {
|
|
|
210
251
|
this.invalidate();
|
|
211
252
|
return;
|
|
212
253
|
}
|
|
213
|
-
const root = ticketRoot(item.dir);
|
|
214
|
-
if (root !== null) {
|
|
215
|
-
// A newly acquired claim makes every older running generation stale
|
|
216
|
-
// before the panel can mint its own work. This only dispatches cleanup.
|
|
217
|
-
const retirement = reconcileVisualRequestsForTicket(root, item.dir, claim.token);
|
|
218
|
-
void retirement.delivery.finally(kickInboxMaintenance);
|
|
219
|
-
}
|
|
220
254
|
// Notifications use the same canonical deck panel as every other deck:
|
|
221
255
|
// opening is not acknowledgement; panel completion is.
|
|
222
256
|
this.activeDeck = deck;
|
|
@@ -601,19 +635,28 @@ export class InboxController {
|
|
|
601
635
|
watcher = undefined;
|
|
602
636
|
settle(outcome);
|
|
603
637
|
};
|
|
604
|
-
let
|
|
638
|
+
let durableRequestId;
|
|
639
|
+
let delivery;
|
|
605
640
|
try {
|
|
606
|
-
|
|
641
|
+
const adopted = latestVisualRequestForInteraction(root, dir, request.interaction);
|
|
642
|
+
if (adopted !== null) {
|
|
643
|
+
durableRequestId = adopted.requestId;
|
|
644
|
+
}
|
|
645
|
+
else {
|
|
646
|
+
const started = startVisualRequest({ root, dir, claimToken, request });
|
|
647
|
+
durableRequestId = started.request.requestId;
|
|
648
|
+
delivery = started.delivery;
|
|
649
|
+
}
|
|
607
650
|
}
|
|
608
651
|
catch (error) {
|
|
609
652
|
finish({ status: 'error', error: error instanceof Error ? error.message : String(error) });
|
|
610
|
-
return { result, cancel: () => { } };
|
|
653
|
+
return { result, detach: () => { }, cancel: () => { } };
|
|
611
654
|
}
|
|
612
655
|
const reread = () => {
|
|
613
656
|
if (settled)
|
|
614
657
|
return;
|
|
615
658
|
try {
|
|
616
|
-
const outcome = readVisualResult(root, dir,
|
|
659
|
+
const outcome = readVisualResult(root, dir, durableRequestId);
|
|
617
660
|
if (outcome === null)
|
|
618
661
|
return;
|
|
619
662
|
finish(outcome.status === 'ready'
|
|
@@ -627,20 +670,26 @@ export class InboxController {
|
|
|
627
670
|
try {
|
|
628
671
|
// Install the watch before the first durable reread so a publication in
|
|
629
672
|
// the narrow setup window is either observed or found by that reread.
|
|
630
|
-
watcher = watch(join(visualsDir(dir),
|
|
673
|
+
watcher = watch(join(visualsDir(dir), durableRequestId), (_event, file) => {
|
|
674
|
+
// Rereading takes a short directory lock, which itself emits watch events on Linux.
|
|
675
|
+
// Only the immutable result can settle this handle; ignoring lock/request churn avoids a self-triggered event loop.
|
|
676
|
+
if (file !== null && file !== 'result.json')
|
|
677
|
+
return;
|
|
678
|
+
reread();
|
|
679
|
+
});
|
|
631
680
|
watcher.once('error', (error) => finish({ status: 'error', error: error.message }));
|
|
632
681
|
reread();
|
|
633
682
|
}
|
|
634
683
|
catch (error) {
|
|
635
684
|
finish({ status: 'error', error: error instanceof Error ? error.message : String(error) });
|
|
636
685
|
}
|
|
637
|
-
void
|
|
686
|
+
void delivery?.then(reread, (error) => finish({ status: 'error', error: error instanceof Error ? error.message : String(error) }));
|
|
638
687
|
return {
|
|
639
688
|
result,
|
|
689
|
+
detach: () => finish({ status: 'error', error: 'Visual view detached' }),
|
|
640
690
|
cancel: () => {
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
void cancelVisualRequest(root, dir, request.requestId).finally(kickInboxMaintenance);
|
|
691
|
+
finish({ status: 'error', error: 'Visual request canceled' });
|
|
692
|
+
void cancelVisualRequest(root, dir, durableRequestId).finally(kickInboxMaintenance);
|
|
644
693
|
},
|
|
645
694
|
};
|
|
646
695
|
}
|
|
@@ -687,8 +736,8 @@ export class InboxController {
|
|
|
687
736
|
return this.previewViewport(this.passiveDetailLines(width), width, rows);
|
|
688
737
|
const focusHint = this.focusAvailable(selected.dir) ? ` ${DIM}g${RESET} chat` : '';
|
|
689
738
|
const footer = selected.kind === 'deck'
|
|
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`];
|
|
739
|
+
? [` ${DIM}Enter${RESET} opens the full ticket ${DIM}u/d${RESET} scroll ${DIM}j/k${RESET} select ${DIM}x${RESET} cancel`, ` ${DIM}Active ask:${RESET} c comment u/d scroll w browser${focusHint} ${DIM}q${RESET} close`]
|
|
740
|
+
: [` ${DIM}Enter${RESET} opens the full review ${DIM}u/d${RESET} scroll ${DIM}j/k${RESET} select ${DIM}x${RESET} cancel`, ` ${focusHint} ${DIM}q${RESET} close`];
|
|
692
741
|
return [...this.previewViewport(this.passiveDetailLines(width), width, Math.max(0, rows - footer.length)), ...footer.map((line) => clipLine(line, width))];
|
|
693
742
|
}
|
|
694
743
|
focusAvailable(dir) {
|
|
@@ -791,12 +840,15 @@ export class InboxController {
|
|
|
791
840
|
return out.map((line) => clipLine(line, width));
|
|
792
841
|
}
|
|
793
842
|
withStatus(lines) {
|
|
794
|
-
|
|
843
|
+
const status = this.cancelConfirmation === undefined
|
|
844
|
+
? this.status
|
|
845
|
+
: `cancel “${this.cancelConfirmation.title}”? y yes · n/Esc no`;
|
|
846
|
+
if (status === undefined || this.rows < 1)
|
|
795
847
|
return lines;
|
|
796
848
|
const next = [...lines];
|
|
797
849
|
while (next.length < this.rows)
|
|
798
850
|
next.push('');
|
|
799
|
-
next[this.rows - 1] = `${YELLOW}${
|
|
851
|
+
next[this.rows - 1] = `${YELLOW}${status}${RESET}`;
|
|
800
852
|
return next;
|
|
801
853
|
}
|
|
802
854
|
repaint(clear = false) {
|
|
@@ -4,7 +4,7 @@ import { dirname, join } from 'node:path';
|
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { inboxRootsDirectory, listInboxRoots } from './registry.js';
|
|
6
6
|
import { reconcileCompletions } from './completion.js';
|
|
7
|
-
import { dispatchVisualCleanup, listVisualCleanupObligationsForRoot
|
|
7
|
+
import { dispatchVisualCleanup, listVisualCleanupObligationsForRoot } from './visual.js';
|
|
8
8
|
const LEASE_STALE_MS = 300_000;
|
|
9
9
|
function leasePath() { return join(dirname(inboxRootsDirectory()), 'maintenance.lock'); }
|
|
10
10
|
function roots() { return listInboxRoots().filter((root) => root.available).map((root) => root.root); }
|
|
@@ -107,15 +107,6 @@ async function repairOnce() {
|
|
|
107
107
|
}
|
|
108
108
|
catch { /* receipt remains durable for the next pass */ }
|
|
109
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
110
|
const tasks = dueTasks(allRoots);
|
|
120
111
|
await Promise.all(tasks.filter((task) => Date.parse(task.nextAttemptAt) <= Date.now()).map((task) => dispatchVisualCleanup(task.root, task.dir, task.requestId)));
|
|
121
112
|
return dueTasks(roots());
|
package/dist/inbox/tui.js
CHANGED
|
@@ -37,19 +37,12 @@ export function buildInboxLines(items, width, selectedIndex, height = Number.MAX
|
|
|
37
37
|
function buildItemLines(item, index, selectedIndex, contentWidth) {
|
|
38
38
|
const kind = item.kind === 'deck' ? item.interactionKind ?? 'decision' : 'review';
|
|
39
39
|
const icon = KIND_ICON[kind] ?? '·';
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
// column, bending the panel divider. Shrink the source only when the row
|
|
43
|
-
// cannot hold it alongside the title's 10-col minimum.
|
|
44
|
-
const rawSource = item.source.sessionName ?? item.source.askedBy ?? item.source.nodeId ?? '';
|
|
40
|
+
// The inbox is a title-first triage surface. Origin details remain available
|
|
41
|
+
// after opening a ticket, rather than consuming the list's scarce row width.
|
|
45
42
|
const age = formatTimeAgo(item.blockedSince);
|
|
46
|
-
const source = truncateRow(rawSource, Math.max(8, contentWidth - age.length - 8 - 10));
|
|
47
43
|
const cursor = index === selectedIndex ? `${CYAN}▸${RESET} ` : ' ';
|
|
48
|
-
const titleWidth = Math.max(
|
|
49
|
-
|
|
50
|
-
if (source)
|
|
51
|
-
row += `${ansiColor(source, 'yellow')} ${DIM}·${RESET} `;
|
|
52
|
-
row += `${BOLD}${truncateRow(item.title || `(${item.id.slice(0, 8)})`, titleWidth)}${RESET} ${DIM}${age}${RESET}`;
|
|
44
|
+
const titleWidth = Math.max(1, contentWidth - age.length - 6);
|
|
45
|
+
const row = `${cursor}${ansiColor(icon, KIND_COLOR[kind] ?? 'cyan')} ${BOLD}${truncateRow(item.title || `(${item.id.slice(0, 8)})`, titleWidth)}${RESET} ${DIM}${age}${RESET}`;
|
|
53
46
|
if (item.claim)
|
|
54
47
|
return [row, ` ${DIM}${truncateRow(`claimed by ${item.claim.owner}`, contentWidth - 6)}${RESET}`];
|
|
55
48
|
if (item.subtitle)
|
package/dist/inbox/visual.d.ts
CHANGED
|
@@ -107,6 +107,9 @@ export declare function submitVisualResult(root: string, dir: string, raw: Visua
|
|
|
107
107
|
export declare function dispatchVisualCleanup(root: string, dir: string, requestId: string): Promise<VisualCleanupDeliveryResult>;
|
|
108
108
|
/** State-first cancellation for one handle; the returned promise is cleanup delivery only. */
|
|
109
109
|
export declare function cancelVisualRequest(root: string, dir: string, requestId: string): Promise<VisualCleanupDeliveryResult>;
|
|
110
|
+
/** Return the latest generation for one unchanged interaction. A canceled latest
|
|
111
|
+
* generation is authoritative and cannot resurrect an older completed result. */
|
|
112
|
+
export declare function latestVisualRequestForInteraction(root: string, dir: string, interaction: Interaction | CanonicalInteraction): VisualProtocolRequest | null;
|
|
110
113
|
/** State-first cancel every running request, then dispatch every owed cleanup (including start failures). */
|
|
111
114
|
export declare function cancelVisualRequestsForTicket(root: string, dir: string): Promise<{
|
|
112
115
|
canceled: number;
|
|
@@ -114,17 +117,6 @@ export declare function cancelVisualRequestsForTicket(root: string, dir: string)
|
|
|
114
117
|
}>;
|
|
115
118
|
/** Durable tasks the next controller-owned retry executor should schedule. */
|
|
116
119
|
export declare function listVisualCleanupObligations(root: string, dir: string): VisualCleanupTask[];
|
|
117
|
-
export interface VisualRequestReconciliation {
|
|
118
|
-
retired: number;
|
|
119
|
-
cleanupOwed: number;
|
|
120
|
-
/** Cleanup delivery only; reconciliation never redelivers a start. */
|
|
121
|
-
delivery: Promise<VisualCleanupDeliveryResult[]>;
|
|
122
|
-
}
|
|
123
|
-
/** Retire abandoned generations under the ticket claim boundary. A live claim is
|
|
124
|
-
* authoritative; a newly acquired claim retires every older generation first. */
|
|
125
|
-
export declare function reconcileVisualRequestsForTicket(root: string, dir: string, currentClaimToken?: string): VisualRequestReconciliation;
|
|
126
120
|
/** Enumerate direct ticket children only, including resolved tickets whose cleanup is still owed. */
|
|
127
121
|
export declare function listVisualCleanupObligationsForRoot(root: string): VisualCleanupTask[];
|
|
128
|
-
/** Startup reconciliation only retires stale/missing-claim work; live owners stay untouched. */
|
|
129
|
-
export declare function reconcileStaleVisualRequestsForRoot(root: string): VisualRequestReconciliation[];
|
|
130
122
|
export {};
|
package/dist/inbox/visual.js
CHANGED
|
@@ -5,7 +5,7 @@ import { INTERACTION_KINDS } from '../types.js';
|
|
|
5
5
|
import { checkMarkdown } from '../render/termrender.js';
|
|
6
6
|
import { atomicWriteJson, deckPath, publishJsonExclusive, readJson, runHandler, visualsDir, withExclusiveDirectoryLock, withExclusiveDirectoryLockAsync, } from './convention.js';
|
|
7
7
|
import { parseDeck } from './deck-schema.js';
|
|
8
|
-
import {
|
|
8
|
+
import { readTicketClaim, withTicketLock } from './claim.js';
|
|
9
9
|
import { registeredInboxRoot } from './registry.js';
|
|
10
10
|
import { requireCanonicalTicket } from './tickets.js';
|
|
11
11
|
export const VISUAL_CAPABILITY = 'humanloop.visual/v1';
|
|
@@ -643,6 +643,17 @@ function requestIdsForTicket(root, dir) {
|
|
|
643
643
|
return [];
|
|
644
644
|
return readdirSync(parent).filter((entry) => uuid.safeParse(entry).success).sort();
|
|
645
645
|
}
|
|
646
|
+
/** Return the latest generation for one unchanged interaction. A canceled latest
|
|
647
|
+
* generation is authoritative and cannot resurrect an older completed result. */
|
|
648
|
+
export function latestVisualRequestForInteraction(root, dir, interaction) {
|
|
649
|
+
const canonical = canonicalInteractionJson(interaction);
|
|
650
|
+
const requests = requestIdsForTicket(root, dir)
|
|
651
|
+
.map((requestId) => readVisualRequest(root, dir, requestId))
|
|
652
|
+
.filter((request) => request !== null && request.interactionId === interaction.id && canonicalInteractionJson(request.interaction) === canonical)
|
|
653
|
+
.sort((left, right) => left.requestedAt.localeCompare(right.requestedAt) || left.requestId.localeCompare(right.requestId));
|
|
654
|
+
const latest = requests.at(-1);
|
|
655
|
+
return latest !== undefined && latest.state !== 'canceled' ? latest : null;
|
|
656
|
+
}
|
|
646
657
|
/** State-first cancel every running request, then dispatch every owed cleanup (including start failures). */
|
|
647
658
|
export function cancelVisualRequestsForTicket(root, dir) {
|
|
648
659
|
const requests = requestIdsForTicket(root, dir).map((requestId) => ({ requestId, canceled: stateFirstCancel(root, dir, requestId) }));
|
|
@@ -664,35 +675,6 @@ export function listVisualCleanupObligations(root, dir) {
|
|
|
664
675
|
}
|
|
665
676
|
return tasks.sort((left, right) => left.nextAttemptAt.localeCompare(right.nextAttemptAt) || left.requestId.localeCompare(right.requestId));
|
|
666
677
|
}
|
|
667
|
-
/** Retire abandoned generations under the ticket claim boundary. A live claim is
|
|
668
|
-
* authoritative; a newly acquired claim retires every older generation first. */
|
|
669
|
-
export function reconcileVisualRequestsForTicket(root, dir, currentClaimToken) {
|
|
670
|
-
const ticket = requireCanonicalTicket(root, dir);
|
|
671
|
-
const reconciled = withTicketLock(ticket.dir, () => {
|
|
672
|
-
const claim = readTicketClaim(ticket.dir);
|
|
673
|
-
if (currentClaimToken !== undefined) {
|
|
674
|
-
uuid.parse(currentClaimToken);
|
|
675
|
-
if (claim?.token !== currentClaimToken)
|
|
676
|
-
return { retired: 0, owed: [] };
|
|
677
|
-
}
|
|
678
|
-
else if (claim !== null && !isStaleClaim(claim)) {
|
|
679
|
-
return { retired: 0, owed: [] };
|
|
680
|
-
}
|
|
681
|
-
const entries = requestIdsForTicket(ticket.root, ticket.dir).map((requestId) => {
|
|
682
|
-
const request = readVisualRequest(ticket.root, ticket.dir, requestId);
|
|
683
|
-
if (request === null || (currentClaimToken !== undefined && request.claim.token === currentClaimToken))
|
|
684
|
-
return { requestId, canceled: null };
|
|
685
|
-
return { requestId, canceled: stateFirstCancel(ticket.root, ticket.dir, requestId) };
|
|
686
|
-
});
|
|
687
|
-
const owed = entries.filter((entry) => entry.canceled?.owed === true).map((entry) => entry.requestId);
|
|
688
|
-
return { retired: entries.filter((entry) => entry.canceled?.changed === true).length, owed };
|
|
689
|
-
});
|
|
690
|
-
return {
|
|
691
|
-
retired: reconciled.retired,
|
|
692
|
-
cleanupOwed: reconciled.owed.length,
|
|
693
|
-
delivery: Promise.all(reconciled.owed.map((requestId) => dispatchVisualCleanup(ticket.root, ticket.dir, requestId))),
|
|
694
|
-
};
|
|
695
|
-
}
|
|
696
678
|
/** Enumerate direct ticket children only, including resolved tickets whose cleanup is still owed. */
|
|
697
679
|
export function listVisualCleanupObligationsForRoot(root) {
|
|
698
680
|
const registration = registeredInboxRoot(root);
|
|
@@ -719,29 +701,3 @@ export function listVisualCleanupObligationsForRoot(root) {
|
|
|
719
701
|
}
|
|
720
702
|
return tasks.sort((left, right) => left.nextAttemptAt.localeCompare(right.nextAttemptAt) || left.dir.localeCompare(right.dir) || left.requestId.localeCompare(right.requestId));
|
|
721
703
|
}
|
|
722
|
-
/** Startup reconciliation only retires stale/missing-claim work; live owners stay untouched. */
|
|
723
|
-
export function reconcileStaleVisualRequestsForRoot(root) {
|
|
724
|
-
const registration = registeredInboxRoot(root);
|
|
725
|
-
if (registration === null)
|
|
726
|
-
return [];
|
|
727
|
-
const reconciled = [];
|
|
728
|
-
for (const entry of readdirSync(registration.root)) {
|
|
729
|
-
const candidate = resolve(registration.root, entry);
|
|
730
|
-
let dir;
|
|
731
|
-
try {
|
|
732
|
-
if (!statSync(candidate).isDirectory())
|
|
733
|
-
continue;
|
|
734
|
-
dir = realpathSync(candidate);
|
|
735
|
-
}
|
|
736
|
-
catch {
|
|
737
|
-
continue;
|
|
738
|
-
}
|
|
739
|
-
if (dirname(dir) !== registration.root)
|
|
740
|
-
continue;
|
|
741
|
-
try {
|
|
742
|
-
reconciled.push(reconcileVisualRequestsForTicket(registration.root, dir));
|
|
743
|
-
}
|
|
744
|
-
catch { /* unrelated or malformed children have no protocol obligation */ }
|
|
745
|
-
}
|
|
746
|
-
return reconciled;
|
|
747
|
-
}
|
package/dist/index.d.ts
CHANGED
|
@@ -14,8 +14,8 @@ export type { SubmitDeckOptions, SubmitReviewOptions } from './inbox/tickets.js'
|
|
|
14
14
|
export { dispatchCompletion, reconcileCompletions } from './inbox/completion.js';
|
|
15
15
|
export { requestFollowUp, cancelFollowUp, submitFollowUpResult, readFollowUp } from './inbox/followup.js';
|
|
16
16
|
export type { FollowUpRequestEvent, FollowUpRequest, FollowUpResult } from './inbox/followup.js';
|
|
17
|
-
export { VISUAL_CAPABILITY, canonicalizeInteraction, canonicalInteractionJson, parseVisualRequestEvent, readVisualRequest, readVisualResult, readVisualRequestForEvent, startVisualRequest, submitVisualResult, cancelVisualRequest, cancelVisualRequestsForTicket, dispatchVisualCleanup, listVisualCleanupObligations, listVisualCleanupObligationsForRoot,
|
|
18
|
-
export type { VisualClaimIdentity, VisualCleanupObligation, VisualProtocolRequest, VisualRequestEvent, VisualProtocolResult, VisualResultSubmission, StartVisualRequestOptions, StartedVisualRequest, VisualStartDeliveryResult, VisualCleanupDeliveryResult, VisualCleanupTask,
|
|
17
|
+
export { VISUAL_CAPABILITY, canonicalizeInteraction, canonicalInteractionJson, parseVisualRequestEvent, readVisualRequest, readVisualResult, readVisualRequestForEvent, startVisualRequest, submitVisualResult, cancelVisualRequest, cancelVisualRequestsForTicket, dispatchVisualCleanup, listVisualCleanupObligations, listVisualCleanupObligationsForRoot, latestVisualRequestForInteraction, } from './inbox/visual.js';
|
|
18
|
+
export type { VisualClaimIdentity, VisualCleanupObligation, VisualProtocolRequest, VisualRequestEvent, VisualProtocolResult, VisualResultSubmission, StartVisualRequestOptions, StartedVisualRequest, VisualStartDeliveryResult, VisualCleanupDeliveryResult, VisualCleanupTask, } from './inbox/visual.js';
|
|
19
19
|
export { claimTicket, heartbeatClaim, releaseClaim, readTicketClaim } from './inbox/claim.js';
|
|
20
20
|
export type { TicketClaim, ClaimOptions } from './inbox/claim.js';
|
|
21
21
|
export { renderMarkdown, checkMarkdown, ensureRenderer, isRendererReady, } from './render/termrender.js';
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@ export { registerInboxRoot, unregisterInboxRoot, listInboxRoots, managedInboxRoo
|
|
|
11
11
|
export { submitDeck, submitReview, readTicketResult, finalizeDeck, finalizeReview, completeDeck, completeReview, cancelTicket, cancelTicketResult, } from './inbox/tickets.js';
|
|
12
12
|
export { dispatchCompletion, reconcileCompletions } from './inbox/completion.js';
|
|
13
13
|
export { requestFollowUp, cancelFollowUp, submitFollowUpResult, readFollowUp } from './inbox/followup.js';
|
|
14
|
-
export { VISUAL_CAPABILITY, canonicalizeInteraction, canonicalInteractionJson, parseVisualRequestEvent, readVisualRequest, readVisualResult, readVisualRequestForEvent, startVisualRequest, submitVisualResult, cancelVisualRequest, cancelVisualRequestsForTicket, dispatchVisualCleanup, listVisualCleanupObligations, listVisualCleanupObligationsForRoot,
|
|
14
|
+
export { VISUAL_CAPABILITY, canonicalizeInteraction, canonicalInteractionJson, parseVisualRequestEvent, readVisualRequest, readVisualResult, readVisualRequestForEvent, startVisualRequest, submitVisualResult, cancelVisualRequest, cancelVisualRequestsForTicket, dispatchVisualCleanup, listVisualCleanupObligations, listVisualCleanupObligationsForRoot, latestVisualRequestForInteraction, } from './inbox/visual.js';
|
|
15
15
|
export { claimTicket, heartbeatClaim, releaseClaim, readTicketClaim } from './inbox/claim.js';
|
|
16
16
|
// Renderer binding — the sole org-wide termrender caller. Consumers
|
|
17
17
|
// (sisyphus md-render / ask-schema) route markdown through these.
|