aoaoe 0.77.0 → 0.79.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 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";
@@ -318,6 +318,9 @@ async function main() {
318
318
  tui.log("system", "returned to overview");
319
319
  return;
320
320
  }
321
+ // compact mode: no per-session click targeting (use quick-switch or /view)
322
+ if (tui.isCompact())
323
+ return;
321
324
  const sessionIdx = hitTestSession(row, 1, tui.getSessionCount());
322
325
  if (sessionIdx !== null) {
323
326
  const ok = tui.enterDrilldown(sessionIdx);
@@ -354,9 +357,30 @@ async function main() {
354
357
  tui.log("system", "search cleared");
355
358
  }
356
359
  });
357
- // wire mouse move to hover highlight on session cards
360
+ // wire /sort command to TUI session sort
361
+ input.onSort((mode) => {
362
+ if (mode && SORT_MODES.includes(mode)) {
363
+ tui.setSortMode(mode);
364
+ tui.log("system", `sort: ${mode}`);
365
+ }
366
+ else if (!mode) {
367
+ const next = nextSortMode(tui.getSortMode());
368
+ tui.setSortMode(next);
369
+ tui.log("system", `sort: ${next}`);
370
+ }
371
+ else {
372
+ tui.log("system", `unknown sort mode: ${mode} (try: status, name, activity, default)`);
373
+ }
374
+ });
375
+ // wire /compact toggle
376
+ input.onCompact(() => {
377
+ const enabled = !tui.isCompact();
378
+ tui.setCompact(enabled);
379
+ tui.log("system", `compact mode: ${enabled ? "on" : "off"}`);
380
+ });
381
+ // wire mouse move to hover highlight on session cards (disabled in compact)
358
382
  input.onMouseMove((row, _col) => {
359
- if (tui.getViewMode() === "overview") {
383
+ if (tui.getViewMode() === "overview" && !tui.isCompact()) {
360
384
  const sessionIdx = hitTestSession(row, 1, tui.getSessionCount());
361
385
  tui.setHoverSession(sessionIdx);
362
386
  }
package/dist/input.d.ts CHANGED
@@ -3,6 +3,8 @@ 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;
7
+ export type CompactHandler = () => void;
6
8
  export interface MouseEvent {
7
9
  button: number;
8
10
  col: number;
@@ -28,6 +30,8 @@ export declare class InputReader {
28
30
  private lastMoveRow;
29
31
  private searchHandler;
30
32
  private quickSwitchHandler;
33
+ private sortHandler;
34
+ private compactHandler;
31
35
  private mouseDataListener;
32
36
  onScroll(handler: (dir: ScrollDirection) => void): void;
33
37
  onQueueChange(handler: (count: number) => void): void;
@@ -37,6 +41,8 @@ export declare class InputReader {
37
41
  onMouseMove(handler: MouseMoveHandler): void;
38
42
  onSearch(handler: SearchHandler): void;
39
43
  onQuickSwitch(handler: QuickSwitchHandler): void;
44
+ onSort(handler: SortHandler): void;
45
+ onCompact(handler: CompactHandler): void;
40
46
  private notifyQueueChange;
41
47
  start(): void;
42
48
  drain(): string[];
package/dist/input.js CHANGED
@@ -35,6 +35,8 @@ 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;
39
+ compactHandler = null;
38
40
  mouseDataListener = null;
39
41
  // register a callback for scroll key events (PgUp/PgDn/Home/End)
40
42
  onScroll(handler) {
@@ -68,6 +70,14 @@ export class InputReader {
68
70
  onQuickSwitch(handler) {
69
71
  this.quickSwitchHandler = handler;
70
72
  }
73
+ // register a callback for sort commands (/sort <mode> or /sort to cycle)
74
+ onSort(handler) {
75
+ this.sortHandler = handler;
76
+ }
77
+ // register a callback for compact mode toggle (/compact)
78
+ onCompact(handler) {
79
+ this.compactHandler = handler;
80
+ }
71
81
  notifyQueueChange() {
72
82
  this.queueChangeHandler?.(this.queue.length);
73
83
  }
@@ -245,6 +255,8 @@ ${BOLD}navigation:${RESET}
245
255
  1-9 quick-switch: jump to session N (type digit + Enter)
246
256
  /view [N|name] drill into a session's live output (default: 1)
247
257
  /back return to overview from drill-down
258
+ /sort [mode] sort sessions: status, name, activity, default (or cycle)
259
+ /compact toggle compact mode (dense session panel)
248
260
  /search <pattern> filter activity entries by substring (case-insensitive)
249
261
  /search clear active search filter
250
262
  click session click an agent card to drill down (click again to go back)
@@ -324,6 +336,24 @@ ${BOLD}other:${RESET}
324
336
  console.error(`${DIM}already in overview${RESET}`);
325
337
  }
326
338
  break;
339
+ case "/sort": {
340
+ const sortArg = line.slice("/sort".length).trim().toLowerCase();
341
+ if (this.sortHandler) {
342
+ this.sortHandler(sortArg || null); // empty = cycle to next mode
343
+ }
344
+ else {
345
+ console.error(`${DIM}sort not available (no TUI)${RESET}`);
346
+ }
347
+ break;
348
+ }
349
+ case "/compact":
350
+ if (this.compactHandler) {
351
+ this.compactHandler();
352
+ }
353
+ else {
354
+ console.error(`${DIM}compact mode not available (no TUI)${RESET}`);
355
+ }
356
+ break;
327
357
  case "/search": {
328
358
  const searchArg = line.slice("/search".length).trim();
329
359
  if (this.searchHandler) {
package/dist/tui.d.ts CHANGED
@@ -1,5 +1,21 @@
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;
9
+ /** Max name length in compact token. */
10
+ declare const COMPACT_NAME_LEN = 10;
11
+ /**
12
+ * Format sessions as inline compact tokens, wrapped to fit maxWidth.
13
+ * Each token: "{idx}{dot}{name}" — e.g. "1●Alpha" with colored dot and bold name.
14
+ * Returns array of formatted row strings (one per display row).
15
+ */
16
+ declare function formatCompactRows(sessions: DaemonSessionState[], maxWidth: number): string[];
17
+ /** Compute how many display rows compact mode needs (minimum 1). */
18
+ declare function computeCompactRowCount(sessions: DaemonSessionState[], maxWidth: number): number;
3
19
  declare function phaseDisplay(phase: DaemonPhase, paused: boolean, spinnerFrame: number): string;
4
20
  export interface ActivityEntry {
5
21
  time: string;
@@ -26,6 +42,10 @@ export declare class TUI {
26
42
  private searchPattern;
27
43
  private hoverSessionIdx;
28
44
  private activityTimestamps;
45
+ private sortMode;
46
+ private lastChangeAt;
47
+ private prevLastActivity;
48
+ private compactMode;
29
49
  private viewMode;
30
50
  private drilldownSessionId;
31
51
  private sessionOutputs;
@@ -43,6 +63,14 @@ export declare class TUI {
43
63
  isActive(): boolean;
44
64
  /** Return the current number of sessions (for mouse hit testing) */
45
65
  getSessionCount(): number;
66
+ /** Set the session sort mode and repaint. */
67
+ setSortMode(mode: SortMode): void;
68
+ /** Return the current sort mode. */
69
+ getSortMode(): SortMode;
70
+ /** Toggle or set compact mode. Recomputes layout and repaints. */
71
+ setCompact(enabled: boolean): void;
72
+ /** Return whether compact mode is enabled. */
73
+ isCompact(): boolean;
46
74
  updateState(opts: {
47
75
  phase?: DaemonPhase;
48
76
  pollCount?: number;
@@ -133,5 +161,5 @@ declare function formatSearchIndicator(pattern: string, matchCount: number, tota
133
161
  * (row = headerHeight + 2 + i for 0-indexed session i)
134
162
  */
135
163
  export declare function hitTestSession(row: number, headerHeight: number, sessionCount: number): number | null;
136
- export { formatActivity, formatSessionCard, truncateAnsi, truncatePlain, padBoxLine, padBoxLineHover, padToWidth, stripAnsiForLen, phaseDisplay, computeScrollSlice, formatScrollIndicator, formatDrilldownScrollIndicator, formatPrompt, formatDrilldownHeader, matchesSearch, formatSearchIndicator, computeSparkline, formatSparkline };
164
+ export { formatActivity, formatSessionCard, truncateAnsi, truncatePlain, padBoxLine, padBoxLineHover, padToWidth, stripAnsiForLen, phaseDisplay, computeScrollSlice, formatScrollIndicator, formatDrilldownScrollIndicator, formatPrompt, formatDrilldownHeader, matchesSearch, formatSearchIndicator, computeSparkline, formatSparkline, sortSessions, nextSortMode, SORT_MODES, formatCompactRows, computeCompactRowCount, COMPACT_NAME_LEN };
137
165
  //# sourceMappingURL=tui.d.ts.map
package/dist/tui.js CHANGED
@@ -21,6 +21,77 @@ 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
+ }
51
+ // ── Compact mode ────────────────────────────────────────────────────────────
52
+ /** Max name length in compact token. */
53
+ const COMPACT_NAME_LEN = 10;
54
+ /**
55
+ * Format sessions as inline compact tokens, wrapped to fit maxWidth.
56
+ * Each token: "{idx}{dot}{name}" — e.g. "1●Alpha" with colored dot and bold name.
57
+ * Returns array of formatted row strings (one per display row).
58
+ */
59
+ function formatCompactRows(sessions, maxWidth) {
60
+ if (sessions.length === 0)
61
+ return [`${DIM}no agents connected${RESET}`];
62
+ const tokens = [];
63
+ const widths = [];
64
+ for (let i = 0; i < sessions.length; i++) {
65
+ const s = sessions[i];
66
+ const idx = String(i + 1);
67
+ const dot = STATUS_DOT[s.status] ?? `${AMBER}${DOT.filled}${RESET}`;
68
+ const name = truncatePlain(s.title, COMPACT_NAME_LEN);
69
+ tokens.push(`${SLATE}${idx}${RESET}${dot}${BOLD}${name}${RESET}`);
70
+ widths.push(idx.length + 1 + name.length); // idx chars + dot char + name chars
71
+ }
72
+ const rows = [];
73
+ let currentRow = "";
74
+ let currentWidth = 0;
75
+ for (let i = 0; i < tokens.length; i++) {
76
+ const gap = currentWidth > 0 ? 2 : 0;
77
+ if (currentWidth + gap + widths[i] > maxWidth && currentWidth > 0) {
78
+ rows.push(currentRow);
79
+ currentRow = tokens[i];
80
+ currentWidth = widths[i];
81
+ }
82
+ else {
83
+ currentRow += (currentWidth > 0 ? " " : "") + tokens[i];
84
+ currentWidth += gap + widths[i];
85
+ }
86
+ }
87
+ if (currentRow)
88
+ rows.push(currentRow);
89
+ return rows;
90
+ }
91
+ /** Compute how many display rows compact mode needs (minimum 1). */
92
+ function computeCompactRowCount(sessions, maxWidth) {
93
+ return Math.max(1, formatCompactRows(sessions, maxWidth).length);
94
+ }
24
95
  // ── Status rendering ────────────────────────────────────────────────────────
25
96
  const STATUS_DOT = {
26
97
  working: `${LIME}${DOT.filled}${RESET}`,
@@ -66,6 +137,10 @@ export class TUI {
66
137
  searchPattern = null; // active search filter pattern
67
138
  hoverSessionIdx = null; // 1-indexed session under mouse cursor (null = none)
68
139
  activityTimestamps = []; // epoch ms of each log() call for sparkline
140
+ sortMode = "default";
141
+ lastChangeAt = new Map(); // session ID → epoch ms of last activity change
142
+ prevLastActivity = new Map(); // session ID → previous lastActivity string
143
+ compactMode = false;
69
144
  // drill-down mode: show a single session's full output
70
145
  viewMode = "overview";
71
146
  drilldownSessionId = null;
@@ -122,6 +197,35 @@ export class TUI {
122
197
  getSessionCount() {
123
198
  return this.sessions.length;
124
199
  }
200
+ /** Set the session sort mode and repaint. */
201
+ setSortMode(mode) {
202
+ if (mode === this.sortMode)
203
+ return;
204
+ this.sortMode = mode;
205
+ // re-sort current sessions and repaint
206
+ this.sessions = sortSessions(this.sessions, this.sortMode, this.lastChangeAt);
207
+ if (this.active) {
208
+ this.paintSessions();
209
+ }
210
+ }
211
+ /** Return the current sort mode. */
212
+ getSortMode() {
213
+ return this.sortMode;
214
+ }
215
+ /** Toggle or set compact mode. Recomputes layout and repaints. */
216
+ setCompact(enabled) {
217
+ if (enabled === this.compactMode)
218
+ return;
219
+ this.compactMode = enabled;
220
+ if (this.active) {
221
+ this.computeLayout(this.sessions.length);
222
+ this.paintAll();
223
+ }
224
+ }
225
+ /** Return whether compact mode is enabled. */
226
+ isCompact() {
227
+ return this.compactMode;
228
+ }
125
229
  // ── State updates ───────────────────────────────────────────────────────
126
230
  updateState(opts) {
127
231
  if (opts.phase !== undefined)
@@ -137,8 +241,19 @@ export class TUI {
137
241
  if (opts.pendingCount !== undefined)
138
242
  this.pendingCount = opts.pendingCount;
139
243
  if (opts.sessions !== undefined) {
140
- const sessionCountChanged = opts.sessions.length !== this.sessions.length;
141
- this.sessions = opts.sessions;
244
+ // track activity changes for sort-by-activity
245
+ const now = Date.now();
246
+ for (const s of opts.sessions) {
247
+ const prev = this.prevLastActivity.get(s.id);
248
+ if (s.lastActivity !== undefined && s.lastActivity !== prev) {
249
+ this.lastChangeAt.set(s.id, now);
250
+ }
251
+ if (s.lastActivity !== undefined)
252
+ this.prevLastActivity.set(s.id, s.lastActivity);
253
+ }
254
+ const sorted = sortSessions(opts.sessions, this.sortMode, this.lastChangeAt);
255
+ const sessionCountChanged = sorted.length !== this.sessions.length;
256
+ this.sessions = sorted;
142
257
  if (sessionCountChanged) {
143
258
  this.computeLayout(this.sessions.length);
144
259
  this.paintAll();
@@ -404,7 +519,9 @@ export class TUI {
404
519
  }
405
520
  else {
406
521
  // overview: header (1) + sessions box + separator + activity + input
407
- const sessBodyRows = Math.max(sessionCount, 1);
522
+ const sessBodyRows = this.compactMode
523
+ ? computeCompactRowCount(this.sessions, this.cols - 2)
524
+ : Math.max(sessionCount, 1);
408
525
  this.sessionRows = sessBodyRows + 2; // + top/bottom borders
409
526
  this.separatorRow = this.headerHeight + this.sessionRows + 1;
410
527
  this.inputRow = this.rows;
@@ -464,8 +581,11 @@ export class TUI {
464
581
  paintSessions() {
465
582
  const startRow = this.headerHeight + 1;
466
583
  const innerWidth = this.cols - 2; // inside the box borders
467
- // top border with label
468
- const label = " agents ";
584
+ // top border with label (includes compact/sort mode tags)
585
+ const sortTag = this.sortMode !== "default" ? this.sortMode : "";
586
+ const compactTag = this.compactMode ? "compact" : "";
587
+ const tags = [compactTag, sortTag].filter(Boolean).join(", ");
588
+ const label = tags ? ` agents (${tags}) ` : " agents ";
469
589
  const borderAfterLabel = Math.max(0, innerWidth - label.length);
470
590
  const topBorder = `${SLATE}${BOX.rtl}${BOX.h}${RESET}${SLATE}${label}${RESET}${SLATE}${BOX.h.repeat(borderAfterLabel)}${BOX.rtr}${RESET}`;
471
591
  process.stderr.write(SAVE_CURSOR + moveTo(startRow, 1) + CLEAR_LINE + truncateAnsi(topBorder, this.cols));
@@ -475,6 +595,15 @@ export class TUI {
475
595
  const padded = padBoxLine(empty, this.cols);
476
596
  process.stderr.write(moveTo(startRow + 1, 1) + CLEAR_LINE + padded);
477
597
  }
598
+ else if (this.compactMode) {
599
+ // compact: inline tokens, multiple per row
600
+ const compactRows = formatCompactRows(this.sessions, innerWidth - 1);
601
+ for (let r = 0; r < compactRows.length; r++) {
602
+ const line = `${SLATE}${BOX.v}${RESET} ${compactRows[r]}`;
603
+ const padded = padBoxLine(line, this.cols);
604
+ process.stderr.write(moveTo(startRow + 1 + r, 1) + CLEAR_LINE + padded);
605
+ }
606
+ }
478
607
  else {
479
608
  for (let i = 0; i < this.sessions.length; i++) {
480
609
  const s = this.sessions[i];
@@ -486,7 +615,9 @@ export class TUI {
486
615
  }
487
616
  }
488
617
  // bottom border
489
- const bodyRows = Math.max(this.sessions.length, 1);
618
+ const bodyRows = this.compactMode
619
+ ? computeCompactRowCount(this.sessions, innerWidth)
620
+ : Math.max(this.sessions.length, 1);
490
621
  const bottomRow = startRow + 1 + bodyRows;
491
622
  const bottomBorder = `${SLATE}${BOX.rbl}${BOX.h.repeat(Math.max(0, this.cols - 2))}${BOX.rbr}${RESET}`;
492
623
  process.stderr.write(moveTo(bottomRow, 1) + CLEAR_LINE + truncateAnsi(bottomBorder, this.cols));
@@ -875,5 +1006,5 @@ export function hitTestSession(row, headerHeight, sessionCount) {
875
1006
  return row - firstSessionRow + 1; // 1-indexed
876
1007
  }
877
1008
  // ── Exported pure helpers (for testing) ─────────────────────────────────────
878
- export { formatActivity, formatSessionCard, truncateAnsi, truncatePlain, padBoxLine, padBoxLineHover, padToWidth, stripAnsiForLen, phaseDisplay, computeScrollSlice, formatScrollIndicator, formatDrilldownScrollIndicator, formatPrompt, formatDrilldownHeader, matchesSearch, formatSearchIndicator, computeSparkline, formatSparkline };
1009
+ export { formatActivity, formatSessionCard, truncateAnsi, truncatePlain, padBoxLine, padBoxLineHover, padToWidth, stripAnsiForLen, phaseDisplay, computeScrollSlice, formatScrollIndicator, formatDrilldownScrollIndicator, formatPrompt, formatDrilldownHeader, matchesSearch, formatSearchIndicator, computeSparkline, formatSparkline, sortSessions, nextSortMode, SORT_MODES, formatCompactRows, computeCompactRowCount, COMPACT_NAME_LEN };
879
1010
  //# sourceMappingURL=tui.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aoaoe",
3
- "version": "0.77.0",
3
+ "version": "0.79.0",
4
4
  "description": "Autonomous supervisor for agent-of-empires sessions using OpenCode or Claude Code",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",