aoaoe 0.45.0 → 0.47.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/README.md CHANGED
@@ -410,8 +410,8 @@ The reasoner returns structured JSON decisions:
410
410
  { "action": "stop_session", "session": "<id>" }
411
411
  { "action": "create_agent", "path": "<dir>", "title": "<name>", "tool": "<agent>" }
412
412
  { "action": "remove_agent", "session": "<id>" }
413
- { "action": "report_progress", "repo": "<path>", "summary": "<milestone>" }
414
- { "action": "complete_task", "repo": "<path>", "summary": "<final status>" }
413
+ { "action": "report_progress", "session": "<id>", "summary": "<milestone>" }
414
+ { "action": "complete_task", "session": "<id>", "summary": "<final status>" }
415
415
  { "action": "wait" }
416
416
  ```
417
417
 
package/dist/chat.js CHANGED
@@ -377,6 +377,8 @@ export function getCountdownFromState(state, daemonRunning, now = Date.now()) {
377
377
  function checkDaemon() {
378
378
  if (isDaemonRunning()) {
379
379
  const state = readState();
380
+ if (!state)
381
+ return;
380
382
  const eta = getCountdown();
381
383
  console.log(`${GREEN}daemon connected${RESET} ${DIM}(${state.sessionCount} sessions, poll #${state.pollCount})${RESET}`);
382
384
  if (eta !== null)
package/dist/colors.d.ts CHANGED
@@ -1,11 +1,44 @@
1
1
  export declare const RESET = "\u001B[0m";
2
2
  export declare const BOLD = "\u001B[1m";
3
3
  export declare const DIM = "\u001B[2m";
4
+ export declare const ITALIC = "\u001B[3m";
4
5
  export declare const RED = "\u001B[31m";
5
6
  export declare const GREEN = "\u001B[32m";
6
7
  export declare const YELLOW = "\u001B[33m";
7
8
  export declare const CYAN = "\u001B[36m";
8
- export declare const MAGENTA = "\u001B[35m";
9
9
  export declare const WHITE = "\u001B[37m";
10
+ export declare const INDIGO = "\u001B[38;5;105m";
11
+ export declare const TEAL = "\u001B[38;5;73m";
12
+ export declare const AMBER = "\u001B[38;5;214m";
13
+ export declare const SLATE = "\u001B[38;5;245m";
14
+ export declare const ROSE = "\u001B[38;5;204m";
15
+ export declare const LIME = "\u001B[38;5;114m";
16
+ export declare const SKY = "\u001B[38;5;117m";
10
17
  export declare const BG_DARK = "\u001B[48;5;236m";
18
+ export declare const BG_DARKER = "\u001B[48;5;234m";
19
+ export declare const BG_PANEL = "\u001B[48;5;237m";
20
+ export declare const BG_HIGHLIGHT = "\u001B[48;5;238m";
21
+ export declare const BOX: {
22
+ readonly tl: "┌";
23
+ readonly tr: "┐";
24
+ readonly bl: "└";
25
+ readonly br: "┘";
26
+ readonly h: "─";
27
+ readonly v: "│";
28
+ readonly ltee: "├";
29
+ readonly rtee: "┤";
30
+ readonly ttee: "┬";
31
+ readonly btee: "┴";
32
+ readonly cross: "┼";
33
+ readonly rtl: "╭";
34
+ readonly rtr: "╮";
35
+ readonly rbl: "╰";
36
+ readonly rbr: "╯";
37
+ };
38
+ export declare const SPINNER: readonly ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
39
+ export declare const DOT: {
40
+ readonly filled: "●";
41
+ readonly hollow: "○";
42
+ readonly half: "◐";
43
+ };
11
44
  //# sourceMappingURL=colors.d.ts.map
package/dist/colors.js CHANGED
@@ -1,12 +1,38 @@
1
1
  // shared ANSI color constants — single source of truth for all CLI output
2
+ // basic 16-color
2
3
  export const RESET = "\x1b[0m";
3
4
  export const BOLD = "\x1b[1m";
4
5
  export const DIM = "\x1b[2m";
6
+ export const ITALIC = "\x1b[3m";
5
7
  export const RED = "\x1b[31m";
6
8
  export const GREEN = "\x1b[32m";
7
9
  export const YELLOW = "\x1b[33m";
8
10
  export const CYAN = "\x1b[36m";
9
- export const MAGENTA = "\x1b[35m";
10
11
  export const WHITE = "\x1b[37m";
11
- export const BG_DARK = "\x1b[48;5;236m";
12
+ // 256-color palette tasteful accents for the TUI
13
+ // use sparingly: these are highlights, not defaults
14
+ export const INDIGO = "\x1b[38;5;105m"; // muted purple-blue for branding
15
+ export const TEAL = "\x1b[38;5;73m"; // cool blue-green for info
16
+ export const AMBER = "\x1b[38;5;214m"; // warm orange for warnings/active
17
+ export const SLATE = "\x1b[38;5;245m"; // neutral gray for secondary text
18
+ export const ROSE = "\x1b[38;5;204m"; // soft red for errors
19
+ export const LIME = "\x1b[38;5;114m"; // fresh green for success/working
20
+ export const SKY = "\x1b[38;5;117m"; // light blue for reasoning
21
+ // background variants (256-color)
22
+ export const BG_DARK = "\x1b[48;5;236m"; // dark gray for header bar
23
+ export const BG_DARKER = "\x1b[48;5;234m"; // near-black for contrast panels
24
+ export const BG_PANEL = "\x1b[48;5;237m"; // subtle panel background
25
+ export const BG_HIGHLIGHT = "\x1b[48;5;238m"; // highlight row
26
+ // box-drawing characters — Unicode block elements
27
+ export const BOX = {
28
+ tl: "┌", tr: "┐", bl: "└", br: "┘",
29
+ h: "─", v: "│",
30
+ ltee: "├", rtee: "┤", ttee: "┬", btee: "┴", cross: "┼",
31
+ // rounded corners (softer look)
32
+ rtl: "╭", rtr: "╮", rbl: "╰", rbr: "╯",
33
+ };
34
+ // braille spinner frames for phase animation
35
+ export const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
36
+ // status dots — filled circle variants
37
+ export const DOT = { filled: "●", hollow: "○", half: "◐" };
12
38
  //# sourceMappingURL=colors.js.map
package/dist/config.js CHANGED
@@ -115,6 +115,32 @@ export function validateConfig(config) {
115
115
  if (config.contextFiles !== undefined && !Array.isArray(config.contextFiles)) {
116
116
  errors.push(`contextFiles must be an array of file paths, got ${typeof config.contextFiles}`);
117
117
  }
118
+ // claudeCode.yolo and claudeCode.resume must be booleans (string "false" is truthy)
119
+ if (config.claudeCode?.yolo !== undefined && typeof config.claudeCode.yolo !== "boolean") {
120
+ errors.push(`claudeCode.yolo must be a boolean, got ${typeof config.claudeCode.yolo}`);
121
+ }
122
+ if (config.claudeCode?.resume !== undefined && typeof config.claudeCode.resume !== "boolean") {
123
+ errors.push(`claudeCode.resume must be a boolean, got ${typeof config.claudeCode.resume}`);
124
+ }
125
+ // aoe.profile must be a non-empty string
126
+ if (config.aoe?.profile !== undefined && (typeof config.aoe.profile !== "string" || !config.aoe.profile)) {
127
+ errors.push(`aoe.profile must be a non-empty string, got ${JSON.stringify(config.aoe?.profile)}`);
128
+ }
129
+ // policies.autoAnswerPermissions must be a boolean
130
+ if (config.policies?.autoAnswerPermissions !== undefined && typeof config.policies.autoAnswerPermissions !== "boolean") {
131
+ errors.push(`policies.autoAnswerPermissions must be a boolean, got ${typeof config.policies.autoAnswerPermissions}`);
132
+ }
133
+ // policies.userActivityThresholdMs must be a non-negative number
134
+ if (config.policies?.userActivityThresholdMs !== undefined) {
135
+ const t = config.policies.userActivityThresholdMs;
136
+ if (typeof t !== "number" || !isFinite(t) || t < 0) {
137
+ errors.push(`policies.userActivityThresholdMs must be a number >= 0, got ${t}`);
138
+ }
139
+ }
140
+ // policies.allowDestructive must be a boolean
141
+ if (config.policies?.allowDestructive !== undefined && typeof config.policies.allowDestructive !== "boolean") {
142
+ errors.push(`policies.allowDestructive must be a boolean, got ${typeof config.policies.allowDestructive}`);
143
+ }
118
144
  if (errors.length > 0) {
119
145
  throw new Error(`invalid config:\n ${errors.join("\n ")}`);
120
146
  }
package/dist/context.js CHANGED
@@ -276,7 +276,7 @@ export function resolveProjectDirWithSource(basePath, sessionTitle, sessionDirs)
276
276
  }
277
277
  return { dir: null, source: null };
278
278
  }
279
- // convenience wrapper — returns just the path (backward compatible)
279
+ // convenience wrapper — returns just the path (used by tests and external callers)
280
280
  export function resolveProjectDir(basePath, sessionTitle, sessionDirs) {
281
281
  return resolveProjectDirWithSource(basePath, sessionTitle, sessionDirs).dir;
282
282
  }
package/dist/init.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type ResolutionSource } from "./context.js";
2
- import type { AoeSession } from "./types.js";
2
+ import { type AoeSession } from "./types.js";
3
3
  interface ToolCheck {
4
4
  name: string;
5
5
  path: string | null;
package/dist/init.js CHANGED
@@ -15,6 +15,7 @@ import { homedir } from "node:os";
15
15
  import { exec } from "./shell.js";
16
16
  import { resolveProjectDirWithSource } from "./context.js";
17
17
  import { saveTaskState, loadTaskState } from "./task-manager.js";
18
+ import { toSessionStatus } from "./types.js";
18
19
  import { createServer } from "node:net";
19
20
  import { BOLD, DIM, GREEN, YELLOW, RED, CYAN, RESET } from "./colors.js";
20
21
  // check if a tool is on PATH and get its version
@@ -66,7 +67,7 @@ async function getSessionStatus(id) {
66
67
  return "unknown";
67
68
  try {
68
69
  const data = JSON.parse(result.stdout);
69
- return String(data.status ?? "unknown");
70
+ return toSessionStatus(data.status);
70
71
  }
71
72
  catch {
72
73
  return "unknown";
package/dist/poller.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AoaoeConfig, Observation } from "./types.js";
1
+ import { type AoaoeConfig, type Observation } from "./types.js";
2
2
  export declare class Poller {
3
3
  private config;
4
4
  private previousSnapshots;
package/dist/poller.js CHANGED
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { exec } from "./shell.js";
3
3
  import { loadSessionContext } from "./context.js";
4
4
  import { getActivityForSessions } from "./activity.js";
5
+ import { toSessionStatus, } from "./types.js";
5
6
  export class Poller {
6
7
  config;
7
8
  previousSnapshots = new Map();
@@ -112,7 +113,7 @@ export class Poller {
112
113
  return "unknown";
113
114
  try {
114
115
  const data = JSON.parse(result.stdout);
115
- return (String(data.status ?? "unknown"));
116
+ return toSessionStatus(data.status);
116
117
  }
117
118
  catch {
118
119
  return "unknown";
@@ -220,7 +221,6 @@ export function quickHash(s) {
220
221
  // covers: CSI (\x1b[...X), OSC (\x1b]...ST), and simple two-char escapes (\x1bX)
221
222
  // also strips \x9b (8-bit CSI) sequences
222
223
  export function stripAnsi(s) {
223
- // eslint-disable-next-line no-control-regex
224
224
  return s.replace(/[\x1b\x9b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[^[\]()#;?0-9A-ORZcf-nqry=><~]/g, "");
225
225
  }
226
226
  export async function listAoeSessionsShared(timeoutMs = 10_000) {
@@ -172,7 +172,9 @@ export class TaskManager {
172
172
  try {
173
173
  sessions = JSON.parse(listResult.stdout);
174
174
  }
175
- catch { }
175
+ catch (e) {
176
+ console.error(`[tasks] failed to parse aoe list output: ${e}`);
177
+ }
176
178
  }
177
179
  for (const task of this.tasks) {
178
180
  if (task.status === "completed")
@@ -208,7 +210,9 @@ export class TaskManager {
208
210
  created.push(task.sessionTitle);
209
211
  }
210
212
  }
211
- catch { }
213
+ catch (e) {
214
+ console.error(`[tasks] failed to parse refreshed session list: ${e}`);
215
+ }
212
216
  }
213
217
  }
214
218
  else {
package/dist/tui.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { DaemonSessionState, DaemonPhase } from "./types.js";
2
+ declare function phaseDisplay(phase: DaemonPhase, paused: boolean, spinnerFrame: number): string;
2
3
  export interface ActivityEntry {
3
4
  time: string;
4
5
  tag: string;
@@ -17,6 +18,7 @@ export declare class TUI {
17
18
  private inputRow;
18
19
  private activityBuffer;
19
20
  private maxActivity;
21
+ private spinnerFrame;
20
22
  private phase;
21
23
  private pollCount;
22
24
  private sessions;
@@ -47,17 +49,17 @@ export declare class TUI {
47
49
  private repaintActivityRegion;
48
50
  private paintInputLine;
49
51
  }
52
+ declare function formatSessionCard(s: DaemonSessionState, maxWidth: number): string;
50
53
  declare function formatActivity(entry: ActivityEntry, maxCols: number): string;
54
+ declare function padBoxLine(line: string, totalWidth: number): string;
55
+ declare function padToWidth(line: string, totalWidth: number): string;
56
+ declare function stripAnsiForLen(str: string): number;
51
57
  declare function truncateAnsi(str: string, maxCols: number): string;
52
58
  declare function truncatePlain(str: string, max: number): string;
53
59
  /**
54
60
  * Format a session state as a plain-English sentence.
55
- * Examples:
56
- * "~ Adventure (opencode) — working on authentication"
57
- * "! Cloud Hypervisor (opencode) — error"
58
- * ". CHV (claude) — idle"
59
- * "~ Adventure (opencode) — you're working here"
61
+ * Kept for backward compatibility — used by non-TUI output paths.
60
62
  */
61
63
  export declare function formatSessionSentence(s: DaemonSessionState, maxCols: number): string;
62
- export { formatActivity, truncateAnsi, truncatePlain };
64
+ export { formatActivity, formatSessionCard, truncateAnsi, truncatePlain, padBoxLine, padToWidth, stripAnsiForLen, phaseDisplay };
63
65
  //# sourceMappingURL=tui.d.ts.map
package/dist/tui.js CHANGED
@@ -1,4 +1,4 @@
1
- import { BOLD, DIM, RESET, GREEN, YELLOW, RED, CYAN, WHITE, BG_DARK } from "./colors.js";
1
+ import { BOLD, DIM, RESET, GREEN, CYAN, WHITE, BG_DARK, INDIGO, TEAL, AMBER, SLATE, ROSE, LIME, SKY, BOX, SPINNER, DOT, } from "./colors.js";
2
2
  // ── ANSI helpers ────────────────────────────────────────────────────────────
3
3
  const ESC = "\x1b";
4
4
  const CSI = `${ESC}[`;
@@ -15,15 +15,30 @@ const RESTORE_CURSOR = `${ESC}8`;
15
15
  const moveTo = (row, col) => `${CSI}${row};${col}H`;
16
16
  const setScrollRegion = (top, bottom) => `${CSI}${top};${bottom}r`;
17
17
  const resetScrollRegion = () => `${CSI}r`;
18
- // status icons
19
- const STATUS_ICONS = {
20
- working: `${GREEN}~${RESET}`,
21
- idle: `${DIM}.${RESET}`,
22
- waiting: `${YELLOW}~${RESET}`,
23
- done: `${GREEN}+${RESET}`,
24
- error: `${RED}!${RESET}`,
25
- stopped: `${DIM}x${RESET}`,
18
+ // ── Status rendering ────────────────────────────────────────────────────────
19
+ const STATUS_DOT = {
20
+ working: `${LIME}${DOT.filled}${RESET}`,
21
+ running: `${LIME}${DOT.filled}${RESET}`,
22
+ idle: `${SLATE}${DOT.hollow}${RESET}`,
23
+ waiting: `${AMBER}${DOT.half}${RESET}`,
24
+ done: `${GREEN}${DOT.filled}${RESET}`,
25
+ error: `${ROSE}${DOT.filled}${RESET}`,
26
+ stopped: `${SLATE}${DOT.hollow}${RESET}`,
26
27
  };
28
+ // phase colors and labels
29
+ function phaseDisplay(phase, paused, spinnerFrame) {
30
+ if (paused)
31
+ return `${AMBER}${BOLD}PAUSED${RESET}`;
32
+ const frame = SPINNER[spinnerFrame % SPINNER.length];
33
+ switch (phase) {
34
+ case "reasoning": return `${SKY}${frame} reasoning${RESET}`;
35
+ case "executing": return `${AMBER}${frame} executing${RESET}`;
36
+ case "polling": return `${LIME}${frame} polling${RESET}`;
37
+ case "interrupted": return `${ROSE}${BOLD}interrupted${RESET}`;
38
+ case "sleeping": return `${SLATE}sleeping${RESET}`;
39
+ default: return `${SLATE}${phase}${RESET}`;
40
+ }
41
+ }
27
42
  // ── TUI class ───────────────────────────────────────────────────────────────
28
43
  export class TUI {
29
44
  active = false;
@@ -31,13 +46,14 @@ export class TUI {
31
46
  cols = 80;
32
47
  rows = 24;
33
48
  headerHeight = 1; // top bar
34
- sessionRows = 0; // dynamic based on session count
49
+ sessionRows = 0; // dynamic: 2 (borders) + N sessions
35
50
  separatorRow = 0; // line between sessions and activity
36
51
  scrollTop = 0; // first row of scroll region
37
52
  scrollBottom = 0; // last row of scroll region
38
53
  inputRow = 0; // bottom input line
39
54
  activityBuffer = []; // ring buffer for activity log
40
55
  maxActivity = 500; // max entries to keep
56
+ spinnerFrame = 0; // current spinner animation frame
41
57
  // current state for repaints
42
58
  phase = "sleeping";
43
59
  pollCount = 0;
@@ -56,12 +72,16 @@ export class TUI {
56
72
  process.stderr.write(ALT_SCREEN_ON + CURSOR_HIDE + CLEAR_SCREEN);
57
73
  // handle terminal resize
58
74
  process.stdout.on("resize", () => this.onResize());
59
- // repaint header every second so countdown timer ticks down
75
+ // tick timer: countdown + spinner animation (~4 fps for smooth braille spin)
60
76
  this.countdownTimer = setInterval(() => {
61
- if (this.active && this.phase === "sleeping" && this.nextTickAt > 0) {
77
+ if (!this.active)
78
+ return;
79
+ this.spinnerFrame = (this.spinnerFrame + 1) % SPINNER.length;
80
+ // repaint header for countdown and spinner
81
+ if (this.phase !== "sleeping" || this.nextTickAt > 0) {
62
82
  this.paintHeader();
63
83
  }
64
- }, 1000);
84
+ }, 250);
65
85
  // initial layout
66
86
  this.computeLayout(0);
67
87
  this.paintAll();
@@ -129,8 +149,10 @@ export class TUI {
129
149
  computeLayout(sessionCount) {
130
150
  this.updateDimensions();
131
151
  // header: 1 row
132
- // sessions: 1 header + N sessions + 1 blank = N+2 rows (min 2 if no sessions)
133
- this.sessionRows = Math.max(sessionCount, 0) + 2;
152
+ // sessions: top border (1) + N session rows + bottom border (1) = N+2
153
+ // if no sessions, just show an empty box (2 rows: top + bottom borders)
154
+ const sessBodyRows = Math.max(sessionCount, 1); // at least 1 row for "no agents"
155
+ this.sessionRows = sessBodyRows + 2; // + top/bottom borders
134
156
  this.separatorRow = this.headerHeight + this.sessionRows + 1;
135
157
  // input line is the last row
136
158
  this.inputRow = this.rows;
@@ -138,7 +160,6 @@ export class TUI {
138
160
  this.scrollTop = this.separatorRow + 1;
139
161
  this.scrollBottom = this.rows - 1;
140
162
  if (this.active) {
141
- // set scroll region so activity log scrolls within bounds
142
163
  process.stderr.write(setScrollRegion(this.scrollTop, this.scrollBottom));
143
164
  }
144
165
  }
@@ -159,54 +180,64 @@ export class TUI {
159
180
  this.paintInputLine();
160
181
  }
161
182
  paintHeader() {
162
- const phaseText = this.paused
163
- ? `${YELLOW}PAUSED${RESET}`
164
- : this.phase === "reasoning"
165
- ? `${CYAN}reasoning...${RESET}`
166
- : this.phase === "executing"
167
- ? `${YELLOW}executing...${RESET}`
168
- : this.phase === "polling"
169
- ? `${GREEN}polling${RESET}`
170
- : `${DIM}sleeping${RESET}`;
171
- const sessCount = `${this.sessions.length} session${this.sessions.length !== 1 ? "s" : ""}`;
183
+ const phaseText = phaseDisplay(this.phase, this.paused, this.spinnerFrame);
184
+ const sessCount = `${this.sessions.length} agent${this.sessions.length !== 1 ? "s" : ""}`;
172
185
  const activeCount = this.sessions.filter((s) => s.userActive).length;
173
- const activeTag = activeCount > 0 ? ` ${DIM}|${RESET} ${YELLOW}${activeCount} user active${RESET}` : "";
186
+ const activeTag = activeCount > 0 ? ` ${SLATE}│${RESET} ${AMBER}${activeCount} user${RESET}` : "";
174
187
  // countdown to next tick (only in sleeping phase)
175
188
  let countdownTag = "";
176
189
  if (this.phase === "sleeping" && this.nextTickAt > 0) {
177
190
  const remaining = Math.max(0, Math.ceil((this.nextTickAt - Date.now()) / 1000));
178
- countdownTag = ` ${DIM}|${RESET} ${DIM}next: ${remaining}s${RESET}`;
191
+ countdownTag = ` ${SLATE}│${RESET} ${SLATE}${remaining}s${RESET}`;
179
192
  }
180
- // reasoner name
181
- const reasonerTag = this.reasonerName ? ` ${DIM}|${RESET} ${DIM}${this.reasonerName}${RESET}` : "";
182
- const line = ` ${BOLD}aoaoe${RESET} ${DIM}${this.version}${RESET} ${DIM}|${RESET} poll #${this.pollCount} ${DIM}|${RESET} ${sessCount} ${DIM}|${RESET} ${phaseText}${activeTag}${countdownTag}${reasonerTag}`;
193
+ // reasoner badge
194
+ const reasonerTag = this.reasonerName ? ` ${SLATE}│${RESET} ${TEAL}${this.reasonerName}${RESET}` : "";
195
+ const line = ` ${INDIGO}${BOLD}aoaoe${RESET} ${SLATE}${this.version}${RESET} ${SLATE}│${RESET} #${this.pollCount} ${SLATE}│${RESET} ${sessCount} ${SLATE}│${RESET} ${phaseText}${activeTag}${countdownTag}${reasonerTag}`;
183
196
  process.stderr.write(SAVE_CURSOR +
184
- moveTo(1, 1) + CLEAR_LINE + BG_DARK + WHITE + truncateAnsi(line, this.cols) + RESET +
197
+ moveTo(1, 1) + CLEAR_LINE + BG_DARK + WHITE + truncateAnsi(line, this.cols) + padToWidth(line, this.cols) + RESET +
185
198
  RESTORE_CURSOR);
186
199
  }
187
200
  paintSessions() {
188
201
  const startRow = this.headerHeight + 1;
189
- // session header friendly label instead of column abbreviations
190
- const hdr = ` ${DIM}agents:${RESET}`;
191
- process.stderr.write(SAVE_CURSOR + moveTo(startRow, 1) + CLEAR_LINE + hdr);
192
- for (let i = 0; i < this.sessions.length; i++) {
193
- const s = this.sessions[i];
194
- const line = ` ${formatSessionSentence(s, this.cols - 4)}`;
195
- process.stderr.write(moveTo(startRow + 1 + i, 1) + CLEAR_LINE + line);
202
+ const innerWidth = this.cols - 2; // inside the box borders
203
+ // top border with label
204
+ const label = " agents ";
205
+ const borderAfterLabel = Math.max(0, innerWidth - label.length);
206
+ const topBorder = `${SLATE}${BOX.rtl}${BOX.h}${RESET}${SLATE}${label}${RESET}${SLATE}${BOX.h.repeat(borderAfterLabel)}${BOX.rtr}${RESET}`;
207
+ process.stderr.write(SAVE_CURSOR + moveTo(startRow, 1) + CLEAR_LINE + truncateAnsi(topBorder, this.cols));
208
+ if (this.sessions.length === 0) {
209
+ // empty state
210
+ const empty = `${SLATE}${BOX.v}${RESET} ${DIM}no agents connected${RESET}`;
211
+ const padded = padBoxLine(empty, this.cols);
212
+ process.stderr.write(moveTo(startRow + 1, 1) + CLEAR_LINE + padded);
213
+ }
214
+ else {
215
+ for (let i = 0; i < this.sessions.length; i++) {
216
+ const s = this.sessions[i];
217
+ const line = `${SLATE}${BOX.v}${RESET} ${formatSessionCard(s, innerWidth - 1)}`;
218
+ const padded = padBoxLine(line, this.cols);
219
+ process.stderr.write(moveTo(startRow + 1 + i, 1) + CLEAR_LINE + padded);
220
+ }
196
221
  }
197
- // clear any leftover rows if session count decreased
198
- const totalSessionLines = this.sessions.length + 1; // +1 for header
199
- for (let r = startRow + totalSessionLines; r < this.separatorRow; r++) {
222
+ // bottom border
223
+ const bodyRows = Math.max(this.sessions.length, 1);
224
+ const bottomRow = startRow + 1 + bodyRows;
225
+ const bottomBorder = `${SLATE}${BOX.rbl}${BOX.h.repeat(Math.max(0, this.cols - 2))}${BOX.rbr}${RESET}`;
226
+ process.stderr.write(moveTo(bottomRow, 1) + CLEAR_LINE + truncateAnsi(bottomBorder, this.cols));
227
+ // clear any leftover rows below the box
228
+ for (let r = bottomRow + 1; r < this.separatorRow; r++) {
200
229
  process.stderr.write(moveTo(r, 1) + CLEAR_LINE);
201
230
  }
202
231
  process.stderr.write(RESTORE_CURSOR);
203
232
  }
204
233
  paintSeparator() {
205
- const hints = " ESC ESC: interrupt /help /explain /pause ";
206
- const prefix = "── activity ";
207
- const totalDecor = prefix.length + hints.length + 2; // 2 for surrounding ──
208
- const fill = Math.max(0, this.cols - totalDecor);
209
- const line = `${DIM}${prefix}${"─".repeat(Math.floor(fill / 2))}${hints}${"─".repeat(Math.ceil(fill / 2))}${RESET}`;
234
+ const hints = " esc esc: interrupt /help /explain /pause ";
235
+ const prefix = `${BOX.h}${BOX.h} activity `;
236
+ const totalLen = prefix.length + hints.length;
237
+ const fill = Math.max(0, this.cols - totalLen);
238
+ const left = Math.floor(fill / 2);
239
+ const right = Math.ceil(fill / 2);
240
+ const line = `${SLATE}${prefix}${BOX.h.repeat(left)}${DIM}${hints}${RESET}${SLATE}${BOX.h.repeat(right)}${RESET}`;
210
241
  process.stderr.write(SAVE_CURSOR + moveTo(this.separatorRow, 1) + CLEAR_LINE + truncateAnsi(line, this.cols) + RESTORE_CURSOR);
211
242
  }
212
243
  writeActivityLine(entry) {
@@ -219,7 +250,6 @@ export class TUI {
219
250
  this.paintInputLine();
220
251
  }
221
252
  repaintActivityRegion() {
222
- // repaint visible portion of activity buffer
223
253
  const visibleLines = this.scrollBottom - this.scrollTop + 1;
224
254
  const entries = this.activityBuffer.slice(-visibleLines);
225
255
  for (let i = 0; i < visibleLines; i++) {
@@ -234,25 +264,62 @@ export class TUI {
234
264
  }
235
265
  }
236
266
  paintInputLine() {
267
+ // phase-aware prompt styling
268
+ const prompt = this.paused
269
+ ? `${AMBER}${BOLD}paused >${RESET} `
270
+ : this.phase === "reasoning"
271
+ ? `${SKY}thinking >${RESET} `
272
+ : `${LIME}>${RESET} `;
237
273
  process.stderr.write(SAVE_CURSOR +
238
- moveTo(this.inputRow, 1) + CLEAR_LINE +
239
- `${GREEN}you >${RESET} ` +
274
+ moveTo(this.inputRow, 1) + CLEAR_LINE + prompt +
240
275
  RESTORE_CURSOR);
241
276
  }
242
277
  }
243
278
  // ── Formatting helpers ──────────────────────────────────────────────────────
279
+ // format a session as a card-style line (inside the box)
280
+ function formatSessionCard(s, maxWidth) {
281
+ const dot = STATUS_DOT[s.status] ?? `${AMBER}${DOT.filled}${RESET}`;
282
+ const name = `${BOLD}${s.title}${RESET}`;
283
+ const toolBadge = `${SLATE}${s.tool}${RESET}`;
284
+ // status description
285
+ let desc;
286
+ if (s.userActive) {
287
+ desc = `${AMBER}you're active${RESET}`;
288
+ }
289
+ else if (s.status === "working" || s.status === "running") {
290
+ desc = s.currentTask
291
+ ? truncatePlain(s.currentTask, Math.max(20, maxWidth - s.title.length - s.tool.length - 16))
292
+ : `${LIME}working${RESET}`;
293
+ }
294
+ else if (s.status === "idle" || s.status === "stopped") {
295
+ desc = `${SLATE}idle${RESET}`;
296
+ }
297
+ else if (s.status === "error") {
298
+ desc = `${ROSE}error${RESET}`;
299
+ }
300
+ else if (s.status === "done") {
301
+ desc = `${GREEN}done${RESET}`;
302
+ }
303
+ else if (s.status === "waiting") {
304
+ desc = `${AMBER}waiting${RESET}`;
305
+ }
306
+ else {
307
+ desc = `${SLATE}${s.status}${RESET}`;
308
+ }
309
+ return truncateAnsi(`${dot} ${name} ${toolBadge} ${SLATE}${BOX.h}${RESET} ${desc}`, maxWidth);
310
+ }
244
311
  // colorize an activity entry based on its tag
245
312
  function formatActivity(entry, maxCols) {
246
313
  const { time, tag, text } = entry;
247
- let color = DIM;
314
+ let color = SLATE;
248
315
  let prefix = tag;
249
316
  switch (tag) {
250
317
  case "observation":
251
- color = DIM;
318
+ color = SLATE;
252
319
  prefix = "obs";
253
320
  break;
254
321
  case "reasoner":
255
- color = CYAN;
322
+ color = SKY;
256
323
  break;
257
324
  case "explain":
258
325
  color = `${BOLD}${CYAN}`;
@@ -260,30 +327,46 @@ function formatActivity(entry, maxCols) {
260
327
  break;
261
328
  case "+ action":
262
329
  case "action":
263
- color = YELLOW;
264
- prefix = "+ action";
330
+ color = AMBER;
331
+ prefix = " action";
265
332
  break;
266
333
  case "! action":
267
334
  case "error":
268
- color = RED;
269
- prefix = "! action";
335
+ color = ROSE;
336
+ prefix = " error";
270
337
  break;
271
338
  case "you":
272
- color = GREEN;
339
+ color = LIME;
273
340
  break;
274
341
  case "system":
275
- color = DIM;
342
+ color = SLATE;
276
343
  break;
277
344
  case "status":
278
- color = DIM;
345
+ color = SLATE;
279
346
  break;
280
347
  default:
281
- color = DIM;
348
+ color = SLATE;
282
349
  break;
283
350
  }
284
- const formatted = ` ${DIM}${time}${RESET} ${color}[${prefix}]${RESET} ${text}`;
351
+ const formatted = ` ${SLATE}${time}${RESET} ${color}${prefix}${RESET} ${DIM}│${RESET} ${text}`;
285
352
  return truncateAnsi(formatted, maxCols);
286
353
  }
354
+ // pad a box line to end with the right border character
355
+ function padBoxLine(line, totalWidth) {
356
+ const visible = stripAnsiForLen(line);
357
+ const pad = Math.max(0, totalWidth - visible - 1); // -1 for closing border
358
+ return line + " ".repeat(pad) + `${SLATE}${BOX.v}${RESET}`;
359
+ }
360
+ // pad the header bar to fill the full width with background color
361
+ function padToWidth(line, totalWidth) {
362
+ const visible = stripAnsiForLen(line);
363
+ const pad = Math.max(0, totalWidth - visible);
364
+ return " ".repeat(pad);
365
+ }
366
+ // count visible characters (strip ANSI escapes)
367
+ function stripAnsiForLen(str) {
368
+ return str.replace(/\x1b\[[0-9;]*m/g, "").length;
369
+ }
287
370
  // truncate a string with ANSI codes to fit a column width (approximate)
288
371
  function truncateAnsi(str, maxCols) {
289
372
  // strip ANSI to count visible chars
@@ -299,46 +382,41 @@ function truncatePlain(str, max) {
299
382
  }
300
383
  /**
301
384
  * Format a session state as a plain-English sentence.
302
- * Examples:
303
- * "~ Adventure (opencode) — working on authentication"
304
- * "! Cloud Hypervisor (opencode) — error"
305
- * ". CHV (claude) — idle"
306
- * "~ Adventure (opencode) — you're working here"
385
+ * Kept for backward compatibility — used by non-TUI output paths.
307
386
  */
308
387
  export function formatSessionSentence(s, maxCols) {
309
- const icon = STATUS_ICONS[s.status] ?? `${YELLOW}?${RESET}`;
388
+ const dot = STATUS_DOT[s.status] ?? `${AMBER}${DOT.filled}${RESET}`;
310
389
  const name = s.title;
311
- const tool = `${DIM}(${s.tool})${RESET}`;
312
- // human-readable status descriptions
390
+ const tool = `${SLATE}(${s.tool})${RESET}`;
313
391
  let statusDesc;
314
392
  if (s.userActive) {
315
- statusDesc = `${YELLOW}you're working here${RESET}`;
393
+ statusDesc = `${AMBER}you're working here${RESET}`;
316
394
  }
317
395
  else if (s.status === "working") {
318
396
  if (s.currentTask) {
319
397
  statusDesc = truncatePlain(s.currentTask, Math.max(30, maxCols - name.length - s.tool.length - 20));
320
398
  }
321
399
  else {
322
- statusDesc = `${GREEN}working${RESET}`;
400
+ statusDesc = `${LIME}working${RESET}`;
323
401
  }
324
402
  }
325
403
  else if (s.status === "idle" || s.status === "stopped") {
326
- statusDesc = `${DIM}idle${RESET}`;
404
+ statusDesc = `${SLATE}idle${RESET}`;
327
405
  }
328
406
  else if (s.status === "error") {
329
- statusDesc = `${RED}hit an error${RESET}`;
407
+ statusDesc = `${ROSE}hit an error${RESET}`;
330
408
  }
331
409
  else if (s.status === "done") {
332
410
  statusDesc = `${GREEN}finished${RESET}`;
333
411
  }
334
412
  else if (s.status === "waiting") {
335
- statusDesc = `${YELLOW}waiting for input${RESET}`;
413
+ statusDesc = `${AMBER}waiting for input${RESET}`;
336
414
  }
337
415
  else {
338
416
  statusDesc = s.status;
339
417
  }
340
- return truncateAnsi(`${icon} ${BOLD}${name}${RESET} ${tool} ${DIM}—${RESET} ${statusDesc}`, maxCols);
418
+ return truncateAnsi(`${dot} ${BOLD}${name}${RESET} ${tool} ${SLATE}—${RESET} ${statusDesc}`, maxCols);
341
419
  }
342
420
  // ── Exported pure helpers (for testing) ─────────────────────────────────────
343
- export { formatActivity, truncateAnsi, truncatePlain };
421
+ export { formatActivity, formatSessionCard, truncateAnsi, truncatePlain, padBoxLine, padToWidth, stripAnsiForLen, phaseDisplay };
344
422
  //# sourceMappingURL=tui.js.map
package/dist/types.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export type AoeSessionStatus = "working" | "running" | "idle" | "waiting" | "done" | "error" | "stopped" | "unknown";
2
+ export declare function toSessionStatus(raw: unknown): AoeSessionStatus;
2
3
  export interface AoeSession {
3
4
  id: string;
4
5
  title: string;
package/dist/types.js CHANGED
@@ -1,3 +1,9 @@
1
+ const VALID_STATUSES = new Set(["working", "running", "idle", "waiting", "done", "error", "stopped", "unknown"]);
2
+ // coerce an arbitrary string (e.g. from CLI JSON output) to a valid AoeSessionStatus
3
+ export function toSessionStatus(raw) {
4
+ const s = String(raw ?? "unknown");
5
+ return VALID_STATUSES.has(s) ? s : "unknown";
6
+ }
1
7
  // extract the session/title identifier from any action (uses discriminated union narrowing)
2
8
  export function actionSession(action) {
3
9
  switch (action.action) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aoaoe",
3
- "version": "0.45.0",
3
+ "version": "0.47.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",