aoaoe 0.79.0 → 0.80.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
@@ -378,6 +378,17 @@ async function main() {
378
378
  tui.setCompact(enabled);
379
379
  tui.log("system", `compact mode: ${enabled ? "on" : "off"}`);
380
380
  });
381
+ // wire /pin toggle
382
+ input.onPin((target) => {
383
+ const num = /^\d+$/.test(target) ? parseInt(target, 10) : undefined;
384
+ const ok = tui.togglePin(num ?? target);
385
+ if (ok) {
386
+ tui.log("system", `pin toggled: ${target}`);
387
+ }
388
+ else {
389
+ tui.log("system", `session not found: ${target}`);
390
+ }
391
+ });
381
392
  // wire mouse move to hover highlight on session cards (disabled in compact)
382
393
  input.onMouseMove((row, _col) => {
383
394
  if (tui.getViewMode() === "overview" && !tui.isCompact()) {
package/dist/input.d.ts CHANGED
@@ -5,6 +5,7 @@ export type SearchHandler = (pattern: string | null) => void;
5
5
  export type QuickSwitchHandler = (sessionNum: number) => void;
6
6
  export type SortHandler = (mode: string | null) => void;
7
7
  export type CompactHandler = () => void;
8
+ export type PinHandler = (target: string) => void;
8
9
  export interface MouseEvent {
9
10
  button: number;
10
11
  col: number;
@@ -32,6 +33,7 @@ export declare class InputReader {
32
33
  private quickSwitchHandler;
33
34
  private sortHandler;
34
35
  private compactHandler;
36
+ private pinHandler;
35
37
  private mouseDataListener;
36
38
  onScroll(handler: (dir: ScrollDirection) => void): void;
37
39
  onQueueChange(handler: (count: number) => void): void;
@@ -43,6 +45,7 @@ export declare class InputReader {
43
45
  onQuickSwitch(handler: QuickSwitchHandler): void;
44
46
  onSort(handler: SortHandler): void;
45
47
  onCompact(handler: CompactHandler): void;
48
+ onPin(handler: PinHandler): void;
46
49
  private notifyQueueChange;
47
50
  start(): void;
48
51
  drain(): string[];
package/dist/input.js CHANGED
@@ -37,6 +37,7 @@ export class InputReader {
37
37
  quickSwitchHandler = null;
38
38
  sortHandler = null;
39
39
  compactHandler = null;
40
+ pinHandler = null;
40
41
  mouseDataListener = null;
41
42
  // register a callback for scroll key events (PgUp/PgDn/Home/End)
42
43
  onScroll(handler) {
@@ -78,6 +79,10 @@ export class InputReader {
78
79
  onCompact(handler) {
79
80
  this.compactHandler = handler;
80
81
  }
82
+ // register a callback for pin/unpin commands (/pin <target>)
83
+ onPin(handler) {
84
+ this.pinHandler = handler;
85
+ }
81
86
  notifyQueueChange() {
82
87
  this.queueChangeHandler?.(this.queue.length);
83
88
  }
@@ -257,6 +262,7 @@ ${BOLD}navigation:${RESET}
257
262
  /back return to overview from drill-down
258
263
  /sort [mode] sort sessions: status, name, activity, default (or cycle)
259
264
  /compact toggle compact mode (dense session panel)
265
+ /pin [N|name] pin/unpin a session to the top (toggle)
260
266
  /search <pattern> filter activity entries by substring (case-insensitive)
261
267
  /search clear active search filter
262
268
  click session click an agent card to drill down (click again to go back)
@@ -354,6 +360,21 @@ ${BOLD}other:${RESET}
354
360
  console.error(`${DIM}compact mode not available (no TUI)${RESET}`);
355
361
  }
356
362
  break;
363
+ case "/pin": {
364
+ const pinArg = line.slice("/pin".length).trim();
365
+ if (this.pinHandler) {
366
+ if (pinArg) {
367
+ this.pinHandler(pinArg);
368
+ }
369
+ else {
370
+ console.error(`${DIM}usage: /pin <N|name> — toggle pin for a session${RESET}`);
371
+ }
372
+ }
373
+ else {
374
+ console.error(`${DIM}pin not available (no TUI)${RESET}`);
375
+ }
376
+ break;
377
+ }
357
378
  case "/search": {
358
379
  const searchArg = line.slice("/search".length).trim();
359
380
  if (this.searchHandler) {
package/dist/tui.d.ts CHANGED
@@ -2,18 +2,20 @@ import type { DaemonSessionState, DaemonPhase } from "./types.js";
2
2
  import type { HistoryEntry } from "./tui-history.js";
3
3
  export type SortMode = "default" | "status" | "name" | "activity";
4
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[];
5
+ /** Sort sessions by mode. Pinned sessions always sort first (stable). Returns a new array (never mutates). */
6
+ declare function sortSessions(sessions: DaemonSessionState[], mode: SortMode, lastChangeAt?: Map<string, number>, pinnedIds?: Set<string>): DaemonSessionState[];
7
7
  /** Cycle to the next sort mode. */
8
8
  declare function nextSortMode(current: SortMode): SortMode;
9
9
  /** Max name length in compact token. */
10
10
  declare const COMPACT_NAME_LEN = 10;
11
+ /** Pin indicator for pinned sessions. */
12
+ declare const PIN_ICON = "\u25B2";
11
13
  /**
12
14
  * Format sessions as inline compact tokens, wrapped to fit maxWidth.
13
- * Each token: "{idx}{dot}{name}" — e.g. "1Alpha" with colored dot and bold name.
15
+ * Each token: "{idx}{pin?}{dot}{name}" — e.g. "1▲●Alpha" for pinned, "2●Bravo" for unpinned.
14
16
  * Returns array of formatted row strings (one per display row).
15
17
  */
16
- declare function formatCompactRows(sessions: DaemonSessionState[], maxWidth: number): string[];
18
+ declare function formatCompactRows(sessions: DaemonSessionState[], maxWidth: number, pinnedIds?: Set<string>): string[];
17
19
  /** Compute how many display rows compact mode needs (minimum 1). */
18
20
  declare function computeCompactRowCount(sessions: DaemonSessionState[], maxWidth: number): number;
19
21
  declare function phaseDisplay(phase: DaemonPhase, paused: boolean, spinnerFrame: number): string;
@@ -46,6 +48,7 @@ export declare class TUI {
46
48
  private lastChangeAt;
47
49
  private prevLastActivity;
48
50
  private compactMode;
51
+ private pinnedIds;
49
52
  private viewMode;
50
53
  private drilldownSessionId;
51
54
  private sessionOutputs;
@@ -71,6 +74,15 @@ export declare class TUI {
71
74
  setCompact(enabled: boolean): void;
72
75
  /** Return whether compact mode is enabled. */
73
76
  isCompact(): boolean;
77
+ /**
78
+ * Toggle pin for a session (by 1-indexed number, ID, ID prefix, or title).
79
+ * Pinned sessions always sort to the top. Returns true if session found.
80
+ */
81
+ togglePin(sessionIdOrIndex: string | number): boolean;
82
+ /** Check if a session ID is pinned. */
83
+ isPinned(id: string): boolean;
84
+ /** Return count of pinned sessions. */
85
+ getPinnedCount(): number;
74
86
  updateState(opts: {
75
87
  phase?: DaemonPhase;
76
88
  pollCount?: number;
@@ -161,5 +173,5 @@ declare function formatSearchIndicator(pattern: string, matchCount: number, tota
161
173
  * (row = headerHeight + 2 + i for 0-indexed session i)
162
174
  */
163
175
  export declare function hitTestSession(row: number, headerHeight: number, sessionCount: number): number | null;
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 };
176
+ 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, PIN_ICON };
165
177
  //# sourceMappingURL=tui.d.ts.map
package/dist/tui.js CHANGED
@@ -26,8 +26,8 @@ const STATUS_PRIORITY = {
26
26
  error: 0, waiting: 1, working: 2, running: 2,
27
27
  idle: 3, done: 4, stopped: 5, unknown: 6,
28
28
  };
29
- /** Sort sessions by mode. Returns a new array (never mutates). */
30
- function sortSessions(sessions, mode, lastChangeAt) {
29
+ /** Sort sessions by mode. Pinned sessions always sort first (stable). Returns a new array (never mutates). */
30
+ function sortSessions(sessions, mode, lastChangeAt, pinnedIds) {
31
31
  const copy = sessions.slice();
32
32
  switch (mode) {
33
33
  case "status":
@@ -41,6 +41,10 @@ function sortSessions(sessions, mode, lastChangeAt) {
41
41
  break;
42
42
  // "default" — preserve original order
43
43
  }
44
+ // stable-sort pinned to top (preserves mode order within each group)
45
+ if (pinnedIds && pinnedIds.size > 0) {
46
+ copy.sort((a, b) => (pinnedIds.has(a.id) ? 0 : 1) - (pinnedIds.has(b.id) ? 0 : 1));
47
+ }
44
48
  return copy;
45
49
  }
46
50
  /** Cycle to the next sort mode. */
@@ -51,12 +55,14 @@ function nextSortMode(current) {
51
55
  // ── Compact mode ────────────────────────────────────────────────────────────
52
56
  /** Max name length in compact token. */
53
57
  const COMPACT_NAME_LEN = 10;
58
+ /** Pin indicator for pinned sessions. */
59
+ const PIN_ICON = "▲";
54
60
  /**
55
61
  * Format sessions as inline compact tokens, wrapped to fit maxWidth.
56
- * Each token: "{idx}{dot}{name}" — e.g. "1Alpha" with colored dot and bold name.
62
+ * Each token: "{idx}{pin?}{dot}{name}" — e.g. "1▲●Alpha" for pinned, "2●Bravo" for unpinned.
57
63
  * Returns array of formatted row strings (one per display row).
58
64
  */
59
- function formatCompactRows(sessions, maxWidth) {
65
+ function formatCompactRows(sessions, maxWidth, pinnedIds) {
60
66
  if (sessions.length === 0)
61
67
  return [`${DIM}no agents connected${RESET}`];
62
68
  const tokens = [];
@@ -65,9 +71,11 @@ function formatCompactRows(sessions, maxWidth) {
65
71
  const s = sessions[i];
66
72
  const idx = String(i + 1);
67
73
  const dot = STATUS_DOT[s.status] ?? `${AMBER}${DOT.filled}${RESET}`;
74
+ const pinned = pinnedIds?.has(s.id) ?? false;
75
+ const pin = pinned ? `${AMBER}${PIN_ICON}${RESET}` : "";
68
76
  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
77
+ tokens.push(`${SLATE}${idx}${RESET}${pin}${dot}${BOLD}${name}${RESET}`);
78
+ widths.push(idx.length + (pinned ? 1 : 0) + 1 + name.length);
71
79
  }
72
80
  const rows = [];
73
81
  let currentRow = "";
@@ -141,6 +149,7 @@ export class TUI {
141
149
  lastChangeAt = new Map(); // session ID → epoch ms of last activity change
142
150
  prevLastActivity = new Map(); // session ID → previous lastActivity string
143
151
  compactMode = false;
152
+ pinnedIds = new Set(); // pinned session IDs (always sort to top)
144
153
  // drill-down mode: show a single session's full output
145
154
  viewMode = "overview";
146
155
  drilldownSessionId = null;
@@ -203,7 +212,7 @@ export class TUI {
203
212
  return;
204
213
  this.sortMode = mode;
205
214
  // re-sort current sessions and repaint
206
- this.sessions = sortSessions(this.sessions, this.sortMode, this.lastChangeAt);
215
+ this.sessions = sortSessions(this.sessions, this.sortMode, this.lastChangeAt, this.pinnedIds);
207
216
  if (this.active) {
208
217
  this.paintSessions();
209
218
  }
@@ -226,6 +235,43 @@ export class TUI {
226
235
  isCompact() {
227
236
  return this.compactMode;
228
237
  }
238
+ /**
239
+ * Toggle pin for a session (by 1-indexed number, ID, ID prefix, or title).
240
+ * Pinned sessions always sort to the top. Returns true if session found.
241
+ */
242
+ togglePin(sessionIdOrIndex) {
243
+ let sessionId;
244
+ if (typeof sessionIdOrIndex === "number") {
245
+ sessionId = this.sessions[sessionIdOrIndex - 1]?.id;
246
+ }
247
+ else {
248
+ const needle = sessionIdOrIndex.toLowerCase();
249
+ const match = this.sessions.find((s) => s.id === sessionIdOrIndex || s.id.startsWith(needle) || s.title.toLowerCase() === needle);
250
+ sessionId = match?.id;
251
+ }
252
+ if (!sessionId)
253
+ return false;
254
+ if (this.pinnedIds.has(sessionId)) {
255
+ this.pinnedIds.delete(sessionId);
256
+ }
257
+ else {
258
+ this.pinnedIds.add(sessionId);
259
+ }
260
+ // re-sort and repaint
261
+ this.sessions = sortSessions(this.sessions, this.sortMode, this.lastChangeAt, this.pinnedIds);
262
+ if (this.active) {
263
+ this.paintSessions();
264
+ }
265
+ return true;
266
+ }
267
+ /** Check if a session ID is pinned. */
268
+ isPinned(id) {
269
+ return this.pinnedIds.has(id);
270
+ }
271
+ /** Return count of pinned sessions. */
272
+ getPinnedCount() {
273
+ return this.pinnedIds.size;
274
+ }
229
275
  // ── State updates ───────────────────────────────────────────────────────
230
276
  updateState(opts) {
231
277
  if (opts.phase !== undefined)
@@ -251,7 +297,7 @@ export class TUI {
251
297
  if (s.lastActivity !== undefined)
252
298
  this.prevLastActivity.set(s.id, s.lastActivity);
253
299
  }
254
- const sorted = sortSessions(opts.sessions, this.sortMode, this.lastChangeAt);
300
+ const sorted = sortSessions(opts.sessions, this.sortMode, this.lastChangeAt, this.pinnedIds);
255
301
  const sessionCountChanged = sorted.length !== this.sessions.length;
256
302
  this.sessions = sorted;
257
303
  if (sessionCountChanged) {
@@ -596,8 +642,8 @@ export class TUI {
596
642
  process.stderr.write(moveTo(startRow + 1, 1) + CLEAR_LINE + padded);
597
643
  }
598
644
  else if (this.compactMode) {
599
- // compact: inline tokens, multiple per row
600
- const compactRows = formatCompactRows(this.sessions, innerWidth - 1);
645
+ // compact: inline tokens, multiple per row (with pin indicators)
646
+ const compactRows = formatCompactRows(this.sessions, innerWidth - 1, this.pinnedIds);
601
647
  for (let r = 0; r < compactRows.length; r++) {
602
648
  const line = `${SLATE}${BOX.v}${RESET} ${compactRows[r]}`;
603
649
  const padded = padBoxLine(line, this.cols);
@@ -609,7 +655,10 @@ export class TUI {
609
655
  const s = this.sessions[i];
610
656
  const isHovered = this.hoverSessionIdx === i + 1; // 1-indexed
611
657
  const bg = isHovered ? BG_HOVER : "";
612
- const line = `${bg}${SLATE}${BOX.v}${RESET}${bg} ${formatSessionCard(s, innerWidth - 1)}`;
658
+ const pinned = this.pinnedIds.has(s.id);
659
+ const pin = pinned ? `${AMBER}${PIN_ICON}${RESET} ` : "";
660
+ const cardWidth = pinned ? innerWidth - 3 : innerWidth - 1; // pin takes 2 extra chars
661
+ const line = `${bg}${SLATE}${BOX.v}${RESET}${bg} ${pin}${formatSessionCard(s, cardWidth)}`;
613
662
  const padded = padBoxLineHover(line, this.cols, isHovered);
614
663
  process.stderr.write(moveTo(startRow + 1 + i, 1) + CLEAR_LINE + padded);
615
664
  }
@@ -639,7 +688,10 @@ export class TUI {
639
688
  const s = this.sessions[i];
640
689
  const isHovered = this.hoverSessionIdx === idx;
641
690
  const bg = isHovered ? BG_HOVER : "";
642
- const line = `${bg}${SLATE}${BOX.v}${RESET}${bg} ${formatSessionCard(s, innerWidth - 1)}`;
691
+ const pinned = this.pinnedIds.has(s.id);
692
+ const pin = pinned ? `${AMBER}${PIN_ICON}${RESET} ` : "";
693
+ const cardWidth = pinned ? innerWidth - 3 : innerWidth - 1;
694
+ const line = `${bg}${SLATE}${BOX.v}${RESET}${bg} ${pin}${formatSessionCard(s, cardWidth)}`;
643
695
  const padded = padBoxLineHover(line, this.cols, isHovered);
644
696
  process.stderr.write(SAVE_CURSOR + moveTo(startRow + 1 + i, 1) + CLEAR_LINE + padded + RESTORE_CURSOR);
645
697
  }
@@ -1006,5 +1058,5 @@ export function hitTestSession(row, headerHeight, sessionCount) {
1006
1058
  return row - firstSessionRow + 1; // 1-indexed
1007
1059
  }
1008
1060
  // ── Exported pure helpers (for testing) ─────────────────────────────────────
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 };
1061
+ 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, PIN_ICON };
1010
1062
  //# sourceMappingURL=tui.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aoaoe",
3
- "version": "0.79.0",
3
+ "version": "0.80.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",