@crouton-kit/humanloop 0.4.9 → 0.4.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.d.ts +1 -1
- package/dist/api.js +4 -4
- package/dist/cli.js +5 -3
- package/dist/editor/terminal-review.d.ts +21 -1
- package/dist/editor/terminal-review.js +88 -19
- package/dist/inbox/controller.js +34 -25
- package/dist/inbox/deck-factories.d.ts +3 -1
- package/dist/inbox/deck-factories.js +2 -1
- package/dist/inbox/deck-schema.d.ts +2 -1
- package/dist/inbox/deck-schema.js +3 -2
- package/dist/inbox/maintenance.js +1 -10
- package/dist/inbox/scan.js +1 -1
- package/dist/inbox/tickets.js +1 -1
- package/dist/inbox/tui.js +6 -16
- package/dist/inbox/visual.d.ts +3 -11
- package/dist/inbox/visual.js +15 -59
- 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 +14 -5
- package/dist/web/assets/{abnfDiagram-VRR7QNED-CeahG6AG.js → abnfDiagram-VRR7QNED-C3G7ucis.js} +1 -1
- package/dist/web/assets/{arc-CDoEbwHt.js → arc-CfHbBVfI.js} +1 -1
- package/dist/web/assets/architecture-TIHT7OUA-XeXU8J5n.js +1 -0
- package/dist/web/assets/{architectureDiagram-ZJ3FMSHR-BPjR0pOT.js → architectureDiagram-ZJ3FMSHR-BCs4FMRj.js} +1 -1
- package/dist/web/assets/{blockDiagram-677ZJIJ3-COPZ3wOy.js → blockDiagram-677ZJIJ3-Ddt1PlNC.js} +1 -1
- package/dist/web/assets/{c4Diagram-LMCZKHZV-BDlHqm5m.js → c4Diagram-LMCZKHZV-BWwkKdPZ.js} +1 -1
- package/dist/web/assets/channel-BfMifkFv.js +1 -0
- package/dist/web/assets/{chunk-32BRIVSS-ChE1ajNa.js → chunk-32BRIVSS-BjyMBLnt.js} +1 -1
- package/dist/web/assets/{chunk-52WLFC77-DN_FDyhc.js → chunk-52WLFC77-DwnRc5bP.js} +1 -1
- package/dist/web/assets/{chunk-C7G6YPKG-B3RJ96Zq.js → chunk-C7G6YPKG-Cv5J31rb.js} +1 -1
- package/dist/web/assets/{chunk-EX3LRPZG-DgUeuz17.js → chunk-EX3LRPZG-Ds-y37uL.js} +1 -1
- package/dist/web/assets/{chunk-FWX5IMBZ-BLUVTbLv.js → chunk-FWX5IMBZ-BzgaWqkI.js} +2 -2
- package/dist/web/assets/{chunk-HOUHSVGY-Be0i0s6_.js → chunk-HOUHSVGY-CB_Rjcvz.js} +1 -1
- package/dist/web/assets/{chunk-ICXQ74PX-3WXYMfu9.js → chunk-ICXQ74PX-a8BpKR5q.js} +1 -1
- package/dist/web/assets/{chunk-MOJQB5TN-D-pOUBYm.js → chunk-MOJQB5TN-b09YHDGP.js} +1 -1
- package/dist/web/assets/{chunk-OGEWGWER-B-il3nZ9.js → chunk-OGEWGWER-CK9U88Zp.js} +1 -1
- package/dist/web/assets/{chunk-PUDLZKDR-D6N4Hutq.js → chunk-PUDLZKDR-DBp4NxwM.js} +1 -1
- package/dist/web/assets/{chunk-Q4XR5HBZ-9cq52Pkk.js → chunk-Q4XR5HBZ-Cn7tHzZm.js} +1 -1
- package/dist/web/assets/{chunk-RYQCIY6F-BVYSEout.js → chunk-RYQCIY6F-DS8RMCZK.js} +1 -1
- package/dist/web/assets/{chunk-V7JOEXUC-BuJn3hRD.js → chunk-V7JOEXUC-BCTcSsiS.js} +1 -1
- package/dist/web/assets/{chunk-VAUOI2AC-C6vQq47C.js → chunk-VAUOI2AC-DQVUYSsu.js} +1 -1
- package/dist/web/assets/{chunk-VR4S4FIN-DqY4BkmV.js → chunk-VR4S4FIN-D5E0Nzn2.js} +1 -1
- package/dist/web/assets/{chunk-WYO6CB5R-DgJ7qTeI.js → chunk-WYO6CB5R-reDXIQan.js} +1 -1
- package/dist/web/assets/{chunk-XXDRQBXY-BA7v7GnL.js → chunk-XXDRQBXY-BFea61gW.js} +1 -1
- package/dist/web/assets/{chunk-ZGVPDNZ5-BbkNKCvq.js → chunk-ZGVPDNZ5-BbVer9LG.js} +1 -1
- package/dist/web/assets/classDiagram-OUVF2IWQ-B3fMukMS.js +1 -0
- package/dist/web/assets/classDiagram-v2-EOCWNBFH-B3fMukMS.js +1 -0
- package/dist/web/assets/{cose-bilkent-JH36ORCC-CXK095PF.js → cose-bilkent-JH36ORCC-Do23kUxV.js} +1 -1
- package/dist/web/assets/{cynefin-VYW2F7L2-C_VXlLiV.js → cynefin-VYW2F7L2-B51RrEfW.js} +1 -1
- package/dist/web/assets/{cynefinDiagram-TSTJHNR4-C1xlbL_1.js → cynefinDiagram-TSTJHNR4-BMRRvJi_.js} +1 -1
- package/dist/web/assets/{dagre-VKFMJZFB-CCTbik65.js → dagre-VKFMJZFB-Cq3oNHAr.js} +1 -1
- package/dist/web/assets/{diagram-FQU43EPY-DsT-Ov2v.js → diagram-FQU43EPY-B_OlWSFb.js} +1 -1
- package/dist/web/assets/{diagram-G47NLZAW-DtoIPU9F.js → diagram-G47NLZAW-CinfsHac.js} +1 -1
- package/dist/web/assets/{diagram-NH7WQ7WH-OFu5gDhD.js → diagram-NH7WQ7WH-B1z5z_xR.js} +1 -1
- package/dist/web/assets/{diagram-OA4YK3LP-Dd7dF-gL.js → diagram-OA4YK3LP-CaXxWKyb.js} +1 -1
- package/dist/web/assets/{diagram-WEI45ONY-Mux4SvIA.js → diagram-WEI45ONY-DJ_ofe0i.js} +1 -1
- package/dist/web/assets/{dist-9QOA8OIw.js → dist-D6gNQP9S.js} +1 -1
- package/dist/web/assets/{ebnfDiagram-CCIWWBDH-BrKI9mhn.js → ebnfDiagram-CCIWWBDH-CKdV58eP.js} +1 -1
- package/dist/web/assets/{erDiagram-Q63AITRT-ByTXMLhH.js → erDiagram-Q63AITRT-DCTuIs_r.js} +1 -1
- package/dist/web/assets/eventmodeling-45OFAUF4-BW_ZjAEZ.js +1 -0
- package/dist/web/assets/flowDiagram-23GEKE2U-kuTegDJX.js +1 -0
- package/dist/web/assets/{ganttDiagram-NO4QXBWP-DrERNlg-.js → ganttDiagram-NO4QXBWP-a2WUc8wp.js} +1 -1
- package/dist/web/assets/{gitGraph-TEB2WS4Q-DD6JbRZi.js → gitGraph-TEB2WS4Q-DMKf1qeU.js} +1 -1
- package/dist/web/assets/{gitGraphDiagram-IHSO6WYX-Btg9UWYR.js → gitGraphDiagram-IHSO6WYX-DW54jWiu.js} +1 -1
- package/dist/web/assets/{index-CkGfXbfm.js → index-BteexwNd.js} +4 -4
- package/dist/web/assets/index-DOlqkLKF.css +2 -0
- package/dist/web/assets/{info-DKCQHKI2-e7fTIPJt.js → info-DKCQHKI2-BeOi6-NO.js} +1 -1
- package/dist/web/assets/{infoDiagram-FWYZ7A6U-D8qwevDR.js → infoDiagram-FWYZ7A6U-DnmfPppA.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-FXEZZL3T-DVk__o1X.js → ishikawaDiagram-FXEZZL3T-CPNmCU4V.js} +1 -1
- package/dist/web/assets/{journeyDiagram-5HDEW3XC-BAqtMRPh.js → journeyDiagram-5HDEW3XC-BJ1dpMIR.js} +1 -1
- package/dist/web/assets/{kanban-definition-HUTT4EX6-BSx4RvJG.js → kanban-definition-HUTT4EX6-CTUMFpnW.js} +1 -1
- package/dist/web/assets/{line-U7gCrM1H.js → line-BxjdmOYH.js} +1 -1
- package/dist/web/assets/{linear-LChDqo68.js → linear-BTFwxw2k.js} +1 -1
- package/dist/web/assets/{mermaid-parser.core-DN-im0ZM.js → mermaid-parser.core-xq_aCbXg.js} +3 -3
- package/dist/web/assets/{mermaid.core-BtXRl0Mb.js → mermaid.core-CJ7BhoQw.js} +3 -3
- package/dist/web/assets/{mindmap-definition-LN4V7U3C-CyKxTVZ0.js → mindmap-definition-LN4V7U3C-CGKOonoC.js} +1 -1
- package/dist/web/assets/{packet-7NZHBO7P-DqwDWu78.js → packet-7NZHBO7P-DcZTUk_n.js} +1 -1
- package/dist/web/assets/{pegDiagram-2B236MQR-BtsNWFkr.js → pegDiagram-2B236MQR-BQhasiHR.js} +1 -1
- package/dist/web/assets/{pie-RZYD4A2V-BHZhIJNY.js → pie-RZYD4A2V-CoNqq_hi.js} +1 -1
- package/dist/web/assets/{pieDiagram-ENE6RG2P-Bg2vN06Z.js → pieDiagram-ENE6RG2P-DjXgajVD.js} +1 -1
- package/dist/web/assets/{quadrantDiagram-ABIIQ3AL-Cy3idFFa.js → quadrantDiagram-ABIIQ3AL-DnMCZ8Lo.js} +1 -1
- package/dist/web/assets/{radar-I7S5WNFK-YJjo6vJv.js → radar-I7S5WNFK-DNuRmGvt.js} +1 -1
- package/dist/web/assets/{railroad-3IZDKUUU-CmVtviO7.js → railroad-3IZDKUUU-BWCSIZRZ.js} +1 -1
- package/dist/web/assets/railroad-abnf-AHOZXSZD-CUq87fi8.js +1 -0
- package/dist/web/assets/railroad-ebnf-EBAXGLYW-Bx6VqkgS.js +1 -0
- package/dist/web/assets/railroad-peg-LSFZ7HO6-DkT6zLYM.js +1 -0
- package/dist/web/assets/{railroadDiagram-RFXS5EU6-H_2GxnX4.js → railroadDiagram-RFXS5EU6-COtqBRRI.js} +1 -1
- package/dist/web/assets/{requirementDiagram-TGXJPOKE-DapbEtLD.js → requirementDiagram-TGXJPOKE-Cgm9XId4.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-HTMAVEWB-CCQHHs1d.js → sankeyDiagram-HTMAVEWB-CCOMrkbd.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-DBY2YBRQ-CgVVV-1t.js → sequenceDiagram-DBY2YBRQ-CL0wy8uA.js} +1 -1
- package/dist/web/assets/{src-BfumX_U8.js → src-CvIIQCAG.js} +1 -1
- package/dist/web/assets/{stateDiagram-2N3HPSRC-BBCtLj7x.js → stateDiagram-2N3HPSRC-C5b_pAiI.js} +1 -1
- package/dist/web/assets/stateDiagram-v2-6OUMAXLB-CbW-xCPn.js +1 -0
- package/dist/web/assets/{swimlanes-5IMT3BWC-DO2t3ahL.js → swimlanes-5IMT3BWC-J11uWvqa.js} +1 -1
- package/dist/web/assets/swimlanesDiagram-G3AALYLV-DpbNWekf.js +8 -0
- package/dist/web/assets/{timeline-definition-FHXFAJF6-lUeG8UJD.js → timeline-definition-FHXFAJF6-Dsvj6fbD.js} +1 -1
- package/dist/web/assets/{treeView-QDETBFTQ-Btl8LjLd.js → treeView-QDETBFTQ-FH3Xeeef.js} +1 -1
- package/dist/web/assets/{treemap-6X3UGDF4-DrCUe046.js → treemap-6X3UGDF4-D1TKNacZ.js} +1 -1
- package/dist/web/assets/{vennDiagram-L72KCM5P-DC1qneaM.js → vennDiagram-L72KCM5P-D4zm__Fz.js} +1 -1
- package/dist/web/assets/{wardley-OPB4EBWU-BuUed_9_.js → wardley-OPB4EBWU-BRLeItQR.js} +1 -1
- package/dist/web/assets/{wardleyDiagram-EHGQE667-CaHmI1FN.js → wardleyDiagram-EHGQE667-B7j5UliQ.js} +1 -1
- package/dist/web/assets/{xychartDiagram-FW5EYKEG-ZhKRrmhh.js → xychartDiagram-FW5EYKEG-Bi0MfuVK.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
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';
|
|
@@ -21,7 +21,7 @@ const preAnsweredSchema = z.object({ selectedOptionId: z.string().optional(), se
|
|
|
21
21
|
const interactionInputSchema = z.object({
|
|
22
22
|
id: z.string().regex(/^[A-Za-z0-9_-]+$/).min(1).max(64),
|
|
23
23
|
title: z.string().min(1),
|
|
24
|
-
subtitle: z.string().
|
|
24
|
+
subtitle: z.string().trim().min(1),
|
|
25
25
|
body: z.string().optional(),
|
|
26
26
|
bodyPath: z.string().optional(),
|
|
27
27
|
options: z.array(optionInputSchema),
|
|
@@ -34,7 +34,7 @@ const interactionInputSchema = z.object({
|
|
|
34
34
|
const canonicalInteractionSchema = z.object({
|
|
35
35
|
id: z.string().regex(/^[A-Za-z0-9_-]+$/).min(1).max(64),
|
|
36
36
|
title: z.string().min(1),
|
|
37
|
-
subtitle: z.string().
|
|
37
|
+
subtitle: z.string().trim().min(1),
|
|
38
38
|
body: z.string().optional(),
|
|
39
39
|
options: z.array(optionSchema),
|
|
40
40
|
multiSelect: z.boolean().optional(),
|
|
@@ -125,7 +125,7 @@ export function canonicalizeInteraction(raw) {
|
|
|
125
125
|
const canonical = {
|
|
126
126
|
id: parsed.id,
|
|
127
127
|
title: parsed.title,
|
|
128
|
-
|
|
128
|
+
subtitle: parsed.subtitle,
|
|
129
129
|
...(parsed.body === undefined ? {} : { body: parsed.body }),
|
|
130
130
|
options: parsed.options.map((option) => ({ id: option.id, label: option.label, ...(option.description === undefined ? {} : { description: option.description }) })),
|
|
131
131
|
...(parsed.multiSelect === undefined ? {} : { multiSelect: parsed.multiSelect }),
|
|
@@ -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.
|
|
@@ -26,8 +26,10 @@ export interface RenderedDoc {
|
|
|
26
26
|
* (a bullet, a table row, a code line…); null for separator rows and rows
|
|
27
27
|
* of unmapped blocks. Same length as `lines`. */
|
|
28
28
|
spans: ([number, number] | null)[];
|
|
29
|
-
/** Per top-level block:
|
|
29
|
+
/** Per top-level block: its termrender block type plus 1-indexed inclusive
|
|
30
|
+
* source-line bounds. Never empty. */
|
|
30
31
|
blocks: {
|
|
32
|
+
type: string;
|
|
31
33
|
start: number;
|
|
32
34
|
end: number;
|
|
33
35
|
}[];
|
|
@@ -35,6 +37,21 @@ export interface RenderedDoc {
|
|
|
35
37
|
/** Render markdown with a row→source-line map via the pinned binary
|
|
36
38
|
* (`doc render --line-map`); plaintext paragraph-block fallback. */
|
|
37
39
|
export declare function renderMarkdownWithMap(md: string, width: number): RenderedDoc;
|
|
40
|
+
/**
|
|
41
|
+
* The shared block-aware mapped render: prose blocks keep the readability cap
|
|
42
|
+
* (`proseWidth`), while diagram blocks are re-rendered at the pane's full
|
|
43
|
+
* `paneWidth` and spliced back in by block index. Both surfaces (terminal
|
|
44
|
+
* review and the ask deck) go through this, so "which blocks may be wide" is
|
|
45
|
+
* decided once, from the renderer's own block types, never by heuristics on
|
|
46
|
+
* the emitted rows.
|
|
47
|
+
*
|
|
48
|
+
* Row order, block list and source spans are the narrow render's; only the
|
|
49
|
+
* rows OF a wide block are replaced, so anchoring stays source-based and
|
|
50
|
+
* unchanged.
|
|
51
|
+
*/
|
|
52
|
+
export declare function renderMarkdownBlockAware(md: string, proseWidth: number, paneWidth: number): RenderedDoc;
|
|
53
|
+
/** Block-aware render as plain rows, for surfaces that do not anchor. */
|
|
54
|
+
export declare function renderMarkdownBlockAwareLines(md: string, proseWidth: number, paneWidth: number): string[];
|
|
38
55
|
/** Validate markdown via `termrender doc check`. */
|
|
39
56
|
export declare function checkMarkdown(md: string): {
|
|
40
57
|
ok: true;
|
|
@@ -492,6 +492,10 @@ export function renderMarkdown(md, width) {
|
|
|
492
492
|
_bodyCache.set(key, fallback);
|
|
493
493
|
return fallback;
|
|
494
494
|
}
|
|
495
|
+
/** Block types allowed to use the full pane width instead of the prose cap:
|
|
496
|
+
* a diagram is a picture, not prose, so a readability column limit only
|
|
497
|
+
* shrinks it below what the pane could show. */
|
|
498
|
+
const WIDE_BLOCK_TYPES = new Set(['mermaid']);
|
|
495
499
|
/** Validate + normalize `doc render --line-map` JSON into a RenderedDoc.
|
|
496
500
|
* Null block bounds (a scanner edge case) are filled from neighbors and
|
|
497
501
|
* clamped into the source range; an empty block list gets one whole-doc
|
|
@@ -520,6 +524,10 @@ function parseRenderedDoc(out, source) {
|
|
|
520
524
|
for (const raw of p.blocks) {
|
|
521
525
|
if (typeof raw !== 'object' || raw === null)
|
|
522
526
|
return null;
|
|
527
|
+
// The block type is what lets callers render diagrams differently from
|
|
528
|
+
// prose; a map without it cannot drive block-aware rendering at all.
|
|
529
|
+
if (typeof raw.type !== 'string' || raw.type === '')
|
|
530
|
+
return null;
|
|
523
531
|
// Bounds are integers or null — a fractional bound would otherwise flow
|
|
524
532
|
// into a recorded comment's line/endLine instead of tripping tool-fault.
|
|
525
533
|
if (raw.start != null && !Number.isInteger(raw.start))
|
|
@@ -530,11 +538,11 @@ function parseRenderedDoc(out, source) {
|
|
|
530
538
|
const end = typeof raw.end === 'number' ? Math.max(raw.end, start) : start;
|
|
531
539
|
const s = Math.max(1, Math.min(start, totalSource));
|
|
532
540
|
const e = Math.max(s, Math.min(end, totalSource));
|
|
533
|
-
blocks.push({ start: s, end: e });
|
|
541
|
+
blocks.push({ type: raw.type, start: s, end: e });
|
|
534
542
|
prevEnd = e;
|
|
535
543
|
}
|
|
536
544
|
if (blocks.length === 0)
|
|
537
|
-
blocks.push({ start: 1, end: totalSource });
|
|
545
|
+
blocks.push({ type: 'paragraph', start: 1, end: totalSource });
|
|
538
546
|
const spans = [];
|
|
539
547
|
for (let r = 0; r < p.spans.length; r++) {
|
|
540
548
|
const raw = p.spans[r];
|
|
@@ -581,14 +589,14 @@ function fallbackDocWithMap(md, width) {
|
|
|
581
589
|
openAt = i;
|
|
582
590
|
}
|
|
583
591
|
else if (openAt !== null) {
|
|
584
|
-
blocks.push({ start: openAt + 1, end: i });
|
|
592
|
+
blocks.push({ type: 'paragraph', start: openAt + 1, end: i });
|
|
585
593
|
openAt = null;
|
|
586
594
|
}
|
|
587
595
|
}
|
|
588
596
|
if (openAt !== null)
|
|
589
|
-
blocks.push({ start: openAt + 1, end: src.length });
|
|
597
|
+
blocks.push({ type: 'paragraph', start: openAt + 1, end: src.length });
|
|
590
598
|
if (blocks.length === 0)
|
|
591
|
-
blocks.push({ start: 1, end: Math.max(1, src.length) });
|
|
599
|
+
blocks.push({ type: 'paragraph', start: 1, end: Math.max(1, src.length) });
|
|
592
600
|
const lines = [];
|
|
593
601
|
const rows = [];
|
|
594
602
|
const spans = [];
|
|
@@ -649,6 +657,59 @@ export function renderMarkdownWithMap(md, width) {
|
|
|
649
657
|
// for the rest of the session when a later re-render could succeed.
|
|
650
658
|
return fallbackDocWithMap(md, width);
|
|
651
659
|
}
|
|
660
|
+
/**
|
|
661
|
+
* The shared block-aware mapped render: prose blocks keep the readability cap
|
|
662
|
+
* (`proseWidth`), while diagram blocks are re-rendered at the pane's full
|
|
663
|
+
* `paneWidth` and spliced back in by block index. Both surfaces (terminal
|
|
664
|
+
* review and the ask deck) go through this, so "which blocks may be wide" is
|
|
665
|
+
* decided once, from the renderer's own block types, never by heuristics on
|
|
666
|
+
* the emitted rows.
|
|
667
|
+
*
|
|
668
|
+
* Row order, block list and source spans are the narrow render's; only the
|
|
669
|
+
* rows OF a wide block are replaced, so anchoring stays source-based and
|
|
670
|
+
* unchanged.
|
|
671
|
+
*/
|
|
672
|
+
export function renderMarkdownBlockAware(md, proseWidth, paneWidth) {
|
|
673
|
+
const base = renderMarkdownWithMap(md, proseWidth);
|
|
674
|
+
if (paneWidth <= proseWidth)
|
|
675
|
+
return base;
|
|
676
|
+
if (!base.blocks.some((b) => WIDE_BLOCK_TYPES.has(b.type)))
|
|
677
|
+
return base;
|
|
678
|
+
const wide = renderMarkdownWithMap(md, paneWidth);
|
|
679
|
+
// Same source, so the two renders describe the same top-level blocks. A
|
|
680
|
+
// disagreement means one of the two maps is degraded (renderer failure mid
|
|
681
|
+
// session) — render the narrow one rather than splice mismatched rows.
|
|
682
|
+
if (wide.blocks.length !== base.blocks.length)
|
|
683
|
+
return base;
|
|
684
|
+
const lines = [];
|
|
685
|
+
const rows = [];
|
|
686
|
+
const spans = [];
|
|
687
|
+
for (let r = 0; r < base.lines.length; r++) {
|
|
688
|
+
const bi = base.rows[r] ?? null;
|
|
689
|
+
if (bi === null || !WIDE_BLOCK_TYPES.has(base.blocks[bi].type)) {
|
|
690
|
+
lines.push(base.lines[r]);
|
|
691
|
+
rows.push(bi);
|
|
692
|
+
spans.push(base.spans[r] ?? null);
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
695
|
+
// First row of this block's run: emit the wide render's rows for the same
|
|
696
|
+
// block, then skip the narrow run (a block's rows are contiguous).
|
|
697
|
+
if (r === 0 || base.rows[r - 1] !== bi) {
|
|
698
|
+
for (let q = 0; q < wide.lines.length; q++) {
|
|
699
|
+
if (wide.rows[q] !== bi)
|
|
700
|
+
continue;
|
|
701
|
+
lines.push(wide.lines[q]);
|
|
702
|
+
rows.push(bi);
|
|
703
|
+
spans.push(wide.spans[q] ?? null);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
return { lines, rows, spans, blocks: base.blocks };
|
|
708
|
+
}
|
|
709
|
+
/** Block-aware render as plain rows, for surfaces that do not anchor. */
|
|
710
|
+
export function renderMarkdownBlockAwareLines(md, proseWidth, paneWidth) {
|
|
711
|
+
return renderMarkdownBlockAware(md, proseWidth, paneWidth).lines;
|
|
712
|
+
}
|
|
652
713
|
/** Validate markdown via `termrender doc check`. */
|
|
653
714
|
export function checkMarkdown(md) {
|
|
654
715
|
ensureRenderer();
|
package/dist/render/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const TERMRENDER_VERSION = "4.12.
|
|
1
|
+
export declare const TERMRENDER_VERSION = "4.12.1";
|
package/dist/render/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const TERMRENDER_VERSION = '4.12.
|
|
1
|
+
export const TERMRENDER_VERSION = '4.12.1';
|
package/dist/tui/ansi.d.ts
CHANGED
|
@@ -33,11 +33,35 @@ export declare function hardWrap(text: string, maxWidth: number): string[];
|
|
|
33
33
|
* them as cheap no-ops.
|
|
34
34
|
*/
|
|
35
35
|
export declare function centerHorizontal(lines: string[], cols: number, contentWidth: number): string[];
|
|
36
|
+
/** Visible width of a line in display cells (string-width ignores ANSI). */
|
|
37
|
+
export declare function visibleWidth(line: string): number;
|
|
36
38
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
39
|
+
* The one display-cell containment primitive: the visible cells
|
|
40
|
+
* `[start, start + width)` of `line`, with escape sequences preserved.
|
|
41
|
+
*
|
|
42
|
+
* Every line written to the terminal must fit within its rectangle, or it
|
|
43
|
+
* physically wraps onto the next row — which breaks diffFrame's
|
|
44
|
+
* one-logical-line-per-row model and strands spillover text on rows the
|
|
45
|
+
* differ believes are empty (so it never erases them). Styling seen before
|
|
46
|
+
* the window is replayed at its head so a slice keeps the colors it had in
|
|
47
|
+
* place, and a double-width glyph straddling either edge becomes spaces so
|
|
48
|
+
* the slice occupies exactly the cells it claims.
|
|
49
|
+
*/
|
|
50
|
+
export declare function sliceCells(line: string, start: number, width: number): string;
|
|
51
|
+
/**
|
|
52
|
+
* ANSI-aware clip: truncate a line's visible width to `maxWidth`. Thin
|
|
53
|
+
* wrapper over `sliceCells` so containment has a single implementation.
|
|
42
54
|
*/
|
|
43
55
|
export declare function clipLine(line: string, maxWidth: number): string;
|
|
56
|
+
/**
|
|
57
|
+
* Horizontal window onto a row that may be wider than its rectangle (a
|
|
58
|
+
* Mermaid diagram rendered at the pane's full width). A row that fits is
|
|
59
|
+
* returned untouched — panning only ever moves content that overflows, so
|
|
60
|
+
* prose stays put while a diagram slides. Hidden content on either side is
|
|
61
|
+
* marked with a dim ‹/› in the edge cell, so the row always states whether
|
|
62
|
+
* more of the diagram exists left or right, and the marker disappears once
|
|
63
|
+
* that edge is reached (nothing is unreachable).
|
|
64
|
+
*/
|
|
65
|
+
export declare function panLine(line: string, offset: number, width: number): string;
|
|
66
|
+
/** Cells of `line` that fall outside a `width`-wide rectangle. */
|
|
67
|
+
export declare function rowOverflow(line: string, width: number): number;
|
package/dist/tui/ansi.js
CHANGED
|
@@ -172,39 +172,135 @@ export function centerHorizontal(lines, cols, contentWidth) {
|
|
|
172
172
|
const pad = ' '.repeat(extraPad);
|
|
173
173
|
return lines.map((line) => (line === '' ? '' : pad + line));
|
|
174
174
|
}
|
|
175
|
+
/** Visible width of a line in display cells (string-width ignores ANSI). */
|
|
176
|
+
export function visibleWidth(line) {
|
|
177
|
+
return stringWidth(line);
|
|
178
|
+
}
|
|
179
|
+
const ANSI_TOKEN = /\x1b\[[0-9;?]*[a-zA-Z]|\x1b[@-_]/g;
|
|
180
|
+
const GRAPHEMES = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
|
|
181
|
+
/** ANSI escapes are tokens of their own; all printable text is segmented into
|
|
182
|
+
* extended grapheme clusters before widths or slice boundaries are calculated.
|
|
183
|
+
* `string-width` assigns display width to the complete cluster (not its
|
|
184
|
+
* constituent code points), which keeps ZWJ emoji, flags, skin tones and
|
|
185
|
+
* combining sequences indivisible while panning. */
|
|
186
|
+
function displayTokens(line) {
|
|
187
|
+
const tokens = [];
|
|
188
|
+
let at = 0;
|
|
189
|
+
ANSI_TOKEN.lastIndex = 0;
|
|
190
|
+
for (let match = ANSI_TOKEN.exec(line); match !== null; match = ANSI_TOKEN.exec(line)) {
|
|
191
|
+
for (const segment of GRAPHEMES.segment(line.slice(at, match.index))) {
|
|
192
|
+
tokens.push({ ansi: false, text: segment.segment, width: stringWidth(segment.segment) });
|
|
193
|
+
}
|
|
194
|
+
tokens.push({ ansi: true, text: match[0] });
|
|
195
|
+
at = match.index + match[0].length;
|
|
196
|
+
}
|
|
197
|
+
for (const segment of GRAPHEMES.segment(line.slice(at))) {
|
|
198
|
+
tokens.push({ ansi: false, text: segment.segment, width: stringWidth(segment.segment) });
|
|
199
|
+
}
|
|
200
|
+
return tokens;
|
|
201
|
+
}
|
|
175
202
|
/**
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
203
|
+
* The one display-cell containment primitive: the visible cells
|
|
204
|
+
* `[start, start + width)` of `line`, with escape sequences preserved.
|
|
205
|
+
*
|
|
206
|
+
* Every line written to the terminal must fit within its rectangle, or it
|
|
207
|
+
* physically wraps onto the next row — which breaks diffFrame's
|
|
208
|
+
* one-logical-line-per-row model and strands spillover text on rows the
|
|
209
|
+
* differ believes are empty (so it never erases them). Styling seen before
|
|
210
|
+
* the window is replayed at its head so a slice keeps the colors it had in
|
|
211
|
+
* place, and a double-width glyph straddling either edge becomes spaces so
|
|
212
|
+
* the slice occupies exactly the cells it claims.
|
|
181
213
|
*/
|
|
182
|
-
export function
|
|
183
|
-
if (
|
|
214
|
+
export function sliceCells(line, start, width) {
|
|
215
|
+
if (width < 1)
|
|
184
216
|
return '';
|
|
185
|
-
|
|
186
|
-
|
|
217
|
+
const from = Math.max(0, start);
|
|
218
|
+
const end = from + width;
|
|
219
|
+
let carried = ''; // styling established before the window
|
|
187
220
|
let out = '';
|
|
188
|
-
let
|
|
189
|
-
let
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
out +=
|
|
196
|
-
|
|
197
|
-
sawAnsi = true;
|
|
198
|
-
continue;
|
|
221
|
+
let emitted = false; // any escape inside the window
|
|
222
|
+
let cell = 0;
|
|
223
|
+
for (const token of displayTokens(line)) {
|
|
224
|
+
if (token.ansi) {
|
|
225
|
+
if (cell < from)
|
|
226
|
+
carried += token.text;
|
|
227
|
+
else {
|
|
228
|
+
out += token.text;
|
|
229
|
+
emitted = true;
|
|
199
230
|
}
|
|
231
|
+
continue;
|
|
200
232
|
}
|
|
201
|
-
|
|
202
|
-
const cw = stringWidth(ch);
|
|
203
|
-
if (w + cw > maxWidth)
|
|
233
|
+
if (cell >= end)
|
|
204
234
|
break;
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
235
|
+
const cw = token.width;
|
|
236
|
+
// A standalone combining mark has no cells but is still a complete
|
|
237
|
+
// grapheme cluster; retain it whenever its zero-width position is inside
|
|
238
|
+
// the window rather than dropping it through the usual boundary check.
|
|
239
|
+
if (cw === 0) {
|
|
240
|
+
if (cell >= from)
|
|
241
|
+
out += token.text;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (cell + cw <= from) {
|
|
245
|
+
cell += cw;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (cell < from) { // whole grapheme straddles the left edge
|
|
249
|
+
out += ' '.repeat(cell + cw - from);
|
|
250
|
+
cell += cw;
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (cell + cw > end) { // whole grapheme straddles the right edge
|
|
254
|
+
out += ' '.repeat(end - cell);
|
|
255
|
+
cell = end;
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
out += token.text;
|
|
259
|
+
cell += cw;
|
|
260
|
+
}
|
|
261
|
+
if (out === '')
|
|
262
|
+
return '';
|
|
263
|
+
if (carried !== '') {
|
|
264
|
+
out = carried + out;
|
|
265
|
+
emitted = true;
|
|
208
266
|
}
|
|
209
|
-
return
|
|
267
|
+
return emitted ? out + RESET : out;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* ANSI-aware clip: truncate a line's visible width to `maxWidth`. Thin
|
|
271
|
+
* wrapper over `sliceCells` so containment has a single implementation.
|
|
272
|
+
*/
|
|
273
|
+
export function clipLine(line, maxWidth) {
|
|
274
|
+
if (maxWidth < 1)
|
|
275
|
+
return '';
|
|
276
|
+
if (stringWidth(line) <= maxWidth)
|
|
277
|
+
return line; // string-width ignores ANSI
|
|
278
|
+
return sliceCells(line, 0, maxWidth);
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Horizontal window onto a row that may be wider than its rectangle (a
|
|
282
|
+
* Mermaid diagram rendered at the pane's full width). A row that fits is
|
|
283
|
+
* returned untouched — panning only ever moves content that overflows, so
|
|
284
|
+
* prose stays put while a diagram slides. Hidden content on either side is
|
|
285
|
+
* marked with a dim ‹/› in the edge cell, so the row always states whether
|
|
286
|
+
* more of the diagram exists left or right, and the marker disappears once
|
|
287
|
+
* that edge is reached (nothing is unreachable).
|
|
288
|
+
*/
|
|
289
|
+
export function panLine(line, offset, width) {
|
|
290
|
+
if (width < 1)
|
|
291
|
+
return '';
|
|
292
|
+
const w = stringWidth(line);
|
|
293
|
+
if (w <= width)
|
|
294
|
+
return line;
|
|
295
|
+
const max = w - width;
|
|
296
|
+
const off = Math.max(0, Math.min(offset, max));
|
|
297
|
+
const left = off > 0;
|
|
298
|
+
const right = off < max;
|
|
299
|
+
const innerWidth = width - (left ? 1 : 0) - (right ? 1 : 0);
|
|
300
|
+
const body = innerWidth > 0 ? sliceCells(line, off + (left ? 1 : 0), innerWidth) : '';
|
|
301
|
+
return `${left ? `${DIM}‹${RESET}` : ''}${body}${right ? `${DIM}›${RESET}` : ''}`;
|
|
302
|
+
}
|
|
303
|
+
/** Cells of `line` that fall outside a `width`-wide rectangle. */
|
|
304
|
+
export function rowOverflow(line, width) {
|
|
305
|
+
return Math.max(0, stringWidth(line) - width);
|
|
210
306
|
}
|
package/dist/tui/app.js
CHANGED
|
@@ -4,7 +4,6 @@ import { dirname, resolve as resolvePath } from 'node:path';
|
|
|
4
4
|
import { setupTerminal, restoreTerminal, parseKeypress, getTerminalSize } from './terminal.js';
|
|
5
5
|
import { diffFrame, renderOverview, renderItemReview, renderFinal, renderHandoff, clampItemReviewScroll } from './render.js';
|
|
6
6
|
import { handleKeypress, assignShortcuts } from './input.js';
|
|
7
|
-
import { renderMarkdown } from '../render/termrender.js';
|
|
8
7
|
import { editBufferInEditor } from '../editor/roundtrip.js';
|
|
9
8
|
import { validateDeck } from '../inbox/deck-schema.js';
|
|
10
9
|
import { canonicalizeInteraction } from '../inbox/visual.js';
|
|
@@ -57,6 +56,8 @@ function buildInitialState(deck, editorAvailable = false, followUpAvailable = fa
|
|
|
57
56
|
bodyMode: 'question',
|
|
58
57
|
scrollOffset: 0,
|
|
59
58
|
bodyScrollOffsets: { question: 0, visual: 0 },
|
|
59
|
+
hscrollOffset: 0,
|
|
60
|
+
hscrollMax: 0,
|
|
60
61
|
editorAvailable,
|
|
61
62
|
followUpAvailable,
|
|
62
63
|
followUp: undefined,
|
|
@@ -107,20 +108,6 @@ function rebindPersist(internals) {
|
|
|
107
108
|
internals.callbacks.onProgress?.(responses);
|
|
108
109
|
};
|
|
109
110
|
}
|
|
110
|
-
function visualRenderWidth(cols) {
|
|
111
|
-
return Math.max(1, Math.min(cols - 4, 76));
|
|
112
|
-
}
|
|
113
|
-
function renderVisualMarkdown(markdown, cols) {
|
|
114
|
-
return renderMarkdown(markdown, visualRenderWidth(cols)).join('\n');
|
|
115
|
-
}
|
|
116
|
-
/** Reflow ready Visual Markdown locally without issuing another request. */
|
|
117
|
-
function rerenderVisuals(internals) {
|
|
118
|
-
for (const [id, visual] of internals.state.visuals) {
|
|
119
|
-
if (visual.status !== 'ready' || visual.markdown === undefined)
|
|
120
|
-
continue;
|
|
121
|
-
internals.state.visuals.set(id, { ...visual, content: renderVisualMarkdown(visual.markdown, internals.cols) });
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
111
|
function visualRequestInteraction(interaction) {
|
|
125
112
|
return canonicalizeInteraction(interaction);
|
|
126
113
|
}
|
|
@@ -136,6 +123,17 @@ function retireVisuals(internals) {
|
|
|
136
123
|
catch { /* stale UI currency remains authoritative even if a host cancel fails */ }
|
|
137
124
|
}
|
|
138
125
|
}
|
|
126
|
+
function detachVisuals(internals) {
|
|
127
|
+
internals.visualGeneration += 1;
|
|
128
|
+
const handles = [...internals.visualHandles.values()];
|
|
129
|
+
internals.visualHandles.clear();
|
|
130
|
+
for (const handle of handles) {
|
|
131
|
+
try {
|
|
132
|
+
handle.detach();
|
|
133
|
+
}
|
|
134
|
+
catch { /* UI teardown remains local even if provider cleanup fails */ }
|
|
135
|
+
}
|
|
136
|
+
}
|
|
139
137
|
function fireVisuals(internals, interactions) {
|
|
140
138
|
const provider = internals.visualProvider;
|
|
141
139
|
if (provider === undefined)
|
|
@@ -163,7 +161,7 @@ function fireVisuals(internals, interactions) {
|
|
|
163
161
|
if (!internals.state.interactions.some((candidate) => candidate.id === interaction.id))
|
|
164
162
|
return;
|
|
165
163
|
internals.state.visuals.set(interaction.id, result.status === 'ready' && typeof result.markdown === 'string'
|
|
166
|
-
? { questionId: interaction.id, content:
|
|
164
|
+
? { questionId: interaction.id, content: result.markdown, markdown: result.markdown, status: 'ready' }
|
|
167
165
|
: { questionId: interaction.id, content: '', status: 'error' });
|
|
168
166
|
internals.callbacks.onDirty?.();
|
|
169
167
|
}).catch(() => {
|
|
@@ -208,7 +206,13 @@ export function mountPanel(opts) {
|
|
|
208
206
|
const renderLines = () => {
|
|
209
207
|
switch (internals.state.phase) {
|
|
210
208
|
case 'overview': return renderOverview(internals.state, internals.cols, internals.rows);
|
|
211
|
-
case 'item-review':
|
|
209
|
+
case 'item-review':
|
|
210
|
+
// The one pre-render clamp: keeps scrollOffset/hscrollOffset inside the
|
|
211
|
+
// current body's bounds (so u/d and h/l stay responsive) and publishes
|
|
212
|
+
// the live horizontal reach the input layer reads. The renderer itself
|
|
213
|
+
// stays pure; running it here covers mount, resize and every keypress.
|
|
214
|
+
clampItemReviewScroll(internals.state, internals.cols, internals.rows);
|
|
215
|
+
return renderItemReview(internals.state, internals.cols, internals.rows);
|
|
212
216
|
case 'final': return renderFinal(internals.state, internals.cols, internals.rows);
|
|
213
217
|
}
|
|
214
218
|
};
|
|
@@ -242,11 +246,6 @@ export function mountPanel(opts) {
|
|
|
242
246
|
request: (question) => internals.callbacks.onFollowUpRequest?.(question),
|
|
243
247
|
cancel: () => internals.callbacks.onFollowUpCancel?.(),
|
|
244
248
|
});
|
|
245
|
-
// Pre-render clamp (input layer): keep scrollOffset within the current
|
|
246
|
-
// body's bounds so u/d stay responsive. The renderer itself is pure.
|
|
247
|
-
if (internals.state.phase === 'item-review') {
|
|
248
|
-
clampItemReviewScroll(internals.state, internals.cols, internals.rows);
|
|
249
|
-
}
|
|
250
249
|
},
|
|
251
250
|
render() {
|
|
252
251
|
if (!internals.mounted)
|
|
@@ -256,19 +255,15 @@ export function mountPanel(opts) {
|
|
|
256
255
|
handleResize(cols, rows) {
|
|
257
256
|
internals.cols = cols;
|
|
258
257
|
internals.rows = rows;
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
rerenderVisuals(internals);
|
|
262
|
-
if (internals.state.phase === 'item-review') {
|
|
263
|
-
clampItemReviewScroll(internals.state, cols, rows);
|
|
264
|
-
}
|
|
258
|
+
// Visual Markdown is re-rendered by the item layout at this width, so
|
|
259
|
+
// the same block-aware geometry drives both resize and initial paint.
|
|
265
260
|
return renderLines();
|
|
266
261
|
},
|
|
267
262
|
unmount() {
|
|
268
263
|
if (!internals.mounted)
|
|
269
264
|
return;
|
|
270
265
|
internals.mounted = false;
|
|
271
|
-
|
|
266
|
+
detachVisuals(internals);
|
|
272
267
|
internals.state.persist = undefined;
|
|
273
268
|
},
|
|
274
269
|
loadDeck(deck, loadOpts) {
|
package/dist/tui/input.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { panItemReview } from './render.js';
|
|
1
2
|
// 'w' is reserved for the host-level "open in browser" handoff (see
|
|
2
3
|
// tui/app.ts resolveInteractionDir) — never auto-assignable as an option
|
|
3
4
|
// shortcut, or pressing it would race the handoff against picking that option.
|
|
@@ -180,6 +181,16 @@ function handleItemReview(input, key, state, render, followUp) {
|
|
|
180
181
|
render();
|
|
181
182
|
return;
|
|
182
183
|
}
|
|
184
|
+
// Body pan: contextual, so h/l only claim the key while the visible body
|
|
185
|
+
// actually overflows its rectangle (a wide diagram) AND no option owns that
|
|
186
|
+
// shortcut. Otherwise they fall through to the option/action handling below.
|
|
187
|
+
if ((input === 'h' || input === 'l' || key.leftArrow || key.rightArrow)
|
|
188
|
+
&& (state.hscrollMax ?? 0) > 0
|
|
189
|
+
&& !interaction.options.some((o) => o.shortcut === input)) {
|
|
190
|
+
panItemReview(state, input === 'h' || key.leftArrow ? -1 : 1);
|
|
191
|
+
render();
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
183
194
|
if (input === 'j' || key.downArrow) {
|
|
184
195
|
const max = actionCount(interaction) - 1;
|
|
185
196
|
state.selectedAction = Math.min(state.selectedAction + 1, max);
|
|
@@ -576,6 +587,7 @@ function advanceItem(state, direction) {
|
|
|
576
587
|
state.bodyMode = 'question';
|
|
577
588
|
state.scrollOffset = 0;
|
|
578
589
|
state.bodyScrollOffsets = { question: 0, visual: 0 };
|
|
590
|
+
state.hscrollOffset = 0;
|
|
579
591
|
}
|
|
580
592
|
/**
|
|
581
593
|
* Move to the next interaction WITHOUT a response, falling through to the
|
|
@@ -598,6 +610,7 @@ function advanceToNextUnanswered(state) {
|
|
|
598
610
|
state.bodyMode = 'question';
|
|
599
611
|
state.scrollOffset = 0;
|
|
600
612
|
state.bodyScrollOffsets = { question: 0, visual: 0 };
|
|
613
|
+
state.hscrollOffset = 0;
|
|
601
614
|
}
|
|
602
615
|
function actionCount(interaction) {
|
|
603
616
|
return interaction.options.length + (interaction.allowFreetext && interaction.options.length > 0 ? 1 : 0);
|