aoaoe 0.153.0 → 0.160.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 +101 -1
- package/dist/input.d.ts +18 -0
- package/dist/input.js +92 -0
- package/dist/loop.d.ts +1 -0
- package/dist/loop.js +5 -1
- package/dist/reasoner/prompt.js +12 -1
- package/dist/tui.d.ts +56 -1
- package/dist/tui.js +173 -8
- package/dist/types.d.ts +1 -0
- 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, formatHealthTrendChart, isOverBudget } 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, DRAIN_ICON, formatSessionsTable } 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,105 @@ async function main() {
|
|
|
1023
1023
|
tui.log("system", `quiet hours: ${specs.join(", ")} — watchdog+burn alerts suppressed`);
|
|
1024
1024
|
persistPrefs();
|
|
1025
1025
|
});
|
|
1026
|
+
// wire /note-history
|
|
1027
|
+
input.onNoteHistory((target) => {
|
|
1028
|
+
const num = /^\d+$/.test(target) ? parseInt(target, 10) : undefined;
|
|
1029
|
+
const sessions = tui.getSessions();
|
|
1030
|
+
const session = num !== undefined
|
|
1031
|
+
? sessions[num - 1]
|
|
1032
|
+
: sessions.find((s) => s.title.toLowerCase() === target.toLowerCase() || s.id.startsWith(target));
|
|
1033
|
+
if (!session) {
|
|
1034
|
+
tui.log("system", `session not found: ${target}`);
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
const hist = tui.getNoteHistory(session.id);
|
|
1038
|
+
if (hist.length === 0) {
|
|
1039
|
+
tui.log("system", `note-history: no previous notes for ${session.title}`);
|
|
1040
|
+
}
|
|
1041
|
+
else {
|
|
1042
|
+
tui.log("system", `note-history for ${session.title} (${hist.length}):`);
|
|
1043
|
+
for (const n of hist)
|
|
1044
|
+
tui.log("system", ` "${n}"`);
|
|
1045
|
+
}
|
|
1046
|
+
});
|
|
1047
|
+
// wire /label
|
|
1048
|
+
input.onLabel((target, label) => {
|
|
1049
|
+
const num = /^\d+$/.test(target) ? parseInt(target, 10) : undefined;
|
|
1050
|
+
const ok = tui.setLabel(num ?? target, label || null);
|
|
1051
|
+
if (ok) {
|
|
1052
|
+
tui.log("system", label ? `label set for ${target}: "${label}"` : `label cleared for ${target}`);
|
|
1053
|
+
}
|
|
1054
|
+
else {
|
|
1055
|
+
tui.log("system", `session not found: ${target}`);
|
|
1056
|
+
}
|
|
1057
|
+
});
|
|
1058
|
+
// wire /sessions enhanced table
|
|
1059
|
+
input.onSessionsTable(() => {
|
|
1060
|
+
const sessions = tui.getSessions();
|
|
1061
|
+
const now = Date.now();
|
|
1062
|
+
const lines = formatSessionsTable(sessions, {
|
|
1063
|
+
groups: tui.getAllGroups(),
|
|
1064
|
+
tags: tui.getAllSessionTags(),
|
|
1065
|
+
colors: tui.getAllSessionColors(),
|
|
1066
|
+
notes: tui.getAllNotes(),
|
|
1067
|
+
labels: tui.getAllLabels(),
|
|
1068
|
+
aliases: tui.getAllSessionAliases(),
|
|
1069
|
+
drainingIds: tui.getDrainingIds(),
|
|
1070
|
+
healthScores: tui.getAllHealthScores(now),
|
|
1071
|
+
costs: tui.getAllSessionCosts(),
|
|
1072
|
+
firstSeen: tui.getAllFirstSeen(),
|
|
1073
|
+
}, now);
|
|
1074
|
+
for (const l of lines)
|
|
1075
|
+
tui.log("system", l);
|
|
1076
|
+
});
|
|
1077
|
+
// wire /flap-log
|
|
1078
|
+
input.onFlapLog(() => {
|
|
1079
|
+
const log = tui.getFlapLog();
|
|
1080
|
+
if (log.length === 0) {
|
|
1081
|
+
tui.log("system", "flap-log: no flap events recorded");
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
tui.log("system", `flap-log: ${log.length} event${log.length !== 1 ? "s" : ""}:`);
|
|
1085
|
+
for (const e of log.slice(-20)) {
|
|
1086
|
+
const time = new Date(e.ts).toLocaleTimeString();
|
|
1087
|
+
tui.log("system", ` ${time} ${e.title}: ${e.count} changes in window`);
|
|
1088
|
+
}
|
|
1089
|
+
});
|
|
1090
|
+
// wire /drain and /undrain
|
|
1091
|
+
input.onDrain((target, drain) => {
|
|
1092
|
+
const num = /^\d+$/.test(target) ? parseInt(target, 10) : undefined;
|
|
1093
|
+
const ok = drain ? tui.drainSession(num ?? target) : tui.undrainSession(num ?? target);
|
|
1094
|
+
if (ok) {
|
|
1095
|
+
tui.log("system", `${drain ? "drain" : "undrain"}: ${target} ${drain ? `marked draining (${DRAIN_ICON})` : "restored"}`);
|
|
1096
|
+
}
|
|
1097
|
+
else {
|
|
1098
|
+
tui.log("system", `session not found: ${target}`);
|
|
1099
|
+
}
|
|
1100
|
+
});
|
|
1101
|
+
// wire /export-all bulk export
|
|
1102
|
+
input.onExportAll(() => {
|
|
1103
|
+
const sessions = tui.getSessions();
|
|
1104
|
+
if (sessions.length === 0) {
|
|
1105
|
+
tui.log("system", "export-all: no sessions");
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
const now = Date.now();
|
|
1109
|
+
const ts = new Date(now).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
1110
|
+
const dir = join(homedir(), ".aoaoe");
|
|
1111
|
+
try {
|
|
1112
|
+
mkdirSync(dir, { recursive: true });
|
|
1113
|
+
// snapshot JSON
|
|
1114
|
+
const snapData = buildSnapshotData(sessions, tui.getAllGroups(), tui.getAllNotes(), tui.getAllFirstSeen(), tui.getSessionErrorCounts(), tui.getAllBurnRates(now), pkg ?? "dev", now);
|
|
1115
|
+
writeFileSync(join(dir, `snapshot-${ts}.json`), formatSnapshotJson(snapData), "utf-8");
|
|
1116
|
+
// stats JSON
|
|
1117
|
+
const statEntries = 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)])));
|
|
1118
|
+
writeFileSync(join(dir, `stats-${ts}.json`), formatStatsJson(statEntries, pkg ?? "dev", now), "utf-8");
|
|
1119
|
+
tui.log("system", `export-all: snapshot + stats saved to ~/.aoaoe/ (${sessions.length} sessions)`);
|
|
1120
|
+
}
|
|
1121
|
+
catch (err) {
|
|
1122
|
+
tui.log("error", `export-all failed: ${err}`);
|
|
1123
|
+
}
|
|
1124
|
+
});
|
|
1026
1125
|
// wire /budget cost alerts
|
|
1027
1126
|
input.onBudget((target, budgetUSD) => {
|
|
1028
1127
|
if (budgetUSD === null) {
|
|
@@ -2046,6 +2145,7 @@ async function daemonTick(config, poller, reasoner, executor, reasonerConsole, p
|
|
|
2046
2145
|
try {
|
|
2047
2146
|
tickResult = await loopTick({
|
|
2048
2147
|
config, poller, reasoner: wrappedReasoner, executor, policyStates, pollCount, userMessage, taskContext, beforeExecute,
|
|
2148
|
+
drainingSessionIds: tui ? [...tui.getDrainingIds()] : undefined,
|
|
2049
2149
|
});
|
|
2050
2150
|
}
|
|
2051
2151
|
catch (err) {
|
package/dist/input.d.ts
CHANGED
|
@@ -62,6 +62,12 @@ export type HealthTrendHandler = (target: string, height: number) => void;
|
|
|
62
62
|
export type AlertMuteHandler = (pattern: string | null) => void;
|
|
63
63
|
export type BudgetsListHandler = () => void;
|
|
64
64
|
export type BudgetStatusHandler = () => void;
|
|
65
|
+
export type FlapLogHandler = () => void;
|
|
66
|
+
export type DrainHandler = (target: string, drain: boolean) => void;
|
|
67
|
+
export type ExportAllHandler = () => void;
|
|
68
|
+
export type NoteHistoryHandler = (target: string) => void;
|
|
69
|
+
export type LabelHandler = (target: string, label: string) => void;
|
|
70
|
+
export type SessionsTableHandler = () => void;
|
|
65
71
|
export interface MouseEvent {
|
|
66
72
|
button: number;
|
|
67
73
|
col: number;
|
|
@@ -146,6 +152,12 @@ export declare class InputReader {
|
|
|
146
152
|
private alertMuteHandler;
|
|
147
153
|
private budgetsListHandler;
|
|
148
154
|
private budgetStatusHandler;
|
|
155
|
+
private flapLogHandler;
|
|
156
|
+
private drainHandler;
|
|
157
|
+
private exportAllHandler;
|
|
158
|
+
private noteHistoryHandler;
|
|
159
|
+
private labelHandler;
|
|
160
|
+
private sessionsTableHandler;
|
|
149
161
|
private aliases;
|
|
150
162
|
private mouseDataListener;
|
|
151
163
|
onScroll(handler: (dir: ScrollDirection) => void): void;
|
|
@@ -213,6 +225,12 @@ export declare class InputReader {
|
|
|
213
225
|
onAlertMute(handler: AlertMuteHandler): void;
|
|
214
226
|
onBudgetsList(handler: BudgetsListHandler): void;
|
|
215
227
|
onBudgetStatus(handler: BudgetStatusHandler): void;
|
|
228
|
+
onFlapLog(handler: FlapLogHandler): void;
|
|
229
|
+
onDrain(handler: DrainHandler): void;
|
|
230
|
+
onExportAll(handler: ExportAllHandler): void;
|
|
231
|
+
onNoteHistory(handler: NoteHistoryHandler): void;
|
|
232
|
+
onLabel(handler: LabelHandler): void;
|
|
233
|
+
onSessionsTable(handler: SessionsTableHandler): void;
|
|
216
234
|
/** Set aliases from persisted prefs. */
|
|
217
235
|
setAliases(aliases: Record<string, string>): void;
|
|
218
236
|
/** Get current aliases as a plain object. */
|
package/dist/input.js
CHANGED
|
@@ -95,6 +95,12 @@ export class InputReader {
|
|
|
95
95
|
alertMuteHandler = null;
|
|
96
96
|
budgetsListHandler = null;
|
|
97
97
|
budgetStatusHandler = null;
|
|
98
|
+
flapLogHandler = null;
|
|
99
|
+
drainHandler = null;
|
|
100
|
+
exportAllHandler = null;
|
|
101
|
+
noteHistoryHandler = null;
|
|
102
|
+
labelHandler = null;
|
|
103
|
+
sessionsTableHandler = null;
|
|
98
104
|
aliases = new Map(); // /shortcut → /full command
|
|
99
105
|
mouseDataListener = null;
|
|
100
106
|
// register a callback for scroll key events (PgUp/PgDn/Home/End)
|
|
@@ -315,6 +321,12 @@ export class InputReader {
|
|
|
315
321
|
onAlertMute(handler) { this.alertMuteHandler = handler; }
|
|
316
322
|
onBudgetsList(handler) { this.budgetsListHandler = handler; }
|
|
317
323
|
onBudgetStatus(handler) { this.budgetStatusHandler = handler; }
|
|
324
|
+
onFlapLog(handler) { this.flapLogHandler = handler; }
|
|
325
|
+
onDrain(handler) { this.drainHandler = handler; }
|
|
326
|
+
onExportAll(handler) { this.exportAllHandler = handler; }
|
|
327
|
+
onNoteHistory(handler) { this.noteHistoryHandler = handler; }
|
|
328
|
+
onLabel(handler) { this.labelHandler = handler; }
|
|
329
|
+
onSessionsTable(handler) { this.sessionsTableHandler = handler; }
|
|
318
330
|
/** Set aliases from persisted prefs. */
|
|
319
331
|
setAliases(aliases) {
|
|
320
332
|
this.aliases.clear();
|
|
@@ -585,6 +597,13 @@ ${BOLD}navigation:${RESET}
|
|
|
585
597
|
/health-trend N show ASCII health score chart for session N [height]
|
|
586
598
|
/budgets list all active cost budgets
|
|
587
599
|
/budget-status show which sessions are over or under budget
|
|
600
|
+
/flap-log show sessions recently flagged as flapping
|
|
601
|
+
/drain N mark session N as draining (supervisor will skip it)
|
|
602
|
+
/undrain N remove drain mark from session N
|
|
603
|
+
/export-all bulk export snapshot + stats JSON for all sessions
|
|
604
|
+
/note-history N show previous notes for a session (before they were cleared)
|
|
605
|
+
/label N [text] set a freeform label shown in the session card (no text = clear)
|
|
606
|
+
/sessions show rich session table (status, health, group, cost, flags)
|
|
588
607
|
/history-stats show aggregate statistics from persisted activity history
|
|
589
608
|
/cost-summary show total estimated spend across all sessions
|
|
590
609
|
/session-report N generate full markdown report for a session → ~/.aoaoe/report-<name>-<ts>.md
|
|
@@ -1212,6 +1231,79 @@ ${BOLD}other:${RESET}
|
|
|
1212
1231
|
else
|
|
1213
1232
|
console.error(`${DIM}budgets not available${RESET}`);
|
|
1214
1233
|
break;
|
|
1234
|
+
case "/flap-log":
|
|
1235
|
+
if (this.flapLogHandler)
|
|
1236
|
+
this.flapLogHandler();
|
|
1237
|
+
else
|
|
1238
|
+
console.error(`${DIM}flap-log not available${RESET}`);
|
|
1239
|
+
break;
|
|
1240
|
+
case "/drain": {
|
|
1241
|
+
const drainArg = line.slice("/drain".length).trim();
|
|
1242
|
+
if (!drainArg) {
|
|
1243
|
+
console.error(`${DIM}usage: /drain <N|name>${RESET}`);
|
|
1244
|
+
break;
|
|
1245
|
+
}
|
|
1246
|
+
if (this.drainHandler)
|
|
1247
|
+
this.drainHandler(drainArg, true);
|
|
1248
|
+
else
|
|
1249
|
+
console.error(`${DIM}drain not available${RESET}`);
|
|
1250
|
+
break;
|
|
1251
|
+
}
|
|
1252
|
+
case "/undrain": {
|
|
1253
|
+
const undrainArg = line.slice("/undrain".length).trim();
|
|
1254
|
+
if (!undrainArg) {
|
|
1255
|
+
console.error(`${DIM}usage: /undrain <N|name>${RESET}`);
|
|
1256
|
+
break;
|
|
1257
|
+
}
|
|
1258
|
+
if (this.drainHandler)
|
|
1259
|
+
this.drainHandler(undrainArg, false);
|
|
1260
|
+
else
|
|
1261
|
+
console.error(`${DIM}undrain not available${RESET}`);
|
|
1262
|
+
break;
|
|
1263
|
+
}
|
|
1264
|
+
case "/note-history": {
|
|
1265
|
+
const nhArg = line.slice("/note-history".length).trim();
|
|
1266
|
+
if (!nhArg) {
|
|
1267
|
+
console.error(`${DIM}usage: /note-history <N|name>${RESET}`);
|
|
1268
|
+
break;
|
|
1269
|
+
}
|
|
1270
|
+
if (this.noteHistoryHandler)
|
|
1271
|
+
this.noteHistoryHandler(nhArg);
|
|
1272
|
+
else
|
|
1273
|
+
console.error(`${DIM}note-history not available${RESET}`);
|
|
1274
|
+
break;
|
|
1275
|
+
}
|
|
1276
|
+
case "/label": {
|
|
1277
|
+
const lblArgs = line.slice("/label".length).trim();
|
|
1278
|
+
if (!lblArgs) {
|
|
1279
|
+
console.error(`${DIM}usage: /label <N|name> [text]${RESET}`);
|
|
1280
|
+
break;
|
|
1281
|
+
}
|
|
1282
|
+
if (this.labelHandler) {
|
|
1283
|
+
const spaceIdx = lblArgs.indexOf(" ");
|
|
1284
|
+
if (spaceIdx > 0) {
|
|
1285
|
+
this.labelHandler(lblArgs.slice(0, spaceIdx), lblArgs.slice(spaceIdx + 1).trim());
|
|
1286
|
+
}
|
|
1287
|
+
else {
|
|
1288
|
+
this.labelHandler(lblArgs, ""); // clear
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
else
|
|
1292
|
+
console.error(`${DIM}label not available${RESET}`);
|
|
1293
|
+
break;
|
|
1294
|
+
}
|
|
1295
|
+
case "/sessions":
|
|
1296
|
+
if (this.sessionsTableHandler)
|
|
1297
|
+
this.sessionsTableHandler();
|
|
1298
|
+
else
|
|
1299
|
+
console.error(`${DIM}sessions not available${RESET}`);
|
|
1300
|
+
break;
|
|
1301
|
+
case "/export-all":
|
|
1302
|
+
if (this.exportAllHandler)
|
|
1303
|
+
this.exportAllHandler();
|
|
1304
|
+
else
|
|
1305
|
+
console.error(`${DIM}export-all not available${RESET}`);
|
|
1306
|
+
break;
|
|
1215
1307
|
case "/budget-status":
|
|
1216
1308
|
if (this.budgetStatusHandler)
|
|
1217
1309
|
this.budgetStatusHandler();
|
package/dist/loop.d.ts
CHANGED
package/dist/loop.js
CHANGED
|
@@ -7,7 +7,7 @@ import { detectPermissionPrompt } from "./reasoner/prompt.js";
|
|
|
7
7
|
* Returns a structured result for testing and inspection.
|
|
8
8
|
*/
|
|
9
9
|
export async function tick(opts) {
|
|
10
|
-
const { config, poller, reasoner, executor, policyStates, pollCount, userMessage, taskContext, beforeExecute } = opts;
|
|
10
|
+
const { config, poller, reasoner, executor, policyStates, pollCount, userMessage, taskContext, beforeExecute, drainingSessionIds } = opts;
|
|
11
11
|
// 1. poll
|
|
12
12
|
const observation = await poller.poll();
|
|
13
13
|
const now = Date.now();
|
|
@@ -65,6 +65,10 @@ export async function tick(opts) {
|
|
|
65
65
|
if (config.protectedSessions && config.protectedSessions.length > 0) {
|
|
66
66
|
observation.protectedSessions = config.protectedSessions;
|
|
67
67
|
}
|
|
68
|
+
// attach draining session IDs for the prompt formatter
|
|
69
|
+
if (drainingSessionIds && drainingSessionIds.length > 0) {
|
|
70
|
+
observation.drainingSessionIds = drainingSessionIds;
|
|
71
|
+
}
|
|
68
72
|
// 2. reason
|
|
69
73
|
const result = await reasoner.decide(observation);
|
|
70
74
|
// 3. execute (skip wait-only results)
|
package/dist/reasoner/prompt.js
CHANGED
|
@@ -126,6 +126,7 @@ export function formatObservation(obs) {
|
|
|
126
126
|
parts.push("");
|
|
127
127
|
// resolve protected sessions list from config (attached by loop.ts)
|
|
128
128
|
const protectedList = obs.protectedSessions ?? [];
|
|
129
|
+
const drainingList = new Set(obs.drainingSessionIds ?? []);
|
|
129
130
|
// session summary table
|
|
130
131
|
parts.push("Sessions:");
|
|
131
132
|
const activeSessions = [];
|
|
@@ -133,7 +134,8 @@ export function formatObservation(obs) {
|
|
|
133
134
|
const s = snap.session;
|
|
134
135
|
const activeTag = snap.userActive ? " [USER ACTIVE]" : "";
|
|
135
136
|
const protectedTag = protectedList.some((p) => p.toLowerCase() === s.title.toLowerCase()) ? " [PROTECTED]" : "";
|
|
136
|
-
|
|
137
|
+
const drainingTag = drainingList.has(s.id) ? " [DRAINING — skip, do not send input]" : "";
|
|
138
|
+
parts.push(` [${s.id.slice(0, 8)}] "${s.title}" tool=${s.tool} status=${s.status} path=${s.path}${activeTag}${protectedTag}${drainingTag}`);
|
|
137
139
|
if (snap.userActive)
|
|
138
140
|
activeSessions.push(s.title);
|
|
139
141
|
}
|
|
@@ -142,6 +144,15 @@ export function formatObservation(obs) {
|
|
|
142
144
|
parts.push(`WARNING: A human user is currently interacting with: ${activeSessions.join(", ")}.`);
|
|
143
145
|
parts.push("Do NOT send input to these sessions. The user is actively working and your input would interfere.");
|
|
144
146
|
}
|
|
147
|
+
if (drainingList.size > 0) {
|
|
148
|
+
const drainingTitles = obs.sessions
|
|
149
|
+
.filter((s) => drainingList.has(s.session.id))
|
|
150
|
+
.map((s) => `"${s.session.title}"`);
|
|
151
|
+
if (drainingTitles.length > 0) {
|
|
152
|
+
parts.push("");
|
|
153
|
+
parts.push(`DRAINING: ${drainingTitles.join(", ")} — do NOT assign new tasks or send_input to these sessions.`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
145
156
|
parts.push("");
|
|
146
157
|
// task context (goals, progress) — injected if tasks are defined
|
|
147
158
|
if (obs.taskContext && obs.taskContext.length > 0) {
|
package/dist/tui.d.ts
CHANGED
|
@@ -160,6 +160,30 @@ export declare const FLAP_THRESHOLD = 5;
|
|
|
160
160
|
* Returns true when there are >= FLAP_THRESHOLD status changes in FLAP_WINDOW_MS.
|
|
161
161
|
*/
|
|
162
162
|
export declare function isFlapping(changes: readonly StatusChange[], now?: number, windowMs?: number, threshold?: number): boolean;
|
|
163
|
+
/**
|
|
164
|
+
* Format sessions as a rich table for /sessions command output.
|
|
165
|
+
* Returns array of lines (one per session + header).
|
|
166
|
+
*/
|
|
167
|
+
export declare function formatSessionsTable(sessions: readonly DaemonSessionState[], opts: {
|
|
168
|
+
groups: ReadonlyMap<string, string>;
|
|
169
|
+
tags: ReadonlyMap<string, ReadonlySet<string>>;
|
|
170
|
+
colors: ReadonlyMap<string, string>;
|
|
171
|
+
notes: ReadonlyMap<string, string>;
|
|
172
|
+
labels: ReadonlyMap<string, string>;
|
|
173
|
+
aliases: ReadonlyMap<string, string>;
|
|
174
|
+
drainingIds: ReadonlySet<string>;
|
|
175
|
+
healthScores: ReadonlyMap<string, number>;
|
|
176
|
+
costs: ReadonlyMap<string, string>;
|
|
177
|
+
firstSeen: ReadonlyMap<string, number>;
|
|
178
|
+
}, now?: number): string[];
|
|
179
|
+
/** Max notes stored per session (in history before clear). */
|
|
180
|
+
export declare const MAX_NOTE_HISTORY = 5;
|
|
181
|
+
/** Max length of a session label (displayed below title in cards). */
|
|
182
|
+
export declare const MAX_LABEL_LEN = 40;
|
|
183
|
+
/** Truncate a label to the max length. */
|
|
184
|
+
export declare function truncateLabel(label: string): string;
|
|
185
|
+
/** Drain icon shown in session cards for draining sessions. */
|
|
186
|
+
export declare const DRAIN_ICON = "\u21E3";
|
|
163
187
|
/**
|
|
164
188
|
* Check if an alert text matches any suppressed pattern (case-insensitive substring).
|
|
165
189
|
* Returns true when the alert should be hidden.
|
|
@@ -440,6 +464,8 @@ export declare class TUI {
|
|
|
440
464
|
private quietHoursRanges;
|
|
441
465
|
private sessionHealthHistory;
|
|
442
466
|
private alertLog;
|
|
467
|
+
private sessionNoteHistory;
|
|
468
|
+
private sessionLabels;
|
|
443
469
|
private sessionBudgets;
|
|
444
470
|
private globalBudget;
|
|
445
471
|
private budgetAlerted;
|
|
@@ -447,6 +473,8 @@ export declare class TUI {
|
|
|
447
473
|
private prevSessionStatus;
|
|
448
474
|
private flapAlerted;
|
|
449
475
|
private alertMutePatterns;
|
|
476
|
+
private drainingIds;
|
|
477
|
+
private flapLog;
|
|
450
478
|
private viewMode;
|
|
451
479
|
private drilldownSessionId;
|
|
452
480
|
private sessionOutputs;
|
|
@@ -527,6 +555,18 @@ export declare class TUI {
|
|
|
527
555
|
getNoteCount(): number;
|
|
528
556
|
/** Return all session notes (for /notes listing). */
|
|
529
557
|
getAllNotes(): ReadonlyMap<string, string>;
|
|
558
|
+
/** Return note history for a session (oldest first). */
|
|
559
|
+
getNoteHistory(id: string): readonly string[];
|
|
560
|
+
/**
|
|
561
|
+
* Set or clear a label for a session (by 1-indexed number, ID, or title).
|
|
562
|
+
* Label is displayed below the session title in normal cards.
|
|
563
|
+
* Returns true if session found.
|
|
564
|
+
*/
|
|
565
|
+
setLabel(sessionIdOrIndex: string | number, label: string | null): boolean;
|
|
566
|
+
/** Get the label for a session ID (or undefined). */
|
|
567
|
+
getLabel(id: string): string | undefined;
|
|
568
|
+
/** Return all session labels. */
|
|
569
|
+
getAllLabels(): ReadonlyMap<string, string>;
|
|
530
570
|
/** Return the current sessions (read-only, for resolving IDs to titles in the UI). */
|
|
531
571
|
getSessions(): readonly DaemonSessionState[];
|
|
532
572
|
/** Return the uptime in ms for a session (0 if not tracked). */
|
|
@@ -590,6 +630,21 @@ export declare class TUI {
|
|
|
590
630
|
getAllSessionBudgets(): ReadonlyMap<string, number>;
|
|
591
631
|
/** Return health history for a session (for sparkline). */
|
|
592
632
|
getSessionHealthHistory(id: string): readonly HealthSnapshot[];
|
|
633
|
+
/** Mark a session as draining (by index, ID, or title). Returns true if found. */
|
|
634
|
+
drainSession(sessionIdOrIndex: string | number): boolean;
|
|
635
|
+
/** Remove drain mark from a session. Returns true if it was draining. */
|
|
636
|
+
undrainSession(sessionIdOrIndex: string | number): boolean;
|
|
637
|
+
/** Check if a session is draining. */
|
|
638
|
+
isDraining(id: string): boolean;
|
|
639
|
+
/** Return all draining session IDs (for reasoner prompt and display). */
|
|
640
|
+
getDrainingIds(): ReadonlySet<string>;
|
|
641
|
+
/** Return recent flap events (newest last). */
|
|
642
|
+
getFlapLog(): readonly {
|
|
643
|
+
sessionId: string;
|
|
644
|
+
title: string;
|
|
645
|
+
ts: number;
|
|
646
|
+
count: number;
|
|
647
|
+
}[];
|
|
593
648
|
/** Return all alert log entries (last 100 "status" tag entries), filtered by mute patterns. */
|
|
594
649
|
getAlertLog(includeAll?: boolean): readonly ActivityEntry[];
|
|
595
650
|
/** Return status change history for a session. */
|
|
@@ -752,7 +807,7 @@ export declare class TUI {
|
|
|
752
807
|
private repaintDrilldownContent;
|
|
753
808
|
private paintInputLine;
|
|
754
809
|
}
|
|
755
|
-
declare function formatSessionCard(s: DaemonSessionState, maxWidth: number, errorSparkline?: string, idleSinceMs?: number, healthBadge?: string, displayName?: string, ageStr?: string): string;
|
|
810
|
+
declare function formatSessionCard(s: DaemonSessionState, maxWidth: number, errorSparkline?: string, idleSinceMs?: number, healthBadge?: string, displayName?: string, ageStr?: string, label?: string): string;
|
|
756
811
|
declare function formatActivity(entry: ActivityEntry, maxCols: number): string;
|
|
757
812
|
declare function padBoxLine(line: string, totalWidth: number): string;
|
|
758
813
|
declare function padBoxLineHover(line: string, totalWidth: number, hovered: boolean): string;
|
package/dist/tui.js
CHANGED
|
@@ -481,6 +481,55 @@ export function isFlapping(changes, now, windowMs = FLAP_WINDOW_MS, threshold =
|
|
|
481
481
|
const recent = changes.filter((c) => c.ts >= cutoff);
|
|
482
482
|
return recent.length >= threshold;
|
|
483
483
|
}
|
|
484
|
+
// ── Sessions table (pure, exported for testing) ────────────────────────────────
|
|
485
|
+
/**
|
|
486
|
+
* Format sessions as a rich table for /sessions command output.
|
|
487
|
+
* Returns array of lines (one per session + header).
|
|
488
|
+
*/
|
|
489
|
+
export function formatSessionsTable(sessions, opts, now) {
|
|
490
|
+
if (sessions.length === 0)
|
|
491
|
+
return [" no sessions"];
|
|
492
|
+
const nowMs = now ?? Date.now();
|
|
493
|
+
const lines = [];
|
|
494
|
+
lines.push(` ${"#".padEnd(3)} ${"title".padEnd(20)} ${"status".padEnd(9)} ${"hlth".padEnd(5)} ${"group".padEnd(10)} ${"cost".padEnd(7)} ${"uptime".padEnd(8)} flags`);
|
|
495
|
+
lines.push(` ${"-".repeat(3)} ${"-".repeat(20)} ${"-".repeat(9)} ${"-".repeat(5)} ${"-".repeat(10)} ${"-".repeat(7)} ${"-".repeat(8)} -----`);
|
|
496
|
+
for (let i = 0; i < sessions.length; i++) {
|
|
497
|
+
const s = sessions[i];
|
|
498
|
+
const alias = opts.aliases.get(s.id);
|
|
499
|
+
const titleStr = (alias || s.title).slice(0, 20).padEnd(20);
|
|
500
|
+
const statusStr = s.status.slice(0, 9).padEnd(9);
|
|
501
|
+
const health = opts.healthScores.get(s.id) ?? 100;
|
|
502
|
+
const healthStr = String(health).padEnd(5);
|
|
503
|
+
const group = opts.groups.get(s.id) ?? "";
|
|
504
|
+
const groupStr = group.slice(0, 10).padEnd(10);
|
|
505
|
+
const cost = opts.costs.get(s.id) ?? "";
|
|
506
|
+
const costStr = cost.slice(0, 7).padEnd(7);
|
|
507
|
+
const fs = opts.firstSeen.get(s.id);
|
|
508
|
+
const uptimeStr = fs !== undefined ? formatUptime(nowMs - fs).padEnd(8) : "?".padEnd(8);
|
|
509
|
+
// flags: D=drain, ⊹=tag, ✎=note, ·=label
|
|
510
|
+
const flags = [
|
|
511
|
+
opts.drainingIds.has(s.id) ? "D" : "",
|
|
512
|
+
(opts.tags.get(s.id)?.size ?? 0) > 0 ? "T" : "",
|
|
513
|
+
opts.notes.has(s.id) ? "N" : "",
|
|
514
|
+
opts.labels.has(s.id) ? "L" : "",
|
|
515
|
+
].filter(Boolean).join("") || "-";
|
|
516
|
+
lines.push(` ${String(i + 1).padEnd(3)} ${titleStr} ${statusStr} ${healthStr} ${groupStr} ${costStr} ${uptimeStr} ${flags}`);
|
|
517
|
+
}
|
|
518
|
+
return lines;
|
|
519
|
+
}
|
|
520
|
+
// ── Session note history ─────────────────────────────────────────────────────
|
|
521
|
+
/** Max notes stored per session (in history before clear). */
|
|
522
|
+
export const MAX_NOTE_HISTORY = 5;
|
|
523
|
+
// ── Session label ────────────────────────────────────────────────────────────
|
|
524
|
+
/** Max length of a session label (displayed below title in cards). */
|
|
525
|
+
export const MAX_LABEL_LEN = 40;
|
|
526
|
+
/** Truncate a label to the max length. */
|
|
527
|
+
export function truncateLabel(label) {
|
|
528
|
+
return label.length > MAX_LABEL_LEN ? label.slice(0, MAX_LABEL_LEN - 2) + ".." : label;
|
|
529
|
+
}
|
|
530
|
+
// ── Session drain mode helpers ────────────────────────────────────────────────
|
|
531
|
+
/** Drain icon shown in session cards for draining sessions. */
|
|
532
|
+
export const DRAIN_ICON = "⇣";
|
|
484
533
|
// ── Alert mute patterns (pure, exported for testing) ──────────────────────────
|
|
485
534
|
/**
|
|
486
535
|
* Check if an alert text matches any suppressed pattern (case-insensitive substring).
|
|
@@ -807,7 +856,8 @@ export const BUILTIN_COMMANDS = new Set([
|
|
|
807
856
|
"/mute-errors", "/prev-goal", "/tag", "/tags", "/tag-filter", "/find", "/reset-health", "/timeline", "/color", "/clear-history",
|
|
808
857
|
"/duplicate", "/color-all", "/quiet-hours", "/quiet-status", "/history-stats", "/cost-summary", "/session-report", "/alert-log",
|
|
809
858
|
"/budget", "/budgets", "/budget-status", "/pause-all", "/resume-all",
|
|
810
|
-
"/health-trend", "/alert-mute",
|
|
859
|
+
"/health-trend", "/alert-mute", "/flap-log", "/drain", "/undrain", "/export-all",
|
|
860
|
+
"/note-history", "/label", "/sessions",
|
|
811
861
|
]);
|
|
812
862
|
/** Resolve a slash command through the alias map. Returns the expanded command or the original. */
|
|
813
863
|
export function resolveAlias(line, aliases) {
|
|
@@ -1081,6 +1131,8 @@ export class TUI {
|
|
|
1081
1131
|
quietHoursRanges = []; // quiet-hour start/end pairs
|
|
1082
1132
|
sessionHealthHistory = new Map(); // session ID → health snapshots
|
|
1083
1133
|
alertLog = []; // recent auto-generated status alerts (ring buffer, max 100)
|
|
1134
|
+
sessionNoteHistory = new Map(); // session ID → last N notes before clear
|
|
1135
|
+
sessionLabels = new Map(); // session ID → freeform display label
|
|
1084
1136
|
sessionBudgets = new Map(); // session ID → USD budget
|
|
1085
1137
|
globalBudget = null; // global fallback budget in USD
|
|
1086
1138
|
budgetAlerted = new Map(); // session ID → epoch ms of last budget alert
|
|
@@ -1088,6 +1140,8 @@ export class TUI {
|
|
|
1088
1140
|
prevSessionStatus = new Map(); // session ID → last known status (for change detection)
|
|
1089
1141
|
flapAlerted = new Map(); // session ID → epoch ms of last flap alert
|
|
1090
1142
|
alertMutePatterns = new Set(); // substrings to hide from /alert-log display
|
|
1143
|
+
drainingIds = new Set(); // session IDs marked as draining (skip by reasoner)
|
|
1144
|
+
flapLog = []; // recent flap events
|
|
1091
1145
|
// drill-down mode: show a single session's full output
|
|
1092
1146
|
viewMode = "overview";
|
|
1093
1147
|
drilldownSessionId = null;
|
|
@@ -1350,6 +1404,15 @@ export class TUI {
|
|
|
1350
1404
|
if (!sessionId)
|
|
1351
1405
|
return false;
|
|
1352
1406
|
if (text.trim() === "") {
|
|
1407
|
+
// push current note into history before clearing
|
|
1408
|
+
const current = this.sessionNotes.get(sessionId);
|
|
1409
|
+
if (current) {
|
|
1410
|
+
const hist = this.sessionNoteHistory.get(sessionId) ?? [];
|
|
1411
|
+
hist.push(current);
|
|
1412
|
+
if (hist.length > MAX_NOTE_HISTORY)
|
|
1413
|
+
hist.shift();
|
|
1414
|
+
this.sessionNoteHistory.set(sessionId, hist);
|
|
1415
|
+
}
|
|
1353
1416
|
this.sessionNotes.delete(sessionId);
|
|
1354
1417
|
}
|
|
1355
1418
|
else {
|
|
@@ -1375,6 +1438,46 @@ export class TUI {
|
|
|
1375
1438
|
getAllNotes() {
|
|
1376
1439
|
return this.sessionNotes;
|
|
1377
1440
|
}
|
|
1441
|
+
/** Return note history for a session (oldest first). */
|
|
1442
|
+
getNoteHistory(id) {
|
|
1443
|
+
return this.sessionNoteHistory.get(id) ?? [];
|
|
1444
|
+
}
|
|
1445
|
+
// ── Session labels ───────────────────────────────────────────────────────
|
|
1446
|
+
/**
|
|
1447
|
+
* Set or clear a label for a session (by 1-indexed number, ID, or title).
|
|
1448
|
+
* Label is displayed below the session title in normal cards.
|
|
1449
|
+
* Returns true if session found.
|
|
1450
|
+
*/
|
|
1451
|
+
setLabel(sessionIdOrIndex, label) {
|
|
1452
|
+
let sessionId;
|
|
1453
|
+
if (typeof sessionIdOrIndex === "number") {
|
|
1454
|
+
sessionId = this.sessions[sessionIdOrIndex - 1]?.id;
|
|
1455
|
+
}
|
|
1456
|
+
else {
|
|
1457
|
+
const needle = sessionIdOrIndex.toLowerCase();
|
|
1458
|
+
const match = this.sessions.find((s) => s.id === sessionIdOrIndex || s.id.startsWith(needle) || s.title.toLowerCase() === needle);
|
|
1459
|
+
sessionId = match?.id;
|
|
1460
|
+
}
|
|
1461
|
+
if (!sessionId)
|
|
1462
|
+
return false;
|
|
1463
|
+
if (!label || label.trim() === "") {
|
|
1464
|
+
this.sessionLabels.delete(sessionId);
|
|
1465
|
+
}
|
|
1466
|
+
else {
|
|
1467
|
+
this.sessionLabels.set(sessionId, truncateLabel(label.trim()));
|
|
1468
|
+
}
|
|
1469
|
+
if (this.active)
|
|
1470
|
+
this.paintSessions();
|
|
1471
|
+
return true;
|
|
1472
|
+
}
|
|
1473
|
+
/** Get the label for a session ID (or undefined). */
|
|
1474
|
+
getLabel(id) {
|
|
1475
|
+
return this.sessionLabels.get(id);
|
|
1476
|
+
}
|
|
1477
|
+
/** Return all session labels. */
|
|
1478
|
+
getAllLabels() {
|
|
1479
|
+
return this.sessionLabels;
|
|
1480
|
+
}
|
|
1378
1481
|
/** Return the current sessions (read-only, for resolving IDs to titles in the UI). */
|
|
1379
1482
|
getSessions() {
|
|
1380
1483
|
return this.sessions;
|
|
@@ -1600,6 +1703,56 @@ export class TUI {
|
|
|
1600
1703
|
getSessionHealthHistory(id) {
|
|
1601
1704
|
return this.sessionHealthHistory.get(id) ?? [];
|
|
1602
1705
|
}
|
|
1706
|
+
// ── Session drain mode ───────────────────────────────────────────────────
|
|
1707
|
+
/** Mark a session as draining (by index, ID, or title). Returns true if found. */
|
|
1708
|
+
drainSession(sessionIdOrIndex) {
|
|
1709
|
+
let sessionId;
|
|
1710
|
+
if (typeof sessionIdOrIndex === "number") {
|
|
1711
|
+
sessionId = this.sessions[sessionIdOrIndex - 1]?.id;
|
|
1712
|
+
}
|
|
1713
|
+
else {
|
|
1714
|
+
const needle = sessionIdOrIndex.toLowerCase();
|
|
1715
|
+
const match = this.sessions.find((s) => s.id === sessionIdOrIndex || s.id.startsWith(needle) || s.title.toLowerCase() === needle);
|
|
1716
|
+
sessionId = match?.id;
|
|
1717
|
+
}
|
|
1718
|
+
if (!sessionId)
|
|
1719
|
+
return false;
|
|
1720
|
+
this.drainingIds.add(sessionId);
|
|
1721
|
+
if (this.active)
|
|
1722
|
+
this.paintSessions();
|
|
1723
|
+
return true;
|
|
1724
|
+
}
|
|
1725
|
+
/** Remove drain mark from a session. Returns true if it was draining. */
|
|
1726
|
+
undrainSession(sessionIdOrIndex) {
|
|
1727
|
+
let sessionId;
|
|
1728
|
+
if (typeof sessionIdOrIndex === "number") {
|
|
1729
|
+
sessionId = this.sessions[sessionIdOrIndex - 1]?.id;
|
|
1730
|
+
}
|
|
1731
|
+
else {
|
|
1732
|
+
const needle = sessionIdOrIndex.toLowerCase();
|
|
1733
|
+
const match = this.sessions.find((s) => s.id === sessionIdOrIndex || s.id.startsWith(needle) || s.title.toLowerCase() === needle);
|
|
1734
|
+
sessionId = match?.id;
|
|
1735
|
+
}
|
|
1736
|
+
if (!sessionId)
|
|
1737
|
+
return false;
|
|
1738
|
+
const had = this.drainingIds.delete(sessionId);
|
|
1739
|
+
if (had && this.active)
|
|
1740
|
+
this.paintSessions();
|
|
1741
|
+
return had;
|
|
1742
|
+
}
|
|
1743
|
+
/** Check if a session is draining. */
|
|
1744
|
+
isDraining(id) {
|
|
1745
|
+
return this.drainingIds.has(id);
|
|
1746
|
+
}
|
|
1747
|
+
/** Return all draining session IDs (for reasoner prompt and display). */
|
|
1748
|
+
getDrainingIds() {
|
|
1749
|
+
return this.drainingIds;
|
|
1750
|
+
}
|
|
1751
|
+
// ── Flap log ─────────────────────────────────────────────────────────────
|
|
1752
|
+
/** Return recent flap events (newest last). */
|
|
1753
|
+
getFlapLog() {
|
|
1754
|
+
return this.flapLog;
|
|
1755
|
+
}
|
|
1603
1756
|
/** Return all alert log entries (last 100 "status" tag entries), filtered by mute patterns. */
|
|
1604
1757
|
getAlertLog(includeAll = false) {
|
|
1605
1758
|
if (includeAll || this.alertMutePatterns.size === 0)
|
|
@@ -1963,7 +2116,11 @@ export class TUI {
|
|
|
1963
2116
|
const lastFlapAlert = this.flapAlerted.get(s.id) ?? 0;
|
|
1964
2117
|
if (now - lastFlapAlert >= 5 * 60_000) {
|
|
1965
2118
|
this.flapAlerted.set(s.id, now);
|
|
1966
|
-
|
|
2119
|
+
const flapCount = statusHist.filter((c) => c.ts >= now - FLAP_WINDOW_MS).length;
|
|
2120
|
+
this.flapLog.push({ sessionId: s.id, title: s.title, ts: now, count: flapCount });
|
|
2121
|
+
if (this.flapLog.length > 50)
|
|
2122
|
+
this.flapLog.shift();
|
|
2123
|
+
this.log("status", `flap: ${s.title} is oscillating rapidly (${flapCount} status changes in ${Math.round(FLAP_WINDOW_MS / 60_000)}m)`, s.id);
|
|
1967
2124
|
}
|
|
1968
2125
|
}
|
|
1969
2126
|
}
|
|
@@ -2563,6 +2720,8 @@ export class TUI {
|
|
|
2563
2720
|
const colorName = this.sessionColors.get(s.id);
|
|
2564
2721
|
const colorDot = colorName ? formatColorDot(colorName) : "";
|
|
2565
2722
|
const colorDotWidth = colorName ? 2 : 0; // dot + space
|
|
2723
|
+
const draining = this.drainingIds.has(s.id);
|
|
2724
|
+
const drainIcon = draining ? `${DIM}${DRAIN_ICON}${RESET} ` : "";
|
|
2566
2725
|
const muteBadge = muted ? formatMuteBadge(this.mutedEntryCounts.get(s.id) ?? 0) : "";
|
|
2567
2726
|
const muteBadgeWidth = muted ? String(Math.min(this.mutedEntryCounts.get(s.id) ?? 0, 9999)).length + 2 : 0; // "(N)" visible chars, 0 when count is 0
|
|
2568
2727
|
const actualBadgeWidth = (this.mutedEntryCounts.get(s.id) ?? 0) > 0 ? muteBadgeWidth + 1 : 0; // +1 for trailing space
|
|
@@ -2572,10 +2731,11 @@ export class TUI {
|
|
|
2572
2731
|
const note = noted ? `${TEAL}${NOTE_ICON}${RESET} ` : "";
|
|
2573
2732
|
const groupBadge = group ? `${formatGroupBadge(group)} ` : "";
|
|
2574
2733
|
const badgeSuffix = muteBadge ? `${muteBadge} ` : "";
|
|
2575
|
-
const iconsWidth = (pinned ? 2 : 0) + (muted ? 2 : 0) + (noted ? 2 : 0) + actualBadgeWidth + groupBadgeWidth + tagsBadgeWidth + colorDotWidth;
|
|
2734
|
+
const iconsWidth = (pinned ? 2 : 0) + (muted ? 2 : 0) + (noted ? 2 : 0) + actualBadgeWidth + groupBadgeWidth + tagsBadgeWidth + colorDotWidth + (draining ? 2 : 0);
|
|
2576
2735
|
const cardWidth = innerWidth - 1 - iconsWidth;
|
|
2577
2736
|
const cardAge = s.createdAt ? formatSessionAge(s.createdAt, nowMs) : undefined;
|
|
2578
|
-
const
|
|
2737
|
+
const sessionLabel = this.sessionLabels.get(s.id);
|
|
2738
|
+
const line = `${bg}${SLATE}${BOX.v}${RESET}${bg} ${pin}${mute}${badgeSuffix}${note}${groupBadge}${tagsBadge}${colorDot}${drainIcon}${formatSessionCard(s, cardWidth, errSparkline || undefined, idleSinceMs, healthBadge || undefined, displayName, cardAge || undefined, sessionLabel)}`;
|
|
2579
2739
|
const padded = padBoxLineHover(line, this.cols, isHovered);
|
|
2580
2740
|
process.stderr.write(moveTo(startRow + 1 + i, 1) + CLEAR_LINE + padded);
|
|
2581
2741
|
}
|
|
@@ -2643,10 +2803,13 @@ export class TUI {
|
|
|
2643
2803
|
const note = noted ? `${TEAL}${NOTE_ICON}${RESET} ` : "";
|
|
2644
2804
|
const groupBadge = group ? `${formatGroupBadge(group)} ` : "";
|
|
2645
2805
|
const badgeSuffix = muteBadge ? `${muteBadge} ` : "";
|
|
2646
|
-
const
|
|
2806
|
+
const draining2 = this.drainingIds.has(s.id);
|
|
2807
|
+
const drainIcon2 = draining2 ? `${DIM}${DRAIN_ICON}${RESET} ` : "";
|
|
2808
|
+
const iconsWidth = (pinned ? 2 : 0) + (muted ? 2 : 0) + (noted ? 2 : 0) + actualBadgeWidth + groupBadgeWidth + tagsBadgeWidth2 + colorDotWidth2 + (draining2 ? 2 : 0);
|
|
2647
2809
|
const cardWidth = innerWidth - 1 - iconsWidth;
|
|
2648
2810
|
const cardAge2 = s.createdAt ? formatSessionAge(s.createdAt, nowMs2) : undefined;
|
|
2649
|
-
const
|
|
2811
|
+
const sessionLabel2 = this.sessionLabels.get(s.id);
|
|
2812
|
+
const line = `${bg}${SLATE}${BOX.v}${RESET}${bg} ${pin}${mute}${badgeSuffix}${note}${groupBadge}${tagsBadge2}${colorDot2}${drainIcon2}${formatSessionCard(s, cardWidth, errSparkline || undefined, idleSinceMs, healthBadge2 || undefined, displayName2, cardAge2 || undefined, sessionLabel2)}`;
|
|
2650
2813
|
const padded = padBoxLineHover(line, this.cols, isHovered);
|
|
2651
2814
|
process.stderr.write(SAVE_CURSOR + moveTo(startRow + 1 + i, 1) + CLEAR_LINE + padded + RESTORE_CURSOR);
|
|
2652
2815
|
}
|
|
@@ -2765,7 +2928,8 @@ export class TUI {
|
|
|
2765
2928
|
// healthBadge: optional pre-formatted health score badge ("⬡83" colored)
|
|
2766
2929
|
// displayName: optional custom name override (from /rename)
|
|
2767
2930
|
// ageStr: optional session age string (from createdAt)
|
|
2768
|
-
|
|
2931
|
+
// label: optional freeform label shown as DIM subtitle
|
|
2932
|
+
function formatSessionCard(s, maxWidth, errorSparkline, idleSinceMs, healthBadge, displayName, ageStr, label) {
|
|
2769
2933
|
const dot = STATUS_DOT[s.status] ?? `${AMBER}${DOT.filled}${RESET}`;
|
|
2770
2934
|
const title = displayName ?? s.title;
|
|
2771
2935
|
const name = displayName ? `${BOLD}${displayName}${DIM} (${s.title})${RESET}` : `${BOLD}${s.title}${RESET}`;
|
|
@@ -2806,7 +2970,8 @@ function formatSessionCard(s, maxWidth, errorSparkline, idleSinceMs, healthBadge
|
|
|
2806
2970
|
desc = `${SLATE}${s.status}${RESET}`;
|
|
2807
2971
|
}
|
|
2808
2972
|
const ageSuffix = ageStr ? ` ${DIM}age:${ageStr}${RESET}` : "";
|
|
2809
|
-
|
|
2973
|
+
const labelSuffix = label ? ` ${DIM}· ${label}${RESET}` : "";
|
|
2974
|
+
return truncateAnsi(`${dot} ${healthPrefix}${name} ${toolBadge}${contextBadge} ${SLATE}${BOX.h}${RESET} ${desc}${ageSuffix}${labelSuffix}${sparkSuffix}`, maxWidth);
|
|
2810
2975
|
}
|
|
2811
2976
|
// colorize an activity entry based on its tag
|
|
2812
2977
|
function formatActivity(entry, maxCols) {
|
package/dist/types.d.ts
CHANGED