aoaoe 0.114.0 → 0.116.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
@@ -757,6 +757,46 @@ async function main() {
757
757
  if (!any)
758
758
  tui.log("system", ` threshold: ${CONTEXT_BURN_THRESHOLD.toLocaleString()} tokens/min`);
759
759
  });
760
+ // wire /copy session pane output to clipboard
761
+ input.onCopySession((target) => {
762
+ // resolve target: null = current drill-down session
763
+ let lines = null;
764
+ let label = "current session";
765
+ if (target === null) {
766
+ const ddId = tui.getDrilldownId();
767
+ if (!ddId) {
768
+ tui.log("system", "no session in view — use /copy N or drill into a session first");
769
+ return;
770
+ }
771
+ lines = tui.getSessionOutput(ddId);
772
+ const s = tui.getSessions().find((s) => s.id === ddId);
773
+ label = s?.title ?? ddId.slice(0, 8);
774
+ }
775
+ else {
776
+ const num = /^\d+$/.test(target) ? parseInt(target, 10) : undefined;
777
+ lines = tui.getSessionOutput(num ?? target);
778
+ label = target;
779
+ }
780
+ if (!lines || lines.length === 0) {
781
+ tui.log("system", `no output stored for ${label} — session may not have been polled yet`);
782
+ return;
783
+ }
784
+ const text = lines.join("\n") + "\n";
785
+ try {
786
+ execSync("pbcopy", { input: text, timeout: 5000 });
787
+ tui.log("system", `copied ${lines.length} lines from ${label} to clipboard`);
788
+ }
789
+ catch {
790
+ try {
791
+ const copyPath = join(homedir(), ".aoaoe", "copy.txt");
792
+ writeFileSync(copyPath, text, "utf-8");
793
+ tui.log("system", `saved ${lines.length} lines from ${label} to ~/.aoaoe/copy.txt`);
794
+ }
795
+ catch (writeErr) {
796
+ tui.log("error", `copy failed: ${writeErr}`);
797
+ }
798
+ }
799
+ });
760
800
  // wire /rename custom display name
761
801
  input.onRename((target, displayName) => {
762
802
  const num = /^\d+$/.test(target) ? parseInt(target, 10) : undefined;
package/dist/input.d.ts CHANGED
@@ -33,6 +33,7 @@ export type WatchdogHandler = (thresholdMinutes: number | null) => void;
33
33
  export type TopHandler = (mode: string) => void;
34
34
  export type CeilingHandler = () => void;
35
35
  export type RenameHandler = (target: string, name: string) => void;
36
+ export type CopySessionHandler = (target: string | null) => void;
36
37
  export interface MouseEvent {
37
38
  button: number;
38
39
  col: number;
@@ -88,6 +89,7 @@ export declare class InputReader {
88
89
  private topHandler;
89
90
  private ceilingHandler;
90
91
  private renameHandler;
92
+ private copySessionHandler;
91
93
  private aliases;
92
94
  private mouseDataListener;
93
95
  onScroll(handler: (dir: ScrollDirection) => void): void;
@@ -126,6 +128,7 @@ export declare class InputReader {
126
128
  onTop(handler: TopHandler): void;
127
129
  onCeiling(handler: CeilingHandler): void;
128
130
  onRename(handler: RenameHandler): void;
131
+ onCopySession(handler: CopySessionHandler): void;
129
132
  /** Set aliases from persisted prefs. */
130
133
  setAliases(aliases: Record<string, string>): void;
131
134
  /** Get current aliases as a plain object. */
package/dist/input.js CHANGED
@@ -66,6 +66,7 @@ export class InputReader {
66
66
  topHandler = null;
67
67
  ceilingHandler = null;
68
68
  renameHandler = null;
69
+ copySessionHandler = null;
69
70
  aliases = new Map(); // /shortcut → /full command
70
71
  mouseDataListener = null;
71
72
  // register a callback for scroll key events (PgUp/PgDn/Home/End)
@@ -212,6 +213,10 @@ export class InputReader {
212
213
  onRename(handler) {
213
214
  this.renameHandler = handler;
214
215
  }
216
+ // register a callback for /copy [N|name] — copy session pane output
217
+ onCopySession(handler) {
218
+ this.copySessionHandler = handler;
219
+ }
215
220
  /** Set aliases from persisted prefs. */
216
221
  setAliases(aliases) {
217
222
  this.aliases.clear();
@@ -448,6 +453,7 @@ ${BOLD}navigation:${RESET}
448
453
  /top [mode] rank sessions by errors (default), burn, or idle
449
454
  /ceiling show context token usage vs limit for all sessions
450
455
  /rename N|name [display] set custom display name in TUI (no display = clear)
456
+ /copy [N|name] copy session's current pane output to clipboard (default: current drill-down)
451
457
  /clip [N] copy last N activity entries to clipboard (default 20)
452
458
  /diff N show activity since bookmark N
453
459
  /mark bookmark current activity position
@@ -837,6 +843,16 @@ ${BOLD}other:${RESET}
837
843
  }
838
844
  break;
839
845
  }
846
+ case "/copy": {
847
+ const copyArg = line.slice("/copy".length).trim() || null;
848
+ if (this.copySessionHandler) {
849
+ this.copySessionHandler(copyArg);
850
+ }
851
+ else {
852
+ console.error(`${DIM}copy not available (no TUI)${RESET}`);
853
+ }
854
+ break;
855
+ }
840
856
  case "/rename": {
841
857
  const renameArg = line.slice("/rename".length).trim();
842
858
  if (!renameArg) {
package/dist/tui.d.ts CHANGED
@@ -28,10 +28,10 @@ declare const COMPACT_NAME_LEN = 10;
28
28
  declare const PIN_ICON = "\u25B2";
29
29
  /**
30
30
  * Format sessions as inline compact tokens, wrapped to fit maxWidth.
31
- * Each token: "{idx}{pin?}{mute?}{dot}{name}" — e.g. "1▲●Alpha" for pinned, "2◌●Bravo" for muted.
31
+ * Each token: "{idx}{pin?}{mute?}{dot}{name}{health?}" — e.g. "1▲●Alpha" for pinned, "2◌●Bravo" for muted.
32
32
  * Returns array of formatted row strings (one per display row).
33
33
  */
34
- declare function formatCompactRows(sessions: DaemonSessionState[], maxWidth: number, pinnedIds?: Set<string>, mutedIds?: Set<string>, noteIds?: Set<string>): string[];
34
+ declare function formatCompactRows(sessions: DaemonSessionState[], maxWidth: number, pinnedIds?: Set<string>, mutedIds?: Set<string>, noteIds?: Set<string>, healthScores?: Map<string, number>): string[];
35
35
  /** Compute how many display rows compact mode needs (minimum 1). */
36
36
  declare function computeCompactRowCount(sessions: DaemonSessionState[], maxWidth: number): number;
37
37
  declare function phaseDisplay(phase: DaemonPhase, paused: boolean, spinnerFrame: number): string;
@@ -304,6 +304,13 @@ export declare class TUI {
304
304
  getAllFirstSeen(): ReadonlyMap<string, number>;
305
305
  /** Return the activity buffer (for /clip export). */
306
306
  getActivityBuffer(): readonly ActivityEntry[];
307
+ /**
308
+ * Return the stored pane output lines for a session (by 1-indexed number, ID, prefix, or title).
309
+ * Returns null if session not found or no output stored.
310
+ */
311
+ getSessionOutput(sessionIdOrIndex: string | number): string[] | null;
312
+ /** Return the current drill-down session ID (for /copy default target). */
313
+ getDrilldownId(): string | null;
307
314
  /** Return per-session error counts (for /who). */
308
315
  getSessionErrorCounts(): ReadonlyMap<string, number>;
309
316
  /** Return recent error timestamps for a session (for sparkline rendering). */
package/dist/tui.js CHANGED
@@ -100,10 +100,10 @@ const COMPACT_NAME_LEN = 10;
100
100
  const PIN_ICON = "▲";
101
101
  /**
102
102
  * Format sessions as inline compact tokens, wrapped to fit maxWidth.
103
- * Each token: "{idx}{pin?}{mute?}{dot}{name}" — e.g. "1▲●Alpha" for pinned, "2◌●Bravo" for muted.
103
+ * Each token: "{idx}{pin?}{mute?}{dot}{name}{health?}" — e.g. "1▲●Alpha" for pinned, "2◌●Bravo" for muted.
104
104
  * Returns array of formatted row strings (one per display row).
105
105
  */
106
- function formatCompactRows(sessions, maxWidth, pinnedIds, mutedIds, noteIds) {
106
+ function formatCompactRows(sessions, maxWidth, pinnedIds, mutedIds, noteIds, healthScores) {
107
107
  if (sessions.length === 0)
108
108
  return [`${DIM}no agents connected${RESET}`];
109
109
  const tokens = [];
@@ -119,8 +119,13 @@ function formatCompactRows(sessions, maxWidth, pinnedIds, mutedIds, noteIds) {
119
119
  const muteIcon = muted ? `${DIM}${MUTE_ICON}${RESET}` : "";
120
120
  const noteIcon = noted ? `${TEAL}${NOTE_ICON}${RESET}` : "";
121
121
  const name = truncatePlain(s.title, COMPACT_NAME_LEN);
122
- tokens.push(`${SLATE}${idx}${RESET}${pin}${muteIcon}${noteIcon}${dot}${BOLD}${name}${RESET}`);
123
- widths.push(idx.length + (pinned ? 1 : 0) + (muted ? 1 : 0) + (noted ? 1 : 0) + 1 + name.length);
122
+ // health indicator: single ⬡ glyph when score < HEALTH_GOOD, colored by severity
123
+ const score = healthScores?.get(s.id);
124
+ const healthGlyph = (score !== undefined && score < HEALTH_GOOD)
125
+ ? `${score < HEALTH_WARN ? ROSE : AMBER}${HEALTH_ICON}${RESET}` : "";
126
+ const healthWidth = (score !== undefined && score < HEALTH_GOOD) ? 1 : 0;
127
+ tokens.push(`${SLATE}${idx}${RESET}${pin}${muteIcon}${noteIcon}${dot}${BOLD}${name}${RESET}${healthGlyph}`);
128
+ widths.push(idx.length + (pinned ? 1 : 0) + (muted ? 1 : 0) + (noted ? 1 : 0) + 1 + name.length + healthWidth);
124
129
  }
125
130
  const rows = [];
126
131
  let currentRow = "";
@@ -281,7 +286,7 @@ export const BUILTIN_COMMANDS = new Set([
281
286
  "/pin", "/bell", "/focus", "/mute", "/unmute-all", "/filter", "/who",
282
287
  "/uptime", "/auto-pin", "/note", "/notes", "/clip", "/diff", "/mark",
283
288
  "/jump", "/marks", "/search", "/alias", "/insist", "/task", "/tasks",
284
- "/group", "/groups", "/group-filter", "/burn-rate", "/snapshot", "/broadcast", "/watchdog", "/top", "/ceiling", "/rename",
289
+ "/group", "/groups", "/group-filter", "/burn-rate", "/snapshot", "/broadcast", "/watchdog", "/top", "/ceiling", "/rename", "/copy",
285
290
  ]);
286
291
  /** Resolve a slash command through the alias map. Returns the expanded command or the original. */
287
292
  export function resolveAlias(line, aliases) {
@@ -829,6 +834,28 @@ export class TUI {
829
834
  getActivityBuffer() {
830
835
  return this.activityBuffer;
831
836
  }
837
+ /**
838
+ * Return the stored pane output lines for a session (by 1-indexed number, ID, prefix, or title).
839
+ * Returns null if session not found or no output stored.
840
+ */
841
+ getSessionOutput(sessionIdOrIndex) {
842
+ let sessionId;
843
+ if (typeof sessionIdOrIndex === "number") {
844
+ sessionId = this.sessions[sessionIdOrIndex - 1]?.id;
845
+ }
846
+ else {
847
+ const needle = sessionIdOrIndex.toLowerCase();
848
+ const match = this.sessions.find((s) => s.id === sessionIdOrIndex || s.id.startsWith(needle) || s.title.toLowerCase() === needle);
849
+ sessionId = match?.id;
850
+ }
851
+ if (!sessionId)
852
+ return null;
853
+ return this.sessionOutputs.get(sessionId) ?? null;
854
+ }
855
+ /** Return the current drill-down session ID (for /copy default target). */
856
+ getDrilldownId() {
857
+ return this.drilldownSessionId;
858
+ }
832
859
  /** Return per-session error counts (for /who). */
833
860
  getSessionErrorCounts() {
834
861
  return this.sessionErrorCounts;
@@ -1521,9 +1548,26 @@ export class TUI {
1521
1548
  process.stderr.write(moveTo(startRow + 1, 1) + CLEAR_LINE + padded);
1522
1549
  }
1523
1550
  else if (this.compactMode) {
1524
- // compact: inline tokens, multiple per row (with pin indicators)
1551
+ // compact: inline tokens, multiple per row (with pin indicators + health glyphs)
1552
+ const nowMsCompact = Date.now();
1525
1553
  const noteIdSet = new Set(this.sessionNotes.keys());
1526
- const compactRows = formatCompactRows(visibleSessions, innerWidth - 1, this.pinnedIds, this.mutedIds, noteIdSet);
1554
+ const compactHealthScores = new Map();
1555
+ for (const s of visibleSessions) {
1556
+ const ceilingC = parseContextCeiling(s.contextTokens);
1557
+ const cfC = ceilingC ? ceilingC.current / ceilingC.max : null;
1558
+ const bhC = this.sessionContextHistory.get(s.id);
1559
+ const brC = bhC ? computeContextBurnRate(bhC, nowMsCompact) : null;
1560
+ const lcC = this.lastChangeAt.get(s.id);
1561
+ const idleC = lcC !== undefined ? nowMsCompact - lcC : null;
1562
+ compactHealthScores.set(s.id, computeHealthScore({
1563
+ errorCount: this.sessionErrorCounts.get(s.id) ?? 0,
1564
+ burnRatePerMin: brC,
1565
+ contextFraction: cfC,
1566
+ idleMs: idleC,
1567
+ watchdogThresholdMs: this.watchdogThresholdMs,
1568
+ }));
1569
+ }
1570
+ const compactRows = formatCompactRows(visibleSessions, innerWidth - 1, this.pinnedIds, this.mutedIds, noteIdSet, compactHealthScores);
1527
1571
  for (let r = 0; r < compactRows.length; r++) {
1528
1572
  const line = `${SLATE}${BOX.v}${RESET} ${compactRows[r]}`;
1529
1573
  const padded = padBoxLine(line, this.cols);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aoaoe",
3
- "version": "0.114.0",
3
+ "version": "0.116.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",