cawdev-cli 1.2.1 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/sonar-analysis.mjs +55 -9
- package/mcp/server.mjs +11 -4
- package/package.json +1 -1
- package/runner/README.md +34 -1
- package/runner/attach.mjs +255 -33
- package/runner/runner.mjs +116 -14
- package/runner/session-cards.mjs +228 -16
- package/runner/split.mjs +35 -10
package/runner/attach.mjs
CHANGED
|
@@ -64,10 +64,10 @@ import { restartNeededFor } from '../lib/runner-config.mjs';
|
|
|
64
64
|
import {
|
|
65
65
|
BOARD_COLUMNS, PROMPT_ROWS, SplitScreen, boardPreviewLines, moveInStrip, readerFrame, stripColumns, tabsLine, visibleSessions, actionsFor, composeFrame,
|
|
66
66
|
batchStartedLines, carriesOnHere, builtNothing, cardLines, columnOf, cardTitleFor, commandWords, wordsText, subtitleFor, agentModelFor, wordWrap, isAutoCheckout, isPullRequest, parseNew, roadmapStrip, sessionTag, titleFor,
|
|
67
|
-
windowLines, showWaitingLine,
|
|
67
|
+
windowLines, showWaitingLine, advancedBlocker,
|
|
68
68
|
} from './split.mjs';
|
|
69
69
|
import {
|
|
70
|
-
CARD_MIN_HEIGHT, cardGrid, cardStatus, nowLine, seedOf, sessionCardLines, stageWalk,
|
|
70
|
+
CARD_MIN_HEIGHT, CLOSE_AFTER_TICKS, cardGrid, cardStatus, nowLine, seedOf, sessionCardLines, stageWalk,
|
|
71
71
|
} from './session-cards.mjs';
|
|
72
72
|
import { KNOWN_AGENTS, agentOfCommand } from '../lib/agents.mjs';
|
|
73
73
|
import { loadPrefs, savePrefs } from './prefs.mjs';
|
|
@@ -94,6 +94,19 @@ import {
|
|
|
94
94
|
// arithmetic import them here.
|
|
95
95
|
export { clip, keyList, keysIn, stripAnsi, visibleWidth, wrap };
|
|
96
96
|
|
|
97
|
+
/** Colored key parts packed into rows of `width`, between keys — none is cut off. */
|
|
98
|
+
function packKeys(parts, width, ink) {
|
|
99
|
+
const sep = ink.muted(' · ');
|
|
100
|
+
const rows = [];
|
|
101
|
+
let row = '';
|
|
102
|
+
for (const part of parts) {
|
|
103
|
+
const next = row ? row + sep + part : part;
|
|
104
|
+
if (row && visibleWidth(next) > width) { rows.push(row); row = part; } else row = next;
|
|
105
|
+
}
|
|
106
|
+
if (row) rows.push(row);
|
|
107
|
+
return rows;
|
|
108
|
+
}
|
|
109
|
+
|
|
97
110
|
const ESC = '\x1b';
|
|
98
111
|
|
|
99
112
|
/** How many times `/review`'s merge is read again (five seconds apart) before it stops following. */
|
|
@@ -663,7 +676,7 @@ export async function attach(argv, { url, ...options } = {}) {
|
|
|
663
676
|
// Spread second so `--attach`'s own wiring wins: there the daemon IS this
|
|
664
677
|
// process and quitting has always stopped it.
|
|
665
678
|
const ui = new Attached(socket, session,
|
|
666
|
-
{ leaveRunning: argv.includes('--leave-running'), ...options });
|
|
679
|
+
{ leaveRunning: argv.includes('--leave-running'), simple: argv.includes('--simple'), ...options });
|
|
667
680
|
await ui.start();
|
|
668
681
|
}
|
|
669
682
|
|
|
@@ -904,6 +917,8 @@ function refOf(word) {
|
|
|
904
917
|
const SPLIT_COMPLETIONS = 6;
|
|
905
918
|
/** How often the cards' rain moves a step (session-cards.mjs `RAIN`): eight a second. */
|
|
906
919
|
const RAIN_STEP_MS = 120;
|
|
920
|
+
/** The cards' clock, from the wall: a tick stamped while the reader was up is still the right tick. */
|
|
921
|
+
const clockTick = () => Math.floor(Date.now() / RAIN_STEP_MS);
|
|
907
922
|
|
|
908
923
|
/** The slash commands, and the one place they are described. */
|
|
909
924
|
const COMMANDS = [
|
|
@@ -929,7 +944,9 @@ const COMMANDS = [
|
|
|
929
944
|
['/refresh', "read the cards and their statuses from cawdev again — advanced mode's roadmap"],
|
|
930
945
|
['/open', "open the session's pull request in your browser — on a finished plan, its card and plan; on one waiting for you or at SHOW, its page on cawdev"],
|
|
931
946
|
['/view', "in advanced mode, a session's log over the whole screen: /view for the focused one, /view 2 for card 2, /view R29 for the session on R29 (v too); a card here in the terminal: /view R29 with no session on it, /view <words of its title> (@project), /view card for the session's; /view alone reads what a session waits on in full (at SHOW, looks and decides), or, outside advanced mode, opens it on cawdev"],
|
|
932
|
-
['/matrix', 'the rain on advanced mode\'s cards,
|
|
947
|
+
['/matrix', 'the rain on advanced mode\'s cards, off by default: /matrix on|off, remembered; the spinner and the robots move either way'],
|
|
948
|
+
['/simple', 'the scrollback terminal (cawdev --simple starts in it); /advanced-mode is the way back; remembered'],
|
|
949
|
+
['/export', "lay the focused session's log into the scrollback so the terminal's own search and copy reach it: /export, /export 3, /export R29"],
|
|
933
950
|
['/shots', 'the screenshots a session waiting at SHOW took: /shots, /shots 2, /shots all, /shots round 1'],
|
|
934
951
|
['/accept', 'accept the screenshots — the pull request opens'],
|
|
935
952
|
['/comments', 'send comments on the screenshots: /comments <text>, #2 for one screenshot'],
|
|
@@ -1153,7 +1170,12 @@ export class Attached {
|
|
|
1153
1170
|
// `/agent`'s choice, from last time — see prefs.mjs.
|
|
1154
1171
|
const prefs = await loadPrefs(this.session.url).catch(() => ({}));
|
|
1155
1172
|
this.agent = prefs.agent ?? null;
|
|
1156
|
-
|
|
1173
|
+
// The rain is off unless asked for (`/matrix on`); the spinner and the
|
|
1174
|
+
// robots move either way.
|
|
1175
|
+
this.matrix = prefs.matrix === true;
|
|
1176
|
+
// Advanced mode is the default where the terminal can draw it; the choice
|
|
1177
|
+
// `/simple` and `/advanced-mode` made last time is remembered.
|
|
1178
|
+
this.screenMode = this.options.simple ? 'simple' : prefs.mode === 'simple' ? 'simple' : 'advanced';
|
|
1157
1179
|
// Raw mode only where there is a terminal to put in it. Through a pipe
|
|
1158
1180
|
// there are no keys, the transcript is the whole output, and that is the
|
|
1159
1181
|
// honest degradation rather than a broken one.
|
|
@@ -1203,7 +1225,13 @@ export class Attached {
|
|
|
1203
1225
|
// `/reload`'s other half: the state the terminal was in before it
|
|
1204
1226
|
// restarted — advanced mode, the focus, the windows of sessions that had
|
|
1205
1227
|
// ended — so a reload changes the code and nothing else.
|
|
1206
|
-
this.
|
|
1228
|
+
const restored = this.options.restore ?? readReloadState();
|
|
1229
|
+
this.restore(restored);
|
|
1230
|
+
// A fresh launch opens in advanced mode wherever the terminal can draw it;
|
|
1231
|
+
// a reload keeps the mode it was in.
|
|
1232
|
+
if (!this.split && !(restored && typeof restored === 'object' && 'advanced' in restored)) {
|
|
1233
|
+
this.openByDefault();
|
|
1234
|
+
}
|
|
1207
1235
|
|
|
1208
1236
|
// One write per tick at most, carrying everything that happened in it. A
|
|
1209
1237
|
// busy session emits hundreds of lines a second, and a write per line means
|
|
@@ -1211,11 +1239,18 @@ export class Attached {
|
|
|
1211
1239
|
const tick = setInterval(() => {
|
|
1212
1240
|
// The cards' rain moves one step every RAIN_STEP_MS, and only while
|
|
1213
1241
|
// somebody can see it: not under the reader, not with `/matrix off`.
|
|
1214
|
-
if (this.split && !this.reader
|
|
1215
|
-
const step =
|
|
1242
|
+
if (this.split && !this.reader) {
|
|
1243
|
+
const step = clockTick();
|
|
1216
1244
|
if (step !== this.rainTick) {
|
|
1217
1245
|
this.rainTick = step;
|
|
1218
1246
|
this.dirty = true;
|
|
1247
|
+
// A landed card closes itself three seconds after the robot waves.
|
|
1248
|
+
for (const meta of this.windows()) {
|
|
1249
|
+
if (meta.merging?.state === 'landed' && step - meta.merging.landedTick >= CLOSE_AFTER_TICKS) {
|
|
1250
|
+
meta.merging = null;
|
|
1251
|
+
this.closeWindowOf(meta);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1219
1254
|
}
|
|
1220
1255
|
}
|
|
1221
1256
|
if (this.dirty || this.pending.length) {
|
|
@@ -1756,9 +1791,19 @@ export class Attached {
|
|
|
1756
1791
|
}
|
|
1757
1792
|
this.interrupting = false;
|
|
1758
1793
|
|
|
1794
|
+
if (this.help && this.mode === 'keys') {
|
|
1795
|
+
// The key table is one window: any of these closes it, nothing else acts.
|
|
1796
|
+
if (key === ESC || key === '?' || key === 'q' || key === '\r') { this.help = false; this.dirty = true; }
|
|
1797
|
+
return undefined;
|
|
1798
|
+
}
|
|
1759
1799
|
if (this.reader && this.mode === 'keys') {
|
|
1760
1800
|
return this.onReaderKey(key);
|
|
1761
1801
|
}
|
|
1802
|
+
if (this.split && key === '?' && this.mode === 'keys' && !this.select) {
|
|
1803
|
+
this.help = true;
|
|
1804
|
+
this.dirty = true;
|
|
1805
|
+
return undefined;
|
|
1806
|
+
}
|
|
1762
1807
|
if (this.mode === 'typing') {
|
|
1763
1808
|
return this.onTyping(key);
|
|
1764
1809
|
}
|
|
@@ -1783,6 +1828,14 @@ export class Attached {
|
|
|
1783
1828
|
if (['\r', '\n', 'i', 'd', 'x'].includes(key) && this.refuseWhileMerging()) {
|
|
1784
1829
|
return undefined;
|
|
1785
1830
|
}
|
|
1831
|
+
// `n` still refuses a permission request the focused card holds: that act came first.
|
|
1832
|
+
if (this.split && (key === 'n' || key === 'N') && this.mode === 'keys' && !this.split.boardFocus
|
|
1833
|
+
&& !this.pendingOn(this.current())) {
|
|
1834
|
+
return this.focusNeedsYou(key === 'n' ? 1 : -1);
|
|
1835
|
+
}
|
|
1836
|
+
if (this.split && key === 'w') {
|
|
1837
|
+
return this.closeWindow();
|
|
1838
|
+
}
|
|
1786
1839
|
if (this.split && key === 'v') {
|
|
1787
1840
|
// A card's log — `/view`'s, over the whole screen.
|
|
1788
1841
|
const meta = this.commandTarget();
|
|
@@ -1915,6 +1968,7 @@ export class Attached {
|
|
|
1915
1968
|
closeEverything() {
|
|
1916
1969
|
const open = this.mode !== 'keys' || Boolean(this.reader) || Boolean(this.split?.boardFocus);
|
|
1917
1970
|
this.reader = null;
|
|
1971
|
+
this.help = false;
|
|
1918
1972
|
if (this.split?.boardFocus) {
|
|
1919
1973
|
this.split.lastBoardFocus = this.split.boardFocus;
|
|
1920
1974
|
this.split.boardFocus = null;
|
|
@@ -3054,10 +3108,15 @@ export class Attached {
|
|
|
3054
3108
|
case 'log':
|
|
3055
3109
|
this.showLog = !this.showLog;
|
|
3056
3110
|
return this.note(this.showLog ? "printing the daemon's log" : 'printing the session');
|
|
3111
|
+
case 'simple':
|
|
3112
|
+
return void this.chooseMode('simple');
|
|
3113
|
+
case 'export':
|
|
3114
|
+
return this.exportLog(rest[0]);
|
|
3057
3115
|
case 'advanced-mode':
|
|
3058
3116
|
case 'advanced': {
|
|
3059
3117
|
const want = rest[0] === 'off' ? false : rest[0] === 'on' ? true : !this.split;
|
|
3060
|
-
if (!want) return this.
|
|
3118
|
+
if (!want) return void this.chooseMode('simple');
|
|
3119
|
+
void savePrefs(this.session.url, { mode: null }).catch(() => undefined);
|
|
3061
3120
|
// `@slug` names the projects outright; otherwise they are asked.
|
|
3062
3121
|
const named = rest.filter((word) => word.startsWith('@')).map((word) => word.slice(1));
|
|
3063
3122
|
return named.length ? this.enterSplit(named) : this.askSplitProjects();
|
|
@@ -3109,7 +3168,7 @@ export class Attached {
|
|
|
3109
3168
|
case 'drop':
|
|
3110
3169
|
return void this.dropCheckout();
|
|
3111
3170
|
case 'close':
|
|
3112
|
-
return this.closeWindow();
|
|
3171
|
+
return this.closeWindow(rest[0]);
|
|
3113
3172
|
case 'reload':
|
|
3114
3173
|
return void this.reload();
|
|
3115
3174
|
case 'quit':
|
|
@@ -3239,6 +3298,7 @@ export class Attached {
|
|
|
3239
3298
|
for (const meta of this.sessions.values()) {
|
|
3240
3299
|
if (!meta.ended && !live.has(meta.id)) {
|
|
3241
3300
|
meta.ended = true;
|
|
3301
|
+
meta.changedTick = this.stamp();
|
|
3242
3302
|
void this.readView(meta, 5);
|
|
3243
3303
|
}
|
|
3244
3304
|
}
|
|
@@ -3499,12 +3559,13 @@ export class Attached {
|
|
|
3499
3559
|
* said meanwhile is laid into it now — or, if the focus moved, that
|
|
3500
3560
|
* session's history, which is what switching sessions has always done.
|
|
3501
3561
|
*/
|
|
3502
|
-
leaveSplit() {
|
|
3562
|
+
leaveSplit({ quiet = false } = {}) {
|
|
3503
3563
|
if (!this.split) return this.note('advanced mode is not on — /advanced-mode starts it');
|
|
3504
3564
|
const { screen, enteredWatching, mark, plannedTimer } = this.split;
|
|
3505
3565
|
clearInterval(plannedTimer);
|
|
3506
3566
|
this.split = null;
|
|
3507
3567
|
screen.leave();
|
|
3568
|
+
if (quiet) return undefined;
|
|
3508
3569
|
const watched = this.watching;
|
|
3509
3570
|
const lines = this.lines.get(watched) ?? [];
|
|
3510
3571
|
if (watched && watched === enteredWatching) {
|
|
@@ -3515,7 +3576,7 @@ export class Attached {
|
|
|
3515
3576
|
this.printed.delete(watched);
|
|
3516
3577
|
this.layHistoryIn(watched, lines);
|
|
3517
3578
|
}
|
|
3518
|
-
this.note('back to the scrollback');
|
|
3579
|
+
this.note('back to the scrollback · /advanced-mode returns');
|
|
3519
3580
|
return undefined;
|
|
3520
3581
|
}
|
|
3521
3582
|
|
|
@@ -3529,6 +3590,32 @@ export class Attached {
|
|
|
3529
3590
|
this.note('');
|
|
3530
3591
|
}
|
|
3531
3592
|
|
|
3593
|
+
/** The tick to stamp a change with: the wall clock's once the cards' clock runs (tests, which never start it, stay at 0). */
|
|
3594
|
+
stamp() {
|
|
3595
|
+
return this.rainTick === undefined ? 0 : clockTick();
|
|
3596
|
+
}
|
|
3597
|
+
|
|
3598
|
+
/** The sessions whose card waits on a person, in card order. */
|
|
3599
|
+
needYou() {
|
|
3600
|
+
return this.windows().filter((meta) => {
|
|
3601
|
+
const run = this.runs.find((each) => each.id === meta.id);
|
|
3602
|
+
return Boolean(run && (this.showOn(run) || this.pendingOn(run) || this.askingOn(run)));
|
|
3603
|
+
});
|
|
3604
|
+
}
|
|
3605
|
+
|
|
3606
|
+
/** `n` / `N`: focus moves to the next / previous card that needs you. */
|
|
3607
|
+
focusNeedsYou(step) {
|
|
3608
|
+
const waiting = this.needYou();
|
|
3609
|
+
if (!waiting.length) return this.note('nothing needs you');
|
|
3610
|
+
const windows = this.windows();
|
|
3611
|
+
const from = windows.findIndex((meta) => meta.id === this.split.focus);
|
|
3612
|
+
for (let i = 1; i <= windows.length; i++) {
|
|
3613
|
+
const meta = windows[(from + step * i + windows.length * i) % windows.length];
|
|
3614
|
+
if (waiting.includes(meta)) return this.focusWindow(meta.id);
|
|
3615
|
+
}
|
|
3616
|
+
return undefined;
|
|
3617
|
+
}
|
|
3618
|
+
|
|
3532
3619
|
cycleFocus(step) {
|
|
3533
3620
|
if (this.split?.boardFocus) this.leaveBoard();
|
|
3534
3621
|
const windows = this.windows();
|
|
@@ -3704,8 +3791,13 @@ export class Attached {
|
|
|
3704
3791
|
const subtitle = gate
|
|
3705
3792
|
? ink.warn(showWaitingLine(gate, gate.ids.length))
|
|
3706
3793
|
: (who ? `${ink.strong(who)} ` : '') + ink.muted(subtitleFor(meta, room - (who ? who.length + 1 : 0)));
|
|
3794
|
+
const tick = this.rainTick ?? 0;
|
|
3795
|
+
const status = cardStatus(meta, waiting, now, tick);
|
|
3796
|
+
// A status that changes is drawn bright for a few ticks: when its key moves.
|
|
3797
|
+
if (meta.lastKey !== undefined && meta.lastKey !== status.key) meta.changedTick = tick;
|
|
3798
|
+
meta.lastKey = status.key;
|
|
3707
3799
|
const progress = !meta.ended
|
|
3708
|
-
? stageWalk(meta.stages, meta.stage, room, ink)
|
|
3800
|
+
? stageWalk(meta.stages, meta.stage, room, ink, tick)
|
|
3709
3801
|
: meta.commits !== undefined && (view.profile ?? 'CODE') === 'CODE' && view.state === 'FINISHED'
|
|
3710
3802
|
? ink.muted(`${meta.commits} commit${meta.commits === 1 ? '' : 's'}${view.branch ? ` on ${view.branch}` : ''}`)
|
|
3711
3803
|
: '';
|
|
@@ -3718,6 +3810,26 @@ export class Attached {
|
|
|
3718
3810
|
: asking?.yours ? 'a answers · /view reads it whole'
|
|
3719
3811
|
: shownGate ? 'a to look and decide' : actionsFor(meta);
|
|
3720
3812
|
const merging = meta.merging;
|
|
3813
|
+
if (merging?.state === 'landed') {
|
|
3814
|
+
const left = Math.max(0, Math.ceil((CLOSE_AFTER_TICKS - (tick - merging.landedTick)) * RAIN_STEP_MS / 1000));
|
|
3815
|
+
return {
|
|
3816
|
+
n,
|
|
3817
|
+
profile: view.profile ?? meta.profile ?? null,
|
|
3818
|
+
title: `${merging.ref} ${view.entryTitle ?? view.label ?? meta.label ?? ''}`.trim(),
|
|
3819
|
+
status: { key: 'landed', mark: '✓', word: `merged · closes in ${left}s` },
|
|
3820
|
+
lines: [
|
|
3821
|
+
ink.muted(`landed on ${merging.base}${merging.sha ? ` as ${merging.sha}` : ''} · the card is MERGED`),
|
|
3822
|
+
],
|
|
3823
|
+
footer: '/open the merge · /close now',
|
|
3824
|
+
focused: meta.id === this.split.focus,
|
|
3825
|
+
seed: seedOf(meta.id),
|
|
3826
|
+
tick,
|
|
3827
|
+
since: tick - merging.landedTick,
|
|
3828
|
+
changedAt: merging.landedTick,
|
|
3829
|
+
rain: false,
|
|
3830
|
+
base: merging.base,
|
|
3831
|
+
};
|
|
3832
|
+
}
|
|
3721
3833
|
if (merging) {
|
|
3722
3834
|
const failed = merging.state === 'failed';
|
|
3723
3835
|
const resolving = merging.state === 'resolving';
|
|
@@ -3738,8 +3850,8 @@ export class Attached {
|
|
|
3738
3850
|
: resolving ? `its development starts again here to resolve it` : '',
|
|
3739
3851
|
focused: meta.id === this.split.focus,
|
|
3740
3852
|
seed: seedOf(meta.id),
|
|
3741
|
-
tick
|
|
3742
|
-
rain: this.matrix
|
|
3853
|
+
tick,
|
|
3854
|
+
rain: this.matrix === true,
|
|
3743
3855
|
base: merging.base,
|
|
3744
3856
|
};
|
|
3745
3857
|
}
|
|
@@ -3749,20 +3861,59 @@ export class Attached {
|
|
|
3749
3861
|
title: view.entryRef
|
|
3750
3862
|
? `${view.entryRef} ${view.entryTitle ?? view.label ?? meta.label ?? ''}`.trim()
|
|
3751
3863
|
: (view.label ?? meta.label ?? 'a session'),
|
|
3752
|
-
status
|
|
3864
|
+
status,
|
|
3753
3865
|
lines: [subtitle, progress || null, doing ? (pending || asking ? doing : ink.text(doing)) : null],
|
|
3754
3866
|
footer: keys,
|
|
3755
3867
|
focused: !this.split.boardFocus && meta.id === this.split.focus,
|
|
3756
3868
|
seed: seedOf(meta.id),
|
|
3757
|
-
tick
|
|
3758
|
-
|
|
3869
|
+
tick,
|
|
3870
|
+
since: meta.ended && Number.isFinite(meta.changedTick) ? tick - meta.changedTick : undefined,
|
|
3871
|
+
changedAt: meta.changedTick,
|
|
3872
|
+
ref: view.entryRef ?? null,
|
|
3873
|
+
rain: this.matrix === true,
|
|
3759
3874
|
};
|
|
3760
3875
|
}
|
|
3761
3876
|
|
|
3877
|
+
/** `?` — every key and command in one window, two columns. */
|
|
3878
|
+
helpScreen(width, height) {
|
|
3879
|
+
const ink = this.ink;
|
|
3880
|
+
const keys = [
|
|
3881
|
+
['enter', 'prompt the focused card'], ['tab ⇧tab 1–9', 'next / previous / pick a card'],
|
|
3882
|
+
['n N', 'next / previous card needing you'], ['v', 'its log'],
|
|
3883
|
+
['b', 'the board: ←→ ↑↓ p d esc'], ['↑↓ PgUp PgDn G', 'scroll the log, G follows the end'],
|
|
3884
|
+
['ctrl+f', 'find in the log'], ['r', 'fold the roadmap'], ['d', 'develop a planned card'],
|
|
3885
|
+
['a y s Y', 'answer · permission'], ['x x', 'cancel the running session'],
|
|
3886
|
+
['w', 'close the ended card'], ['c', 'config'], ['L', 'runs'], ['?', 'this window'], ['q', 'quit'],
|
|
3887
|
+
];
|
|
3888
|
+
const commands = [
|
|
3889
|
+
['/new', 'plan · dev · ask'], ['/view v', 'read a session'], ['/board b', 'the roadmap'],
|
|
3890
|
+
['/continue /pr', 'carry on · open the PR'], ['/open', 'on cawdev'], ['/export', 'log to scrollback'],
|
|
3891
|
+
['/simple', 'the scrollback terminal'], ['/matrix', 'the rain, off by default'],
|
|
3892
|
+
['/projects /refresh', 'the roadmap'], ['/agent', 'which agent'], ['/quit q', 'leave'],
|
|
3893
|
+
['/close N|all', 'close ended cards'],
|
|
3894
|
+
];
|
|
3895
|
+
const half = Math.max(20, Math.floor((width - 4) / 2));
|
|
3896
|
+
const cell = (pair, w) => {
|
|
3897
|
+
if (!pair) return ' '.repeat(w);
|
|
3898
|
+
const key = ink.accent(pair[0]);
|
|
3899
|
+
const gap = Math.max(1, 16 - visibleWidth(pair[0]));
|
|
3900
|
+
return padVisible(clip(`${key}${' '.repeat(gap)}${ink.muted(pair[1])}`, w), w);
|
|
3901
|
+
};
|
|
3902
|
+
const body = [ink.bold(padVisible(' keys', half)) + ink.bold(' commands')];
|
|
3903
|
+
for (let i = 0; i < Math.max(keys.length, commands.length); i++) {
|
|
3904
|
+
body.push(` ${cell(keys[i], half - 1)}${cell(commands[i], half - 1)}`);
|
|
3905
|
+
}
|
|
3906
|
+
body.push('', ink.muted(' the key line at the bottom shows what fits · this is all of it · esc closes'));
|
|
3907
|
+
return windowLines({ width, height }, { title: 'cawdev ▸ keys', body, focused: true }, ink);
|
|
3908
|
+
}
|
|
3909
|
+
|
|
3762
3910
|
/** The whole alternate screen, as rows — see split.mjs. */
|
|
3763
3911
|
splitFrame() {
|
|
3764
3912
|
const { width, height } = this.split.screen;
|
|
3765
3913
|
const ink = this.ink;
|
|
3914
|
+
if (this.help && this.mode === 'keys') {
|
|
3915
|
+
return composeFrame(width, height, [{ rect: { top: 0, left: 0, width, height }, lines: this.helpScreen(width, height) }]);
|
|
3916
|
+
}
|
|
3766
3917
|
if (this.reader) {
|
|
3767
3918
|
return composeFrame(width, height, [{ rect: { top: 0, left: 0, width, height }, lines: this.readerScreen(width, height) }]);
|
|
3768
3919
|
}
|
|
@@ -3781,8 +3932,8 @@ export class Attached {
|
|
|
3781
3932
|
const input = this.mode === 'typing'
|
|
3782
3933
|
? `${tag}${this.input.label} ${this.input.line.render(
|
|
3783
3934
|
Math.max(8, inner - visibleWidth(tag) - visibleWidth(this.input.label) - 1), ink)}`
|
|
3784
|
-
|
|
3785
|
-
|
|
3935
|
+
// Only what enter acts on: the keys live once, in the window's footer.
|
|
3936
|
+
: [`${tag}${ink.muted(`enter prompts ${current ? `“${current.label}”` : 'the focused session'}`)}`];
|
|
3786
3937
|
// A question or a request on screen says which window it came from.
|
|
3787
3938
|
const from = banner.length ? [ink.bold(ink.warn(` from ${this.tagOf(current)}`))] : [];
|
|
3788
3939
|
// Under the banner and right above the line, so a long one never pushes
|
|
@@ -3799,8 +3950,12 @@ export class Attached {
|
|
|
3799
3950
|
const completing = completionLines(
|
|
3800
3951
|
matches.rows.slice(first, first + SPLIT_COMPLETIONS), matches.at - first, inner, ink,
|
|
3801
3952
|
);
|
|
3802
|
-
const fixed = [...from, ...banner, ...whole, ...overlay, ...completing, input];
|
|
3803
|
-
|
|
3953
|
+
const fixed = [...from, ...banner, ...whole, ...overlay, ...completing, ...[input].flat()];
|
|
3954
|
+
// The window's key footer wraps between keys, so the prompt grows with it.
|
|
3955
|
+
const footerRows = this.status
|
|
3956
|
+
? [ink.warn(`${this.statusWindow ? `[${this.statusWindow}] ` : ''}${this.status}`)]
|
|
3957
|
+
: packKeys(this.keys(), inner, ink);
|
|
3958
|
+
const promptRows = Math.max(PROMPT_ROWS, fixed.length + 4 + footerRows.length);
|
|
3804
3959
|
// The roadmap across the top: as tall as its fullest column needs, never
|
|
3805
3960
|
// more than a third of the screen, and never so tall that no window fits
|
|
3806
3961
|
// under it — a short terminal drops it before it drops the sessions.
|
|
@@ -3893,7 +4048,8 @@ export class Attached {
|
|
|
3893
4048
|
});
|
|
3894
4049
|
const promptTitle = `cawdev ▸ ${this.runner?.name ?? 'runner'}`
|
|
3895
4050
|
+ `${this.session.email ? ` · ${this.session.email}` : ' · not signed in'}`
|
|
3896
|
-
+ `${this.agent ? ` · ${this.agent}` : ''}${unseen}
|
|
4051
|
+
+ `${this.agent ? ` · ${this.agent}` : ''}${unseen}`
|
|
4052
|
+
+ (this.needYou().length ? ` · ${ink.warn(`${this.needYou().length} need you`)}` : '');
|
|
3897
4053
|
pieces.push({
|
|
3898
4054
|
rect: layout.prompt,
|
|
3899
4055
|
lines: windowLines(layout.prompt, {
|
|
@@ -3906,9 +4062,7 @@ export class Attached {
|
|
|
3906
4062
|
...messages,
|
|
3907
4063
|
...fixed,
|
|
3908
4064
|
],
|
|
3909
|
-
footer:
|
|
3910
|
-
? ink.warn(`${this.statusWindow ? `[${this.statusWindow}] ` : ''}${this.status}`)
|
|
3911
|
-
: keyList(this.keys(), '', inner, ink),
|
|
4065
|
+
footer: footerRows,
|
|
3912
4066
|
}, ink),
|
|
3913
4067
|
});
|
|
3914
4068
|
if (strip) {
|
|
@@ -5139,8 +5293,14 @@ export class Attached {
|
|
|
5139
5293
|
const merge = card?.workItem?.merge;
|
|
5140
5294
|
if (card?.status === 'MERGED' || card?.status === 'SHIPPED' || merge?.state === 'DONE') {
|
|
5141
5295
|
this.say(this.ink.success(` ${review.ref} landed: ${review.branch} is in ${review.base}${merge?.result ? ` (${printable(merge.result)})` : ''}`));
|
|
5142
|
-
// Landed: the card's work is over, and so are its cards here.
|
|
5296
|
+
// Landed: the card's work is over, and so are its cards here. In
|
|
5297
|
+
// advanced mode the robot waves first and the card closes itself.
|
|
5298
|
+
const sha = /\b[0-9a-f]{7,40}\b/.exec(String(merge?.result ?? ''))?.[0]?.slice(0, 7) ?? null;
|
|
5143
5299
|
for (const meta of this.mergingCards(review)) {
|
|
5300
|
+
if (this.split && meta.ended) {
|
|
5301
|
+
meta.merging = { state: 'landed', ref: review.ref, branch: review.branch, base: review.base, sha, landedTick: this.stamp() };
|
|
5302
|
+
continue;
|
|
5303
|
+
}
|
|
5144
5304
|
meta.merging = null;
|
|
5145
5305
|
if (meta.ended) this.closeWindowOf(meta);
|
|
5146
5306
|
}
|
|
@@ -5219,12 +5379,12 @@ export class Attached {
|
|
|
5219
5379
|
async chooseMatrix(word) {
|
|
5220
5380
|
const wanted = String(word ?? '').toLowerCase();
|
|
5221
5381
|
if (!['on', 'off'].includes(wanted)) {
|
|
5222
|
-
return this.note(`the rain is ${this.matrix
|
|
5382
|
+
return this.note(`the rain is ${this.matrix ? 'on' : 'off'} — /matrix on|off`);
|
|
5223
5383
|
}
|
|
5224
5384
|
this.matrix = wanted === 'on';
|
|
5225
5385
|
this.dirty = true;
|
|
5226
|
-
await savePrefs(this.session.url, { matrix: this.matrix ?
|
|
5227
|
-
return this.note(this.matrix ? 'the cards rain
|
|
5386
|
+
await savePrefs(this.session.url, { matrix: this.matrix ? true : null }).catch(() => undefined);
|
|
5387
|
+
return this.note(this.matrix ? 'the cards rain' : 'no rain — the spinners and the robots still move');
|
|
5228
5388
|
}
|
|
5229
5389
|
|
|
5230
5390
|
openReader() {
|
|
@@ -5552,14 +5712,64 @@ export class Attached {
|
|
|
5552
5712
|
}
|
|
5553
5713
|
|
|
5554
5714
|
/** `/close` — an ended session's window goes; a live one's stays. */
|
|
5555
|
-
closeWindow() {
|
|
5556
|
-
|
|
5715
|
+
closeWindow(which = null) {
|
|
5716
|
+
// `/close now` is the landed card's own footer: the focused card, without the countdown.
|
|
5717
|
+
const word = String(which ?? '').toLowerCase().replace(/^now$/, '');
|
|
5718
|
+
if (word === 'all') {
|
|
5719
|
+
const ended = this.windows().filter((meta) => meta.ended);
|
|
5720
|
+
for (const meta of ended) this.closeWindowOf(meta);
|
|
5721
|
+
return this.note(ended.length ? `closed ${ended.length} ended card${ended.length === 1 ? '' : 's'}` : 'no ended card to close');
|
|
5722
|
+
}
|
|
5723
|
+
const meta = word && this.split ? this.windowNamed([word]) : this.commandTarget();
|
|
5724
|
+
if (word && this.split && !meta) return this.note(`no card ${word} — /close, /close 3 or /close all`);
|
|
5557
5725
|
if (!meta) return this.note('no window to close');
|
|
5558
|
-
if (!meta.ended) return this.note('that session is still running
|
|
5726
|
+
if (!meta.ended) return this.note('that session is still running · x x cancels it');
|
|
5559
5727
|
this.closeWindowOf(meta);
|
|
5560
5728
|
return this.note('closed');
|
|
5561
5729
|
}
|
|
5562
5730
|
|
|
5731
|
+
/** `/simple` and `/advanced-mode off`: the scrollback, remembered for next time. */
|
|
5732
|
+
async chooseMode(mode) {
|
|
5733
|
+
this.screenMode = mode;
|
|
5734
|
+
// Left first and remembered after: the next command must find it left.
|
|
5735
|
+
const left = this.split ? this.leaveSplit() : this.note('already the scrollback — /advanced-mode is the way back');
|
|
5736
|
+
await savePrefs(this.session.url, { mode: 'simple' }).catch(() => undefined);
|
|
5737
|
+
return left;
|
|
5738
|
+
}
|
|
5739
|
+
|
|
5740
|
+
/**
|
|
5741
|
+
* A fresh launch: advanced mode where this terminal can draw it, else the
|
|
5742
|
+
* scrollback with one line saying why. `--simple` and a remembered `/simple`
|
|
5743
|
+
* are the person's own choice and need no line.
|
|
5744
|
+
*/
|
|
5745
|
+
openByDefault() {
|
|
5746
|
+
if (this.screenMode === 'simple' || this.stopped) return;
|
|
5747
|
+
const out = this.options.out ?? process.stdout;
|
|
5748
|
+
const why = advancedBlocker({
|
|
5749
|
+
tty: !this.plain && Boolean(process.stdin.isTTY), term: process.env.TERM, columns: out.columns, rows: out.rows,
|
|
5750
|
+
});
|
|
5751
|
+
if (why) {
|
|
5752
|
+
this.say(this.ink.muted(` the scrollback, not advanced mode: ${why}`));
|
|
5753
|
+
return;
|
|
5754
|
+
}
|
|
5755
|
+
this.enterSplit(null);
|
|
5756
|
+
}
|
|
5757
|
+
|
|
5758
|
+
/**
|
|
5759
|
+
* `/export`: a session's log laid into the normal screen's scrollback, so
|
|
5760
|
+
* the terminal's own search and copy reach it. Leaves advanced mode's screen
|
|
5761
|
+
* for the scrollback and says how to come back.
|
|
5762
|
+
*/
|
|
5763
|
+
exportLog(which = null) {
|
|
5764
|
+
const meta = which && this.split ? this.windowNamed([which]) : this.commandTarget();
|
|
5765
|
+
if (!meta) return this.note(which ? `no session ${which} here` : 'no session to export');
|
|
5766
|
+
const lines = this.lines.get(meta.id) ?? [];
|
|
5767
|
+
if (this.split) this.leaveSplit({ quiet: true });
|
|
5768
|
+
this.printed.delete(meta.id);
|
|
5769
|
+
this.layHistoryIn(meta.id, lines);
|
|
5770
|
+
return this.note('exported — /advanced-mode is the way back');
|
|
5771
|
+
}
|
|
5772
|
+
|
|
5563
5773
|
/** An ended session's window goes; focus moves to the first one left. */
|
|
5564
5774
|
closeWindowOf(meta) {
|
|
5565
5775
|
this.closed.add(meta.id);
|
|
@@ -6185,8 +6395,17 @@ export class Attached {
|
|
|
6185
6395
|
// lands on a buffer that vanishes the moment the program exits.
|
|
6186
6396
|
if (this.split) {
|
|
6187
6397
|
clearInterval(this.split.plannedTimer);
|
|
6398
|
+
const { enteredWatching, mark } = this.split;
|
|
6188
6399
|
this.split.screen.release();
|
|
6189
6400
|
this.split = null;
|
|
6401
|
+
// What the watched session said while the screen was up goes into the
|
|
6402
|
+
// scrollback, as `leaveSplit` does: the terminal's own search reaches it.
|
|
6403
|
+
const watched = this.watching;
|
|
6404
|
+
if (watched && watched === enteredWatching) {
|
|
6405
|
+
for (const line of (this.lines.get(watched) ?? []).slice(mark.get(watched) ?? 0)) {
|
|
6406
|
+
this.say(this.render(line));
|
|
6407
|
+
}
|
|
6408
|
+
}
|
|
6190
6409
|
}
|
|
6191
6410
|
try {
|
|
6192
6411
|
this.client?.destroy();
|
|
@@ -6505,6 +6724,7 @@ export class Attached {
|
|
|
6505
6724
|
const parts = [
|
|
6506
6725
|
`${ink.text('enter')} ${ink.muted('prompt')}`,
|
|
6507
6726
|
`${ink.text('/')} ${ink.muted('commands')}`,
|
|
6727
|
+
...(this.split ? [`${ink.text('?')} ${ink.muted('help')}`] : []),
|
|
6508
6728
|
`${ink.text('L')} ${ink.muted('runs')}`,
|
|
6509
6729
|
];
|
|
6510
6730
|
if (this.askingOn(this.current())?.yours) {
|
|
@@ -6521,6 +6741,8 @@ export class Attached {
|
|
|
6521
6741
|
parts.push(`${ink.text('b')} ${ink.muted('board')}`);
|
|
6522
6742
|
parts.push(`${ink.text('tab')} ${ink.muted('next card')}`);
|
|
6523
6743
|
parts.push(`${ink.text('v')} ${ink.muted('its log')}`);
|
|
6744
|
+
if (this.needYou().length) parts.push(`${ink.warn('n')} ${ink.muted('next needing you')}`);
|
|
6745
|
+
if (this.commandTarget()?.ended) parts.push(`${ink.text('w')} ${ink.muted('close card')}`);
|
|
6524
6746
|
parts.push(`${ink.text('d')} ${ink.muted(this.finishedPlan(this.commandTarget())
|
|
6525
6747
|
? `develop ${this.commandTarget().view.entryRef}` : 'develop a planned card')}`);
|
|
6526
6748
|
}
|