aoaoe 0.146.0 → 0.153.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +118 -1
- package/dist/input.d.ts +18 -0
- package/dist/input.js +99 -2
- package/dist/tui.d.ts +64 -3
- package/dist/tui.js +193 -9
- package/package.json +1 -1
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, formatQuietStatus, formatSessionAge } 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, formatHealthTrendChart, isOverBudget } 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";
|
|
@@ -1023,6 +1023,123 @@ async function main() {
|
|
|
1023
1023
|
tui.log("system", `quiet hours: ${specs.join(", ")} — watchdog+burn alerts suppressed`);
|
|
1024
1024
|
persistPrefs();
|
|
1025
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 /health-trend
|
|
1066
|
+
input.onHealthTrend((target, height) => {
|
|
1067
|
+
const num = /^\d+$/.test(target) ? parseInt(target, 10) : undefined;
|
|
1068
|
+
const sessions = tui.getSessions();
|
|
1069
|
+
const session = num !== undefined
|
|
1070
|
+
? sessions[num - 1]
|
|
1071
|
+
: sessions.find((s) => s.title.toLowerCase() === target.toLowerCase() || s.id.startsWith(target));
|
|
1072
|
+
if (!session) {
|
|
1073
|
+
tui.log("system", `session not found: ${target}`);
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
const hist = tui.getSessionHealthHistory(session.id);
|
|
1077
|
+
const lines = formatHealthTrendChart(hist, session.title, height);
|
|
1078
|
+
for (const l of lines)
|
|
1079
|
+
tui.log("system", l);
|
|
1080
|
+
});
|
|
1081
|
+
// wire /alert-mute
|
|
1082
|
+
input.onAlertMute((pattern) => {
|
|
1083
|
+
if (pattern === null) {
|
|
1084
|
+
// null = "clear" keyword
|
|
1085
|
+
tui.clearAlertMutePatterns();
|
|
1086
|
+
tui.log("system", "alert-mute: all patterns cleared");
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
if (!pattern) {
|
|
1090
|
+
// empty = list patterns
|
|
1091
|
+
const pats = tui.getAlertMutePatterns();
|
|
1092
|
+
if (pats.size === 0) {
|
|
1093
|
+
tui.log("system", "alert-mute: no patterns set — use /alert-mute <text> to suppress");
|
|
1094
|
+
}
|
|
1095
|
+
else {
|
|
1096
|
+
tui.log("system", `alert-mute: ${pats.size} pattern${pats.size !== 1 ? "s" : ""}:`);
|
|
1097
|
+
for (const p of pats)
|
|
1098
|
+
tui.log("system", ` "${p}"`);
|
|
1099
|
+
}
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
tui.addAlertMutePattern(pattern);
|
|
1103
|
+
tui.log("system", `alert-mute: added "${pattern}" — matching alerts hidden from /alert-log`);
|
|
1104
|
+
});
|
|
1105
|
+
// wire /budgets list
|
|
1106
|
+
input.onBudgetsList(() => {
|
|
1107
|
+
const global = tui.getGlobalBudget();
|
|
1108
|
+
const perSession = tui.getAllSessionBudgets();
|
|
1109
|
+
if (global === null && perSession.size === 0) {
|
|
1110
|
+
tui.log("system", "budgets: none set — use /budget $N (global) or /budget <N> $N (per-session)");
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
if (global !== null)
|
|
1114
|
+
tui.log("system", ` global: $${global.toFixed(2)}`);
|
|
1115
|
+
const sessions = tui.getSessions();
|
|
1116
|
+
for (const [id, budget] of perSession) {
|
|
1117
|
+
const s = sessions.find((s) => s.id === id);
|
|
1118
|
+
const label = s?.title ?? id.slice(0, 8);
|
|
1119
|
+
tui.log("system", ` ${label}: $${budget.toFixed(2)}`);
|
|
1120
|
+
}
|
|
1121
|
+
});
|
|
1122
|
+
// wire /budget-status
|
|
1123
|
+
input.onBudgetStatus(() => {
|
|
1124
|
+
const sessions = tui.getSessions();
|
|
1125
|
+
const costs = tui.getAllSessionCosts();
|
|
1126
|
+
const global = tui.getGlobalBudget();
|
|
1127
|
+
const perSession = tui.getAllSessionBudgets();
|
|
1128
|
+
let shown = 0;
|
|
1129
|
+
for (const s of sessions) {
|
|
1130
|
+
const budget = perSession.get(s.id) ?? global;
|
|
1131
|
+
const costStr = costs.get(s.id);
|
|
1132
|
+
if (budget === null)
|
|
1133
|
+
continue;
|
|
1134
|
+
const over = isOverBudget(costStr, budget);
|
|
1135
|
+
const costLabel = costStr ?? "(no data)";
|
|
1136
|
+
const status = over ? `OVER ($${budget.toFixed(2)} budget)` : `ok ($${budget.toFixed(2)} budget)`;
|
|
1137
|
+
tui.log("system", ` ${s.title}: ${costLabel} — ${status}`);
|
|
1138
|
+
shown++;
|
|
1139
|
+
}
|
|
1140
|
+
if (shown === 0)
|
|
1141
|
+
tui.log("system", "budget-status: no sessions with budgets configured");
|
|
1142
|
+
});
|
|
1026
1143
|
// wire /quiet-status
|
|
1027
1144
|
input.onQuietStatus(() => {
|
|
1028
1145
|
const { active, message } = formatQuietStatus(tui.getQuietHours());
|
package/dist/input.d.ts
CHANGED
|
@@ -56,6 +56,12 @@ export type CostSummaryHandler = () => void;
|
|
|
56
56
|
export type SessionReportHandler = (target: string) => void;
|
|
57
57
|
export type QuietStatusHandler = () => void;
|
|
58
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;
|
|
61
|
+
export type HealthTrendHandler = (target: string, height: number) => void;
|
|
62
|
+
export type AlertMuteHandler = (pattern: string | null) => void;
|
|
63
|
+
export type BudgetsListHandler = () => void;
|
|
64
|
+
export type BudgetStatusHandler = () => void;
|
|
59
65
|
export interface MouseEvent {
|
|
60
66
|
button: number;
|
|
61
67
|
col: number;
|
|
@@ -134,6 +140,12 @@ export declare class InputReader {
|
|
|
134
140
|
private sessionReportHandler;
|
|
135
141
|
private quietStatusHandler;
|
|
136
142
|
private alertLogHandler;
|
|
143
|
+
private budgetHandler;
|
|
144
|
+
private bulkControlHandler;
|
|
145
|
+
private healthTrendHandler;
|
|
146
|
+
private alertMuteHandler;
|
|
147
|
+
private budgetsListHandler;
|
|
148
|
+
private budgetStatusHandler;
|
|
137
149
|
private aliases;
|
|
138
150
|
private mouseDataListener;
|
|
139
151
|
onScroll(handler: (dir: ScrollDirection) => void): void;
|
|
@@ -195,6 +207,12 @@ export declare class InputReader {
|
|
|
195
207
|
onSessionReport(handler: SessionReportHandler): void;
|
|
196
208
|
onQuietStatus(handler: QuietStatusHandler): void;
|
|
197
209
|
onAlertLog(handler: AlertLogHandler): void;
|
|
210
|
+
onBudget(handler: BudgetHandler): void;
|
|
211
|
+
onBulkControl(handler: BulkControlHandler): void;
|
|
212
|
+
onHealthTrend(handler: HealthTrendHandler): void;
|
|
213
|
+
onAlertMute(handler: AlertMuteHandler): void;
|
|
214
|
+
onBudgetsList(handler: BudgetsListHandler): void;
|
|
215
|
+
onBudgetStatus(handler: BudgetStatusHandler): void;
|
|
198
216
|
/** Set aliases from persisted prefs. */
|
|
199
217
|
setAliases(aliases: Record<string, string>): void;
|
|
200
218
|
/** Get current aliases as a plain object. */
|
package/dist/input.js
CHANGED
|
@@ -89,6 +89,12 @@ export class InputReader {
|
|
|
89
89
|
sessionReportHandler = null;
|
|
90
90
|
quietStatusHandler = null;
|
|
91
91
|
alertLogHandler = null;
|
|
92
|
+
budgetHandler = null;
|
|
93
|
+
bulkControlHandler = null;
|
|
94
|
+
healthTrendHandler = null;
|
|
95
|
+
alertMuteHandler = null;
|
|
96
|
+
budgetsListHandler = null;
|
|
97
|
+
budgetStatusHandler = null;
|
|
92
98
|
aliases = new Map(); // /shortcut → /full command
|
|
93
99
|
mouseDataListener = null;
|
|
94
100
|
// register a callback for scroll key events (PgUp/PgDn/Home/End)
|
|
@@ -303,6 +309,12 @@ export class InputReader {
|
|
|
303
309
|
onSessionReport(handler) { this.sessionReportHandler = handler; }
|
|
304
310
|
onQuietStatus(handler) { this.quietStatusHandler = handler; }
|
|
305
311
|
onAlertLog(handler) { this.alertLogHandler = handler; }
|
|
312
|
+
onBudget(handler) { this.budgetHandler = handler; }
|
|
313
|
+
onBulkControl(handler) { this.bulkControlHandler = handler; }
|
|
314
|
+
onHealthTrend(handler) { this.healthTrendHandler = handler; }
|
|
315
|
+
onAlertMute(handler) { this.alertMuteHandler = handler; }
|
|
316
|
+
onBudgetsList(handler) { this.budgetsListHandler = handler; }
|
|
317
|
+
onBudgetStatus(handler) { this.budgetStatusHandler = handler; }
|
|
306
318
|
/** Set aliases from persisted prefs. */
|
|
307
319
|
setAliases(aliases) {
|
|
308
320
|
this.aliases.clear();
|
|
@@ -565,8 +577,15 @@ ${BOLD}navigation:${RESET}
|
|
|
565
577
|
/color-all [c] set accent color for all sessions (no color = clear all)
|
|
566
578
|
/quiet-hours [H-H] suppress watchdog+burn alerts during hours (e.g. 22-06; no arg = clear)
|
|
567
579
|
/quiet-status show whether quiet hours are currently active
|
|
568
|
-
/
|
|
569
|
-
/
|
|
580
|
+
/budget [N] [$] set cost budget: /budget 1 2.50 (session), /budget 2.50 (global), /budget clear
|
|
581
|
+
/pause-all send interrupt to all sessions
|
|
582
|
+
/resume-all send resume to all sessions
|
|
583
|
+
/alert-log [N] show last N auto-generated alerts (burn-rate/watchdog/ceiling; default 20)
|
|
584
|
+
/alert-mute [pat] suppress alerts containing pattern; no arg = list; clear = remove all
|
|
585
|
+
/health-trend N show ASCII health score chart for session N [height]
|
|
586
|
+
/budgets list all active cost budgets
|
|
587
|
+
/budget-status show which sessions are over or under budget
|
|
588
|
+
/history-stats show aggregate statistics from persisted activity history
|
|
570
589
|
/cost-summary show total estimated spend across all sessions
|
|
571
590
|
/session-report N generate full markdown report for a session → ~/.aoaoe/report-<name>-<ts>.md
|
|
572
591
|
/clip [N] copy last N activity entries to clipboard (default 20)
|
|
@@ -1121,6 +1140,84 @@ ${BOLD}other:${RESET}
|
|
|
1121
1140
|
console.error(`${DIM}history-stats not available (no TUI)${RESET}`);
|
|
1122
1141
|
}
|
|
1123
1142
|
break;
|
|
1143
|
+
case "/budget": {
|
|
1144
|
+
const bArgs = line.slice("/budget".length).trim().split(/\s+/).filter(Boolean);
|
|
1145
|
+
if (bArgs.length === 0 || bArgs[0] === "clear") {
|
|
1146
|
+
// clear global budget
|
|
1147
|
+
if (this.budgetHandler)
|
|
1148
|
+
this.budgetHandler(null, null);
|
|
1149
|
+
else
|
|
1150
|
+
console.error(`${DIM}budget not available${RESET}`);
|
|
1151
|
+
break;
|
|
1152
|
+
}
|
|
1153
|
+
if (this.budgetHandler) {
|
|
1154
|
+
// /budget <$N> → global, /budget <N|name> <$N> → per-session
|
|
1155
|
+
const maybeUSD = parseFloat(bArgs[bArgs.length - 1].replace("$", ""));
|
|
1156
|
+
if (!isNaN(maybeUSD) && bArgs.length === 1) {
|
|
1157
|
+
this.budgetHandler(null, maybeUSD); // global
|
|
1158
|
+
}
|
|
1159
|
+
else if (!isNaN(maybeUSD) && bArgs.length >= 2) {
|
|
1160
|
+
this.budgetHandler(bArgs[0], maybeUSD); // per-session
|
|
1161
|
+
}
|
|
1162
|
+
else {
|
|
1163
|
+
console.error(`${DIM}usage: /budget [$N.NN] — global, /budget <N|name> $N.NN — per-session, /budget clear — remove${RESET}`);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
else {
|
|
1167
|
+
console.error(`${DIM}budget not available${RESET}`);
|
|
1168
|
+
}
|
|
1169
|
+
break;
|
|
1170
|
+
}
|
|
1171
|
+
case "/pause-all":
|
|
1172
|
+
if (this.bulkControlHandler)
|
|
1173
|
+
this.bulkControlHandler("pause");
|
|
1174
|
+
else
|
|
1175
|
+
console.error(`${DIM}pause-all not available${RESET}`);
|
|
1176
|
+
break;
|
|
1177
|
+
case "/resume-all":
|
|
1178
|
+
if (this.bulkControlHandler)
|
|
1179
|
+
this.bulkControlHandler("resume");
|
|
1180
|
+
else
|
|
1181
|
+
console.error(`${DIM}resume-all not available${RESET}`);
|
|
1182
|
+
break;
|
|
1183
|
+
case "/health-trend": {
|
|
1184
|
+
const htArgs = line.slice("/health-trend".length).trim().split(/\s+/).filter(Boolean);
|
|
1185
|
+
const htTarget = htArgs[0] ?? "";
|
|
1186
|
+
const htHeight = htArgs[1] ? parseInt(htArgs[1], 10) : 6;
|
|
1187
|
+
if (!htTarget) {
|
|
1188
|
+
console.error(`${DIM}usage: /health-trend <N|name> [height]${RESET}`);
|
|
1189
|
+
break;
|
|
1190
|
+
}
|
|
1191
|
+
if (this.healthTrendHandler)
|
|
1192
|
+
this.healthTrendHandler(htTarget, isNaN(htHeight) || htHeight < 2 ? 6 : Math.min(htHeight, 20));
|
|
1193
|
+
else
|
|
1194
|
+
console.error(`${DIM}health-trend not available${RESET}`);
|
|
1195
|
+
break;
|
|
1196
|
+
}
|
|
1197
|
+
case "/alert-mute": {
|
|
1198
|
+
const amArg = line.slice("/alert-mute".length).trim();
|
|
1199
|
+
if (this.alertMuteHandler) {
|
|
1200
|
+
if (amArg.toLowerCase() === "clear")
|
|
1201
|
+
this.alertMuteHandler(null);
|
|
1202
|
+
else
|
|
1203
|
+
this.alertMuteHandler(amArg || null);
|
|
1204
|
+
}
|
|
1205
|
+
else
|
|
1206
|
+
console.error(`${DIM}alert-mute not available${RESET}`);
|
|
1207
|
+
break;
|
|
1208
|
+
}
|
|
1209
|
+
case "/budgets":
|
|
1210
|
+
if (this.budgetsListHandler)
|
|
1211
|
+
this.budgetsListHandler();
|
|
1212
|
+
else
|
|
1213
|
+
console.error(`${DIM}budgets not available${RESET}`);
|
|
1214
|
+
break;
|
|
1215
|
+
case "/budget-status":
|
|
1216
|
+
if (this.budgetStatusHandler)
|
|
1217
|
+
this.budgetStatusHandler();
|
|
1218
|
+
else
|
|
1219
|
+
console.error(`${DIM}budget-status not available${RESET}`);
|
|
1220
|
+
break;
|
|
1124
1221
|
case "/quiet-status":
|
|
1125
1222
|
if (this.quietStatusHandler)
|
|
1126
1223
|
this.quietStatusHandler();
|
package/dist/tui.d.ts
CHANGED
|
@@ -137,6 +137,34 @@ export interface HealthSnapshot {
|
|
|
137
137
|
* Each bucket covers 1/5 of the history window. Color: LIME/AMBER/ROSE by value.
|
|
138
138
|
*/
|
|
139
139
|
export declare function formatHealthSparkline(history: readonly HealthSnapshot[], now?: number): string;
|
|
140
|
+
/**
|
|
141
|
+
* Format health score history as a multi-line ASCII bar chart.
|
|
142
|
+
* Returns an array of lines suitable for logging to the activity area.
|
|
143
|
+
* Each column is one snapshot, ordered oldest→newest.
|
|
144
|
+
* Bar height 0–8 rows. Color: LIME/AMBER/ROSE.
|
|
145
|
+
*/
|
|
146
|
+
export declare function formatHealthTrendChart(history: readonly HealthSnapshot[], title: string, height?: number): string[];
|
|
147
|
+
/** Status change entry for flap detection. */
|
|
148
|
+
export interface StatusChange {
|
|
149
|
+
status: string;
|
|
150
|
+
ts: number;
|
|
151
|
+
}
|
|
152
|
+
/** Max status change entries stored per session. */
|
|
153
|
+
export declare const MAX_STATUS_HISTORY = 30;
|
|
154
|
+
/** Flap detection window: check for oscillation in the last N minutes. */
|
|
155
|
+
export declare const FLAP_WINDOW_MS: number;
|
|
156
|
+
/** Min status changes in window to be considered flapping. */
|
|
157
|
+
export declare const FLAP_THRESHOLD = 5;
|
|
158
|
+
/**
|
|
159
|
+
* Detect if a session is "flapping" — rapidly oscillating between statuses.
|
|
160
|
+
* Returns true when there are >= FLAP_THRESHOLD status changes in FLAP_WINDOW_MS.
|
|
161
|
+
*/
|
|
162
|
+
export declare function isFlapping(changes: readonly StatusChange[], now?: number, windowMs?: number, threshold?: number): boolean;
|
|
163
|
+
/**
|
|
164
|
+
* Check if an alert text matches any suppressed pattern (case-insensitive substring).
|
|
165
|
+
* Returns true when the alert should be hidden.
|
|
166
|
+
*/
|
|
167
|
+
export declare function isAlertMuted(text: string, patterns: ReadonlySet<string>): boolean;
|
|
140
168
|
/**
|
|
141
169
|
* Parse a cost string like "$3.42" → 3.42, or null if unparseable.
|
|
142
170
|
*/
|
|
@@ -180,6 +208,10 @@ export interface SessionReportData {
|
|
|
180
208
|
}
|
|
181
209
|
/** Format a session report as a Markdown document. */
|
|
182
210
|
export declare function formatSessionReport(data: SessionReportData): string;
|
|
211
|
+
/** Check if a cost string exceeds a budget value. Returns true when over budget. */
|
|
212
|
+
export declare function isOverBudget(costStr: string | undefined, budgetUSD: number): boolean;
|
|
213
|
+
/** Format a budget-exceeded alert message. */
|
|
214
|
+
export declare function formatBudgetAlert(title: string, costStr: string, budgetUSD: number): string;
|
|
183
215
|
/**
|
|
184
216
|
* Build args for duplicating a session: given a source session, return
|
|
185
217
|
* { path, tool, title } for use in a create_agent action.
|
|
@@ -408,6 +440,13 @@ export declare class TUI {
|
|
|
408
440
|
private quietHoursRanges;
|
|
409
441
|
private sessionHealthHistory;
|
|
410
442
|
private alertLog;
|
|
443
|
+
private sessionBudgets;
|
|
444
|
+
private globalBudget;
|
|
445
|
+
private budgetAlerted;
|
|
446
|
+
private sessionStatusHistory;
|
|
447
|
+
private prevSessionStatus;
|
|
448
|
+
private flapAlerted;
|
|
449
|
+
private alertMutePatterns;
|
|
411
450
|
private viewMode;
|
|
412
451
|
private drilldownSessionId;
|
|
413
452
|
private sessionOutputs;
|
|
@@ -539,10 +578,32 @@ export declare class TUI {
|
|
|
539
578
|
tool: string;
|
|
540
579
|
title: string;
|
|
541
580
|
} | null;
|
|
581
|
+
/** Set a per-session budget in USD. Pass null to clear. */
|
|
582
|
+
setSessionBudget(sessionIdOrIndex: string | number, budgetUSD: number | null): boolean;
|
|
583
|
+
/** Set global fallback budget (applies to all sessions without per-session budget). */
|
|
584
|
+
setGlobalBudget(budgetUSD: number | null): void;
|
|
585
|
+
/** Get the per-session budget (or null). */
|
|
586
|
+
getSessionBudget(id: string): number | null;
|
|
587
|
+
/** Get the global budget (or null if not set). */
|
|
588
|
+
getGlobalBudget(): number | null;
|
|
589
|
+
/** Return all per-session budgets. */
|
|
590
|
+
getAllSessionBudgets(): ReadonlyMap<string, number>;
|
|
542
591
|
/** Return health history for a session (for sparkline). */
|
|
543
592
|
getSessionHealthHistory(id: string): readonly HealthSnapshot[];
|
|
544
|
-
/** Return all alert log entries (last 100 "status" tag entries). */
|
|
545
|
-
getAlertLog(): readonly ActivityEntry[];
|
|
593
|
+
/** Return all alert log entries (last 100 "status" tag entries), filtered by mute patterns. */
|
|
594
|
+
getAlertLog(includeAll?: boolean): readonly ActivityEntry[];
|
|
595
|
+
/** Return status change history for a session. */
|
|
596
|
+
getSessionStatusHistory(id: string): readonly StatusChange[];
|
|
597
|
+
/** Check if a session is currently flapping. */
|
|
598
|
+
isSessionFlapping(id: string, now?: number): boolean;
|
|
599
|
+
/** Add a pattern to suppress from alert log display. */
|
|
600
|
+
addAlertMutePattern(pattern: string): void;
|
|
601
|
+
/** Remove a pattern from alert mute list. Returns true if it was present. */
|
|
602
|
+
removeAlertMutePattern(pattern: string): boolean;
|
|
603
|
+
/** Clear all alert mute patterns. */
|
|
604
|
+
clearAlertMutePatterns(): void;
|
|
605
|
+
/** Return all alert mute patterns (for display/persistence). */
|
|
606
|
+
getAlertMutePatterns(): ReadonlySet<string>;
|
|
546
607
|
/** Get the latest cost string for a session (or undefined). */
|
|
547
608
|
getSessionCost(id: string): string | undefined;
|
|
548
609
|
/** Return all session costs (for /stats). */
|
|
@@ -691,7 +752,7 @@ export declare class TUI {
|
|
|
691
752
|
private repaintDrilldownContent;
|
|
692
753
|
private paintInputLine;
|
|
693
754
|
}
|
|
694
|
-
declare function formatSessionCard(s: DaemonSessionState, maxWidth: number, errorSparkline?: string, idleSinceMs?: number, healthBadge?: string, displayName?: string): string;
|
|
755
|
+
declare function formatSessionCard(s: DaemonSessionState, maxWidth: number, errorSparkline?: string, idleSinceMs?: number, healthBadge?: string, displayName?: string, ageStr?: string): string;
|
|
695
756
|
declare function formatActivity(entry: ActivityEntry, maxCols: number): string;
|
|
696
757
|
declare function padBoxLine(line: string, totalWidth: number): string;
|
|
697
758
|
declare function padBoxLineHover(line: string, totalWidth: number, hovered: boolean): string;
|
package/dist/tui.js
CHANGED
|
@@ -428,6 +428,74 @@ export function formatHealthSparkline(history, now) {
|
|
|
428
428
|
return `${color}${SPARK_BLOCKS[Math.min(SPARK_BLOCKS.length - 1, Math.floor(score / 100 * (SPARK_BLOCKS.length - 1)))]}${RESET}`;
|
|
429
429
|
}).join("");
|
|
430
430
|
}
|
|
431
|
+
// ── Health trend chart (pure, exported for testing) ───────────────────────────
|
|
432
|
+
/**
|
|
433
|
+
* Format health score history as a multi-line ASCII bar chart.
|
|
434
|
+
* Returns an array of lines suitable for logging to the activity area.
|
|
435
|
+
* Each column is one snapshot, ordered oldest→newest.
|
|
436
|
+
* Bar height 0–8 rows. Color: LIME/AMBER/ROSE.
|
|
437
|
+
*/
|
|
438
|
+
export function formatHealthTrendChart(history, title, height = 6) {
|
|
439
|
+
if (history.length === 0)
|
|
440
|
+
return [` ${title}: no health history`];
|
|
441
|
+
const MAX_COLS = 40;
|
|
442
|
+
const samples = history.slice(-MAX_COLS);
|
|
443
|
+
const lines = [];
|
|
444
|
+
// header
|
|
445
|
+
const minScore = Math.min(...samples.map((h) => h.score));
|
|
446
|
+
const maxScore = Math.max(...samples.map((h) => h.score));
|
|
447
|
+
lines.push(` ${DIM}${title}${RESET} health trend (${samples.length} samples, ${minScore}–${maxScore})`);
|
|
448
|
+
// chart rows (top = high score, bottom = low score)
|
|
449
|
+
for (let row = height - 1; row >= 0; row--) {
|
|
450
|
+
const threshold = Math.round(((row + 1) / height) * 100);
|
|
451
|
+
const prevThreshold = Math.round((row / height) * 100);
|
|
452
|
+
const yLabel = row === height - 1 ? "100" : row === 0 ? " 0" : ` `;
|
|
453
|
+
const bar = samples.map((h) => {
|
|
454
|
+
if (h.score >= threshold) {
|
|
455
|
+
const color = h.score >= HEALTH_GOOD ? LIME : h.score >= HEALTH_WARN ? AMBER : ROSE;
|
|
456
|
+
return `${color}█${RESET}`;
|
|
457
|
+
}
|
|
458
|
+
if (h.score >= prevThreshold) {
|
|
459
|
+
const color = h.score >= HEALTH_GOOD ? LIME : h.score >= HEALTH_WARN ? AMBER : ROSE;
|
|
460
|
+
return `${color}▄${RESET}`;
|
|
461
|
+
}
|
|
462
|
+
return `${DIM}·${RESET}`;
|
|
463
|
+
}).join("");
|
|
464
|
+
lines.push(` ${DIM}${yLabel}${RESET}│${bar}`);
|
|
465
|
+
}
|
|
466
|
+
lines.push(` └${"─".repeat(samples.length)}`);
|
|
467
|
+
return lines;
|
|
468
|
+
}
|
|
469
|
+
/** Max status change entries stored per session. */
|
|
470
|
+
export const MAX_STATUS_HISTORY = 30;
|
|
471
|
+
/** Flap detection window: check for oscillation in the last N minutes. */
|
|
472
|
+
export const FLAP_WINDOW_MS = 10 * 60_000;
|
|
473
|
+
/** Min status changes in window to be considered flapping. */
|
|
474
|
+
export const FLAP_THRESHOLD = 5;
|
|
475
|
+
/**
|
|
476
|
+
* Detect if a session is "flapping" — rapidly oscillating between statuses.
|
|
477
|
+
* Returns true when there are >= FLAP_THRESHOLD status changes in FLAP_WINDOW_MS.
|
|
478
|
+
*/
|
|
479
|
+
export function isFlapping(changes, now, windowMs = FLAP_WINDOW_MS, threshold = FLAP_THRESHOLD) {
|
|
480
|
+
const cutoff = (now ?? Date.now()) - windowMs;
|
|
481
|
+
const recent = changes.filter((c) => c.ts >= cutoff);
|
|
482
|
+
return recent.length >= threshold;
|
|
483
|
+
}
|
|
484
|
+
// ── Alert mute patterns (pure, exported for testing) ──────────────────────────
|
|
485
|
+
/**
|
|
486
|
+
* Check if an alert text matches any suppressed pattern (case-insensitive substring).
|
|
487
|
+
* Returns true when the alert should be hidden.
|
|
488
|
+
*/
|
|
489
|
+
export function isAlertMuted(text, patterns) {
|
|
490
|
+
if (patterns.size === 0)
|
|
491
|
+
return false;
|
|
492
|
+
const lower = text.toLowerCase();
|
|
493
|
+
for (const p of patterns) {
|
|
494
|
+
if (lower.includes(p.toLowerCase()))
|
|
495
|
+
return true;
|
|
496
|
+
}
|
|
497
|
+
return false;
|
|
498
|
+
}
|
|
431
499
|
// ── Cost summary (pure, exported for testing) ────────────────────────────────
|
|
432
500
|
/**
|
|
433
501
|
* Parse a cost string like "$3.42" → 3.42, or null if unparseable.
|
|
@@ -518,6 +586,18 @@ export function formatSessionReport(data) {
|
|
|
518
586
|
}
|
|
519
587
|
return lines.join("\n");
|
|
520
588
|
}
|
|
589
|
+
// ── Session cost budget (pure, exported for testing) ─────────────────────────
|
|
590
|
+
/** Check if a cost string exceeds a budget value. Returns true when over budget. */
|
|
591
|
+
export function isOverBudget(costStr, budgetUSD) {
|
|
592
|
+
if (!costStr)
|
|
593
|
+
return false;
|
|
594
|
+
const val = parseCostValue(costStr);
|
|
595
|
+
return val !== null && val > budgetUSD;
|
|
596
|
+
}
|
|
597
|
+
/** Format a budget-exceeded alert message. */
|
|
598
|
+
export function formatBudgetAlert(title, costStr, budgetUSD) {
|
|
599
|
+
return `${title}: cost ${costStr} exceeded budget $${budgetUSD.toFixed(2)}`;
|
|
600
|
+
}
|
|
521
601
|
// ── Duplicate session helpers (pure, exported for testing) ───────────────────
|
|
522
602
|
/**
|
|
523
603
|
* Build args for duplicating a session: given a source session, return
|
|
@@ -726,6 +806,8 @@ export const BUILTIN_COMMANDS = new Set([
|
|
|
726
806
|
"/group", "/groups", "/group-filter", "/burn-rate", "/snapshot", "/broadcast", "/watchdog", "/top", "/ceiling", "/rename", "/copy", "/stats", "/recall", "/pin-all-errors", "/export-stats",
|
|
727
807
|
"/mute-errors", "/prev-goal", "/tag", "/tags", "/tag-filter", "/find", "/reset-health", "/timeline", "/color", "/clear-history",
|
|
728
808
|
"/duplicate", "/color-all", "/quiet-hours", "/quiet-status", "/history-stats", "/cost-summary", "/session-report", "/alert-log",
|
|
809
|
+
"/budget", "/budgets", "/budget-status", "/pause-all", "/resume-all",
|
|
810
|
+
"/health-trend", "/alert-mute",
|
|
729
811
|
]);
|
|
730
812
|
/** Resolve a slash command through the alias map. Returns the expanded command or the original. */
|
|
731
813
|
export function resolveAlias(line, aliases) {
|
|
@@ -999,6 +1081,13 @@ export class TUI {
|
|
|
999
1081
|
quietHoursRanges = []; // quiet-hour start/end pairs
|
|
1000
1082
|
sessionHealthHistory = new Map(); // session ID → health snapshots
|
|
1001
1083
|
alertLog = []; // recent auto-generated status alerts (ring buffer, max 100)
|
|
1084
|
+
sessionBudgets = new Map(); // session ID → USD budget
|
|
1085
|
+
globalBudget = null; // global fallback budget in USD
|
|
1086
|
+
budgetAlerted = new Map(); // session ID → epoch ms of last budget alert
|
|
1087
|
+
sessionStatusHistory = new Map(); // session ID → status change log
|
|
1088
|
+
prevSessionStatus = new Map(); // session ID → last known status (for change detection)
|
|
1089
|
+
flapAlerted = new Map(); // session ID → epoch ms of last flap alert
|
|
1090
|
+
alertMutePatterns = new Set(); // substrings to hide from /alert-log display
|
|
1002
1091
|
// drill-down mode: show a single session's full output
|
|
1003
1092
|
viewMode = "overview";
|
|
1004
1093
|
drilldownSessionId = null;
|
|
@@ -1471,13 +1560,77 @@ export class TUI {
|
|
|
1471
1560
|
return buildDuplicateArgs(this.sessions, sessionIdOrIndex, newTitle);
|
|
1472
1561
|
}
|
|
1473
1562
|
// ── Session timeline ─────────────────────────────────────────────────────
|
|
1563
|
+
// ── Session cost budget ──────────────────────────────────────────────────
|
|
1564
|
+
/** Set a per-session budget in USD. Pass null to clear. */
|
|
1565
|
+
setSessionBudget(sessionIdOrIndex, budgetUSD) {
|
|
1566
|
+
let sessionId;
|
|
1567
|
+
if (typeof sessionIdOrIndex === "number") {
|
|
1568
|
+
sessionId = this.sessions[sessionIdOrIndex - 1]?.id;
|
|
1569
|
+
}
|
|
1570
|
+
else {
|
|
1571
|
+
const needle = sessionIdOrIndex.toLowerCase();
|
|
1572
|
+
const match = this.sessions.find((s) => s.id === sessionIdOrIndex || s.id.startsWith(needle) || s.title.toLowerCase() === needle);
|
|
1573
|
+
sessionId = match?.id;
|
|
1574
|
+
}
|
|
1575
|
+
if (!sessionId)
|
|
1576
|
+
return false;
|
|
1577
|
+
if (budgetUSD === null)
|
|
1578
|
+
this.sessionBudgets.delete(sessionId);
|
|
1579
|
+
else
|
|
1580
|
+
this.sessionBudgets.set(sessionId, budgetUSD);
|
|
1581
|
+
return true;
|
|
1582
|
+
}
|
|
1583
|
+
/** Set global fallback budget (applies to all sessions without per-session budget). */
|
|
1584
|
+
setGlobalBudget(budgetUSD) {
|
|
1585
|
+
this.globalBudget = budgetUSD;
|
|
1586
|
+
}
|
|
1587
|
+
/** Get the per-session budget (or null). */
|
|
1588
|
+
getSessionBudget(id) {
|
|
1589
|
+
return this.sessionBudgets.get(id) ?? null;
|
|
1590
|
+
}
|
|
1591
|
+
/** Get the global budget (or null if not set). */
|
|
1592
|
+
getGlobalBudget() {
|
|
1593
|
+
return this.globalBudget;
|
|
1594
|
+
}
|
|
1595
|
+
/** Return all per-session budgets. */
|
|
1596
|
+
getAllSessionBudgets() {
|
|
1597
|
+
return this.sessionBudgets;
|
|
1598
|
+
}
|
|
1474
1599
|
/** Return health history for a session (for sparkline). */
|
|
1475
1600
|
getSessionHealthHistory(id) {
|
|
1476
1601
|
return this.sessionHealthHistory.get(id) ?? [];
|
|
1477
1602
|
}
|
|
1478
|
-
/** Return all alert log entries (last 100 "status" tag entries). */
|
|
1479
|
-
getAlertLog() {
|
|
1480
|
-
|
|
1603
|
+
/** Return all alert log entries (last 100 "status" tag entries), filtered by mute patterns. */
|
|
1604
|
+
getAlertLog(includeAll = false) {
|
|
1605
|
+
if (includeAll || this.alertMutePatterns.size === 0)
|
|
1606
|
+
return this.alertLog;
|
|
1607
|
+
return this.alertLog.filter((e) => !isAlertMuted(e.text, this.alertMutePatterns));
|
|
1608
|
+
}
|
|
1609
|
+
/** Return status change history for a session. */
|
|
1610
|
+
getSessionStatusHistory(id) {
|
|
1611
|
+
return this.sessionStatusHistory.get(id) ?? [];
|
|
1612
|
+
}
|
|
1613
|
+
/** Check if a session is currently flapping. */
|
|
1614
|
+
isSessionFlapping(id, now) {
|
|
1615
|
+
const hist = this.sessionStatusHistory.get(id);
|
|
1616
|
+
return hist ? isFlapping(hist, now) : false;
|
|
1617
|
+
}
|
|
1618
|
+
// ── Alert mute patterns ──────────────────────────────────────────────────
|
|
1619
|
+
/** Add a pattern to suppress from alert log display. */
|
|
1620
|
+
addAlertMutePattern(pattern) {
|
|
1621
|
+
this.alertMutePatterns.add(pattern.toLowerCase().trim());
|
|
1622
|
+
}
|
|
1623
|
+
/** Remove a pattern from alert mute list. Returns true if it was present. */
|
|
1624
|
+
removeAlertMutePattern(pattern) {
|
|
1625
|
+
return this.alertMutePatterns.delete(pattern.toLowerCase().trim());
|
|
1626
|
+
}
|
|
1627
|
+
/** Clear all alert mute patterns. */
|
|
1628
|
+
clearAlertMutePatterns() {
|
|
1629
|
+
this.alertMutePatterns.clear();
|
|
1630
|
+
}
|
|
1631
|
+
/** Return all alert mute patterns (for display/persistence). */
|
|
1632
|
+
getAlertMutePatterns() {
|
|
1633
|
+
return this.alertMutePatterns;
|
|
1481
1634
|
}
|
|
1482
1635
|
/** Get the latest cost string for a session (or undefined). */
|
|
1483
1636
|
getSessionCost(id) {
|
|
@@ -1797,15 +1950,42 @@ export class TUI {
|
|
|
1797
1950
|
for (const s of opts.sessions) {
|
|
1798
1951
|
if (!this.sessionFirstSeen.has(s.id))
|
|
1799
1952
|
this.sessionFirstSeen.set(s.id, now);
|
|
1953
|
+
// track status changes for flap detection
|
|
1954
|
+
const prevStatus = this.prevSessionStatus.get(s.id);
|
|
1955
|
+
if (prevStatus !== undefined && prevStatus !== s.status) {
|
|
1956
|
+
const statusHist = this.sessionStatusHistory.get(s.id) ?? [];
|
|
1957
|
+
statusHist.push({ status: s.status, ts: now });
|
|
1958
|
+
if (statusHist.length > MAX_STATUS_HISTORY)
|
|
1959
|
+
statusHist.shift();
|
|
1960
|
+
this.sessionStatusHistory.set(s.id, statusHist);
|
|
1961
|
+
// check for flapping
|
|
1962
|
+
if (isFlapping(statusHist, now) && !quietNow) {
|
|
1963
|
+
const lastFlapAlert = this.flapAlerted.get(s.id) ?? 0;
|
|
1964
|
+
if (now - lastFlapAlert >= 5 * 60_000) {
|
|
1965
|
+
this.flapAlerted.set(s.id, now);
|
|
1966
|
+
this.log("status", `flap: ${s.title} is oscillating rapidly (${statusHist.filter((c) => c.ts >= now - FLAP_WINDOW_MS).length} status changes in ${Math.round(FLAP_WINDOW_MS / 60_000)}m)`, s.id);
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
this.prevSessionStatus.set(s.id, s.status);
|
|
1800
1971
|
const prev = this.prevLastActivity.get(s.id);
|
|
1801
1972
|
if (s.lastActivity !== undefined && s.lastActivity !== prev) {
|
|
1802
1973
|
this.lastChangeAt.set(s.id, now);
|
|
1803
1974
|
}
|
|
1804
1975
|
if (s.lastActivity !== undefined)
|
|
1805
1976
|
this.prevLastActivity.set(s.id, s.lastActivity);
|
|
1806
|
-
// track cost string
|
|
1807
|
-
if (s.costStr)
|
|
1977
|
+
// track cost string + check budget
|
|
1978
|
+
if (s.costStr) {
|
|
1808
1979
|
this.sessionCosts.set(s.id, s.costStr);
|
|
1980
|
+
const budget = this.sessionBudgets.get(s.id) ?? this.globalBudget;
|
|
1981
|
+
if (budget !== null && isOverBudget(s.costStr, budget) && !quietNow) {
|
|
1982
|
+
const lastAlert = this.budgetAlerted.get(s.id) ?? 0;
|
|
1983
|
+
if (now - lastAlert >= 5 * 60_000) {
|
|
1984
|
+
this.budgetAlerted.set(s.id, now);
|
|
1985
|
+
this.log("status", formatBudgetAlert(s.title, s.costStr, budget), s.id);
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1809
1989
|
// track context token history for burn-rate alerts
|
|
1810
1990
|
const tokens = parseContextTokenNumber(s.contextTokens);
|
|
1811
1991
|
if (tokens !== null) {
|
|
@@ -2394,7 +2574,8 @@ export class TUI {
|
|
|
2394
2574
|
const badgeSuffix = muteBadge ? `${muteBadge} ` : "";
|
|
2395
2575
|
const iconsWidth = (pinned ? 2 : 0) + (muted ? 2 : 0) + (noted ? 2 : 0) + actualBadgeWidth + groupBadgeWidth + tagsBadgeWidth + colorDotWidth;
|
|
2396
2576
|
const cardWidth = innerWidth - 1 - iconsWidth;
|
|
2397
|
-
const
|
|
2577
|
+
const cardAge = s.createdAt ? formatSessionAge(s.createdAt, nowMs) : undefined;
|
|
2578
|
+
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)}`;
|
|
2398
2579
|
const padded = padBoxLineHover(line, this.cols, isHovered);
|
|
2399
2580
|
process.stderr.write(moveTo(startRow + 1 + i, 1) + CLEAR_LINE + padded);
|
|
2400
2581
|
}
|
|
@@ -2464,7 +2645,8 @@ export class TUI {
|
|
|
2464
2645
|
const badgeSuffix = muteBadge ? `${muteBadge} ` : "";
|
|
2465
2646
|
const iconsWidth = (pinned ? 2 : 0) + (muted ? 2 : 0) + (noted ? 2 : 0) + actualBadgeWidth + groupBadgeWidth + tagsBadgeWidth2 + colorDotWidth2;
|
|
2466
2647
|
const cardWidth = innerWidth - 1 - iconsWidth;
|
|
2467
|
-
const
|
|
2648
|
+
const cardAge2 = s.createdAt ? formatSessionAge(s.createdAt, nowMs2) : undefined;
|
|
2649
|
+
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)}`;
|
|
2468
2650
|
const padded = padBoxLineHover(line, this.cols, isHovered);
|
|
2469
2651
|
process.stderr.write(SAVE_CURSOR + moveTo(startRow + 1 + i, 1) + CLEAR_LINE + padded + RESTORE_CURSOR);
|
|
2470
2652
|
}
|
|
@@ -2582,7 +2764,8 @@ export class TUI {
|
|
|
2582
2764
|
// idleSinceMs: optional ms since last activity change (shown when idle/stopped)
|
|
2583
2765
|
// healthBadge: optional pre-formatted health score badge ("⬡83" colored)
|
|
2584
2766
|
// displayName: optional custom name override (from /rename)
|
|
2585
|
-
|
|
2767
|
+
// ageStr: optional session age string (from createdAt)
|
|
2768
|
+
function formatSessionCard(s, maxWidth, errorSparkline, idleSinceMs, healthBadge, displayName, ageStr) {
|
|
2586
2769
|
const dot = STATUS_DOT[s.status] ?? `${AMBER}${DOT.filled}${RESET}`;
|
|
2587
2770
|
const title = displayName ?? s.title;
|
|
2588
2771
|
const name = displayName ? `${BOLD}${displayName}${DIM} (${s.title})${RESET}` : `${BOLD}${s.title}${RESET}`;
|
|
@@ -2622,7 +2805,8 @@ function formatSessionCard(s, maxWidth, errorSparkline, idleSinceMs, healthBadge
|
|
|
2622
2805
|
else {
|
|
2623
2806
|
desc = `${SLATE}${s.status}${RESET}`;
|
|
2624
2807
|
}
|
|
2625
|
-
|
|
2808
|
+
const ageSuffix = ageStr ? ` ${DIM}age:${ageStr}${RESET}` : "";
|
|
2809
|
+
return truncateAnsi(`${dot} ${healthPrefix}${name} ${toolBadge}${contextBadge} ${SLATE}${BOX.h}${RESET} ${desc}${ageSuffix}${sparkSuffix}`, maxWidth);
|
|
2626
2810
|
}
|
|
2627
2811
|
// colorize an activity entry based on its tag
|
|
2628
2812
|
function formatActivity(entry, maxCols) {
|