aoaoe 0.76.0 → 0.78.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/index.js +16 -1
- package/dist/input.d.ts +3 -0
- package/dist/input.js +16 -0
- package/dist/tui.d.ts +19 -1
- package/dist/tui.js +102 -6
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -15,7 +15,7 @@ import { wakeableSleep } from "./wake.js";
|
|
|
15
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
|
-
import { TUI, hitTestSession } from "./tui.js";
|
|
18
|
+
import { TUI, hitTestSession, nextSortMode, SORT_MODES } from "./tui.js";
|
|
19
19
|
import { isDaemonRunningFromState } from "./chat.js";
|
|
20
20
|
import { sendNotification, sendTestNotification } from "./notify.js";
|
|
21
21
|
import { startHealthServer } from "./health.js";
|
|
@@ -354,6 +354,21 @@ async function main() {
|
|
|
354
354
|
tui.log("system", "search cleared");
|
|
355
355
|
}
|
|
356
356
|
});
|
|
357
|
+
// wire /sort command to TUI session sort
|
|
358
|
+
input.onSort((mode) => {
|
|
359
|
+
if (mode && SORT_MODES.includes(mode)) {
|
|
360
|
+
tui.setSortMode(mode);
|
|
361
|
+
tui.log("system", `sort: ${mode}`);
|
|
362
|
+
}
|
|
363
|
+
else if (!mode) {
|
|
364
|
+
const next = nextSortMode(tui.getSortMode());
|
|
365
|
+
tui.setSortMode(next);
|
|
366
|
+
tui.log("system", `sort: ${next}`);
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
tui.log("system", `unknown sort mode: ${mode} (try: status, name, activity, default)`);
|
|
370
|
+
}
|
|
371
|
+
});
|
|
357
372
|
// wire mouse move to hover highlight on session cards
|
|
358
373
|
input.onMouseMove((row, _col) => {
|
|
359
374
|
if (tui.getViewMode() === "overview") {
|
package/dist/input.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export declare const INSIST_PREFIX = "__INSIST__";
|
|
|
3
3
|
export type ViewHandler = (target: string | null) => void;
|
|
4
4
|
export type SearchHandler = (pattern: string | null) => void;
|
|
5
5
|
export type QuickSwitchHandler = (sessionNum: number) => void;
|
|
6
|
+
export type SortHandler = (mode: string | null) => void;
|
|
6
7
|
export interface MouseEvent {
|
|
7
8
|
button: number;
|
|
8
9
|
col: number;
|
|
@@ -28,6 +29,7 @@ export declare class InputReader {
|
|
|
28
29
|
private lastMoveRow;
|
|
29
30
|
private searchHandler;
|
|
30
31
|
private quickSwitchHandler;
|
|
32
|
+
private sortHandler;
|
|
31
33
|
private mouseDataListener;
|
|
32
34
|
onScroll(handler: (dir: ScrollDirection) => void): void;
|
|
33
35
|
onQueueChange(handler: (count: number) => void): void;
|
|
@@ -37,6 +39,7 @@ export declare class InputReader {
|
|
|
37
39
|
onMouseMove(handler: MouseMoveHandler): void;
|
|
38
40
|
onSearch(handler: SearchHandler): void;
|
|
39
41
|
onQuickSwitch(handler: QuickSwitchHandler): void;
|
|
42
|
+
onSort(handler: SortHandler): void;
|
|
40
43
|
private notifyQueueChange;
|
|
41
44
|
start(): void;
|
|
42
45
|
drain(): string[];
|
package/dist/input.js
CHANGED
|
@@ -35,6 +35,7 @@ export class InputReader {
|
|
|
35
35
|
lastMoveRow = 0; // debounce: only fire move handler when row changes
|
|
36
36
|
searchHandler = null;
|
|
37
37
|
quickSwitchHandler = null;
|
|
38
|
+
sortHandler = null;
|
|
38
39
|
mouseDataListener = null;
|
|
39
40
|
// register a callback for scroll key events (PgUp/PgDn/Home/End)
|
|
40
41
|
onScroll(handler) {
|
|
@@ -68,6 +69,10 @@ export class InputReader {
|
|
|
68
69
|
onQuickSwitch(handler) {
|
|
69
70
|
this.quickSwitchHandler = handler;
|
|
70
71
|
}
|
|
72
|
+
// register a callback for sort commands (/sort <mode> or /sort to cycle)
|
|
73
|
+
onSort(handler) {
|
|
74
|
+
this.sortHandler = handler;
|
|
75
|
+
}
|
|
71
76
|
notifyQueueChange() {
|
|
72
77
|
this.queueChangeHandler?.(this.queue.length);
|
|
73
78
|
}
|
|
@@ -245,6 +250,7 @@ ${BOLD}navigation:${RESET}
|
|
|
245
250
|
1-9 quick-switch: jump to session N (type digit + Enter)
|
|
246
251
|
/view [N|name] drill into a session's live output (default: 1)
|
|
247
252
|
/back return to overview from drill-down
|
|
253
|
+
/sort [mode] sort sessions: status, name, activity, default (or cycle)
|
|
248
254
|
/search <pattern> filter activity entries by substring (case-insensitive)
|
|
249
255
|
/search clear active search filter
|
|
250
256
|
click session click an agent card to drill down (click again to go back)
|
|
@@ -324,6 +330,16 @@ ${BOLD}other:${RESET}
|
|
|
324
330
|
console.error(`${DIM}already in overview${RESET}`);
|
|
325
331
|
}
|
|
326
332
|
break;
|
|
333
|
+
case "/sort": {
|
|
334
|
+
const sortArg = line.slice("/sort".length).trim().toLowerCase();
|
|
335
|
+
if (this.sortHandler) {
|
|
336
|
+
this.sortHandler(sortArg || null); // empty = cycle to next mode
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
console.error(`${DIM}sort not available (no TUI)${RESET}`);
|
|
340
|
+
}
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
327
343
|
case "/search": {
|
|
328
344
|
const searchArg = line.slice("/search".length).trim();
|
|
329
345
|
if (this.searchHandler) {
|
package/dist/tui.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { DaemonSessionState, DaemonPhase } from "./types.js";
|
|
2
2
|
import type { HistoryEntry } from "./tui-history.js";
|
|
3
|
+
export type SortMode = "default" | "status" | "name" | "activity";
|
|
4
|
+
declare const SORT_MODES: SortMode[];
|
|
5
|
+
/** Sort sessions by mode. Returns a new array (never mutates). */
|
|
6
|
+
declare function sortSessions(sessions: DaemonSessionState[], mode: SortMode, lastChangeAt?: Map<string, number>): DaemonSessionState[];
|
|
7
|
+
/** Cycle to the next sort mode. */
|
|
8
|
+
declare function nextSortMode(current: SortMode): SortMode;
|
|
3
9
|
declare function phaseDisplay(phase: DaemonPhase, paused: boolean, spinnerFrame: number): string;
|
|
4
10
|
export interface ActivityEntry {
|
|
5
11
|
time: string;
|
|
@@ -25,6 +31,10 @@ export declare class TUI {
|
|
|
25
31
|
private pendingCount;
|
|
26
32
|
private searchPattern;
|
|
27
33
|
private hoverSessionIdx;
|
|
34
|
+
private activityTimestamps;
|
|
35
|
+
private sortMode;
|
|
36
|
+
private lastChangeAt;
|
|
37
|
+
private prevLastActivity;
|
|
28
38
|
private viewMode;
|
|
29
39
|
private drilldownSessionId;
|
|
30
40
|
private sessionOutputs;
|
|
@@ -42,6 +52,10 @@ export declare class TUI {
|
|
|
42
52
|
isActive(): boolean;
|
|
43
53
|
/** Return the current number of sessions (for mouse hit testing) */
|
|
44
54
|
getSessionCount(): number;
|
|
55
|
+
/** Set the session sort mode and repaint. */
|
|
56
|
+
setSortMode(mode: SortMode): void;
|
|
57
|
+
/** Return the current sort mode. */
|
|
58
|
+
getSortMode(): SortMode;
|
|
45
59
|
updateState(opts: {
|
|
46
60
|
phase?: DaemonPhase;
|
|
47
61
|
pollCount?: number;
|
|
@@ -116,6 +130,10 @@ declare function computeScrollSlice(bufferLen: number, visibleLines: number, scr
|
|
|
116
130
|
};
|
|
117
131
|
declare function formatScrollIndicator(offset: number, totalEntries: number, visibleLines: number, newCount: number): string;
|
|
118
132
|
declare function formatDrilldownScrollIndicator(offset: number, totalLines: number, visibleLines: number, newCount: number): string;
|
|
133
|
+
/** Compute sparkline bucket counts from activity timestamps. Returns array of SPARK_BUCKETS counts. */
|
|
134
|
+
declare function computeSparkline(timestamps: number[], now?: number, buckets?: number, windowMs?: number): number[];
|
|
135
|
+
/** Format sparkline bucket counts as a colored Unicode block string. Returns empty string if all zeros. */
|
|
136
|
+
declare function formatSparkline(counts: number[]): string;
|
|
119
137
|
/** Case-insensitive substring match against an activity entry's tag, text, and time. */
|
|
120
138
|
declare function matchesSearch(entry: ActivityEntry, pattern: string): boolean;
|
|
121
139
|
/** Format the search indicator text for the separator bar. */
|
|
@@ -128,5 +146,5 @@ declare function formatSearchIndicator(pattern: string, matchCount: number, tota
|
|
|
128
146
|
* (row = headerHeight + 2 + i for 0-indexed session i)
|
|
129
147
|
*/
|
|
130
148
|
export declare function hitTestSession(row: number, headerHeight: number, sessionCount: number): number | null;
|
|
131
|
-
export { formatActivity, formatSessionCard, truncateAnsi, truncatePlain, padBoxLine, padBoxLineHover, padToWidth, stripAnsiForLen, phaseDisplay, computeScrollSlice, formatScrollIndicator, formatDrilldownScrollIndicator, formatPrompt, formatDrilldownHeader, matchesSearch, formatSearchIndicator };
|
|
149
|
+
export { formatActivity, formatSessionCard, truncateAnsi, truncatePlain, padBoxLine, padBoxLineHover, padToWidth, stripAnsiForLen, phaseDisplay, computeScrollSlice, formatScrollIndicator, formatDrilldownScrollIndicator, formatPrompt, formatDrilldownHeader, matchesSearch, formatSearchIndicator, computeSparkline, formatSparkline, sortSessions, nextSortMode, SORT_MODES };
|
|
132
150
|
//# sourceMappingURL=tui.d.ts.map
|
package/dist/tui.js
CHANGED
|
@@ -21,6 +21,33 @@ const MOUSE_OFF = `${CSI}?1003l${CSI}?1006l`;
|
|
|
21
21
|
const moveTo = (row, col) => `${CSI}${row};${col}H`;
|
|
22
22
|
const setScrollRegion = (top, bottom) => `${CSI}${top};${bottom}r`;
|
|
23
23
|
const resetScrollRegion = () => `${CSI}r`;
|
|
24
|
+
const SORT_MODES = ["default", "status", "name", "activity"];
|
|
25
|
+
const STATUS_PRIORITY = {
|
|
26
|
+
error: 0, waiting: 1, working: 2, running: 2,
|
|
27
|
+
idle: 3, done: 4, stopped: 5, unknown: 6,
|
|
28
|
+
};
|
|
29
|
+
/** Sort sessions by mode. Returns a new array (never mutates). */
|
|
30
|
+
function sortSessions(sessions, mode, lastChangeAt) {
|
|
31
|
+
const copy = sessions.slice();
|
|
32
|
+
switch (mode) {
|
|
33
|
+
case "status":
|
|
34
|
+
copy.sort((a, b) => (STATUS_PRIORITY[a.status] ?? 6) - (STATUS_PRIORITY[b.status] ?? 6));
|
|
35
|
+
break;
|
|
36
|
+
case "name":
|
|
37
|
+
copy.sort((a, b) => a.title.toLowerCase().localeCompare(b.title.toLowerCase()));
|
|
38
|
+
break;
|
|
39
|
+
case "activity":
|
|
40
|
+
copy.sort((a, b) => (lastChangeAt?.get(b.id) ?? 0) - (lastChangeAt?.get(a.id) ?? 0));
|
|
41
|
+
break;
|
|
42
|
+
// "default" — preserve original order
|
|
43
|
+
}
|
|
44
|
+
return copy;
|
|
45
|
+
}
|
|
46
|
+
/** Cycle to the next sort mode. */
|
|
47
|
+
function nextSortMode(current) {
|
|
48
|
+
const idx = SORT_MODES.indexOf(current);
|
|
49
|
+
return SORT_MODES[(idx + 1) % SORT_MODES.length];
|
|
50
|
+
}
|
|
24
51
|
// ── Status rendering ────────────────────────────────────────────────────────
|
|
25
52
|
const STATUS_DOT = {
|
|
26
53
|
working: `${LIME}${DOT.filled}${RESET}`,
|
|
@@ -65,6 +92,10 @@ export class TUI {
|
|
|
65
92
|
pendingCount = 0; // queued user messages awaiting next tick
|
|
66
93
|
searchPattern = null; // active search filter pattern
|
|
67
94
|
hoverSessionIdx = null; // 1-indexed session under mouse cursor (null = none)
|
|
95
|
+
activityTimestamps = []; // epoch ms of each log() call for sparkline
|
|
96
|
+
sortMode = "default";
|
|
97
|
+
lastChangeAt = new Map(); // session ID → epoch ms of last activity change
|
|
98
|
+
prevLastActivity = new Map(); // session ID → previous lastActivity string
|
|
68
99
|
// drill-down mode: show a single session's full output
|
|
69
100
|
viewMode = "overview";
|
|
70
101
|
drilldownSessionId = null;
|
|
@@ -121,6 +152,21 @@ export class TUI {
|
|
|
121
152
|
getSessionCount() {
|
|
122
153
|
return this.sessions.length;
|
|
123
154
|
}
|
|
155
|
+
/** Set the session sort mode and repaint. */
|
|
156
|
+
setSortMode(mode) {
|
|
157
|
+
if (mode === this.sortMode)
|
|
158
|
+
return;
|
|
159
|
+
this.sortMode = mode;
|
|
160
|
+
// re-sort current sessions and repaint
|
|
161
|
+
this.sessions = sortSessions(this.sessions, this.sortMode, this.lastChangeAt);
|
|
162
|
+
if (this.active) {
|
|
163
|
+
this.paintSessions();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/** Return the current sort mode. */
|
|
167
|
+
getSortMode() {
|
|
168
|
+
return this.sortMode;
|
|
169
|
+
}
|
|
124
170
|
// ── State updates ───────────────────────────────────────────────────────
|
|
125
171
|
updateState(opts) {
|
|
126
172
|
if (opts.phase !== undefined)
|
|
@@ -136,8 +182,19 @@ export class TUI {
|
|
|
136
182
|
if (opts.pendingCount !== undefined)
|
|
137
183
|
this.pendingCount = opts.pendingCount;
|
|
138
184
|
if (opts.sessions !== undefined) {
|
|
139
|
-
|
|
140
|
-
|
|
185
|
+
// track activity changes for sort-by-activity
|
|
186
|
+
const now = Date.now();
|
|
187
|
+
for (const s of opts.sessions) {
|
|
188
|
+
const prev = this.prevLastActivity.get(s.id);
|
|
189
|
+
if (s.lastActivity !== undefined && s.lastActivity !== prev) {
|
|
190
|
+
this.lastChangeAt.set(s.id, now);
|
|
191
|
+
}
|
|
192
|
+
if (s.lastActivity !== undefined)
|
|
193
|
+
this.prevLastActivity.set(s.id, s.lastActivity);
|
|
194
|
+
}
|
|
195
|
+
const sorted = sortSessions(opts.sessions, this.sortMode, this.lastChangeAt);
|
|
196
|
+
const sessionCountChanged = sorted.length !== this.sessions.length;
|
|
197
|
+
this.sessions = sorted;
|
|
141
198
|
if (sessionCountChanged) {
|
|
142
199
|
this.computeLayout(this.sessions.length);
|
|
143
200
|
this.paintAll();
|
|
@@ -158,8 +215,10 @@ export class TUI {
|
|
|
158
215
|
const time = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}:${String(now.getSeconds()).padStart(2, "0")}`;
|
|
159
216
|
const entry = { time, tag, text };
|
|
160
217
|
this.activityBuffer.push(entry);
|
|
218
|
+
this.activityTimestamps.push(now.getTime());
|
|
161
219
|
if (this.activityBuffer.length > this.maxActivity) {
|
|
162
220
|
this.activityBuffer = this.activityBuffer.slice(-this.maxActivity);
|
|
221
|
+
this.activityTimestamps = this.activityTimestamps.slice(-this.maxActivity);
|
|
163
222
|
}
|
|
164
223
|
if (this.active) {
|
|
165
224
|
if (this.searchPattern) {
|
|
@@ -461,8 +520,8 @@ export class TUI {
|
|
|
461
520
|
paintSessions() {
|
|
462
521
|
const startRow = this.headerHeight + 1;
|
|
463
522
|
const innerWidth = this.cols - 2; // inside the box borders
|
|
464
|
-
// top border with label
|
|
465
|
-
const label = " agents "
|
|
523
|
+
// top border with label (includes sort mode when not default)
|
|
524
|
+
const label = this.sortMode === "default" ? " agents " : ` agents (${this.sortMode}) `;
|
|
466
525
|
const borderAfterLabel = Math.max(0, innerWidth - label.length);
|
|
467
526
|
const topBorder = `${SLATE}${BOX.rtl}${BOX.h}${RESET}${SLATE}${label}${RESET}${SLATE}${BOX.h.repeat(borderAfterLabel)}${BOX.rtr}${RESET}`;
|
|
468
527
|
process.stderr.write(SAVE_CURSOR + moveTo(startRow, 1) + CLEAR_LINE + truncateAnsi(topBorder, this.cols));
|
|
@@ -520,7 +579,9 @@ export class TUI {
|
|
|
520
579
|
hints = formatScrollIndicator(this.scrollOffset, this.activityBuffer.length, this.scrollBottom - this.scrollTop + 1, this.newWhileScrolled);
|
|
521
580
|
}
|
|
522
581
|
else {
|
|
523
|
-
|
|
582
|
+
// live mode: show sparkline + minimal hints
|
|
583
|
+
const spark = formatSparkline(computeSparkline(this.activityTimestamps));
|
|
584
|
+
hints = spark ? ` ${spark} /help ` : " click agent to view esc esc: interrupt /help ";
|
|
524
585
|
}
|
|
525
586
|
const totalLen = prefix.length + hints.length;
|
|
526
587
|
const fill = Math.max(0, this.cols - totalLen);
|
|
@@ -803,6 +864,41 @@ function formatDrilldownScrollIndicator(offset, totalLines, visibleLines, newCou
|
|
|
803
864
|
const newTag = newCount > 0 ? ` ${newCount} new ↓` : "";
|
|
804
865
|
return ` ↑ ${offset} lines │ ${position}/${totalLines} │ scroll: navigate End=live${newTag} `;
|
|
805
866
|
}
|
|
867
|
+
// ── Sparkline helpers (pure, exported for testing) ──────────────────────────
|
|
868
|
+
const SPARK_BLOCKS = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"];
|
|
869
|
+
const SPARK_BUCKETS = 20; // number of time buckets
|
|
870
|
+
const SPARK_WINDOW_MS = 10 * 60 * 1000; // 10 minutes
|
|
871
|
+
/** Compute sparkline bucket counts from activity timestamps. Returns array of SPARK_BUCKETS counts. */
|
|
872
|
+
function computeSparkline(timestamps, now, buckets, windowMs) {
|
|
873
|
+
const n = buckets ?? SPARK_BUCKETS;
|
|
874
|
+
const window = windowMs ?? SPARK_WINDOW_MS;
|
|
875
|
+
const nowMs = now ?? Date.now();
|
|
876
|
+
const cutoff = nowMs - window;
|
|
877
|
+
const bucketSize = window / n;
|
|
878
|
+
const counts = new Array(n).fill(0);
|
|
879
|
+
for (const ts of timestamps) {
|
|
880
|
+
if (ts < cutoff)
|
|
881
|
+
continue;
|
|
882
|
+
const idx = Math.min(n - 1, Math.floor((ts - cutoff) / bucketSize));
|
|
883
|
+
counts[idx]++;
|
|
884
|
+
}
|
|
885
|
+
return counts;
|
|
886
|
+
}
|
|
887
|
+
/** Format sparkline bucket counts as a colored Unicode block string. Returns empty string if all zeros. */
|
|
888
|
+
function formatSparkline(counts) {
|
|
889
|
+
const max = Math.max(...counts);
|
|
890
|
+
if (max === 0)
|
|
891
|
+
return "";
|
|
892
|
+
const blocks = counts.map((c) => {
|
|
893
|
+
if (c === 0)
|
|
894
|
+
return `${SLATE} ${RESET}`;
|
|
895
|
+
const level = Math.min(SPARK_BLOCKS.length - 1, Math.floor((c / max) * (SPARK_BLOCKS.length - 1)));
|
|
896
|
+
// color gradient: low=SLATE, mid=SKY, high=LIME
|
|
897
|
+
const color = level < 3 ? SLATE : level < 6 ? SKY : LIME;
|
|
898
|
+
return `${color}${SPARK_BLOCKS[level]}${RESET}`;
|
|
899
|
+
});
|
|
900
|
+
return blocks.join("");
|
|
901
|
+
}
|
|
806
902
|
// ── Search helpers (pure, exported for testing) ─────────────────────────────
|
|
807
903
|
/** Case-insensitive substring match against an activity entry's tag, text, and time. */
|
|
808
904
|
function matchesSearch(entry, pattern) {
|
|
@@ -835,5 +931,5 @@ export function hitTestSession(row, headerHeight, sessionCount) {
|
|
|
835
931
|
return row - firstSessionRow + 1; // 1-indexed
|
|
836
932
|
}
|
|
837
933
|
// ── Exported pure helpers (for testing) ─────────────────────────────────────
|
|
838
|
-
export { formatActivity, formatSessionCard, truncateAnsi, truncatePlain, padBoxLine, padBoxLineHover, padToWidth, stripAnsiForLen, phaseDisplay, computeScrollSlice, formatScrollIndicator, formatDrilldownScrollIndicator, formatPrompt, formatDrilldownHeader, matchesSearch, formatSearchIndicator };
|
|
934
|
+
export { formatActivity, formatSessionCard, truncateAnsi, truncatePlain, padBoxLine, padBoxLineHover, padToWidth, stripAnsiForLen, phaseDisplay, computeScrollSlice, formatScrollIndicator, formatDrilldownScrollIndicator, formatPrompt, formatDrilldownHeader, matchesSearch, formatSearchIndicator, computeSparkline, formatSparkline, sortSessions, nextSortMode, SORT_MODES };
|
|
839
935
|
//# sourceMappingURL=tui.js.map
|