aoaoe 0.142.0 → 0.149.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/daemon-state.js +1 -0
- package/dist/index.js +65 -4
- package/dist/input.d.ts +12 -0
- package/dist/input.js +68 -0
- package/dist/tui.d.ts +52 -2
- package/dist/tui.js +185 -9
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
package/dist/daemon-state.js
CHANGED
|
@@ -148,6 +148,7 @@ export function buildSessionStates(obs) {
|
|
|
148
148
|
tool: s.tool,
|
|
149
149
|
status: s.status,
|
|
150
150
|
path: snap.session.path,
|
|
151
|
+
createdAt: snap.session.created_at,
|
|
151
152
|
currentTask: sessionTasks.get(s.id),
|
|
152
153
|
lastActivity: lastActivity && lastActivity.length > 100
|
|
153
154
|
? lastActivity.slice(0, 97) + "..."
|
package/dist/index.js
CHANGED
|
@@ -16,7 +16,7 @@ import { wakeableSleep } from "./wake.js";
|
|
|
16
16
|
import { classifyMessages, formatUserMessages, buildReceipts, shouldSkipSleep, hasPendingFile, isInsistMessage, stripInsistPrefix } from "./message.js";
|
|
17
17
|
import { TaskManager, loadTaskDefinitions, loadTaskState, formatTaskTable, importAoeSessionsToTasks } from "./task-manager.js";
|
|
18
18
|
import { runTaskCli, handleTaskSlashCommand, quickTaskUpdate } from "./task-cli.js";
|
|
19
|
-
import { TUI, hitTestSession, nextSortMode, SORT_MODES, formatUptime, formatClipText, loadTuiPrefs, saveTuiPrefs, validateGroupName, CONTEXT_BURN_THRESHOLD, buildSnapshotData, formatSnapshotJson, formatSnapshotMarkdown, formatBroadcastSummary, rankSessions, TOP_SORT_MODES, formatIdleSince, CONTEXT_CEILING_THRESHOLD, buildSessionStats, formatSessionStatsLines, formatStatsJson, validateSessionTag, validateColorName, computeErrorTrend, parseQuietHoursRange, computeCostSummary, formatSessionReport } from "./tui.js";
|
|
19
|
+
import { TUI, hitTestSession, nextSortMode, SORT_MODES, formatUptime, formatClipText, loadTuiPrefs, saveTuiPrefs, validateGroupName, CONTEXT_BURN_THRESHOLD, buildSnapshotData, formatSnapshotJson, formatSnapshotMarkdown, formatBroadcastSummary, rankSessions, TOP_SORT_MODES, formatIdleSince, CONTEXT_CEILING_THRESHOLD, buildSessionStats, formatSessionStatsLines, formatStatsJson, validateSessionTag, validateColorName, computeErrorTrend, parseQuietHoursRange, computeCostSummary, formatSessionReport, formatQuietStatus, formatSessionAge } from "./tui.js";
|
|
20
20
|
import { isDaemonRunningFromState } from "./chat.js";
|
|
21
21
|
import { sendNotification, sendTestNotification } from "./notify.js";
|
|
22
22
|
import { startHealthServer } from "./health.js";
|
|
@@ -606,7 +606,9 @@ async function main() {
|
|
|
606
606
|
const lastChange = lastChangeAt.get(s.id);
|
|
607
607
|
const idleStr = (lastChange && (s.status === "idle" || s.status === "stopped" || s.status === "done"))
|
|
608
608
|
? ` idle ${formatUptime(now - lastChange)}` : "";
|
|
609
|
-
|
|
609
|
+
// session age from AoE created_at
|
|
610
|
+
const ageStr = s.createdAt ? ` age:${formatSessionAge(s.createdAt, now)}` : "";
|
|
611
|
+
tui.log("system", ` ${s.title}${groupStr} — ${s.status} ${up}${ctxStr}${costStr}${errStr}${idleStr}${ageStr}${noteStr}`);
|
|
610
612
|
}
|
|
611
613
|
});
|
|
612
614
|
// wire /uptime listing
|
|
@@ -953,7 +955,7 @@ async function main() {
|
|
|
953
955
|
input.onExportStats(() => {
|
|
954
956
|
const sessions = tui.getSessions();
|
|
955
957
|
const now = Date.now();
|
|
956
|
-
const entries = buildSessionStats(sessions, tui.getSessionErrorCounts(), tui.getAllBurnRates(now), tui.getAllFirstSeen(), tui.getAllLastChangeAt(), tui.getAllHealthScores(now), tui.getAllSessionAliases(), now, new Map(sessions.map((s) => [s.id, tui.getSessionErrorTimestamps(s.id)])), tui.getAllSessionCosts());
|
|
958
|
+
const entries = buildSessionStats(sessions, tui.getSessionErrorCounts(), tui.getAllBurnRates(now), tui.getAllFirstSeen(), tui.getAllLastChangeAt(), tui.getAllHealthScores(now), tui.getAllSessionAliases(), now, new Map(sessions.map((s) => [s.id, tui.getSessionErrorTimestamps(s.id)])), tui.getAllSessionCosts(), new Map(sessions.map((s) => [s.id, tui.getSessionHealthHistory(s.id)])));
|
|
957
959
|
const ts = new Date(now).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
958
960
|
const dir = join(homedir(), ".aoaoe");
|
|
959
961
|
const path = join(dir, `stats-${ts}.json`);
|
|
@@ -1021,6 +1023,65 @@ async function main() {
|
|
|
1021
1023
|
tui.log("system", `quiet hours: ${specs.join(", ")} — watchdog+burn alerts suppressed`);
|
|
1022
1024
|
persistPrefs();
|
|
1023
1025
|
});
|
|
1026
|
+
// wire /budget cost alerts
|
|
1027
|
+
input.onBudget((target, budgetUSD) => {
|
|
1028
|
+
if (budgetUSD === null) {
|
|
1029
|
+
// clear global budget
|
|
1030
|
+
tui.setGlobalBudget(null);
|
|
1031
|
+
tui.log("system", "budget: global budget cleared");
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
if (target === null) {
|
|
1035
|
+
tui.setGlobalBudget(budgetUSD);
|
|
1036
|
+
tui.log("system", `budget: global budget set to $${budgetUSD.toFixed(2)}`);
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
const num = /^\d+$/.test(target) ? parseInt(target, 10) : undefined;
|
|
1040
|
+
const ok = tui.setSessionBudget(num ?? target, budgetUSD);
|
|
1041
|
+
if (ok)
|
|
1042
|
+
tui.log("system", `budget: $${budgetUSD.toFixed(2)} set for ${target}`);
|
|
1043
|
+
else
|
|
1044
|
+
tui.log("system", `session not found: ${target}`);
|
|
1045
|
+
});
|
|
1046
|
+
// wire /pause-all and /resume-all
|
|
1047
|
+
input.onBulkControl((action) => {
|
|
1048
|
+
const sessions = tui.getSessions();
|
|
1049
|
+
if (sessions.length === 0) {
|
|
1050
|
+
tui.log("system", `${action}-all: no sessions`);
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
// ESC for pause, Enter for resume (nudge sessions out of waiting)
|
|
1054
|
+
const keys = action === "pause" ? "Escape" : "Enter";
|
|
1055
|
+
let count = 0;
|
|
1056
|
+
for (const s of sessions) {
|
|
1057
|
+
const tmuxName = computeTmuxName(s.title, s.id);
|
|
1058
|
+
shellExec("tmux", ["send-keys", "-t", tmuxName, "", keys])
|
|
1059
|
+
.then(() => { })
|
|
1060
|
+
.catch((_e) => { });
|
|
1061
|
+
count++;
|
|
1062
|
+
}
|
|
1063
|
+
tui.log("system", `${action}-all: sent to ${count} session${count !== 1 ? "s" : ""}`);
|
|
1064
|
+
});
|
|
1065
|
+
// wire /quiet-status
|
|
1066
|
+
input.onQuietStatus(() => {
|
|
1067
|
+
const { active, message } = formatQuietStatus(tui.getQuietHours());
|
|
1068
|
+
tui.log("system", `quiet-status: ${message}`);
|
|
1069
|
+
if (active)
|
|
1070
|
+
tui.log("system", " watchdog, burn-rate, and ceiling alerts are suppressed");
|
|
1071
|
+
});
|
|
1072
|
+
// wire /alert-log
|
|
1073
|
+
input.onAlertLog((count) => {
|
|
1074
|
+
const alerts = tui.getAlertLog();
|
|
1075
|
+
const recent = alerts.slice(-count);
|
|
1076
|
+
if (recent.length === 0) {
|
|
1077
|
+
tui.log("system", "alert-log: no auto-generated alerts yet");
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
tui.log("system", `alert-log: last ${recent.length} alert${recent.length !== 1 ? "s" : ""}:`);
|
|
1081
|
+
for (const e of recent) {
|
|
1082
|
+
tui.log("system", ` ${e.time} ${e.text}`);
|
|
1083
|
+
}
|
|
1084
|
+
});
|
|
1024
1085
|
// wire /cost-summary
|
|
1025
1086
|
input.onCostSummary(() => {
|
|
1026
1087
|
const sessions = tui.getSessions();
|
|
@@ -1133,7 +1194,7 @@ async function main() {
|
|
|
1133
1194
|
return;
|
|
1134
1195
|
}
|
|
1135
1196
|
const now = Date.now();
|
|
1136
|
-
const entries = buildSessionStats(sessions, tui.getSessionErrorCounts(), tui.getAllBurnRates(now), tui.getAllFirstSeen(), tui.getAllLastChangeAt(), tui.getAllHealthScores(now), tui.getAllSessionAliases(), now, new Map(sessions.map((s) => [s.id, tui.getSessionErrorTimestamps(s.id)])), tui.getAllSessionCosts());
|
|
1197
|
+
const entries = buildSessionStats(sessions, tui.getSessionErrorCounts(), tui.getAllBurnRates(now), tui.getAllFirstSeen(), tui.getAllLastChangeAt(), tui.getAllHealthScores(now), tui.getAllSessionAliases(), now, new Map(sessions.map((s) => [s.id, tui.getSessionErrorTimestamps(s.id)])), tui.getAllSessionCosts(), new Map(sessions.map((s) => [s.id, tui.getSessionHealthHistory(s.id)])));
|
|
1137
1198
|
tui.log("system", `/stats — ${entries.length} session${entries.length !== 1 ? "s" : ""}:`);
|
|
1138
1199
|
for (const line of formatSessionStatsLines(entries)) {
|
|
1139
1200
|
tui.log("system", line);
|
package/dist/input.d.ts
CHANGED
|
@@ -54,6 +54,10 @@ export type QuietHoursHandler = (specs: string[]) => void;
|
|
|
54
54
|
export type HistoryStatsHandler = () => void;
|
|
55
55
|
export type CostSummaryHandler = () => void;
|
|
56
56
|
export type SessionReportHandler = (target: string) => void;
|
|
57
|
+
export type QuietStatusHandler = () => void;
|
|
58
|
+
export type AlertLogHandler = (count: number) => void;
|
|
59
|
+
export type BudgetHandler = (target: string | null, budgetUSD: number | null) => void;
|
|
60
|
+
export type BulkControlHandler = (action: "pause" | "resume") => void;
|
|
57
61
|
export interface MouseEvent {
|
|
58
62
|
button: number;
|
|
59
63
|
col: number;
|
|
@@ -130,6 +134,10 @@ export declare class InputReader {
|
|
|
130
134
|
private historyStatsHandler;
|
|
131
135
|
private costSummaryHandler;
|
|
132
136
|
private sessionReportHandler;
|
|
137
|
+
private quietStatusHandler;
|
|
138
|
+
private alertLogHandler;
|
|
139
|
+
private budgetHandler;
|
|
140
|
+
private bulkControlHandler;
|
|
133
141
|
private aliases;
|
|
134
142
|
private mouseDataListener;
|
|
135
143
|
onScroll(handler: (dir: ScrollDirection) => void): void;
|
|
@@ -189,6 +197,10 @@ export declare class InputReader {
|
|
|
189
197
|
onHistoryStats(handler: HistoryStatsHandler): void;
|
|
190
198
|
onCostSummary(handler: CostSummaryHandler): void;
|
|
191
199
|
onSessionReport(handler: SessionReportHandler): void;
|
|
200
|
+
onQuietStatus(handler: QuietStatusHandler): void;
|
|
201
|
+
onAlertLog(handler: AlertLogHandler): void;
|
|
202
|
+
onBudget(handler: BudgetHandler): void;
|
|
203
|
+
onBulkControl(handler: BulkControlHandler): void;
|
|
192
204
|
/** Set aliases from persisted prefs. */
|
|
193
205
|
setAliases(aliases: Record<string, string>): void;
|
|
194
206
|
/** Get current aliases as a plain object. */
|
package/dist/input.js
CHANGED
|
@@ -87,6 +87,10 @@ export class InputReader {
|
|
|
87
87
|
historyStatsHandler = null;
|
|
88
88
|
costSummaryHandler = null;
|
|
89
89
|
sessionReportHandler = null;
|
|
90
|
+
quietStatusHandler = null;
|
|
91
|
+
alertLogHandler = null;
|
|
92
|
+
budgetHandler = null;
|
|
93
|
+
bulkControlHandler = null;
|
|
90
94
|
aliases = new Map(); // /shortcut → /full command
|
|
91
95
|
mouseDataListener = null;
|
|
92
96
|
// register a callback for scroll key events (PgUp/PgDn/Home/End)
|
|
@@ -299,6 +303,10 @@ export class InputReader {
|
|
|
299
303
|
onHistoryStats(handler) { this.historyStatsHandler = handler; }
|
|
300
304
|
onCostSummary(handler) { this.costSummaryHandler = handler; }
|
|
301
305
|
onSessionReport(handler) { this.sessionReportHandler = handler; }
|
|
306
|
+
onQuietStatus(handler) { this.quietStatusHandler = handler; }
|
|
307
|
+
onAlertLog(handler) { this.alertLogHandler = handler; }
|
|
308
|
+
onBudget(handler) { this.budgetHandler = handler; }
|
|
309
|
+
onBulkControl(handler) { this.bulkControlHandler = handler; }
|
|
302
310
|
/** Set aliases from persisted prefs. */
|
|
303
311
|
setAliases(aliases) {
|
|
304
312
|
this.aliases.clear();
|
|
@@ -560,6 +568,11 @@ ${BOLD}navigation:${RESET}
|
|
|
560
568
|
/duplicate N [t] spawn a new session cloned from session N (same tool/path)
|
|
561
569
|
/color-all [c] set accent color for all sessions (no color = clear all)
|
|
562
570
|
/quiet-hours [H-H] suppress watchdog+burn alerts during hours (e.g. 22-06; no arg = clear)
|
|
571
|
+
/quiet-status show whether quiet hours are currently active
|
|
572
|
+
/budget [N] [$] set cost budget: /budget 1 2.50 (session), /budget 2.50 (global), /budget clear
|
|
573
|
+
/pause-all send interrupt to all sessions
|
|
574
|
+
/resume-all send resume to all sessions
|
|
575
|
+
/alert-log [N] show last N auto-generated alerts (burn-rate/watchdog/ceiling; default 20)
|
|
563
576
|
/history-stats show aggregate statistics from persisted activity history
|
|
564
577
|
/cost-summary show total estimated spend across all sessions
|
|
565
578
|
/session-report N generate full markdown report for a session → ~/.aoaoe/report-<name>-<ts>.md
|
|
@@ -1115,6 +1128,61 @@ ${BOLD}other:${RESET}
|
|
|
1115
1128
|
console.error(`${DIM}history-stats not available (no TUI)${RESET}`);
|
|
1116
1129
|
}
|
|
1117
1130
|
break;
|
|
1131
|
+
case "/budget": {
|
|
1132
|
+
const bArgs = line.slice("/budget".length).trim().split(/\s+/).filter(Boolean);
|
|
1133
|
+
if (bArgs.length === 0 || bArgs[0] === "clear") {
|
|
1134
|
+
// clear global budget
|
|
1135
|
+
if (this.budgetHandler)
|
|
1136
|
+
this.budgetHandler(null, null);
|
|
1137
|
+
else
|
|
1138
|
+
console.error(`${DIM}budget not available${RESET}`);
|
|
1139
|
+
break;
|
|
1140
|
+
}
|
|
1141
|
+
if (this.budgetHandler) {
|
|
1142
|
+
// /budget <$N> → global, /budget <N|name> <$N> → per-session
|
|
1143
|
+
const maybeUSD = parseFloat(bArgs[bArgs.length - 1].replace("$", ""));
|
|
1144
|
+
if (!isNaN(maybeUSD) && bArgs.length === 1) {
|
|
1145
|
+
this.budgetHandler(null, maybeUSD); // global
|
|
1146
|
+
}
|
|
1147
|
+
else if (!isNaN(maybeUSD) && bArgs.length >= 2) {
|
|
1148
|
+
this.budgetHandler(bArgs[0], maybeUSD); // per-session
|
|
1149
|
+
}
|
|
1150
|
+
else {
|
|
1151
|
+
console.error(`${DIM}usage: /budget [$N.NN] — global, /budget <N|name> $N.NN — per-session, /budget clear — remove${RESET}`);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
else {
|
|
1155
|
+
console.error(`${DIM}budget not available${RESET}`);
|
|
1156
|
+
}
|
|
1157
|
+
break;
|
|
1158
|
+
}
|
|
1159
|
+
case "/pause-all":
|
|
1160
|
+
if (this.bulkControlHandler)
|
|
1161
|
+
this.bulkControlHandler("pause");
|
|
1162
|
+
else
|
|
1163
|
+
console.error(`${DIM}pause-all not available${RESET}`);
|
|
1164
|
+
break;
|
|
1165
|
+
case "/resume-all":
|
|
1166
|
+
if (this.bulkControlHandler)
|
|
1167
|
+
this.bulkControlHandler("resume");
|
|
1168
|
+
else
|
|
1169
|
+
console.error(`${DIM}resume-all not available${RESET}`);
|
|
1170
|
+
break;
|
|
1171
|
+
case "/quiet-status":
|
|
1172
|
+
if (this.quietStatusHandler)
|
|
1173
|
+
this.quietStatusHandler();
|
|
1174
|
+
else
|
|
1175
|
+
console.error(`${DIM}quiet-status not available (no TUI)${RESET}`);
|
|
1176
|
+
break;
|
|
1177
|
+
case "/alert-log": {
|
|
1178
|
+
const alN = parseInt(line.slice("/alert-log".length).trim() || "20", 10);
|
|
1179
|
+
const alCount = isNaN(alN) || alN < 1 ? 20 : Math.min(alN, 200);
|
|
1180
|
+
if (this.alertLogHandler)
|
|
1181
|
+
this.alertLogHandler(alCount);
|
|
1182
|
+
else
|
|
1183
|
+
console.error(`${DIM}alert-log not available (no TUI)${RESET}`);
|
|
1184
|
+
break;
|
|
1185
|
+
}
|
|
1118
1186
|
case "/cost-summary":
|
|
1119
1187
|
if (this.costSummaryHandler)
|
|
1120
1188
|
this.costSummaryHandler();
|
package/dist/tui.d.ts
CHANGED
|
@@ -111,6 +111,32 @@ export declare const TIMELINE_DEFAULT_COUNT = 30;
|
|
|
111
111
|
* Returns up to `count` entries.
|
|
112
112
|
*/
|
|
113
113
|
export declare function filterSessionTimeline(buffer: readonly ActivityEntry[], sessionId: string, count?: number): ActivityEntry[];
|
|
114
|
+
/**
|
|
115
|
+
* Given quiet-hours ranges and current hour, return a human-readable status string.
|
|
116
|
+
* Returns { active, message } where message explains current state.
|
|
117
|
+
*/
|
|
118
|
+
export declare function formatQuietStatus(ranges: ReadonlyArray<[number, number]>, now?: Date): {
|
|
119
|
+
active: boolean;
|
|
120
|
+
message: string;
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* Parse a session creation time from an ISO 8601 string.
|
|
124
|
+
* Returns null if unparseable.
|
|
125
|
+
*/
|
|
126
|
+
export declare function parseSessionAge(createdAt: string | undefined, now?: number): number | null;
|
|
127
|
+
/** Format session age as compact string: "3d", "2h", "45m", "< 1m". */
|
|
128
|
+
export declare function formatSessionAge(createdAt: string | undefined, now?: number): string;
|
|
129
|
+
/** Max health score snapshots stored per session. */
|
|
130
|
+
export declare const MAX_HEALTH_HISTORY = 20;
|
|
131
|
+
export interface HealthSnapshot {
|
|
132
|
+
score: number;
|
|
133
|
+
ts: number;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Format health history as a compact 5-bucket sparkline.
|
|
137
|
+
* Each bucket covers 1/5 of the history window. Color: LIME/AMBER/ROSE by value.
|
|
138
|
+
*/
|
|
139
|
+
export declare function formatHealthSparkline(history: readonly HealthSnapshot[], now?: number): string;
|
|
114
140
|
/**
|
|
115
141
|
* Parse a cost string like "$3.42" → 3.42, or null if unparseable.
|
|
116
142
|
*/
|
|
@@ -154,6 +180,10 @@ export interface SessionReportData {
|
|
|
154
180
|
}
|
|
155
181
|
/** Format a session report as a Markdown document. */
|
|
156
182
|
export declare function formatSessionReport(data: SessionReportData): string;
|
|
183
|
+
/** Check if a cost string exceeds a budget value. Returns true when over budget. */
|
|
184
|
+
export declare function isOverBudget(costStr: string | undefined, budgetUSD: number): boolean;
|
|
185
|
+
/** Format a budget-exceeded alert message. */
|
|
186
|
+
export declare function formatBudgetAlert(title: string, costStr: string, budgetUSD: number): string;
|
|
157
187
|
/**
|
|
158
188
|
* Build args for duplicating a session: given a source session, return
|
|
159
189
|
* { path, tool, title } for use in a create_agent action.
|
|
@@ -208,13 +238,14 @@ export interface SessionStatEntry {
|
|
|
208
238
|
burnRatePerMin: number | null;
|
|
209
239
|
contextPct: number | null;
|
|
210
240
|
costStr?: string;
|
|
241
|
+
healthSparkline?: string;
|
|
211
242
|
uptimeMs: number | null;
|
|
212
243
|
idleSinceMs: number | null;
|
|
213
244
|
}
|
|
214
245
|
/**
|
|
215
246
|
* Build stats entries for all sessions — pure, testable, no side effects.
|
|
216
247
|
*/
|
|
217
|
-
export declare function buildSessionStats(sessions: readonly DaemonSessionState[], errorCounts: ReadonlyMap<string, number>, burnRates: ReadonlyMap<string, number | null>, firstSeen: ReadonlyMap<string, number>, lastChangeAt: ReadonlyMap<string, number>, healthScores: ReadonlyMap<string, number>, sessionAliases: ReadonlyMap<string, string>, now?: number, errorTimestamps?: ReadonlyMap<string, readonly number[]>, sessionCosts?: ReadonlyMap<string, string>): SessionStatEntry[];
|
|
248
|
+
export declare function buildSessionStats(sessions: readonly DaemonSessionState[], errorCounts: ReadonlyMap<string, number>, burnRates: ReadonlyMap<string, number | null>, firstSeen: ReadonlyMap<string, number>, lastChangeAt: ReadonlyMap<string, number>, healthScores: ReadonlyMap<string, number>, sessionAliases: ReadonlyMap<string, string>, now?: number, errorTimestamps?: ReadonlyMap<string, readonly number[]>, sessionCosts?: ReadonlyMap<string, string>, healthHistories?: ReadonlyMap<string, readonly HealthSnapshot[]>): SessionStatEntry[];
|
|
218
249
|
/**
|
|
219
250
|
* Format session stats entries as a multi-line activity-log-friendly string.
|
|
220
251
|
* Each line is one session summary.
|
|
@@ -379,6 +410,11 @@ export declare class TUI {
|
|
|
379
410
|
private sessionColors;
|
|
380
411
|
private sessionCosts;
|
|
381
412
|
private quietHoursRanges;
|
|
413
|
+
private sessionHealthHistory;
|
|
414
|
+
private alertLog;
|
|
415
|
+
private sessionBudgets;
|
|
416
|
+
private globalBudget;
|
|
417
|
+
private budgetAlerted;
|
|
382
418
|
private viewMode;
|
|
383
419
|
private drilldownSessionId;
|
|
384
420
|
private sessionOutputs;
|
|
@@ -510,6 +546,20 @@ export declare class TUI {
|
|
|
510
546
|
tool: string;
|
|
511
547
|
title: string;
|
|
512
548
|
} | null;
|
|
549
|
+
/** Set a per-session budget in USD. Pass null to clear. */
|
|
550
|
+
setSessionBudget(sessionIdOrIndex: string | number, budgetUSD: number | null): boolean;
|
|
551
|
+
/** Set global fallback budget (applies to all sessions without per-session budget). */
|
|
552
|
+
setGlobalBudget(budgetUSD: number | null): void;
|
|
553
|
+
/** Get the per-session budget (or null). */
|
|
554
|
+
getSessionBudget(id: string): number | null;
|
|
555
|
+
/** Get the global budget (or null if not set). */
|
|
556
|
+
getGlobalBudget(): number | null;
|
|
557
|
+
/** Return all per-session budgets. */
|
|
558
|
+
getAllSessionBudgets(): ReadonlyMap<string, number>;
|
|
559
|
+
/** Return health history for a session (for sparkline). */
|
|
560
|
+
getSessionHealthHistory(id: string): readonly HealthSnapshot[];
|
|
561
|
+
/** Return all alert log entries (last 100 "status" tag entries). */
|
|
562
|
+
getAlertLog(): readonly ActivityEntry[];
|
|
513
563
|
/** Get the latest cost string for a session (or undefined). */
|
|
514
564
|
getSessionCost(id: string): string | undefined;
|
|
515
565
|
/** Return all session costs (for /stats). */
|
|
@@ -658,7 +708,7 @@ export declare class TUI {
|
|
|
658
708
|
private repaintDrilldownContent;
|
|
659
709
|
private paintInputLine;
|
|
660
710
|
}
|
|
661
|
-
declare function formatSessionCard(s: DaemonSessionState, maxWidth: number, errorSparkline?: string, idleSinceMs?: number, healthBadge?: string, displayName?: string): string;
|
|
711
|
+
declare function formatSessionCard(s: DaemonSessionState, maxWidth: number, errorSparkline?: string, idleSinceMs?: number, healthBadge?: string, displayName?: string, ageStr?: string): string;
|
|
662
712
|
declare function formatActivity(entry: ActivityEntry, maxCols: number): string;
|
|
663
713
|
declare function padBoxLine(line: string, totalWidth: number): string;
|
|
664
714
|
declare function padBoxLineHover(line: string, totalWidth: number, hovered: boolean): string;
|
package/dist/tui.js
CHANGED
|
@@ -358,6 +358,76 @@ export function filterSessionTimeline(buffer, sessionId, count = TIMELINE_DEFAUL
|
|
|
358
358
|
const matching = buffer.filter((e) => e.sessionId === sessionId);
|
|
359
359
|
return matching.slice(-count);
|
|
360
360
|
}
|
|
361
|
+
// ── Quiet status (pure, exported for testing) ────────────────────────────────
|
|
362
|
+
/**
|
|
363
|
+
* Given quiet-hours ranges and current hour, return a human-readable status string.
|
|
364
|
+
* Returns { active, message } where message explains current state.
|
|
365
|
+
*/
|
|
366
|
+
export function formatQuietStatus(ranges, now) {
|
|
367
|
+
if (ranges.length === 0)
|
|
368
|
+
return { active: false, message: "quiet hours not configured" };
|
|
369
|
+
const d = now ?? new Date();
|
|
370
|
+
const hour = d.getHours();
|
|
371
|
+
const active = isQuietHour(hour, ranges);
|
|
372
|
+
const rangeStrs = ranges.map(([s, e]) => `${String(s).padStart(2, "0")}:00–${String(e).padStart(2, "0")}:00`);
|
|
373
|
+
if (active) {
|
|
374
|
+
return { active: true, message: `quiet hours ACTIVE — alerts suppressed (${rangeStrs.join(", ")})` };
|
|
375
|
+
}
|
|
376
|
+
return { active: false, message: `quiet hours inactive — configured: ${rangeStrs.join(", ")}` };
|
|
377
|
+
}
|
|
378
|
+
// ── Session age (pure, exported for testing) ──────────────────────────────────
|
|
379
|
+
/**
|
|
380
|
+
* Parse a session creation time from an ISO 8601 string.
|
|
381
|
+
* Returns null if unparseable.
|
|
382
|
+
*/
|
|
383
|
+
export function parseSessionAge(createdAt, now) {
|
|
384
|
+
if (!createdAt)
|
|
385
|
+
return null;
|
|
386
|
+
const ts = Date.parse(createdAt);
|
|
387
|
+
if (isNaN(ts))
|
|
388
|
+
return null;
|
|
389
|
+
return (now ?? Date.now()) - ts;
|
|
390
|
+
}
|
|
391
|
+
/** Format session age as compact string: "3d", "2h", "45m", "< 1m". */
|
|
392
|
+
export function formatSessionAge(createdAt, now) {
|
|
393
|
+
const ms = parseSessionAge(createdAt, now);
|
|
394
|
+
if (ms === null)
|
|
395
|
+
return "";
|
|
396
|
+
return formatUptime(ms);
|
|
397
|
+
}
|
|
398
|
+
// ── Health history (pure, exported for testing) ───────────────────────────────
|
|
399
|
+
/** Max health score snapshots stored per session. */
|
|
400
|
+
export const MAX_HEALTH_HISTORY = 20;
|
|
401
|
+
/**
|
|
402
|
+
* Format health history as a compact 5-bucket sparkline.
|
|
403
|
+
* Each bucket covers 1/5 of the history window. Color: LIME/AMBER/ROSE by value.
|
|
404
|
+
*/
|
|
405
|
+
export function formatHealthSparkline(history, now) {
|
|
406
|
+
if (history.length === 0)
|
|
407
|
+
return "";
|
|
408
|
+
const BUCKETS = 5;
|
|
409
|
+
const WINDOW_MS = 30 * 60_000; // last 30 minutes
|
|
410
|
+
const nowMs = now ?? Date.now();
|
|
411
|
+
const cutoff = nowMs - WINDOW_MS;
|
|
412
|
+
const recent = history.filter((h) => h.ts >= cutoff);
|
|
413
|
+
if (recent.length === 0)
|
|
414
|
+
return "";
|
|
415
|
+
const bucketMs = WINDOW_MS / BUCKETS;
|
|
416
|
+
const buckets = Array(BUCKETS).fill(-1); // -1 = no data
|
|
417
|
+
for (const h of recent) {
|
|
418
|
+
const idx = Math.min(BUCKETS - 1, Math.floor((h.ts - cutoff) / bucketMs));
|
|
419
|
+
// take the most recent reading per bucket
|
|
420
|
+
if (buckets[idx] === -1 || h.ts > (recent.find((r) => r.ts >= cutoff + idx * bucketMs)?.ts ?? 0)) {
|
|
421
|
+
buckets[idx] = h.score;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return buckets.map((score) => {
|
|
425
|
+
if (score === -1)
|
|
426
|
+
return `${DIM}·${RESET}`;
|
|
427
|
+
const color = score >= HEALTH_GOOD ? LIME : score >= HEALTH_WARN ? AMBER : ROSE;
|
|
428
|
+
return `${color}${SPARK_BLOCKS[Math.min(SPARK_BLOCKS.length - 1, Math.floor(score / 100 * (SPARK_BLOCKS.length - 1)))]}${RESET}`;
|
|
429
|
+
}).join("");
|
|
430
|
+
}
|
|
361
431
|
// ── Cost summary (pure, exported for testing) ────────────────────────────────
|
|
362
432
|
/**
|
|
363
433
|
* Parse a cost string like "$3.42" → 3.42, or null if unparseable.
|
|
@@ -448,6 +518,18 @@ export function formatSessionReport(data) {
|
|
|
448
518
|
}
|
|
449
519
|
return lines.join("\n");
|
|
450
520
|
}
|
|
521
|
+
// ── Session cost budget (pure, exported for testing) ─────────────────────────
|
|
522
|
+
/** Check if a cost string exceeds a budget value. Returns true when over budget. */
|
|
523
|
+
export function isOverBudget(costStr, budgetUSD) {
|
|
524
|
+
if (!costStr)
|
|
525
|
+
return false;
|
|
526
|
+
const val = parseCostValue(costStr);
|
|
527
|
+
return val !== null && val > budgetUSD;
|
|
528
|
+
}
|
|
529
|
+
/** Format a budget-exceeded alert message. */
|
|
530
|
+
export function formatBudgetAlert(title, costStr, budgetUSD) {
|
|
531
|
+
return `${title}: cost ${costStr} exceeded budget $${budgetUSD.toFixed(2)}`;
|
|
532
|
+
}
|
|
451
533
|
// ── Duplicate session helpers (pure, exported for testing) ───────────────────
|
|
452
534
|
/**
|
|
453
535
|
* Build args for duplicating a session: given a source session, return
|
|
@@ -574,7 +656,7 @@ export function formatSessionTagsBadge(tags) {
|
|
|
574
656
|
/**
|
|
575
657
|
* Build stats entries for all sessions — pure, testable, no side effects.
|
|
576
658
|
*/
|
|
577
|
-
export function buildSessionStats(sessions, errorCounts, burnRates, firstSeen, lastChangeAt, healthScores, sessionAliases, now, errorTimestamps, sessionCosts) {
|
|
659
|
+
export function buildSessionStats(sessions, errorCounts, burnRates, firstSeen, lastChangeAt, healthScores, sessionAliases, now, errorTimestamps, sessionCosts, healthHistories) {
|
|
578
660
|
const nowMs = now ?? Date.now();
|
|
579
661
|
return sessions.map((s) => {
|
|
580
662
|
const ceiling = parseContextCeiling(s.contextTokens);
|
|
@@ -583,6 +665,8 @@ export function buildSessionStats(sessions, errorCounts, burnRates, firstSeen, l
|
|
|
583
665
|
const lc = lastChangeAt.get(s.id);
|
|
584
666
|
const errTs = errorTimestamps?.get(s.id);
|
|
585
667
|
const errTrend = errTs ? computeErrorTrend(errTs, nowMs) : undefined;
|
|
668
|
+
const healthHist = healthHistories?.get(s.id);
|
|
669
|
+
const healthSparkline = healthHist ? formatHealthSparkline(healthHist, nowMs) : undefined;
|
|
586
670
|
return {
|
|
587
671
|
title: s.title,
|
|
588
672
|
displayName: sessionAliases.get(s.id),
|
|
@@ -593,6 +677,7 @@ export function buildSessionStats(sessions, errorCounts, burnRates, firstSeen, l
|
|
|
593
677
|
burnRatePerMin: burnRates.get(s.id) ?? null,
|
|
594
678
|
contextPct: ctxPct,
|
|
595
679
|
costStr: sessionCosts?.get(s.id),
|
|
680
|
+
healthSparkline: healthSparkline || undefined,
|
|
596
681
|
uptimeMs: fs !== undefined ? nowMs - fs : null,
|
|
597
682
|
idleSinceMs: lc !== undefined ? nowMs - lc : null,
|
|
598
683
|
};
|
|
@@ -614,9 +699,10 @@ export function formatSessionStatsLines(entries) {
|
|
|
614
699
|
? ` ${Math.round(e.burnRatePerMin / 100) * 100}tok/min` : "";
|
|
615
700
|
const ctxStr = e.contextPct !== null ? ` ctx:${e.contextPct}%` : "";
|
|
616
701
|
const costStr = e.costStr ? ` ${e.costStr}` : "";
|
|
702
|
+
const sparkStr = e.healthSparkline ? ` ${e.healthSparkline}` : "";
|
|
617
703
|
const upStr = e.uptimeMs !== null ? ` up:${formatUptime(e.uptimeMs)}` : "";
|
|
618
704
|
const idleStr = e.idleSinceMs !== null ? ` ${formatIdleSince(e.idleSinceMs)}` : "";
|
|
619
|
-
return ` ${label} [${e.status}] ${healthStr}${errStr}${burnStr}${ctxStr}${costStr}${upStr}${idleStr}`;
|
|
705
|
+
return ` ${label} [${e.status}] ${healthStr}${sparkStr}${errStr}${burnStr}${ctxStr}${costStr}${upStr}${idleStr}`;
|
|
620
706
|
});
|
|
621
707
|
}
|
|
622
708
|
/** Format session stats entries as a JSON object for export. */
|
|
@@ -651,7 +737,8 @@ export const BUILTIN_COMMANDS = new Set([
|
|
|
651
737
|
"/jump", "/marks", "/search", "/alias", "/insist", "/task", "/tasks",
|
|
652
738
|
"/group", "/groups", "/group-filter", "/burn-rate", "/snapshot", "/broadcast", "/watchdog", "/top", "/ceiling", "/rename", "/copy", "/stats", "/recall", "/pin-all-errors", "/export-stats",
|
|
653
739
|
"/mute-errors", "/prev-goal", "/tag", "/tags", "/tag-filter", "/find", "/reset-health", "/timeline", "/color", "/clear-history",
|
|
654
|
-
"/duplicate", "/color-all", "/quiet-hours", "/history-stats", "/cost-summary", "/session-report",
|
|
740
|
+
"/duplicate", "/color-all", "/quiet-hours", "/quiet-status", "/history-stats", "/cost-summary", "/session-report", "/alert-log",
|
|
741
|
+
"/budget", "/pause-all", "/resume-all",
|
|
655
742
|
]);
|
|
656
743
|
/** Resolve a slash command through the alias map. Returns the expanded command or the original. */
|
|
657
744
|
export function resolveAlias(line, aliases) {
|
|
@@ -923,6 +1010,11 @@ export class TUI {
|
|
|
923
1010
|
sessionColors = new Map(); // session ID → accent color name
|
|
924
1011
|
sessionCosts = new Map(); // session ID → latest cost string ("$N.NN")
|
|
925
1012
|
quietHoursRanges = []; // quiet-hour start/end pairs
|
|
1013
|
+
sessionHealthHistory = new Map(); // session ID → health snapshots
|
|
1014
|
+
alertLog = []; // recent auto-generated status alerts (ring buffer, max 100)
|
|
1015
|
+
sessionBudgets = new Map(); // session ID → USD budget
|
|
1016
|
+
globalBudget = null; // global fallback budget in USD
|
|
1017
|
+
budgetAlerted = new Map(); // session ID → epoch ms of last budget alert
|
|
926
1018
|
// drill-down mode: show a single session's full output
|
|
927
1019
|
viewMode = "overview";
|
|
928
1020
|
drilldownSessionId = null;
|
|
@@ -1395,6 +1487,50 @@ export class TUI {
|
|
|
1395
1487
|
return buildDuplicateArgs(this.sessions, sessionIdOrIndex, newTitle);
|
|
1396
1488
|
}
|
|
1397
1489
|
// ── Session timeline ─────────────────────────────────────────────────────
|
|
1490
|
+
// ── Session cost budget ──────────────────────────────────────────────────
|
|
1491
|
+
/** Set a per-session budget in USD. Pass null to clear. */
|
|
1492
|
+
setSessionBudget(sessionIdOrIndex, budgetUSD) {
|
|
1493
|
+
let sessionId;
|
|
1494
|
+
if (typeof sessionIdOrIndex === "number") {
|
|
1495
|
+
sessionId = this.sessions[sessionIdOrIndex - 1]?.id;
|
|
1496
|
+
}
|
|
1497
|
+
else {
|
|
1498
|
+
const needle = sessionIdOrIndex.toLowerCase();
|
|
1499
|
+
const match = this.sessions.find((s) => s.id === sessionIdOrIndex || s.id.startsWith(needle) || s.title.toLowerCase() === needle);
|
|
1500
|
+
sessionId = match?.id;
|
|
1501
|
+
}
|
|
1502
|
+
if (!sessionId)
|
|
1503
|
+
return false;
|
|
1504
|
+
if (budgetUSD === null)
|
|
1505
|
+
this.sessionBudgets.delete(sessionId);
|
|
1506
|
+
else
|
|
1507
|
+
this.sessionBudgets.set(sessionId, budgetUSD);
|
|
1508
|
+
return true;
|
|
1509
|
+
}
|
|
1510
|
+
/** Set global fallback budget (applies to all sessions without per-session budget). */
|
|
1511
|
+
setGlobalBudget(budgetUSD) {
|
|
1512
|
+
this.globalBudget = budgetUSD;
|
|
1513
|
+
}
|
|
1514
|
+
/** Get the per-session budget (or null). */
|
|
1515
|
+
getSessionBudget(id) {
|
|
1516
|
+
return this.sessionBudgets.get(id) ?? null;
|
|
1517
|
+
}
|
|
1518
|
+
/** Get the global budget (or null if not set). */
|
|
1519
|
+
getGlobalBudget() {
|
|
1520
|
+
return this.globalBudget;
|
|
1521
|
+
}
|
|
1522
|
+
/** Return all per-session budgets. */
|
|
1523
|
+
getAllSessionBudgets() {
|
|
1524
|
+
return this.sessionBudgets;
|
|
1525
|
+
}
|
|
1526
|
+
/** Return health history for a session (for sparkline). */
|
|
1527
|
+
getSessionHealthHistory(id) {
|
|
1528
|
+
return this.sessionHealthHistory.get(id) ?? [];
|
|
1529
|
+
}
|
|
1530
|
+
/** Return all alert log entries (last 100 "status" tag entries). */
|
|
1531
|
+
getAlertLog() {
|
|
1532
|
+
return this.alertLog;
|
|
1533
|
+
}
|
|
1398
1534
|
/** Get the latest cost string for a session (or undefined). */
|
|
1399
1535
|
getSessionCost(id) {
|
|
1400
1536
|
return this.sessionCosts.get(id);
|
|
@@ -1719,9 +1855,18 @@ export class TUI {
|
|
|
1719
1855
|
}
|
|
1720
1856
|
if (s.lastActivity !== undefined)
|
|
1721
1857
|
this.prevLastActivity.set(s.id, s.lastActivity);
|
|
1722
|
-
// track cost string
|
|
1723
|
-
if (s.costStr)
|
|
1858
|
+
// track cost string + check budget
|
|
1859
|
+
if (s.costStr) {
|
|
1724
1860
|
this.sessionCosts.set(s.id, s.costStr);
|
|
1861
|
+
const budget = this.sessionBudgets.get(s.id) ?? this.globalBudget;
|
|
1862
|
+
if (budget !== null && isOverBudget(s.costStr, budget) && !quietNow) {
|
|
1863
|
+
const lastAlert = this.budgetAlerted.get(s.id) ?? 0;
|
|
1864
|
+
if (now - lastAlert >= 5 * 60_000) {
|
|
1865
|
+
this.budgetAlerted.set(s.id, now);
|
|
1866
|
+
this.log("status", formatBudgetAlert(s.title, s.costStr, budget), s.id);
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1725
1870
|
// track context token history for burn-rate alerts
|
|
1726
1871
|
const tokens = parseContextTokenNumber(s.contextTokens);
|
|
1727
1872
|
if (tokens !== null) {
|
|
@@ -1773,6 +1918,27 @@ export class TUI {
|
|
|
1773
1918
|
if (!currentIds.has(id))
|
|
1774
1919
|
this.sessionContextHistory.delete(id);
|
|
1775
1920
|
}
|
|
1921
|
+
// record health snapshots
|
|
1922
|
+
for (const s of opts.sessions) {
|
|
1923
|
+
const ceiling2 = parseContextCeiling(s.contextTokens);
|
|
1924
|
+
const cf2 = ceiling2 ? ceiling2.current / ceiling2.max : null;
|
|
1925
|
+
const bh2 = this.sessionContextHistory.get(s.id);
|
|
1926
|
+
const br2 = bh2 ? computeContextBurnRate(bh2, now) : null;
|
|
1927
|
+
const lc2 = this.lastChangeAt.get(s.id);
|
|
1928
|
+
const idle2 = lc2 !== undefined ? now - lc2 : null;
|
|
1929
|
+
const hs = computeHealthScore({
|
|
1930
|
+
errorCount: this.sessionErrorCounts.get(s.id) ?? 0,
|
|
1931
|
+
burnRatePerMin: br2,
|
|
1932
|
+
contextFraction: cf2,
|
|
1933
|
+
idleMs: idle2,
|
|
1934
|
+
watchdogThresholdMs: this.watchdogThresholdMs,
|
|
1935
|
+
});
|
|
1936
|
+
const hist = this.sessionHealthHistory.get(s.id) ?? [];
|
|
1937
|
+
hist.push({ score: hs, ts: now });
|
|
1938
|
+
if (hist.length > MAX_HEALTH_HISTORY)
|
|
1939
|
+
hist.shift();
|
|
1940
|
+
this.sessionHealthHistory.set(s.id, hist);
|
|
1941
|
+
}
|
|
1776
1942
|
const sorted = sortSessions(opts.sessions, this.sortMode, this.lastChangeAt, this.pinnedIds);
|
|
1777
1943
|
const prevVisibleCount = this.getVisibleCount();
|
|
1778
1944
|
this.sessions = sorted;
|
|
@@ -1811,6 +1977,12 @@ export class TUI {
|
|
|
1811
1977
|
process.stderr.write("\x07");
|
|
1812
1978
|
}
|
|
1813
1979
|
}
|
|
1980
|
+
// collect "status" alerts into alert log (for /alert-log)
|
|
1981
|
+
if (tag === "status") {
|
|
1982
|
+
this.alertLog.push(entry);
|
|
1983
|
+
if (this.alertLog.length > 100)
|
|
1984
|
+
this.alertLog.shift();
|
|
1985
|
+
}
|
|
1814
1986
|
// track per-session error counts + timestamps (for sparklines)
|
|
1815
1987
|
if (sessionId && shouldAutoPin(tag)) {
|
|
1816
1988
|
this.sessionErrorCounts.set(sessionId, (this.sessionErrorCounts.get(sessionId) ?? 0) + 1);
|
|
@@ -2283,7 +2455,8 @@ export class TUI {
|
|
|
2283
2455
|
const badgeSuffix = muteBadge ? `${muteBadge} ` : "";
|
|
2284
2456
|
const iconsWidth = (pinned ? 2 : 0) + (muted ? 2 : 0) + (noted ? 2 : 0) + actualBadgeWidth + groupBadgeWidth + tagsBadgeWidth + colorDotWidth;
|
|
2285
2457
|
const cardWidth = innerWidth - 1 - iconsWidth;
|
|
2286
|
-
const
|
|
2458
|
+
const cardAge = s.createdAt ? formatSessionAge(s.createdAt, nowMs) : undefined;
|
|
2459
|
+
const line = `${bg}${SLATE}${BOX.v}${RESET}${bg} ${pin}${mute}${badgeSuffix}${note}${groupBadge}${tagsBadge}${colorDot}${formatSessionCard(s, cardWidth, errSparkline || undefined, idleSinceMs, healthBadge || undefined, displayName, cardAge || undefined)}`;
|
|
2287
2460
|
const padded = padBoxLineHover(line, this.cols, isHovered);
|
|
2288
2461
|
process.stderr.write(moveTo(startRow + 1 + i, 1) + CLEAR_LINE + padded);
|
|
2289
2462
|
}
|
|
@@ -2353,7 +2526,8 @@ export class TUI {
|
|
|
2353
2526
|
const badgeSuffix = muteBadge ? `${muteBadge} ` : "";
|
|
2354
2527
|
const iconsWidth = (pinned ? 2 : 0) + (muted ? 2 : 0) + (noted ? 2 : 0) + actualBadgeWidth + groupBadgeWidth + tagsBadgeWidth2 + colorDotWidth2;
|
|
2355
2528
|
const cardWidth = innerWidth - 1 - iconsWidth;
|
|
2356
|
-
const
|
|
2529
|
+
const cardAge2 = s.createdAt ? formatSessionAge(s.createdAt, nowMs2) : undefined;
|
|
2530
|
+
const line = `${bg}${SLATE}${BOX.v}${RESET}${bg} ${pin}${mute}${badgeSuffix}${note}${groupBadge}${tagsBadge2}${colorDot2}${formatSessionCard(s, cardWidth, errSparkline || undefined, idleSinceMs, healthBadge2 || undefined, displayName2, cardAge2 || undefined)}`;
|
|
2357
2531
|
const padded = padBoxLineHover(line, this.cols, isHovered);
|
|
2358
2532
|
process.stderr.write(SAVE_CURSOR + moveTo(startRow + 1 + i, 1) + CLEAR_LINE + padded + RESTORE_CURSOR);
|
|
2359
2533
|
}
|
|
@@ -2471,7 +2645,8 @@ export class TUI {
|
|
|
2471
2645
|
// idleSinceMs: optional ms since last activity change (shown when idle/stopped)
|
|
2472
2646
|
// healthBadge: optional pre-formatted health score badge ("⬡83" colored)
|
|
2473
2647
|
// displayName: optional custom name override (from /rename)
|
|
2474
|
-
|
|
2648
|
+
// ageStr: optional session age string (from createdAt)
|
|
2649
|
+
function formatSessionCard(s, maxWidth, errorSparkline, idleSinceMs, healthBadge, displayName, ageStr) {
|
|
2475
2650
|
const dot = STATUS_DOT[s.status] ?? `${AMBER}${DOT.filled}${RESET}`;
|
|
2476
2651
|
const title = displayName ?? s.title;
|
|
2477
2652
|
const name = displayName ? `${BOLD}${displayName}${DIM} (${s.title})${RESET}` : `${BOLD}${s.title}${RESET}`;
|
|
@@ -2511,7 +2686,8 @@ function formatSessionCard(s, maxWidth, errorSparkline, idleSinceMs, healthBadge
|
|
|
2511
2686
|
else {
|
|
2512
2687
|
desc = `${SLATE}${s.status}${RESET}`;
|
|
2513
2688
|
}
|
|
2514
|
-
|
|
2689
|
+
const ageSuffix = ageStr ? ` ${DIM}age:${ageStr}${RESET}` : "";
|
|
2690
|
+
return truncateAnsi(`${dot} ${healthPrefix}${name} ${toolBadge}${contextBadge} ${SLATE}${BOX.h}${RESET} ${desc}${ageSuffix}${sparkSuffix}`, maxWidth);
|
|
2515
2691
|
}
|
|
2516
2692
|
// colorize an activity entry based on its tag
|
|
2517
2693
|
function formatActivity(entry, maxCols) {
|
package/dist/types.d.ts
CHANGED