aoaoe 0.156.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 +53 -1
- package/dist/input.d.ts +9 -0
- package/dist/input.js +46 -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 +37 -1
- package/dist/tui.js +106 -4
- 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, DRAIN_ICON } 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,57 @@ 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
|
+
});
|
|
1026
1077
|
// wire /flap-log
|
|
1027
1078
|
input.onFlapLog(() => {
|
|
1028
1079
|
const log = tui.getFlapLog();
|
|
@@ -2094,6 +2145,7 @@ async function daemonTick(config, poller, reasoner, executor, reasonerConsole, p
|
|
|
2094
2145
|
try {
|
|
2095
2146
|
tickResult = await loopTick({
|
|
2096
2147
|
config, poller, reasoner: wrappedReasoner, executor, policyStates, pollCount, userMessage, taskContext, beforeExecute,
|
|
2148
|
+
drainingSessionIds: tui ? [...tui.getDrainingIds()] : undefined,
|
|
2097
2149
|
});
|
|
2098
2150
|
}
|
|
2099
2151
|
catch (err) {
|
package/dist/input.d.ts
CHANGED
|
@@ -65,6 +65,9 @@ export type BudgetStatusHandler = () => void;
|
|
|
65
65
|
export type FlapLogHandler = () => void;
|
|
66
66
|
export type DrainHandler = (target: string, drain: boolean) => void;
|
|
67
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;
|
|
68
71
|
export interface MouseEvent {
|
|
69
72
|
button: number;
|
|
70
73
|
col: number;
|
|
@@ -152,6 +155,9 @@ export declare class InputReader {
|
|
|
152
155
|
private flapLogHandler;
|
|
153
156
|
private drainHandler;
|
|
154
157
|
private exportAllHandler;
|
|
158
|
+
private noteHistoryHandler;
|
|
159
|
+
private labelHandler;
|
|
160
|
+
private sessionsTableHandler;
|
|
155
161
|
private aliases;
|
|
156
162
|
private mouseDataListener;
|
|
157
163
|
onScroll(handler: (dir: ScrollDirection) => void): void;
|
|
@@ -222,6 +228,9 @@ export declare class InputReader {
|
|
|
222
228
|
onFlapLog(handler: FlapLogHandler): void;
|
|
223
229
|
onDrain(handler: DrainHandler): void;
|
|
224
230
|
onExportAll(handler: ExportAllHandler): void;
|
|
231
|
+
onNoteHistory(handler: NoteHistoryHandler): void;
|
|
232
|
+
onLabel(handler: LabelHandler): void;
|
|
233
|
+
onSessionsTable(handler: SessionsTableHandler): void;
|
|
225
234
|
/** Set aliases from persisted prefs. */
|
|
226
235
|
setAliases(aliases: Record<string, string>): void;
|
|
227
236
|
/** Get current aliases as a plain object. */
|
package/dist/input.js
CHANGED
|
@@ -98,6 +98,9 @@ export class InputReader {
|
|
|
98
98
|
flapLogHandler = null;
|
|
99
99
|
drainHandler = null;
|
|
100
100
|
exportAllHandler = null;
|
|
101
|
+
noteHistoryHandler = null;
|
|
102
|
+
labelHandler = null;
|
|
103
|
+
sessionsTableHandler = null;
|
|
101
104
|
aliases = new Map(); // /shortcut → /full command
|
|
102
105
|
mouseDataListener = null;
|
|
103
106
|
// register a callback for scroll key events (PgUp/PgDn/Home/End)
|
|
@@ -321,6 +324,9 @@ export class InputReader {
|
|
|
321
324
|
onFlapLog(handler) { this.flapLogHandler = handler; }
|
|
322
325
|
onDrain(handler) { this.drainHandler = handler; }
|
|
323
326
|
onExportAll(handler) { this.exportAllHandler = handler; }
|
|
327
|
+
onNoteHistory(handler) { this.noteHistoryHandler = handler; }
|
|
328
|
+
onLabel(handler) { this.labelHandler = handler; }
|
|
329
|
+
onSessionsTable(handler) { this.sessionsTableHandler = handler; }
|
|
324
330
|
/** Set aliases from persisted prefs. */
|
|
325
331
|
setAliases(aliases) {
|
|
326
332
|
this.aliases.clear();
|
|
@@ -595,6 +601,9 @@ ${BOLD}navigation:${RESET}
|
|
|
595
601
|
/drain N mark session N as draining (supervisor will skip it)
|
|
596
602
|
/undrain N remove drain mark from session N
|
|
597
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)
|
|
598
607
|
/history-stats show aggregate statistics from persisted activity history
|
|
599
608
|
/cost-summary show total estimated spend across all sessions
|
|
600
609
|
/session-report N generate full markdown report for a session → ~/.aoaoe/report-<name>-<ts>.md
|
|
@@ -1252,6 +1261,43 @@ ${BOLD}other:${RESET}
|
|
|
1252
1261
|
console.error(`${DIM}undrain not available${RESET}`);
|
|
1253
1262
|
break;
|
|
1254
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;
|
|
1255
1301
|
case "/export-all":
|
|
1256
1302
|
if (this.exportAllHandler)
|
|
1257
1303
|
this.exportAllHandler();
|
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,28 @@ 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;
|
|
163
185
|
/** Drain icon shown in session cards for draining sessions. */
|
|
164
186
|
export declare const DRAIN_ICON = "\u21E3";
|
|
165
187
|
/**
|
|
@@ -442,6 +464,8 @@ export declare class TUI {
|
|
|
442
464
|
private quietHoursRanges;
|
|
443
465
|
private sessionHealthHistory;
|
|
444
466
|
private alertLog;
|
|
467
|
+
private sessionNoteHistory;
|
|
468
|
+
private sessionLabels;
|
|
445
469
|
private sessionBudgets;
|
|
446
470
|
private globalBudget;
|
|
447
471
|
private budgetAlerted;
|
|
@@ -531,6 +555,18 @@ export declare class TUI {
|
|
|
531
555
|
getNoteCount(): number;
|
|
532
556
|
/** Return all session notes (for /notes listing). */
|
|
533
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>;
|
|
534
570
|
/** Return the current sessions (read-only, for resolving IDs to titles in the UI). */
|
|
535
571
|
getSessions(): readonly DaemonSessionState[];
|
|
536
572
|
/** Return the uptime in ms for a session (0 if not tracked). */
|
|
@@ -771,7 +807,7 @@ export declare class TUI {
|
|
|
771
807
|
private repaintDrilldownContent;
|
|
772
808
|
private paintInputLine;
|
|
773
809
|
}
|
|
774
|
-
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;
|
|
775
811
|
declare function formatActivity(entry: ActivityEntry, maxCols: number): string;
|
|
776
812
|
declare function padBoxLine(line: string, totalWidth: number): string;
|
|
777
813
|
declare function padBoxLineHover(line: string, totalWidth: number, hovered: boolean): string;
|
package/dist/tui.js
CHANGED
|
@@ -481,6 +481,52 @@ 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
|
+
}
|
|
484
530
|
// ── Session drain mode helpers ────────────────────────────────────────────────
|
|
485
531
|
/** Drain icon shown in session cards for draining sessions. */
|
|
486
532
|
export const DRAIN_ICON = "⇣";
|
|
@@ -811,6 +857,7 @@ export const BUILTIN_COMMANDS = new Set([
|
|
|
811
857
|
"/duplicate", "/color-all", "/quiet-hours", "/quiet-status", "/history-stats", "/cost-summary", "/session-report", "/alert-log",
|
|
812
858
|
"/budget", "/budgets", "/budget-status", "/pause-all", "/resume-all",
|
|
813
859
|
"/health-trend", "/alert-mute", "/flap-log", "/drain", "/undrain", "/export-all",
|
|
860
|
+
"/note-history", "/label", "/sessions",
|
|
814
861
|
]);
|
|
815
862
|
/** Resolve a slash command through the alias map. Returns the expanded command or the original. */
|
|
816
863
|
export function resolveAlias(line, aliases) {
|
|
@@ -1084,6 +1131,8 @@ export class TUI {
|
|
|
1084
1131
|
quietHoursRanges = []; // quiet-hour start/end pairs
|
|
1085
1132
|
sessionHealthHistory = new Map(); // session ID → health snapshots
|
|
1086
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
|
|
1087
1136
|
sessionBudgets = new Map(); // session ID → USD budget
|
|
1088
1137
|
globalBudget = null; // global fallback budget in USD
|
|
1089
1138
|
budgetAlerted = new Map(); // session ID → epoch ms of last budget alert
|
|
@@ -1355,6 +1404,15 @@ export class TUI {
|
|
|
1355
1404
|
if (!sessionId)
|
|
1356
1405
|
return false;
|
|
1357
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
|
+
}
|
|
1358
1416
|
this.sessionNotes.delete(sessionId);
|
|
1359
1417
|
}
|
|
1360
1418
|
else {
|
|
@@ -1380,6 +1438,46 @@ export class TUI {
|
|
|
1380
1438
|
getAllNotes() {
|
|
1381
1439
|
return this.sessionNotes;
|
|
1382
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
|
+
}
|
|
1383
1481
|
/** Return the current sessions (read-only, for resolving IDs to titles in the UI). */
|
|
1384
1482
|
getSessions() {
|
|
1385
1483
|
return this.sessions;
|
|
@@ -2636,7 +2734,8 @@ export class TUI {
|
|
|
2636
2734
|
const iconsWidth = (pinned ? 2 : 0) + (muted ? 2 : 0) + (noted ? 2 : 0) + actualBadgeWidth + groupBadgeWidth + tagsBadgeWidth + colorDotWidth + (draining ? 2 : 0);
|
|
2637
2735
|
const cardWidth = innerWidth - 1 - iconsWidth;
|
|
2638
2736
|
const cardAge = s.createdAt ? formatSessionAge(s.createdAt, nowMs) : undefined;
|
|
2639
|
-
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)}`;
|
|
2640
2739
|
const padded = padBoxLineHover(line, this.cols, isHovered);
|
|
2641
2740
|
process.stderr.write(moveTo(startRow + 1 + i, 1) + CLEAR_LINE + padded);
|
|
2642
2741
|
}
|
|
@@ -2709,7 +2808,8 @@ export class TUI {
|
|
|
2709
2808
|
const iconsWidth = (pinned ? 2 : 0) + (muted ? 2 : 0) + (noted ? 2 : 0) + actualBadgeWidth + groupBadgeWidth + tagsBadgeWidth2 + colorDotWidth2 + (draining2 ? 2 : 0);
|
|
2710
2809
|
const cardWidth = innerWidth - 1 - iconsWidth;
|
|
2711
2810
|
const cardAge2 = s.createdAt ? formatSessionAge(s.createdAt, nowMs2) : undefined;
|
|
2712
|
-
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)}`;
|
|
2713
2813
|
const padded = padBoxLineHover(line, this.cols, isHovered);
|
|
2714
2814
|
process.stderr.write(SAVE_CURSOR + moveTo(startRow + 1 + i, 1) + CLEAR_LINE + padded + RESTORE_CURSOR);
|
|
2715
2815
|
}
|
|
@@ -2828,7 +2928,8 @@ export class TUI {
|
|
|
2828
2928
|
// healthBadge: optional pre-formatted health score badge ("⬡83" colored)
|
|
2829
2929
|
// displayName: optional custom name override (from /rename)
|
|
2830
2930
|
// ageStr: optional session age string (from createdAt)
|
|
2831
|
-
|
|
2931
|
+
// label: optional freeform label shown as DIM subtitle
|
|
2932
|
+
function formatSessionCard(s, maxWidth, errorSparkline, idleSinceMs, healthBadge, displayName, ageStr, label) {
|
|
2832
2933
|
const dot = STATUS_DOT[s.status] ?? `${AMBER}${DOT.filled}${RESET}`;
|
|
2833
2934
|
const title = displayName ?? s.title;
|
|
2834
2935
|
const name = displayName ? `${BOLD}${displayName}${DIM} (${s.title})${RESET}` : `${BOLD}${s.title}${RESET}`;
|
|
@@ -2869,7 +2970,8 @@ function formatSessionCard(s, maxWidth, errorSparkline, idleSinceMs, healthBadge
|
|
|
2869
2970
|
desc = `${SLATE}${s.status}${RESET}`;
|
|
2870
2971
|
}
|
|
2871
2972
|
const ageSuffix = ageStr ? ` ${DIM}age:${ageStr}${RESET}` : "";
|
|
2872
|
-
|
|
2973
|
+
const labelSuffix = label ? ` ${DIM}· ${label}${RESET}` : "";
|
|
2974
|
+
return truncateAnsi(`${dot} ${healthPrefix}${name} ${toolBadge}${contextBadge} ${SLATE}${BOX.h}${RESET} ${desc}${ageSuffix}${labelSuffix}${sparkSuffix}`, maxWidth);
|
|
2873
2975
|
}
|
|
2874
2976
|
// colorize an activity entry based on its tag
|
|
2875
2977
|
function formatActivity(entry, maxCols) {
|
package/dist/types.d.ts
CHANGED