@rse/ase 2.0.6 → 2.0.8
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/dst/ase-agent-definition-api.d.ts +2 -0
- package/dst/ase-agent-definition-core.js +24 -3
- package/dst/ase-agent-fleet.js +2 -1
- package/dst/ase-agent-relay-client.js +3 -4
- package/dst/ase-agent-runtime-engine.js +22 -4
- package/dst/ase-agent-runtime-tool.js +10 -8
- package/dst/ase-agent-runtime.js +8 -4
- package/dst/ase-config-schema.js +4 -1
- package/dst/ase-lib-fetch.js +12 -0
- package/dst/ase-lib-server-cli.js +8 -5
- package/dst/ase-man-catalog.js +1 -1
- package/dst/ase-task-board-diff.js +120 -0
- package/dst/ase-task-board-tui-control.js +8 -5
- package/dst/ase-task-board-tui-model.js +54 -14
- package/dst/ase-task-board-tui-popup.js +41 -8
- package/dst/ase-task-board-tui.js +42 -27
- package/dst/ase-task-board-web-client.css +1 -1
- package/dst/ase-task-board-web-client.html +16 -0
- package/dst/ase-task-board-web-client.js +33 -33
- package/dst/ase-task-board-web-server.js +44 -9
- package/dst/ase-task-board.js +12 -4
- package/dst/ase-task-store-client.js +4 -5
- package/dst/ase-task-store-plugin-gitea.js +3 -3
- package/dst/man/ase-agent-adhoc.md +25 -6
- package/dst/man/ase-agent-condenser.md +7 -0
- package/dst/man/ase-agent-editor.md +7 -0
- package/dst/man/ase-agent-estimator.md +7 -0
- package/dst/man/ase-agent-fleet.md +7 -4
- package/dst/man/ase-agent-implementer.md +7 -0
- package/dst/man/ase-agent-integrator.md +7 -0
- package/dst/man/ase-agent-qualifier.md +7 -0
- package/dst/man/ase-agent-rebooter.md +7 -0
- package/dst/man/ase-agent-researcher.md +7 -0
- package/dst/man/ase-config-init.md +2 -1
- package/dst/man/ase-config.md +5 -0
- package/dst/man/ase-task-board.md +44 -2
- package/package.json +11 -10
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/.github/plugin/plugin.json +1 -1
- package/plugin/meta/ase-common-task.md +33 -1
- package/plugin/meta/ase-control.md +14 -0
- package/plugin/meta/ase-format-specbook.yaml +9 -9
- package/plugin/meta/ase-skill.md +7 -0
- package/plugin/mods/ase-mods-common.ts +73 -42
- package/plugin/mods/ase-mods-help-hint.ts +1 -1
- package/plugin/mods/ase-mods-task-edit.ts +427 -0
- package/plugin/mods/ase-mods.ts +45 -14
- package/plugin/skills/ase-code-edit/SKILL.md +53 -16
- package/plugin/skills/ase-code-edit/help.md +26 -13
- package/plugin/skills/ase-code-lint/SKILL.md +7 -3
- package/plugin/skills/ase-help-intent/data.md +248 -36
- package/plugin/skills/ase-help-skill/catalog.md +1 -0
- package/plugin/skills/ase-meta-workflow/workflow.txt +2 -0
- package/plugin/skills/ase-repo-changelog/SKILL.md +29 -10
- package/plugin/skills/ase-repo-changelog/help.md +5 -1
- package/plugin/skills/ase-repo-commit/SKILL.md +4 -1
- package/plugin/skills/ase-repo-dissect/help.md +2 -1
- package/plugin/skills/ase-repo-stage/SKILL.md +459 -0
- package/plugin/skills/ase-repo-stage/help.md +120 -0
- package/plugin/skills/ase-spec-edit/SKILL.md +20 -12
- package/plugin/skills/ase-spec-edit/help.md +22 -14
- package/plugin/skills/ase-task-edit/SKILL.md +114 -9
- package/plugin/skills/ase-task-edit/help.md +60 -0
- package/plugin/skills/ase-task-grill/SKILL.md +35 -20
- package/plugin/skills/ase-task-grill/help.md +18 -8
- /package/plugin/mods/{ase-mods-evaluate.ts → ase-mods-meta-evaluate.ts} +0 -0
- /package/plugin/mods/{ase-mods-grill.ts → ase-mods-task-grill.ts} +0 -0
|
@@ -19,7 +19,7 @@ import { statsOf, statsRanges, effortScale } from "./ase-task-board-stats.js";
|
|
|
19
19
|
import { layoutGraph } from "./ase-task-board-graph.js";
|
|
20
20
|
import { flowOf, layoutFlow, flowState } from "./ase-task-board-flow.js";
|
|
21
21
|
import { fitGroups, agentLogWidth, agentOpLines, agentOpHeight, AGENT_INFO_H } from "./ase-task-board-tui-view.js";
|
|
22
|
-
import { dialogTabs, planLines, attachmentLines, agentLogLines, agentLogKey, DIALOG_CHROME, DIALOG_WIDTH } from "./ase-task-board-tui-popup.js";
|
|
22
|
+
import { dialogTabs, planLines, planChanged, planDiffLines, attachmentLines, agentLogLines, agentLogKey, DIALOG_CHROME, DIALOG_WIDTH } from "./ase-task-board-tui-popup.js";
|
|
23
23
|
import { pulseFrames } from "./ase-task-board-tui-style.js";
|
|
24
24
|
/* the suggested request names of a request by prompt, matching its request name as last
|
|
25
25
|
typed (all of them for an empty or an exactly suggested request name, to offer the
|
|
@@ -407,13 +407,14 @@ const usePromptActions = (ctx) => {
|
|
|
407
407
|
};
|
|
408
408
|
return { startPrompt, startRequest, cancelPrompt, submitPrompt };
|
|
409
409
|
};
|
|
410
|
-
/* the state of the terminal board
|
|
411
|
-
|
|
410
|
+
/* the state of the terminal board (resp. with a task id, of the
|
|
411
|
+
task viewer): all state, the derived values, and the actions */
|
|
412
|
+
export const useBoardState = (log, initial, relay, watched) => {
|
|
412
413
|
const { exit, suspendTerminal } = useApp();
|
|
413
414
|
const { columns, rows } = useWindowSize();
|
|
414
415
|
const [all, setBoard] = React.useState(initial);
|
|
415
416
|
const [surface, setSurface] = React.useState(() => BoardState.load().tui);
|
|
416
|
-
const [view, setView] = React.useState(() => surface.view);
|
|
417
|
+
const [view, setView] = React.useState(() => watched !== undefined ? "lanes" : surface.view);
|
|
417
418
|
const [filter, setFilter] = React.useState("");
|
|
418
419
|
const [typing, setTyping] = React.useState(false);
|
|
419
420
|
/* the shown board: all tasks reduced onto the ones matching the filter
|
|
@@ -424,7 +425,7 @@ export const useBoardState = (log, initial, relay) => {
|
|
|
424
425
|
return view === "graph" && !surface.standalone ? dropStandalone(found) : found;
|
|
425
426
|
}, [all, filter, view, surface.standalone]);
|
|
426
427
|
const [sel, setSel] = React.useState(() => relocate(board, { g: 0, l: 0, id: "" }, surface));
|
|
427
|
-
const [dialog, setDialog] = React.useState(null);
|
|
428
|
+
const [dialog, setDialog] = React.useState(watched !== undefined ? { id: watched, tab: 0, first: 0, scrolls: {} } : null);
|
|
428
429
|
const [plan, setPlan] = React.useState(null);
|
|
429
430
|
const [files, setFiles] = React.useState(new Map());
|
|
430
431
|
const [first, setFirst] = React.useState(0);
|
|
@@ -480,6 +481,8 @@ export const useBoardState = (log, initial, relay) => {
|
|
|
480
481
|
}, [log, all]);
|
|
481
482
|
/* persist the shown view for the next start of the terminal board */
|
|
482
483
|
React.useEffect(() => {
|
|
484
|
+
if (watched !== undefined)
|
|
485
|
+
return;
|
|
483
486
|
BoardState.setView("tui", view).catch((err) => {
|
|
484
487
|
log.write("warning", `board: persisting view failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
485
488
|
});
|
|
@@ -523,6 +526,21 @@ export const useBoardState = (log, initial, relay) => {
|
|
|
523
526
|
setNotice(`editing task "${prompt.id}" by prompt cancelled: task vanished in the meantime`);
|
|
524
527
|
}
|
|
525
528
|
}, [all, prompt]);
|
|
529
|
+
/* quit the task viewer once its dialog is closed, resp. with a warning once
|
|
530
|
+
its task vanished (except while an edit may rename it, which the dialog follows) */
|
|
531
|
+
const seen = React.useRef("");
|
|
532
|
+
React.useEffect(() => {
|
|
533
|
+
if (watched === undefined)
|
|
534
|
+
return;
|
|
535
|
+
if (dialog === null)
|
|
536
|
+
exit();
|
|
537
|
+
else if (all.cards.has(dialog.id))
|
|
538
|
+
seen.current = dialog.id;
|
|
539
|
+
else if (seen.current === dialog.id && !editing.current) {
|
|
540
|
+
log.write("warning", `board: task "${dialog.id}" no longer exists`);
|
|
541
|
+
exit();
|
|
542
|
+
}
|
|
543
|
+
}, [all, dialog?.id]);
|
|
526
544
|
/* remember the box of every graph node for the spatial navigation and the scrolling */
|
|
527
545
|
const places = React.useMemo(() => placesOf(layout?.graph), [layout]);
|
|
528
546
|
/* follow changes of the task storage and of the lifecycle mode,
|
|
@@ -624,18 +642,26 @@ export const useBoardState = (log, initial, relay) => {
|
|
|
624
642
|
};
|
|
625
643
|
/* fetch the plan (with its attachments) of the read dialog whenever it is opened or the board changes
|
|
626
644
|
(the latter in the background, i.e. without the busy popup), together with loading the
|
|
627
|
-
syntax highlighter of YAML or JSON attachments (on the first opening only)
|
|
645
|
+
syntax highlighter of YAML or JSON attachments (on the first opening only), where a
|
|
646
|
+
change with a shown effect remembers its predecessor for the diff of the read dialog,
|
|
647
|
+
and where closing the dialog forgets the plan, as its changes are followed only while open */
|
|
628
648
|
const dialogId = dialog?.id;
|
|
629
649
|
const dialogTab = dialog?.tab ?? 0;
|
|
630
650
|
React.useEffect(() => {
|
|
631
|
-
if (dialogId === undefined)
|
|
651
|
+
if (dialogId === undefined) {
|
|
652
|
+
setPlan(null);
|
|
632
653
|
return;
|
|
654
|
+
}
|
|
633
655
|
let live = true;
|
|
634
656
|
const load = Promise.all([Task.parts(log, dialogId), Task.attachments(log, dialogId), dataLoad()]);
|
|
635
657
|
const run = plan?.id === dialogId ? load : track({ text: "loading task", id: dialogId }, load);
|
|
636
658
|
run.then(([parts, atts]) => {
|
|
637
659
|
if (live)
|
|
638
|
-
setPlan(
|
|
660
|
+
setPlan((prev) => {
|
|
661
|
+
const next = parts === null ? null : { ...parts, atts };
|
|
662
|
+
const same = prev?.id === dialogId && next !== null;
|
|
663
|
+
return { id: dialogId, parts: next, before: !same ? undefined : planChanged(prev.parts, next) ? prev.parts : prev.before };
|
|
664
|
+
});
|
|
639
665
|
}).catch((err) => {
|
|
640
666
|
log.write("warning", `board: loading plan failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
641
667
|
if (live)
|
|
@@ -708,6 +734,12 @@ export const useBoardState = (log, initial, relay) => {
|
|
|
708
734
|
const ref = process.platform === "win32" ? "\"%ASE_TASK_FILE%\"" : "\"$ASE_TASK_FILE\"";
|
|
709
735
|
await suspendTerminal(async () => {
|
|
710
736
|
mouseReporting(false);
|
|
737
|
+
/* stop the read of stdin still pending from Ink, which only removes its
|
|
738
|
+
listener, as it else takes the next input away from the editor and
|
|
739
|
+
passes it to the board afterwards (a pause in non-flowing mode does
|
|
740
|
+
not emit the event on which Node stops reading, and the stop is deferred) */
|
|
741
|
+
process.stdin.emit("pause");
|
|
742
|
+
await new Promise((resolve) => { process.nextTick(resolve); });
|
|
711
743
|
try {
|
|
712
744
|
await execa(`${editor} ${ref}`, { shell: true, stdio: "inherit", env: { ASE_TASK_FILE: file } });
|
|
713
745
|
}
|
|
@@ -791,9 +823,10 @@ export const useBoardState = (log, initial, relay) => {
|
|
|
791
823
|
setNotice(`creating task failed: ${msg} (press "N" to re-edit, or empty it to discard)`);
|
|
792
824
|
}
|
|
793
825
|
};
|
|
794
|
-
/* delete a task (after its confirmation), closing its read dialog
|
|
826
|
+
/* delete a task (after its confirmation), closing its read dialog
|
|
827
|
+
(in the task viewer only once it vanished, as closing it quits the process) */
|
|
795
828
|
const remove = (id) => {
|
|
796
|
-
if (dialog?.id === id)
|
|
829
|
+
if (dialog?.id === id && watched === undefined)
|
|
797
830
|
setDialog(null);
|
|
798
831
|
track({ text: "deleting task", id }, Task.delete(log, id)).then((existed) => {
|
|
799
832
|
setNotice(existed ? `task "${id}" deleted` : `task "${id}" no longer exists`);
|
|
@@ -910,16 +943,23 @@ export const useBoardState = (log, initial, relay) => {
|
|
|
910
943
|
const { agentList, agentIdx, agent, agentOp, logLines, logH, logFirst, logMax, selectAgent, scrollLog, focusAgent } = useAgentsView(agents, columns, boardH);
|
|
911
944
|
/* the tabs of the read dialog, the lines of its selected tab (clamped to
|
|
912
945
|
the existing tabs), and the maximum scroll offset of these lines */
|
|
913
|
-
const dialogW = Math.min(columns - 2, DIALOG_WIDTH);
|
|
946
|
+
const dialogW = watched !== undefined ? columns : Math.min(columns - 2, DIALOG_WIDTH);
|
|
914
947
|
const dialogParts = dialog !== null && plan?.id === dialog.id ? plan.parts : undefined;
|
|
915
948
|
const tabLabels = dialogTabs(dialogParts);
|
|
916
949
|
const dialogSel = Math.min(dialogTab, tabLabels.length - 1);
|
|
917
950
|
const dialogAtt = dialogSel > 0 && dialogParts !== undefined && dialogParts !== null && !(dialogParts instanceof Error) ?
|
|
918
951
|
dialogParts.atts[dialogSel - 1] : undefined;
|
|
952
|
+
const planDiff = React.useMemo(() => plan?.before !== undefined ?
|
|
953
|
+
planDiffLines(plan.before, plan.parts, plan.id, Math.max(1, dialogW - 5)) : null, [plan, dialogW]);
|
|
954
|
+
const dialogDiff = planDiff !== null && dialogSel === 0 && plan?.id === dialog?.id;
|
|
919
955
|
const dialogLines = dialog === null ? [] : dialogAtt !== undefined ?
|
|
920
956
|
attachmentLines(dialogAtt, files.get(`${dialog.id}:${dialogSel}`), Math.max(1, dialogW - 5)) :
|
|
921
|
-
planLines(dialogParts, dialog.id, Math.max(1, dialogW - 5));
|
|
957
|
+
dialogDiff ? planDiff : planLines(dialogParts, dialog.id, Math.max(1, dialogW - 5));
|
|
922
958
|
const dialogMax = Math.max(0, dialogLines.length - (rows - DIALOG_CHROME));
|
|
959
|
+
/* drop the diff of the last change from the read dialog, so just the latest state remains */
|
|
960
|
+
const clearDiff = () => {
|
|
961
|
+
setPlan((prev) => prev !== null ? { ...prev, before: undefined } : prev);
|
|
962
|
+
};
|
|
923
963
|
/* the entries of the transfer popup of its (still existing) task */
|
|
924
964
|
const transferCard = transfer !== null ? all.cards.get(transfer.id) : undefined;
|
|
925
965
|
const transferList = transferCard !== undefined && cycle !== null ? transferEntries(all, cycle, transferCard) : [];
|
|
@@ -1015,11 +1055,11 @@ export const useBoardState = (log, initial, relay) => {
|
|
|
1015
1055
|
/* the wall-clock and the heartbeats of the agents (for the agents view and the agent graph) */
|
|
1016
1056
|
const { now, beatOf } = useBeats(agents, view);
|
|
1017
1057
|
return {
|
|
1018
|
-
log, exit, columns, rows, all, board, surface, view, setView, filter, setFilter, typing, setTyping,
|
|
1058
|
+
log, exit, watched, columns, rows, all, board, surface, view, setView, filter, setFilter, typing, setTyping,
|
|
1019
1059
|
sel, setSel, dialog, setDialog, scroll, layout, notice, setNotice, carry, setCarry, cycle,
|
|
1020
1060
|
confirm, setConfirm, transfer, setTransfer, assign, setAssign, prompt, setPrompt, mouse, setMouse, lastClick, grown, setGrow,
|
|
1021
1061
|
cardBoxes, laneBoxes, groupBoxes, headBoxes, agentBoxes, rangeBoxes, cardRef, laneRef, groupRef, headRef, agentRef, rangeRef, boxAt, graphView,
|
|
1022
|
-
places, graphTitles, boardH, dialogW, tabLabels, dialogSel, dialogLines, dialogScroll,
|
|
1062
|
+
places, graphTitles, boardH, dialogW, tabLabels, dialogSel, dialogLines, dialogScroll, dialogDiff, clearDiff,
|
|
1023
1063
|
transferCard, transferList, fit, viewH, viewW, x, y, nodes, dim, pulse, now, beatOf, store, busy, busyTick,
|
|
1024
1064
|
agents, working, cycleView, agentList, agentIdx, agent, agentOp, logLines, logH, logFirst, logMax, selectAgent, scrollLog, stats, usage, range, setRange, stepRange,
|
|
1025
1065
|
flow, flowLayout, flowSel, setFlowSel, flowPlaces, focusAgent, scrollFlow,
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { Box, Text } from "ink";
|
|
7
7
|
import { cardLabel, groupLabel, attachmentTabs, isDiff, diffTones, dataLang, dataSpans, clampLines } from "./ase-task-board-core.js";
|
|
8
|
+
import { diffHunks, changedWords } from "./ase-task-board-diff.js";
|
|
8
9
|
import { h } from "./ase-task-board-tui-view.js";
|
|
9
10
|
import { palette, cx, spinnerFrames, eighths } from "./ase-task-board-tui-style.js";
|
|
10
11
|
const BOLD = 1;
|
|
@@ -13,6 +14,7 @@ const CODE = 4;
|
|
|
13
14
|
const ACCENT = 8;
|
|
14
15
|
const SIGNAL = 16;
|
|
15
16
|
const DIM = 32;
|
|
17
|
+
const CHANGED = 64;
|
|
16
18
|
/* strip the Markdown inline markers of a line ("`" for code, "**" and
|
|
17
19
|
"__" for bold, "*" for italic), recording the style of the marked text */
|
|
18
20
|
const emphasis = (line) => {
|
|
@@ -109,6 +111,35 @@ export const planLines = (parts, id, width) => {
|
|
|
109
111
|
...markdownLines(parts.body, width)
|
|
110
112
|
];
|
|
111
113
|
};
|
|
114
|
+
/* determine whether a task plan differs from its predecessor in what the read
|
|
115
|
+
dialog shows (and not, for instance, only in its attachments) */
|
|
116
|
+
export const planChanged = (before, after) => {
|
|
117
|
+
const shown = (parts) => parts === undefined || parts === null || parts instanceof Error ? null :
|
|
118
|
+
JSON.stringify([[...parts.keys].filter(([key]) => key !== "Type"), parts.body]);
|
|
119
|
+
return shown(before) !== null && shown(after) !== null && shown(before) !== shown(after);
|
|
120
|
+
};
|
|
121
|
+
/* the lines of the diff of two task plans for the read dialog: the rendered lines of
|
|
122
|
+
both, where removed lines are red and added lines blue, with their sign in the
|
|
123
|
+
otherwise empty first column and their changed words marked, and where only the
|
|
124
|
+
shown content of the lines decides about a change */
|
|
125
|
+
export const planDiffLines = (before, after, id, width) => {
|
|
126
|
+
const changed = (line, sign, tint, spans) => {
|
|
127
|
+
const mask = [...(line.mask ?? new Array(line.text.length).fill(0))];
|
|
128
|
+
for (const { from, to } of spans)
|
|
129
|
+
for (let k = from; k < to; k++)
|
|
130
|
+
mask[k] |= CHANGED;
|
|
131
|
+
return { ...line, mask, sign, tint };
|
|
132
|
+
};
|
|
133
|
+
return diffHunks(planLines(before, id, width), planLines(after, id, width), (line) => JSON.stringify(line))
|
|
134
|
+
.flatMap(({ kept, removed, added }) => {
|
|
135
|
+
const [del, add] = changedWords(removed.map((line) => line.text), added.map((line) => line.text));
|
|
136
|
+
return [
|
|
137
|
+
...kept,
|
|
138
|
+
...removed.map((line, k) => changed(line, "-", "red", del[k])),
|
|
139
|
+
...added.map((line, k) => changed(line, "+", "blue", add[k]))
|
|
140
|
+
];
|
|
141
|
+
});
|
|
142
|
+
};
|
|
112
143
|
/* the lines of a task plan attachment for the read dialog: its embedded
|
|
113
144
|
data or its (lazily loaded) file content, rendered as Markdown for a
|
|
114
145
|
Markdown type, syntax-highlighted for a YAML or JSON type, colored per
|
|
@@ -549,8 +580,7 @@ export const renderAssign = (assign, choices, unknown, columns, rows) => {
|
|
|
549
580
|
};
|
|
550
581
|
/* render the read dialog: full height, horizontally centered, with a
|
|
551
582
|
vertical scroll bar in its right border */
|
|
552
|
-
export const renderDialog = ({ card, group, id, pred, succ, tint, tabs, tab, first, scroll, lines, columns, rows, notice }) => {
|
|
553
|
-
const width = Math.min(columns - 2, DIALOG_WIDTH);
|
|
583
|
+
export const renderDialog = ({ card, group, id, pred, succ, tint, tabs, tab, first, scroll, lines, width, columns, rows, notice, viewer, diffed }) => {
|
|
554
584
|
const left = Math.floor((columns - width) / 2);
|
|
555
585
|
const bodyW = width - 5;
|
|
556
586
|
const viewH = rows - DIALOG_CHROME;
|
|
@@ -599,14 +629,16 @@ export const renderDialog = ({ card, group, id, pred, succ, tint, tabs, tab, fir
|
|
|
599
629
|
/* the key hints, or instead the status notice of an edit, truncated to the free width */
|
|
600
630
|
const free = Math.max(0, innerW - pos.length - 1);
|
|
601
631
|
const keys = (notice !== null ? " " + sanitize(notice) :
|
|
602
|
-
|
|
603
|
-
|
|
632
|
+
(diffed ? " #: clear diff ·" : "") +
|
|
633
|
+
" ←/→/⇤/⇥: switch tab · ↑/↓/⇈/⇊: scroll · e: edit · E: edit by prompt · R: request by prompt · T: transition · A: assign · D: delete · M: toggle mouse · " +
|
|
634
|
+
(viewer ? "q: quit" : "⏎/ESC: close")).slice(0, free);
|
|
635
|
+
return h(Box, { key: "dialog", top: 0, left, width, height: rows, ...cx("popup", "border-dim") }, h(Text, {}, " ", ...(prefix !== "" ? [h(Text, { ...cx("bold"), color: tint(id) }, prefix)] : []), h(Text, { ...cx("badge"), color: tint(id) }, ` ${epic ? "♛ " : ""}≡ ${head.slice(0, id.length)} `), h(Text, { color: tint(id) }, head.slice(id.length)), h(Text, cx("dim"), right.slice(0, right.length - lane.length)), h(Text, cx("dim", "bold"), lane), " ", h(Text, cx("dim", !viewer && "inverse"), viewer ? " " : " X "), " "), separator("sep-title"), h(Text, {}, ...segs.map((seg, k) => h(Text, { key: k, color: seg.color, inverse: seg.inverse }, seg.text))), separator("sep-head"), h(Text, { wrap: "truncate" }, ...tabBar.map((seg, k) => h(Text, { key: k, color: seg.color, bold: seg.bold, inverse: seg.inverse }, seg.text))), separator("sep-tabs"), ...Array.from({ length: viewH }, (_, i) => {
|
|
604
636
|
const line = lines[s + i];
|
|
605
637
|
const bar = lines.length <= viewH ? " " : (i >= thumbY && i < thumbY + thumbH ? "█" : "░");
|
|
606
638
|
/* split the padded line into runs of equal inline style (cut to the body width,
|
|
607
639
|
as a wider line would wrap and push the rows of the dialog out of place) */
|
|
608
|
-
const text = " " + (line?.text ?? "").slice(0, bodyW).padEnd(bodyW);
|
|
609
|
-
const mask = [0, ...(line?.mask ?? [])];
|
|
640
|
+
const text = (line?.sign ?? " ") + (line?.text ?? "").slice(0, bodyW).padEnd(bodyW);
|
|
641
|
+
const mask = [line?.sign !== undefined ? CHANGED : 0, ...(line?.mask ?? [])];
|
|
610
642
|
const runs = [];
|
|
611
643
|
for (let k = 0; k < text.length; k++) {
|
|
612
644
|
const style = mask[k] ?? 0;
|
|
@@ -617,8 +649,9 @@ export const renderDialog = ({ card, group, id, pred, succ, tint, tabs, tab, fir
|
|
|
617
649
|
}
|
|
618
650
|
return h(Box, { key: i }, ...runs.map((run, k) => h(Text, {
|
|
619
651
|
key: k, wrap: "truncate",
|
|
620
|
-
color: (run.style & (CODE | ACCENT)) !== 0 ? palette.accent : (run.style & SIGNAL) !== 0 ? palette.signal :
|
|
621
|
-
(run.style & DIM) !== 0 ? palette.dim : line?.color,
|
|
652
|
+
color: line?.tint ?? ((run.style & (CODE | ACCENT)) !== 0 ? palette.accent : (run.style & SIGNAL) !== 0 ? palette.signal :
|
|
653
|
+
(run.style & DIM) !== 0 ? palette.dim : line?.color),
|
|
654
|
+
inverse: (run.style & CHANGED) !== 0,
|
|
622
655
|
bold: (line?.bold ?? false) || (run.style & BOLD) !== 0, italic: (run.style & ITALIC) !== 0
|
|
623
656
|
}, run.text)), h(Text, cx("signal"), ` ${bar}`));
|
|
624
657
|
}), separator("sep-foot"), h(Box, {}, h(Text, cx("hint", notice !== null && "signal"), keys), h(Text, cx("hint"), pos.padStart(Math.max(0, innerW - keys.length)))));
|
|
@@ -16,10 +16,45 @@ import { palette, loadPalette, cx } from "./ase-task-board-tui-style.js";
|
|
|
16
16
|
import { sanitize, renderDialog, renderConfirm, renderTransfer, renderAssign, renderPrompt, renderBusy } from "./ase-task-board-tui-popup.js";
|
|
17
17
|
/* render the whole screen */
|
|
18
18
|
const renderScreen = (ctx, project) => {
|
|
19
|
-
const { columns, rows, all, board, surface, view, filter, typing, dialog, notice, carry, confirm, transfer, assign, prompt, headRef, tabLabels, dialogSel, dialogLines, transferCard, transferList, dim, store, busy, busyTick, agents } = ctx;
|
|
19
|
+
const { columns, rows, all, board, surface, view, filter, typing, dialog, notice, carry, confirm, transfer, assign, prompt, headRef, tabLabels, dialogW, dialogSel, dialogLines, dialogDiff, transferCard, transferList, dim, store, busy, busyTick, agents, watched } = ctx;
|
|
20
20
|
/* refuse to draw into a too small window */
|
|
21
21
|
if (columns < 40 || rows < 16)
|
|
22
22
|
return h(Box, { width: columns, height: rows, justifyContent: "center", alignItems: "center" }, h(Text, cx("signal"), `window too small (${columns}×${rows}) — needs at least 40×16`));
|
|
23
|
+
/* the dialog and popups on top of the screen */
|
|
24
|
+
const overlays = [
|
|
25
|
+
dialog !== null ? renderDialog({
|
|
26
|
+
card: all.cards.get(dialog.id),
|
|
27
|
+
group: all.groups.find((g) => g.lanes.some((l) => l.status === all.cards.get(dialog.id)?.status))?.title,
|
|
28
|
+
id: dialog.id,
|
|
29
|
+
pred: all.pred.get(dialog.id) ?? [],
|
|
30
|
+
succ: all.succ.get(dialog.id) ?? [],
|
|
31
|
+
tint: (ref) => {
|
|
32
|
+
const c = all.cards.get(ref);
|
|
33
|
+
const tone = c !== undefined ? toneOf(all, c) : "idle";
|
|
34
|
+
return tone === "active" ? palette.accent : tone === "done" ? palette.dim : palette.normal;
|
|
35
|
+
},
|
|
36
|
+
tabs: tabLabels,
|
|
37
|
+
tab: dialogSel,
|
|
38
|
+
first: dialog.first,
|
|
39
|
+
scroll: dialog.scrolls[dialogSel] ?? 0,
|
|
40
|
+
lines: dialogLines,
|
|
41
|
+
width: dialogW,
|
|
42
|
+
columns,
|
|
43
|
+
rows,
|
|
44
|
+
notice,
|
|
45
|
+
viewer: watched !== undefined,
|
|
46
|
+
diffed: dialogDiff
|
|
47
|
+
}) : null,
|
|
48
|
+
confirm !== null ? renderConfirm(confirm.id, confirm.yes, columns, rows) : null,
|
|
49
|
+
transfer !== null && transferCard !== undefined ?
|
|
50
|
+
renderTransfer(transfer.id, transferCard.status, transferList, transfer.at, columns, rows) : null,
|
|
51
|
+
assign !== null ? renderAssign(assign, assignChoices(assign), assignUnknown(assign), columns, rows) : null,
|
|
52
|
+
prompt !== null ? renderPrompt(prompt, columns, rows, prompt.request !== null ? requestChoices(prompt.request) : []) : null,
|
|
53
|
+
busy !== null ? renderBusy(busy.label, busy.since, busyTick, columns, rows) : null
|
|
54
|
+
];
|
|
55
|
+
/* the task viewer shows only the dialog and popups, without the board */
|
|
56
|
+
if (watched !== undefined)
|
|
57
|
+
return h(Box, { width: columns, height: rows, flexDirection: "column" }, ...overlays);
|
|
23
58
|
/* the status line in the last line of the screen, below the key hints, enclosed
|
|
24
59
|
on its left and right side by the downward lines of the rule above it,
|
|
25
60
|
showing the kind and connection state of the task store (a disconnected one in
|
|
@@ -59,27 +94,7 @@ const renderScreen = (ctx, project) => {
|
|
|
59
94
|
view === "flow" ? renderFlow(ctx) : renderAgents(ctx))
|
|
60
95
|
.filter((el) => surface.keys || !String(el.key).startsWith("keys")),
|
|
61
96
|
/* the horizontal rule between the key hints and the status line, turning downward with rounded corners at both ends */
|
|
62
|
-
h(Text, cx("dim", dim && "dimmed"), " ╭" + "─".repeat(Math.max(0, columns - 4)) + "╮ "), status,
|
|
63
|
-
card: all.cards.get(dialog.id),
|
|
64
|
-
group: all.groups.find((g) => g.lanes.some((l) => l.status === all.cards.get(dialog.id)?.status))?.title,
|
|
65
|
-
id: dialog.id,
|
|
66
|
-
pred: all.pred.get(dialog.id) ?? [],
|
|
67
|
-
succ: all.succ.get(dialog.id) ?? [],
|
|
68
|
-
tint: (ref) => {
|
|
69
|
-
const c = all.cards.get(ref);
|
|
70
|
-
const tone = c !== undefined ? toneOf(all, c) : "idle";
|
|
71
|
-
return tone === "active" ? palette.accent : tone === "done" ? palette.dim : palette.normal;
|
|
72
|
-
},
|
|
73
|
-
tabs: tabLabels,
|
|
74
|
-
tab: dialogSel,
|
|
75
|
-
first: dialog.first,
|
|
76
|
-
scroll: dialog.scrolls[dialogSel] ?? 0,
|
|
77
|
-
lines: dialogLines,
|
|
78
|
-
columns,
|
|
79
|
-
rows,
|
|
80
|
-
notice
|
|
81
|
-
}) : null, confirm !== null ? renderConfirm(confirm.id, confirm.yes, columns, rows) : null, transfer !== null && transferCard !== undefined ?
|
|
82
|
-
renderTransfer(transfer.id, transferCard.status, transferList, transfer.at, columns, rows) : null, assign !== null ? renderAssign(assign, assignChoices(assign), assignUnknown(assign), columns, rows) : null, prompt !== null ? renderPrompt(prompt, columns, rows, prompt.request !== null ? requestChoices(prompt.request) : []) : null, busy !== null ? renderBusy(busy.label, busy.since, busyTick, columns, rows) : null);
|
|
97
|
+
h(Text, cx("dim", dim && "dimmed"), " ╭" + "─".repeat(Math.max(0, columns - 4)) + "╮ "), status, ...overlays);
|
|
83
98
|
};
|
|
84
99
|
/* pass each pressed SGR mouse report ("ESC[<b;x;y" plus "M"/"m" for press/release, 1-based)
|
|
85
100
|
with its 0-based column/row, read directly from stdin, as Ink 8 drops them before
|
|
@@ -138,8 +153,8 @@ export const useMouseReports = (onPress) => {
|
|
|
138
153
|
}, [stdin]);
|
|
139
154
|
};
|
|
140
155
|
/* the root component of the terminal board */
|
|
141
|
-
const App = ({ log, initial, project, relay }) => {
|
|
142
|
-
const ctx = useBoardState(log, initial, relay);
|
|
156
|
+
const App = ({ log, initial, project, relay, watched }) => {
|
|
157
|
+
const ctx = useBoardState(log, initial, relay, watched);
|
|
143
158
|
/* handle the mouse, but ignore it while the modal busy popup is shown */
|
|
144
159
|
useMouseReports((button, col, row) => {
|
|
145
160
|
if (ctx.busy === null)
|
|
@@ -180,8 +195,8 @@ const lateErasing = (stream) => {
|
|
|
180
195
|
}
|
|
181
196
|
});
|
|
182
197
|
};
|
|
183
|
-
/* run the terminal board until the user quits */
|
|
184
|
-
export const runTUI = async (log, relay) => {
|
|
198
|
+
/* run the terminal board (resp. with a task id, the task viewer) until the user quits */
|
|
199
|
+
export const runTUI = async (log, relay, watched) => {
|
|
185
200
|
const initial = await buildBoard(log);
|
|
186
201
|
const project = Task.projectIdOf(Task.projectRoot());
|
|
187
202
|
loadPalette(log);
|
|
@@ -199,7 +214,7 @@ export const runTUI = async (log, relay) => {
|
|
|
199
214
|
rewrite only the changed lines of a frame, as otherwise every frame
|
|
200
215
|
is erased and redrawn entirely, which flickers in terminals not
|
|
201
216
|
honoring synchronized output (DEC mode 2026), like tmux popups */
|
|
202
|
-
const app = render(h(App, { log, initial, project, relay }), {
|
|
217
|
+
const app = render(h(App, { log, initial, project, relay, watched }), {
|
|
203
218
|
alternateScreen: true, exitOnCtrlC: true, patchConsole: true, kittyKeyboard: { mode: "auto" },
|
|
204
219
|
incrementalRendering: true, stdout: lateErasing(process.stdout)
|
|
205
220
|
});
|