aoaoe 0.64.0 → 0.66.0
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/config.js +3 -1
- package/dist/index.js +35 -1
- package/dist/input.d.ts +8 -0
- package/dist/input.js +67 -2
- package/dist/message.d.ts +13 -0
- package/dist/message.js +17 -0
- package/dist/tui.d.ts +16 -1
- package/dist/tui.js +91 -11
- package/package.json +1 -1
package/dist/config.js
CHANGED
|
@@ -592,6 +592,7 @@ example config:
|
|
|
592
592
|
interactive commands (while daemon is running):
|
|
593
593
|
/help show available commands
|
|
594
594
|
/explain ask the AI to explain what's happening in plain English
|
|
595
|
+
/insist <msg> interrupt + deliver message immediately (skip queue)
|
|
595
596
|
/status request daemon status
|
|
596
597
|
/dashboard request full dashboard output
|
|
597
598
|
/pause pause the daemon
|
|
@@ -600,6 +601,7 @@ interactive commands (while daemon is running):
|
|
|
600
601
|
/verbose toggle verbose logging
|
|
601
602
|
/clear clear the screen
|
|
602
603
|
ESC ESC interrupt the current reasoner (shortcut)
|
|
603
|
-
|
|
604
|
+
!message insist shortcut — same as /insist message
|
|
605
|
+
(anything) send a message to the AI — queued for next cycle`);
|
|
604
606
|
}
|
|
605
607
|
//# sourceMappingURL=config.js.map
|
package/dist/index.js
CHANGED
|
@@ -12,7 +12,7 @@ import { loadGlobalContext, resolveProjectDirWithSource, discoverContextFiles, l
|
|
|
12
12
|
import { tick as loopTick } from "./loop.js";
|
|
13
13
|
import { exec as shellExec } from "./shell.js";
|
|
14
14
|
import { wakeableSleep } from "./wake.js";
|
|
15
|
-
import { classifyMessages, formatUserMessages, buildReceipts, shouldSkipSleep, hasPendingFile } from "./message.js";
|
|
15
|
+
import { classifyMessages, formatUserMessages, buildReceipts, shouldSkipSleep, hasPendingFile, isInsistMessage, stripInsistPrefix } from "./message.js";
|
|
16
16
|
import { TaskManager, loadTaskDefinitions, loadTaskState, formatTaskTable } from "./task-manager.js";
|
|
17
17
|
import { runTaskCli, handleTaskSlashCommand } from "./task-cli.js";
|
|
18
18
|
import { TUI } from "./tui.js";
|
|
@@ -235,6 +235,29 @@ async function main() {
|
|
|
235
235
|
// start interactive input listener and conversation log
|
|
236
236
|
input.start();
|
|
237
237
|
await reasonerConsole.start();
|
|
238
|
+
// wire scroll keys to TUI (PgUp/PgDn/Home/End)
|
|
239
|
+
if (tui) {
|
|
240
|
+
input.onScroll((dir) => {
|
|
241
|
+
switch (dir) {
|
|
242
|
+
case "up":
|
|
243
|
+
tui.scrollUp();
|
|
244
|
+
break;
|
|
245
|
+
case "down":
|
|
246
|
+
tui.scrollDown();
|
|
247
|
+
break;
|
|
248
|
+
case "top":
|
|
249
|
+
tui.scrollToTop();
|
|
250
|
+
break;
|
|
251
|
+
case "bottom":
|
|
252
|
+
tui.scrollToBottom();
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
// wire queue count changes to TUI prompt display
|
|
257
|
+
input.onQueueChange((count) => {
|
|
258
|
+
tui.updateState({ pendingCount: count });
|
|
259
|
+
});
|
|
260
|
+
}
|
|
238
261
|
// start TUI (alternate screen buffer) after input is ready
|
|
239
262
|
if (tui) {
|
|
240
263
|
// replay persisted history from previous runs before entering alt screen
|
|
@@ -374,6 +397,17 @@ async function main() {
|
|
|
374
397
|
const allMessages = [...stdinMessages, ...consoleMessages];
|
|
375
398
|
// classify into commands vs. real user messages
|
|
376
399
|
const { commands, userMessages } = classifyMessages(allMessages);
|
|
400
|
+
// strip insist prefix from priority messages and log them distinctly
|
|
401
|
+
for (let i = 0; i < userMessages.length; i++) {
|
|
402
|
+
if (isInsistMessage(userMessages[i])) {
|
|
403
|
+
const raw = stripInsistPrefix(userMessages[i]);
|
|
404
|
+
userMessages[i] = raw;
|
|
405
|
+
if (tui)
|
|
406
|
+
tui.log("you", `! ${raw}`);
|
|
407
|
+
else
|
|
408
|
+
log(`[insist] ${raw}`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
377
411
|
// auto-explain on first tick: inject an explain prompt so the AI introduces itself
|
|
378
412
|
if (autoExplainPending && pollCount === 1) {
|
|
379
413
|
const autoExplainPrompt = "This is your first observation. Please briefly introduce what you see: " +
|
package/dist/input.d.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
|
+
export type ScrollDirection = "up" | "down" | "top" | "bottom";
|
|
2
|
+
export declare const INSIST_PREFIX = "__INSIST__";
|
|
1
3
|
export declare class InputReader {
|
|
2
4
|
private rl;
|
|
3
5
|
private queue;
|
|
4
6
|
private paused;
|
|
5
7
|
private lastEscTime;
|
|
8
|
+
private scrollHandler;
|
|
9
|
+
private queueChangeHandler;
|
|
10
|
+
onScroll(handler: (dir: ScrollDirection) => void): void;
|
|
11
|
+
onQueueChange(handler: (count: number) => void): void;
|
|
12
|
+
private notifyQueueChange;
|
|
6
13
|
start(): void;
|
|
7
14
|
drain(): string[];
|
|
8
15
|
isPaused(): boolean;
|
|
@@ -11,6 +18,7 @@ export declare class InputReader {
|
|
|
11
18
|
prompt(): void;
|
|
12
19
|
stop(): void;
|
|
13
20
|
private handleEscInterrupt;
|
|
21
|
+
private handleInsist;
|
|
14
22
|
private handleLine;
|
|
15
23
|
private handleCommand;
|
|
16
24
|
}
|
package/dist/input.js
CHANGED
|
@@ -6,11 +6,25 @@ import { requestInterrupt } from "./daemon-state.js";
|
|
|
6
6
|
import { GREEN, DIM, YELLOW, RED, BOLD, RESET } from "./colors.js";
|
|
7
7
|
// ESC-ESC interrupt detection
|
|
8
8
|
const ESC_DOUBLE_TAP_MS = 500;
|
|
9
|
+
export const INSIST_PREFIX = "__INSIST__";
|
|
9
10
|
export class InputReader {
|
|
10
11
|
rl = null;
|
|
11
12
|
queue = []; // pending user messages for the reasoner
|
|
12
13
|
paused = false;
|
|
13
14
|
lastEscTime = 0;
|
|
15
|
+
scrollHandler = null;
|
|
16
|
+
queueChangeHandler = null;
|
|
17
|
+
// register a callback for scroll key events (PgUp/PgDn/Home/End)
|
|
18
|
+
onScroll(handler) {
|
|
19
|
+
this.scrollHandler = handler;
|
|
20
|
+
}
|
|
21
|
+
// register a callback for queue size changes (for TUI pending count display)
|
|
22
|
+
onQueueChange(handler) {
|
|
23
|
+
this.queueChangeHandler = handler;
|
|
24
|
+
}
|
|
25
|
+
notifyQueueChange() {
|
|
26
|
+
this.queueChangeHandler?.(this.queue.length);
|
|
27
|
+
}
|
|
14
28
|
start() {
|
|
15
29
|
// only works if stdin is a TTY (not piped)
|
|
16
30
|
if (!process.stdin.isTTY)
|
|
@@ -39,6 +53,21 @@ export class InputReader {
|
|
|
39
53
|
else {
|
|
40
54
|
this.lastEscTime = 0;
|
|
41
55
|
}
|
|
56
|
+
// scroll key detection (PgUp, PgDn, Home, End)
|
|
57
|
+
if (this.scrollHandler) {
|
|
58
|
+
if (key?.name === "pageup" || key?.sequence === "\x1b[5~") {
|
|
59
|
+
this.scrollHandler("up");
|
|
60
|
+
}
|
|
61
|
+
else if (key?.name === "pagedown" || key?.sequence === "\x1b[6~") {
|
|
62
|
+
this.scrollHandler("down");
|
|
63
|
+
}
|
|
64
|
+
else if (key?.name === "home" || key?.sequence === "\x1b[1~") {
|
|
65
|
+
this.scrollHandler("top");
|
|
66
|
+
}
|
|
67
|
+
else if (key?.name === "end" || key?.sequence === "\x1b[4~") {
|
|
68
|
+
this.scrollHandler("bottom");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
42
71
|
});
|
|
43
72
|
// show hint on startup
|
|
44
73
|
console.error(`${DIM}type a message to talk to the AI supervisor, /help for commands, ESC ESC to interrupt${RESET}`);
|
|
@@ -47,6 +76,8 @@ export class InputReader {
|
|
|
47
76
|
// drain all pending user messages (called each tick)
|
|
48
77
|
drain() {
|
|
49
78
|
const msgs = this.queue.splice(0);
|
|
79
|
+
if (msgs.length > 0)
|
|
80
|
+
this.notifyQueueChange();
|
|
50
81
|
return msgs;
|
|
51
82
|
}
|
|
52
83
|
isPaused() {
|
|
@@ -59,6 +90,7 @@ export class InputReader {
|
|
|
59
90
|
// inject a message directly into the queue (used after interrupt to feed text into next tick)
|
|
60
91
|
inject(msg) {
|
|
61
92
|
this.queue.push(msg);
|
|
93
|
+
this.notifyQueueChange();
|
|
62
94
|
}
|
|
63
95
|
// re-show the prompt (called after daemon prints output)
|
|
64
96
|
prompt() {
|
|
@@ -71,10 +103,18 @@ export class InputReader {
|
|
|
71
103
|
handleEscInterrupt() {
|
|
72
104
|
requestInterrupt();
|
|
73
105
|
this.queue.push("__CMD_INTERRUPT__");
|
|
106
|
+
this.notifyQueueChange();
|
|
74
107
|
console.error(`\n${RED}${BOLD}>>> interrupting reasoner <<<${RESET}`);
|
|
75
108
|
console.error(`${YELLOW}type your message now -- it will be sent before the next cycle${RESET}`);
|
|
76
109
|
this.rl?.prompt(true);
|
|
77
110
|
}
|
|
111
|
+
handleInsist(msg) {
|
|
112
|
+
requestInterrupt();
|
|
113
|
+
this.queue.push("__CMD_INTERRUPT__");
|
|
114
|
+
this.queue.push(`${INSIST_PREFIX}${msg}`);
|
|
115
|
+
this.notifyQueueChange();
|
|
116
|
+
console.error(`${RED}${BOLD}!${RESET} ${GREEN}insist${RESET} ${DIM}— interrupting + delivering your message immediately${RESET}`);
|
|
117
|
+
}
|
|
78
118
|
handleLine(line) {
|
|
79
119
|
if (!line) {
|
|
80
120
|
this.rl?.prompt();
|
|
@@ -86,9 +126,20 @@ export class InputReader {
|
|
|
86
126
|
this.rl?.prompt();
|
|
87
127
|
return;
|
|
88
128
|
}
|
|
129
|
+
// ! prefix = insist mode: interrupt + priority message
|
|
130
|
+
if (line.startsWith("!") && line.length > 1) {
|
|
131
|
+
const msg = line.slice(1).trim();
|
|
132
|
+
if (msg) {
|
|
133
|
+
this.handleInsist(msg);
|
|
134
|
+
this.rl?.prompt();
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
89
138
|
// queue as a user message for the reasoner
|
|
90
139
|
this.queue.push(line);
|
|
91
|
-
|
|
140
|
+
this.notifyQueueChange();
|
|
141
|
+
const pending = this.queue.filter(m => !m.startsWith("__CMD_")).length;
|
|
142
|
+
console.error(`${GREEN}queued${RESET} ${DIM}(${pending} pending) — will be read next cycle${RESET}`);
|
|
92
143
|
this.rl?.prompt();
|
|
93
144
|
}
|
|
94
145
|
handleCommand(line) {
|
|
@@ -97,7 +148,9 @@ export class InputReader {
|
|
|
97
148
|
case "/help":
|
|
98
149
|
console.error(`
|
|
99
150
|
${BOLD}talking to the AI:${RESET}
|
|
100
|
-
just type send a message
|
|
151
|
+
just type send a message — queued for next cycle
|
|
152
|
+
!message insist — interrupt + deliver message immediately
|
|
153
|
+
/insist <msg> same as !message
|
|
101
154
|
/explain ask the AI to explain what's happening right now
|
|
102
155
|
|
|
103
156
|
${BOLD}controls:${RESET}
|
|
@@ -105,6 +158,8 @@ ${BOLD}controls:${RESET}
|
|
|
105
158
|
/resume resume the supervisor
|
|
106
159
|
/interrupt interrupt the AI mid-thought
|
|
107
160
|
ESC ESC same as /interrupt (shortcut)
|
|
161
|
+
PgUp / PgDn scroll through activity history
|
|
162
|
+
Home / End jump to oldest / return to live
|
|
108
163
|
|
|
109
164
|
${BOLD}info:${RESET}
|
|
110
165
|
/status show daemon state
|
|
@@ -141,6 +196,16 @@ ${BOLD}other:${RESET}
|
|
|
141
196
|
case "/interrupt":
|
|
142
197
|
this.handleEscInterrupt();
|
|
143
198
|
break;
|
|
199
|
+
case "/insist": {
|
|
200
|
+
const insistMsg = line.slice("/insist".length).trim();
|
|
201
|
+
if (insistMsg) {
|
|
202
|
+
this.handleInsist(insistMsg);
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
console.error(`${DIM}usage: /insist <message> — interrupts and delivers your message immediately${RESET}`);
|
|
206
|
+
}
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
144
209
|
case "/tasks":
|
|
145
210
|
this.queue.push("__CMD_TASK__list");
|
|
146
211
|
break;
|
package/dist/message.d.ts
CHANGED
|
@@ -36,4 +36,17 @@ export declare function shouldSkipSleep(state: {
|
|
|
36
36
|
* Lightweight stat-only check — does not read or drain the file.
|
|
37
37
|
*/
|
|
38
38
|
export declare function hasPendingFile(filePath: string): boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Insist prefix marker — messages prefixed with this bypass the normal queue
|
|
41
|
+
* and trigger an immediate interrupt + delivery.
|
|
42
|
+
*/
|
|
43
|
+
export declare const INSIST_PREFIX = "__INSIST__";
|
|
44
|
+
/**
|
|
45
|
+
* Check if a message is an insist (priority) message.
|
|
46
|
+
*/
|
|
47
|
+
export declare function isInsistMessage(msg: string): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Strip the insist prefix from a message, returning the raw user text.
|
|
50
|
+
*/
|
|
51
|
+
export declare function stripInsistPrefix(msg: string): string;
|
|
39
52
|
//# sourceMappingURL=message.d.ts.map
|
package/dist/message.js
CHANGED
|
@@ -73,6 +73,23 @@ export function hasPendingFile(filePath) {
|
|
|
73
73
|
return false;
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Insist prefix marker — messages prefixed with this bypass the normal queue
|
|
78
|
+
* and trigger an immediate interrupt + delivery.
|
|
79
|
+
*/
|
|
80
|
+
export const INSIST_PREFIX = "__INSIST__";
|
|
81
|
+
/**
|
|
82
|
+
* Check if a message is an insist (priority) message.
|
|
83
|
+
*/
|
|
84
|
+
export function isInsistMessage(msg) {
|
|
85
|
+
return msg.startsWith(INSIST_PREFIX);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Strip the insist prefix from a message, returning the raw user text.
|
|
89
|
+
*/
|
|
90
|
+
export function stripInsistPrefix(msg) {
|
|
91
|
+
return msg.startsWith(INSIST_PREFIX) ? msg.slice(INSIST_PREFIX.length) : msg;
|
|
92
|
+
}
|
|
76
93
|
function truncate(s, max) {
|
|
77
94
|
return s.length <= max ? s : s.slice(0, max - 3) + "...";
|
|
78
95
|
}
|
package/dist/tui.d.ts
CHANGED
|
@@ -20,6 +20,9 @@ export declare class TUI {
|
|
|
20
20
|
private activityBuffer;
|
|
21
21
|
private maxActivity;
|
|
22
22
|
private spinnerFrame;
|
|
23
|
+
private scrollOffset;
|
|
24
|
+
private newWhileScrolled;
|
|
25
|
+
private pendingCount;
|
|
23
26
|
private phase;
|
|
24
27
|
private pollCount;
|
|
25
28
|
private sessions;
|
|
@@ -37,9 +40,15 @@ export declare class TUI {
|
|
|
37
40
|
paused?: boolean;
|
|
38
41
|
reasonerName?: string;
|
|
39
42
|
nextTickAt?: number;
|
|
43
|
+
pendingCount?: number;
|
|
40
44
|
}): void;
|
|
41
45
|
log(tag: string, text: string): void;
|
|
42
46
|
replayHistory(entries: HistoryEntry[]): void;
|
|
47
|
+
scrollUp(lines?: number): void;
|
|
48
|
+
scrollDown(lines?: number): void;
|
|
49
|
+
scrollToTop(): void;
|
|
50
|
+
scrollToBottom(): void;
|
|
51
|
+
isScrolledBack(): boolean;
|
|
43
52
|
private updateDimensions;
|
|
44
53
|
private computeLayout;
|
|
45
54
|
private onResize;
|
|
@@ -63,5 +72,11 @@ declare function truncatePlain(str: string, max: number): string;
|
|
|
63
72
|
* Kept for backward compatibility — used by non-TUI output paths.
|
|
64
73
|
*/
|
|
65
74
|
export declare function formatSessionSentence(s: DaemonSessionState, maxCols: number): string;
|
|
66
|
-
|
|
75
|
+
declare function formatPrompt(phase: DaemonPhase, paused: boolean, pendingCount: number): string;
|
|
76
|
+
declare function computeScrollSlice(bufferLen: number, visibleLines: number, scrollOffset: number): {
|
|
77
|
+
start: number;
|
|
78
|
+
end: number;
|
|
79
|
+
};
|
|
80
|
+
declare function formatScrollIndicator(offset: number, totalEntries: number, visibleLines: number, newCount: number): string;
|
|
81
|
+
export { formatActivity, formatSessionCard, truncateAnsi, truncatePlain, padBoxLine, padToWidth, stripAnsiForLen, phaseDisplay, computeScrollSlice, formatScrollIndicator, formatPrompt };
|
|
67
82
|
//# sourceMappingURL=tui.d.ts.map
|
package/dist/tui.js
CHANGED
|
@@ -55,6 +55,9 @@ export class TUI {
|
|
|
55
55
|
activityBuffer = []; // ring buffer for activity log
|
|
56
56
|
maxActivity = 500; // max entries to keep
|
|
57
57
|
spinnerFrame = 0; // current spinner animation frame
|
|
58
|
+
scrollOffset = 0; // 0 = live (bottom), >0 = scrolled back N entries
|
|
59
|
+
newWhileScrolled = 0; // entries added while user is scrolled back
|
|
60
|
+
pendingCount = 0; // queued user messages awaiting next tick
|
|
58
61
|
// current state for repaints
|
|
59
62
|
phase = "sleeping";
|
|
60
63
|
pollCount = 0;
|
|
@@ -113,6 +116,8 @@ export class TUI {
|
|
|
113
116
|
this.reasonerName = opts.reasonerName;
|
|
114
117
|
if (opts.nextTickAt !== undefined)
|
|
115
118
|
this.nextTickAt = opts.nextTickAt;
|
|
119
|
+
if (opts.pendingCount !== undefined)
|
|
120
|
+
this.pendingCount = opts.pendingCount;
|
|
116
121
|
if (opts.sessions !== undefined) {
|
|
117
122
|
const sessionCountChanged = opts.sessions.length !== this.sessions.length;
|
|
118
123
|
this.sessions = opts.sessions;
|
|
@@ -139,8 +144,16 @@ export class TUI {
|
|
|
139
144
|
if (this.activityBuffer.length > this.maxActivity) {
|
|
140
145
|
this.activityBuffer = this.activityBuffer.slice(-this.maxActivity);
|
|
141
146
|
}
|
|
142
|
-
if (this.active)
|
|
143
|
-
this.
|
|
147
|
+
if (this.active) {
|
|
148
|
+
if (this.scrollOffset > 0) {
|
|
149
|
+
// user is scrolled back — don't auto-scroll, just show indicator
|
|
150
|
+
this.newWhileScrolled++;
|
|
151
|
+
this.paintSeparator();
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
this.writeActivityLine(entry);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
144
157
|
// persist to disk (fire-and-forget, never blocks)
|
|
145
158
|
appendHistoryEntry({ ts: now.getTime(), time, tag, text });
|
|
146
159
|
}
|
|
@@ -155,6 +168,48 @@ export class TUI {
|
|
|
155
168
|
this.activityBuffer = this.activityBuffer.slice(-this.maxActivity);
|
|
156
169
|
}
|
|
157
170
|
}
|
|
171
|
+
// ── Scroll navigation ────────────────────────────────────────────────────
|
|
172
|
+
scrollUp(lines) {
|
|
173
|
+
if (!this.active)
|
|
174
|
+
return;
|
|
175
|
+
const visibleLines = this.scrollBottom - this.scrollTop + 1;
|
|
176
|
+
const n = lines ?? Math.max(1, Math.floor(visibleLines / 2));
|
|
177
|
+
const maxOffset = Math.max(0, this.activityBuffer.length - visibleLines);
|
|
178
|
+
this.scrollOffset = Math.min(maxOffset, this.scrollOffset + n);
|
|
179
|
+
this.repaintActivityRegion();
|
|
180
|
+
this.paintSeparator();
|
|
181
|
+
}
|
|
182
|
+
scrollDown(lines) {
|
|
183
|
+
if (!this.active)
|
|
184
|
+
return;
|
|
185
|
+
const visibleLines = this.scrollBottom - this.scrollTop + 1;
|
|
186
|
+
const n = lines ?? Math.max(1, Math.floor(visibleLines / 2));
|
|
187
|
+
const wasScrolled = this.scrollOffset > 0;
|
|
188
|
+
this.scrollOffset = Math.max(0, this.scrollOffset - n);
|
|
189
|
+
if (wasScrolled && this.scrollOffset === 0)
|
|
190
|
+
this.newWhileScrolled = 0;
|
|
191
|
+
this.repaintActivityRegion();
|
|
192
|
+
this.paintSeparator();
|
|
193
|
+
}
|
|
194
|
+
scrollToTop() {
|
|
195
|
+
if (!this.active)
|
|
196
|
+
return;
|
|
197
|
+
const visibleLines = this.scrollBottom - this.scrollTop + 1;
|
|
198
|
+
this.scrollOffset = Math.max(0, this.activityBuffer.length - visibleLines);
|
|
199
|
+
this.repaintActivityRegion();
|
|
200
|
+
this.paintSeparator();
|
|
201
|
+
}
|
|
202
|
+
scrollToBottom() {
|
|
203
|
+
if (!this.active)
|
|
204
|
+
return;
|
|
205
|
+
this.scrollOffset = 0;
|
|
206
|
+
this.newWhileScrolled = 0;
|
|
207
|
+
this.repaintActivityRegion();
|
|
208
|
+
this.paintSeparator();
|
|
209
|
+
}
|
|
210
|
+
isScrolledBack() {
|
|
211
|
+
return this.scrollOffset > 0;
|
|
212
|
+
}
|
|
158
213
|
// ── Layout computation ──────────────────────────────────────────────────
|
|
159
214
|
updateDimensions() {
|
|
160
215
|
this.cols = process.stderr.columns || 80;
|
|
@@ -245,8 +300,14 @@ export class TUI {
|
|
|
245
300
|
process.stderr.write(RESTORE_CURSOR);
|
|
246
301
|
}
|
|
247
302
|
paintSeparator() {
|
|
248
|
-
const hints = " esc esc: interrupt /help /explain /pause ";
|
|
249
303
|
const prefix = `${BOX.h}${BOX.h} activity `;
|
|
304
|
+
let hints;
|
|
305
|
+
if (this.scrollOffset > 0) {
|
|
306
|
+
hints = formatScrollIndicator(this.scrollOffset, this.activityBuffer.length, this.scrollBottom - this.scrollTop + 1, this.newWhileScrolled);
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
hints = " esc esc: interrupt /help /explain /pause ";
|
|
310
|
+
}
|
|
250
311
|
const totalLen = prefix.length + hints.length;
|
|
251
312
|
const fill = Math.max(0, this.cols - totalLen);
|
|
252
313
|
const left = Math.floor(fill / 2);
|
|
@@ -265,7 +326,8 @@ export class TUI {
|
|
|
265
326
|
}
|
|
266
327
|
repaintActivityRegion() {
|
|
267
328
|
const visibleLines = this.scrollBottom - this.scrollTop + 1;
|
|
268
|
-
const
|
|
329
|
+
const { start, end } = computeScrollSlice(this.activityBuffer.length, visibleLines, this.scrollOffset);
|
|
330
|
+
const entries = this.activityBuffer.slice(start, end);
|
|
269
331
|
for (let i = 0; i < visibleLines; i++) {
|
|
270
332
|
const row = this.scrollTop + i;
|
|
271
333
|
if (i < entries.length) {
|
|
@@ -278,12 +340,7 @@ export class TUI {
|
|
|
278
340
|
}
|
|
279
341
|
}
|
|
280
342
|
paintInputLine() {
|
|
281
|
-
|
|
282
|
-
const prompt = this.paused
|
|
283
|
-
? `${AMBER}${BOLD}paused >${RESET} `
|
|
284
|
-
: this.phase === "reasoning"
|
|
285
|
-
? `${SKY}thinking >${RESET} `
|
|
286
|
-
: `${LIME}>${RESET} `;
|
|
343
|
+
const prompt = formatPrompt(this.phase, this.paused, this.pendingCount);
|
|
287
344
|
process.stderr.write(SAVE_CURSOR +
|
|
288
345
|
moveTo(this.inputRow, 1) + CLEAR_LINE + prompt +
|
|
289
346
|
RESTORE_CURSOR);
|
|
@@ -431,6 +488,29 @@ export function formatSessionSentence(s, maxCols) {
|
|
|
431
488
|
}
|
|
432
489
|
return truncateAnsi(`${dot} ${BOLD}${name}${RESET} ${tool} ${SLATE}—${RESET} ${statusDesc}`, maxCols);
|
|
433
490
|
}
|
|
491
|
+
// ── Prompt helpers (pure, exported for testing) ─────────────────────────────
|
|
492
|
+
// format the input prompt based on phase, pause state, and pending queue count
|
|
493
|
+
function formatPrompt(phase, paused, pendingCount) {
|
|
494
|
+
const queueTag = pendingCount > 0 ? `${AMBER}${pendingCount} queued${RESET} ` : "";
|
|
495
|
+
if (paused)
|
|
496
|
+
return `${queueTag}${AMBER}${BOLD}paused >${RESET} `;
|
|
497
|
+
if (phase === "reasoning")
|
|
498
|
+
return `${queueTag}${SKY}thinking >${RESET} `;
|
|
499
|
+
return `${queueTag}${LIME}>${RESET} `;
|
|
500
|
+
}
|
|
501
|
+
// ── Scroll helpers (pure, exported for testing) ─────────────────────────────
|
|
502
|
+
// compute the slice indices for the activity buffer given scroll state
|
|
503
|
+
function computeScrollSlice(bufferLen, visibleLines, scrollOffset) {
|
|
504
|
+
const end = Math.max(0, bufferLen - scrollOffset);
|
|
505
|
+
const start = Math.max(0, end - visibleLines);
|
|
506
|
+
return { start, end };
|
|
507
|
+
}
|
|
508
|
+
// format the scroll indicator text for the separator bar
|
|
509
|
+
function formatScrollIndicator(offset, totalEntries, visibleLines, newCount) {
|
|
510
|
+
const position = totalEntries - offset;
|
|
511
|
+
const newTag = newCount > 0 ? ` ${newCount} new ↓` : "";
|
|
512
|
+
return ` ↑ ${offset} older │ ${position}/${totalEntries} │ PgUp/PgDn End=live${newTag} `;
|
|
513
|
+
}
|
|
434
514
|
// ── Exported pure helpers (for testing) ─────────────────────────────────────
|
|
435
|
-
export { formatActivity, formatSessionCard, truncateAnsi, truncatePlain, padBoxLine, padToWidth, stripAnsiForLen, phaseDisplay };
|
|
515
|
+
export { formatActivity, formatSessionCard, truncateAnsi, truncatePlain, padBoxLine, padToWidth, stripAnsiForLen, phaseDisplay, computeScrollSlice, formatScrollIndicator, formatPrompt };
|
|
436
516
|
//# sourceMappingURL=tui.js.map
|