aoaoe 0.46.0 → 0.48.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/colors.d.ts +30 -0
- package/dist/colors.js +24 -1
- package/dist/config.js +2 -1
- package/dist/daemon-state.js +2 -1
- package/dist/index.js +2 -0
- package/dist/init.js +4 -1
- package/dist/prompt-watcher.js +1 -1
- package/dist/reasoner/opencode.js +3 -1
- package/dist/reasoner/prompt.js +1 -1
- package/dist/task-cli.js +2 -1
- package/dist/task-manager.js +11 -7
- package/dist/tui.d.ts +8 -6
- package/dist/tui.js +155 -77
- package/dist/types.d.ts +7 -0
- package/dist/types.js +75 -0
- package/package.json +1 -1
package/dist/colors.d.ts
CHANGED
|
@@ -6,5 +6,35 @@ export declare const GREEN = "\u001B[32m";
|
|
|
6
6
|
export declare const YELLOW = "\u001B[33m";
|
|
7
7
|
export declare const CYAN = "\u001B[36m";
|
|
8
8
|
export declare const WHITE = "\u001B[37m";
|
|
9
|
+
export declare const INDIGO = "\u001B[38;5;105m";
|
|
10
|
+
export declare const TEAL = "\u001B[38;5;73m";
|
|
11
|
+
export declare const AMBER = "\u001B[38;5;214m";
|
|
12
|
+
export declare const SLATE = "\u001B[38;5;245m";
|
|
13
|
+
export declare const ROSE = "\u001B[38;5;204m";
|
|
14
|
+
export declare const LIME = "\u001B[38;5;114m";
|
|
15
|
+
export declare const SKY = "\u001B[38;5;117m";
|
|
9
16
|
export declare const BG_DARK = "\u001B[48;5;236m";
|
|
17
|
+
export declare const BOX: {
|
|
18
|
+
readonly tl: "┌";
|
|
19
|
+
readonly tr: "┐";
|
|
20
|
+
readonly bl: "└";
|
|
21
|
+
readonly br: "┘";
|
|
22
|
+
readonly h: "─";
|
|
23
|
+
readonly v: "│";
|
|
24
|
+
readonly ltee: "├";
|
|
25
|
+
readonly rtee: "┤";
|
|
26
|
+
readonly ttee: "┬";
|
|
27
|
+
readonly btee: "┴";
|
|
28
|
+
readonly cross: "┼";
|
|
29
|
+
readonly rtl: "╭";
|
|
30
|
+
readonly rtr: "╮";
|
|
31
|
+
readonly rbl: "╰";
|
|
32
|
+
readonly rbr: "╯";
|
|
33
|
+
};
|
|
34
|
+
export declare const SPINNER: readonly ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
35
|
+
export declare const DOT: {
|
|
36
|
+
readonly filled: "●";
|
|
37
|
+
readonly hollow: "○";
|
|
38
|
+
readonly half: "◐";
|
|
39
|
+
};
|
|
10
40
|
//# sourceMappingURL=colors.d.ts.map
|
package/dist/colors.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
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";
|
|
@@ -7,5 +8,27 @@ export const GREEN = "\x1b[32m";
|
|
|
7
8
|
export const YELLOW = "\x1b[33m";
|
|
8
9
|
export const CYAN = "\x1b[36m";
|
|
9
10
|
export const WHITE = "\x1b[37m";
|
|
10
|
-
|
|
11
|
+
// 256-color palette — tasteful accents for the TUI
|
|
12
|
+
// use sparingly: these are highlights, not defaults
|
|
13
|
+
export const INDIGO = "\x1b[38;5;105m"; // muted purple-blue for branding
|
|
14
|
+
export const TEAL = "\x1b[38;5;73m"; // cool blue-green for info
|
|
15
|
+
export const AMBER = "\x1b[38;5;214m"; // warm orange for warnings/active
|
|
16
|
+
export const SLATE = "\x1b[38;5;245m"; // neutral gray for secondary text
|
|
17
|
+
export const ROSE = "\x1b[38;5;204m"; // soft red for errors
|
|
18
|
+
export const LIME = "\x1b[38;5;114m"; // fresh green for success/working
|
|
19
|
+
export const SKY = "\x1b[38;5;117m"; // light blue for reasoning
|
|
20
|
+
// background variants (256-color)
|
|
21
|
+
export const BG_DARK = "\x1b[48;5;236m"; // dark gray for header bar
|
|
22
|
+
// box-drawing characters — Unicode block elements
|
|
23
|
+
export const BOX = {
|
|
24
|
+
tl: "┌", tr: "┐", bl: "└", br: "┘",
|
|
25
|
+
h: "─", v: "│",
|
|
26
|
+
ltee: "├", rtee: "┤", ttee: "┬", btee: "┴", cross: "┼",
|
|
27
|
+
// rounded corners (softer look)
|
|
28
|
+
rtl: "╭", rtr: "╮", rbl: "╰", rbr: "╯",
|
|
29
|
+
};
|
|
30
|
+
// braille spinner frames for phase animation
|
|
31
|
+
export const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
32
|
+
// status dots — filled circle variants
|
|
33
|
+
export const DOT = { filled: "●", hollow: "○", half: "◐" };
|
|
11
34
|
//# sourceMappingURL=colors.js.map
|
package/dist/config.js
CHANGED
|
@@ -3,6 +3,7 @@ import { resolve, join } from "node:path";
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { execFile as execFileCb } from "node:child_process";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
|
+
import { toReasonerBackend } from "./types.js";
|
|
6
7
|
const execFileAsync = promisify(execFileCb);
|
|
7
8
|
const AOAOE_DIR = join(homedir(), ".aoaoe");
|
|
8
9
|
const CONFIG_NAMES = ["aoaoe.config.json", ".aoaoe.json"];
|
|
@@ -250,7 +251,7 @@ export function parseCliArgs(argv) {
|
|
|
250
251
|
const arg = argv[i];
|
|
251
252
|
switch (arg) {
|
|
252
253
|
case "--reasoner":
|
|
253
|
-
overrides.reasoner = nextArg(i, arg);
|
|
254
|
+
overrides.reasoner = toReasonerBackend(nextArg(i, arg));
|
|
254
255
|
i++;
|
|
255
256
|
break;
|
|
256
257
|
case "--poll-interval": {
|
package/dist/daemon-state.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { writeFileSync, readFileSync, existsSync, unlinkSync, mkdirSync, renameSync } from "node:fs";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
|
+
import { toDaemonState } from "./types.js";
|
|
7
8
|
import { parseTasks, formatTaskList } from "./task-parser.js";
|
|
8
9
|
const AOAOE_DIR = join(homedir(), ".aoaoe");
|
|
9
10
|
const STATE_FILE = join(AOAOE_DIR, "daemon-state.json");
|
|
@@ -122,7 +123,7 @@ export function readState() {
|
|
|
122
123
|
try {
|
|
123
124
|
if (!existsSync(STATE_FILE))
|
|
124
125
|
return null;
|
|
125
|
-
return JSON.parse(readFileSync(STATE_FILE, "utf-8"));
|
|
126
|
+
return toDaemonState(JSON.parse(readFileSync(STATE_FILE, "utf-8")));
|
|
126
127
|
}
|
|
127
128
|
catch {
|
|
128
129
|
return null;
|
package/dist/index.js
CHANGED
|
@@ -467,6 +467,8 @@ async function main() {
|
|
|
467
467
|
else {
|
|
468
468
|
// ── normal mode: full tick ─────────────────────────────────────────
|
|
469
469
|
const activeTaskContext = taskManager ? taskManager.tasks.filter((t) => t.status !== "completed") : undefined;
|
|
470
|
+
if (!reasoner || !executor)
|
|
471
|
+
throw new Error("reasoner/executor unexpectedly null in normal mode");
|
|
470
472
|
const { interrupted, decisionsThisTick, actionsOk, actionsFail } = await daemonTick(config, poller, reasoner, executor, reasonerConsole, pollCount, policyStates, userMessage, forceDashboard, activeTaskContext, taskManager, tui);
|
|
471
473
|
totalDecisions += decisionsThisTick;
|
|
472
474
|
totalActionsExecuted += actionsOk;
|
package/dist/init.js
CHANGED
|
@@ -36,7 +36,10 @@ async function discoverSessions() {
|
|
|
36
36
|
if (result.exitCode !== 0)
|
|
37
37
|
return [];
|
|
38
38
|
try {
|
|
39
|
-
const
|
|
39
|
+
const parsed = JSON.parse(result.stdout);
|
|
40
|
+
if (!Array.isArray(parsed))
|
|
41
|
+
return [];
|
|
42
|
+
const raw = parsed;
|
|
40
43
|
// fetch status for each session in parallel (allSettled so one failure doesn't kill all)
|
|
41
44
|
const results = await Promise.allSettled(raw.map(async (r) => {
|
|
42
45
|
const id = String(r.id ?? "");
|
package/dist/prompt-watcher.js
CHANGED
|
@@ -80,6 +80,8 @@ export class OpencodeReasoner {
|
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
async decideViaSDK(prompt, signal) {
|
|
83
|
+
if (!this.client)
|
|
84
|
+
throw new Error("decideViaSDK called without a connected client");
|
|
83
85
|
const client = this.client;
|
|
84
86
|
// create session on first call, after rotation, or after error reset
|
|
85
87
|
if (!this.sessionId) {
|
|
@@ -281,7 +283,7 @@ class OpencodeClient {
|
|
|
281
283
|
// extract text from assistant response parts
|
|
282
284
|
const textParts = (data.parts ?? [])
|
|
283
285
|
.filter((p) => p.type === "text" && p.text)
|
|
284
|
-
.map((p) => p.text);
|
|
286
|
+
.map((p) => p.text ?? "");
|
|
285
287
|
return textParts.join("\n");
|
|
286
288
|
}
|
|
287
289
|
}
|
package/dist/reasoner/prompt.js
CHANGED
|
@@ -160,7 +160,7 @@ export function formatObservation(obs) {
|
|
|
160
160
|
parts.push("Project context for sessions:");
|
|
161
161
|
let contextBudget = 50_000; // max bytes for all project context combined
|
|
162
162
|
for (const snap of sortedContextSessions) {
|
|
163
|
-
const ctx = snap.projectContext;
|
|
163
|
+
const ctx = snap.projectContext ?? "";
|
|
164
164
|
const ctxBytes = Buffer.byteLength(ctx, "utf-8");
|
|
165
165
|
if (ctxBytes > contextBudget) {
|
|
166
166
|
// truncate this context to fit remaining budget
|
package/dist/task-cli.js
CHANGED
|
@@ -4,6 +4,7 @@ import { exec } from "./shell.js";
|
|
|
4
4
|
import { existsSync } from "node:fs";
|
|
5
5
|
import { resolve, basename } from "node:path";
|
|
6
6
|
import { loadTaskState, saveTaskState, formatTaskTable } from "./task-manager.js";
|
|
7
|
+
import { toAoeSessionList } from "./types.js";
|
|
7
8
|
import { BOLD, DIM, GREEN, YELLOW, RED, RESET } from "./colors.js";
|
|
8
9
|
// resolve a fuzzy reference to a task: match by title, repo basename, or session ID prefix
|
|
9
10
|
export function resolveTask(ref, tasks) {
|
|
@@ -112,7 +113,7 @@ export async function taskNew(title, path, tool = "opencode") {
|
|
|
112
113
|
let sessionId;
|
|
113
114
|
if (listResult.exitCode === 0) {
|
|
114
115
|
try {
|
|
115
|
-
const sessions = JSON.parse(listResult.stdout);
|
|
116
|
+
const sessions = toAoeSessionList(JSON.parse(listResult.stdout));
|
|
116
117
|
const found = sessions.find((s) => s.title.toLowerCase() === lower);
|
|
117
118
|
sessionId = found?.id;
|
|
118
119
|
}
|
package/dist/task-manager.js
CHANGED
|
@@ -5,6 +5,7 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync } from "
|
|
|
5
5
|
import { join, resolve, basename } from "node:path";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
import { exec } from "./shell.js";
|
|
8
|
+
import { toTaskState, toAoeSessionList } from "./types.js";
|
|
8
9
|
import { RESET, BOLD, DIM, GREEN, YELLOW, RED, CYAN } from "./colors.js";
|
|
9
10
|
const AOAOE_DIR = join(homedir(), ".aoaoe");
|
|
10
11
|
const STATE_FILE = join(AOAOE_DIR, "task-state.json");
|
|
@@ -84,8 +85,9 @@ export function loadTaskState() {
|
|
|
84
85
|
const raw = JSON.parse(readFileSync(STATE_FILE, "utf-8"));
|
|
85
86
|
if (raw && typeof raw.tasks === "object") {
|
|
86
87
|
for (const [repo, state] of Object.entries(raw.tasks)) {
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
const validated = toTaskState(state);
|
|
89
|
+
if (validated) {
|
|
90
|
+
map.set(repo, validated);
|
|
89
91
|
}
|
|
90
92
|
}
|
|
91
93
|
}
|
|
@@ -138,10 +140,12 @@ export class TaskManager {
|
|
|
138
140
|
else {
|
|
139
141
|
// update goal/tool if definition changed (don't reset progress)
|
|
140
142
|
const existing = this.states.get(def.repo);
|
|
141
|
-
if (
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
143
|
+
if (existing) {
|
|
144
|
+
if (def.goal)
|
|
145
|
+
existing.goal = def.goal;
|
|
146
|
+
if (def.tool)
|
|
147
|
+
existing.tool = def.tool;
|
|
148
|
+
}
|
|
145
149
|
}
|
|
146
150
|
}
|
|
147
151
|
this.save();
|
|
@@ -201,7 +205,7 @@ export class TaskManager {
|
|
|
201
205
|
const refreshResult = await exec("aoe", ["list", "--json"]);
|
|
202
206
|
if (refreshResult.exitCode === 0) {
|
|
203
207
|
try {
|
|
204
|
-
const refreshed = JSON.parse(refreshResult.stdout);
|
|
208
|
+
const refreshed = toAoeSessionList(JSON.parse(refreshResult.stdout));
|
|
205
209
|
const newSession = refreshed.find((s) => s.title.toLowerCase() === task.sessionTitle.toLowerCase());
|
|
206
210
|
if (newSession) {
|
|
207
211
|
task.sessionId = newSession.id;
|
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
|
-
*
|
|
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,
|
|
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
|
-
//
|
|
19
|
-
const
|
|
20
|
-
working: `${
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
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
|
-
//
|
|
75
|
+
// tick timer: countdown + spinner animation (~4 fps for smooth braille spin)
|
|
60
76
|
this.countdownTimer = setInterval(() => {
|
|
61
|
-
if (this.active
|
|
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
|
-
},
|
|
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
|
|
133
|
-
|
|
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
|
-
|
|
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 ? ` ${
|
|
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 = ` ${
|
|
191
|
+
countdownTag = ` ${SLATE}│${RESET} ${SLATE}${remaining}s${RESET}`;
|
|
179
192
|
}
|
|
180
|
-
// reasoner
|
|
181
|
-
const reasonerTag = this.reasonerName ? ` ${
|
|
182
|
-
const line = ` ${BOLD}aoaoe${RESET} ${
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
//
|
|
198
|
-
const
|
|
199
|
-
|
|
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 = "
|
|
206
|
-
const prefix =
|
|
207
|
-
const
|
|
208
|
-
const fill = Math.max(0, this.cols -
|
|
209
|
-
const
|
|
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 =
|
|
314
|
+
let color = SLATE;
|
|
248
315
|
let prefix = tag;
|
|
249
316
|
switch (tag) {
|
|
250
317
|
case "observation":
|
|
251
|
-
color =
|
|
318
|
+
color = SLATE;
|
|
252
319
|
prefix = "obs";
|
|
253
320
|
break;
|
|
254
321
|
case "reasoner":
|
|
255
|
-
color =
|
|
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 =
|
|
264
|
-
prefix = "
|
|
330
|
+
color = AMBER;
|
|
331
|
+
prefix = "→ action";
|
|
265
332
|
break;
|
|
266
333
|
case "! action":
|
|
267
334
|
case "error":
|
|
268
|
-
color =
|
|
269
|
-
prefix = "
|
|
335
|
+
color = ROSE;
|
|
336
|
+
prefix = "✗ error";
|
|
270
337
|
break;
|
|
271
338
|
case "you":
|
|
272
|
-
color =
|
|
339
|
+
color = LIME;
|
|
273
340
|
break;
|
|
274
341
|
case "system":
|
|
275
|
-
color =
|
|
342
|
+
color = SLATE;
|
|
276
343
|
break;
|
|
277
344
|
case "status":
|
|
278
|
-
color =
|
|
345
|
+
color = SLATE;
|
|
279
346
|
break;
|
|
280
347
|
default:
|
|
281
|
-
color =
|
|
348
|
+
color = SLATE;
|
|
282
349
|
break;
|
|
283
350
|
}
|
|
284
|
-
const formatted = ` ${
|
|
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
|
-
*
|
|
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
|
|
388
|
+
const dot = STATUS_DOT[s.status] ?? `${AMBER}${DOT.filled}${RESET}`;
|
|
310
389
|
const name = s.title;
|
|
311
|
-
const tool = `${
|
|
312
|
-
// human-readable status descriptions
|
|
390
|
+
const tool = `${SLATE}(${s.tool})${RESET}`;
|
|
313
391
|
let statusDesc;
|
|
314
392
|
if (s.userActive) {
|
|
315
|
-
statusDesc = `${
|
|
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 = `${
|
|
400
|
+
statusDesc = `${LIME}working${RESET}`;
|
|
323
401
|
}
|
|
324
402
|
}
|
|
325
403
|
else if (s.status === "idle" || s.status === "stopped") {
|
|
326
|
-
statusDesc = `${
|
|
404
|
+
statusDesc = `${SLATE}idle${RESET}`;
|
|
327
405
|
}
|
|
328
406
|
else if (s.status === "error") {
|
|
329
|
-
statusDesc = `${
|
|
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 = `${
|
|
413
|
+
statusDesc = `${AMBER}waiting for input${RESET}`;
|
|
336
414
|
}
|
|
337
415
|
else {
|
|
338
416
|
statusDesc = s.status;
|
|
339
417
|
}
|
|
340
|
-
return truncateAnsi(`${
|
|
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
|
@@ -162,4 +162,11 @@ export interface TaskState {
|
|
|
162
162
|
completedAt?: number;
|
|
163
163
|
progress: TaskProgress[];
|
|
164
164
|
}
|
|
165
|
+
export declare function toTaskState(raw: unknown): TaskState | null;
|
|
166
|
+
export declare function toDaemonState(raw: unknown): DaemonState | null;
|
|
167
|
+
export declare function toAoeSessionList(raw: unknown): Array<{
|
|
168
|
+
id: string;
|
|
169
|
+
title: string;
|
|
170
|
+
}>;
|
|
171
|
+
export declare function toReasonerBackend(raw: string): ReasonerBackend;
|
|
165
172
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.js
CHANGED
|
@@ -37,4 +37,79 @@ export function actionDetail(action) {
|
|
|
37
37
|
return undefined;
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
+
const VALID_TASK_STATUSES = new Set(["pending", "active", "completed", "paused", "failed"]);
|
|
41
|
+
// validate an unknown value (e.g. from JSON.parse) as a TaskState, returning null if invalid
|
|
42
|
+
export function toTaskState(raw) {
|
|
43
|
+
if (!raw || typeof raw !== "object")
|
|
44
|
+
return null;
|
|
45
|
+
const r = raw;
|
|
46
|
+
if (typeof r.repo !== "string" || !r.repo)
|
|
47
|
+
return null;
|
|
48
|
+
if (typeof r.sessionTitle !== "string")
|
|
49
|
+
return null;
|
|
50
|
+
if (typeof r.tool !== "string")
|
|
51
|
+
return null;
|
|
52
|
+
if (typeof r.goal !== "string")
|
|
53
|
+
return null;
|
|
54
|
+
if (typeof r.status !== "string" || !VALID_TASK_STATUSES.has(r.status))
|
|
55
|
+
return null;
|
|
56
|
+
if (!Array.isArray(r.progress))
|
|
57
|
+
return null;
|
|
58
|
+
return {
|
|
59
|
+
repo: r.repo,
|
|
60
|
+
sessionTitle: r.sessionTitle,
|
|
61
|
+
tool: r.tool,
|
|
62
|
+
goal: r.goal,
|
|
63
|
+
status: r.status,
|
|
64
|
+
sessionId: typeof r.sessionId === "string" ? r.sessionId : undefined,
|
|
65
|
+
createdAt: typeof r.createdAt === "number" ? r.createdAt : undefined,
|
|
66
|
+
lastProgressAt: typeof r.lastProgressAt === "number" ? r.lastProgressAt : undefined,
|
|
67
|
+
completedAt: typeof r.completedAt === "number" ? r.completedAt : undefined,
|
|
68
|
+
progress: r.progress.filter((p) => !!p && typeof p === "object" &&
|
|
69
|
+
typeof p.at === "number" &&
|
|
70
|
+
typeof p.summary === "string"),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
// validate an unknown value as a DaemonState, returning null if invalid
|
|
74
|
+
export function toDaemonState(raw) {
|
|
75
|
+
if (!raw || typeof raw !== "object")
|
|
76
|
+
return null;
|
|
77
|
+
const r = raw;
|
|
78
|
+
if (typeof r.tickStartedAt !== "number")
|
|
79
|
+
return null;
|
|
80
|
+
if (typeof r.nextTickAt !== "number")
|
|
81
|
+
return null;
|
|
82
|
+
if (typeof r.pollIntervalMs !== "number")
|
|
83
|
+
return null;
|
|
84
|
+
if (typeof r.phase !== "string")
|
|
85
|
+
return null;
|
|
86
|
+
if (typeof r.phaseStartedAt !== "number")
|
|
87
|
+
return null;
|
|
88
|
+
if (typeof r.pollCount !== "number")
|
|
89
|
+
return null;
|
|
90
|
+
if (typeof r.paused !== "boolean")
|
|
91
|
+
return null;
|
|
92
|
+
if (typeof r.sessionCount !== "number")
|
|
93
|
+
return null;
|
|
94
|
+
if (typeof r.changeCount !== "number")
|
|
95
|
+
return null;
|
|
96
|
+
if (!Array.isArray(r.sessions))
|
|
97
|
+
return null;
|
|
98
|
+
return raw;
|
|
99
|
+
}
|
|
100
|
+
// validate an unknown array as an AoE session list (from `aoe list --json`)
|
|
101
|
+
export function toAoeSessionList(raw) {
|
|
102
|
+
if (!Array.isArray(raw))
|
|
103
|
+
return [];
|
|
104
|
+
return raw.filter((item) => !!item && typeof item === "object" &&
|
|
105
|
+
typeof item.id === "string" &&
|
|
106
|
+
typeof item.title === "string");
|
|
107
|
+
}
|
|
108
|
+
// validate a string as a ReasonerBackend, throwing on invalid input
|
|
109
|
+
const VALID_REASONER_BACKENDS = new Set(["opencode", "claude-code"]);
|
|
110
|
+
export function toReasonerBackend(raw) {
|
|
111
|
+
if (VALID_REASONER_BACKENDS.has(raw))
|
|
112
|
+
return raw;
|
|
113
|
+
throw new Error(`--reasoner must be "opencode" or "claude-code", got "${raw}"`);
|
|
114
|
+
}
|
|
40
115
|
//# sourceMappingURL=types.js.map
|