aoaoe 0.132.0 → 0.139.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/daemon-state.js +12 -1
- package/dist/index.js +96 -6
- package/dist/input.d.ts +15 -0
- package/dist/input.js +72 -0
- package/dist/tui-history.d.ts +14 -0
- package/dist/tui-history.js +33 -0
- package/dist/tui.d.ts +54 -1
- package/dist/tui.js +156 -8
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
package/dist/daemon-state.js
CHANGED
|
@@ -5,7 +5,7 @@ import { writeFileSync, readFileSync, existsSync, unlinkSync, mkdirSync, renameS
|
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
import { toDaemonState } from "./types.js";
|
|
8
|
-
import { parseTasks, formatTaskList, parseContext } from "./task-parser.js";
|
|
8
|
+
import { parseTasks, formatTaskList, parseContext, parseCost } from "./task-parser.js";
|
|
9
9
|
// default state directory — overridable via setStateDir() for test isolation
|
|
10
10
|
let AOAOE_DIR = join(homedir(), ".aoaoe");
|
|
11
11
|
let STATE_FILE = join(AOAOE_DIR, "daemon-state.json");
|
|
@@ -52,6 +52,7 @@ const sessionTasks = new Map();
|
|
|
52
52
|
const todoCache = new Map();
|
|
53
53
|
// cache parsed context token usage for unchanged sessions
|
|
54
54
|
const contextCache = new Map();
|
|
55
|
+
const costCache = new Map();
|
|
55
56
|
export function setSessionTask(sessionId, task) {
|
|
56
57
|
// keep task text short for display
|
|
57
58
|
sessionTasks.set(sessionId, task.length > 80 ? task.slice(0, 77) + "..." : task);
|
|
@@ -108,6 +109,10 @@ export function buildSessionStates(obs) {
|
|
|
108
109
|
if (!currentIds.has(id))
|
|
109
110
|
contextCache.delete(id);
|
|
110
111
|
}
|
|
112
|
+
for (const id of costCache.keys()) {
|
|
113
|
+
if (!currentIds.has(id))
|
|
114
|
+
costCache.delete(id);
|
|
115
|
+
}
|
|
111
116
|
// only re-parse TODO items for sessions that have new output
|
|
112
117
|
const changedIds = new Set(obs.changes.map((c) => c.sessionId));
|
|
113
118
|
return obs.sessions.map((snap) => {
|
|
@@ -126,18 +131,23 @@ export function buildSessionStates(obs) {
|
|
|
126
131
|
todoSummary = todoCache.get(s.id);
|
|
127
132
|
}
|
|
128
133
|
let contextTokens;
|
|
134
|
+
let costStr;
|
|
129
135
|
if (changedIds.has(s.id)) {
|
|
130
136
|
contextTokens = parseContext(snap.output);
|
|
131
137
|
contextCache.set(s.id, contextTokens);
|
|
138
|
+
costStr = parseCost(snap.output);
|
|
139
|
+
costCache.set(s.id, costStr);
|
|
132
140
|
}
|
|
133
141
|
else {
|
|
134
142
|
contextTokens = contextCache.get(s.id);
|
|
143
|
+
costStr = costCache.get(s.id);
|
|
135
144
|
}
|
|
136
145
|
return {
|
|
137
146
|
id: s.id,
|
|
138
147
|
title: s.title,
|
|
139
148
|
tool: s.tool,
|
|
140
149
|
status: s.status,
|
|
150
|
+
path: snap.session.path,
|
|
141
151
|
currentTask: sessionTasks.get(s.id),
|
|
142
152
|
lastActivity: lastActivity && lastActivity.length > 100
|
|
143
153
|
? lastActivity.slice(0, 97) + "..."
|
|
@@ -145,6 +155,7 @@ export function buildSessionStates(obs) {
|
|
|
145
155
|
contextTokens,
|
|
146
156
|
todoSummary,
|
|
147
157
|
userActive: snap.userActive ?? false,
|
|
158
|
+
costStr,
|
|
148
159
|
};
|
|
149
160
|
});
|
|
150
161
|
}
|
package/dist/index.js
CHANGED
|
@@ -16,11 +16,11 @@ 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 } 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 } 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";
|
|
23
|
-
import { loadTuiHistory, searchHistory } from "./tui-history.js";
|
|
23
|
+
import { loadTuiHistory, searchHistory, TUI_HISTORY_FILE, computeHistoryStats } from "./tui-history.js";
|
|
24
24
|
import { ConfigWatcher, formatConfigChange } from "./config-watcher.js";
|
|
25
25
|
import { parseActionLogEntries, parseActivityEntries, mergeTimeline, filterByAge, parseDuration, formatTimelineJson, formatTimelineMarkdown } from "./export.js";
|
|
26
26
|
import { actionSession, actionDetail, toActionLogEntry } from "./types.js";
|
|
@@ -194,6 +194,11 @@ async function main() {
|
|
|
194
194
|
tui.restoreSessionTags(prefs.sessionTags);
|
|
195
195
|
if (prefs.sessionColors)
|
|
196
196
|
tui.restoreSessionColors(prefs.sessionColors);
|
|
197
|
+
if (prefs.quietHours && prefs.quietHours.length > 0) {
|
|
198
|
+
const ranges = prefs.quietHours.map(parseQuietHoursRange).filter(Boolean);
|
|
199
|
+
if (ranges.length > 0)
|
|
200
|
+
tui.setQuietHours(ranges);
|
|
201
|
+
}
|
|
197
202
|
}
|
|
198
203
|
const persistPrefs = () => {
|
|
199
204
|
if (!tui)
|
|
@@ -223,6 +228,7 @@ async function main() {
|
|
|
223
228
|
sessionAliases: aliasesObj,
|
|
224
229
|
sessionTags: sTagsObj,
|
|
225
230
|
sessionColors: sColorsObj,
|
|
231
|
+
quietHours: tui.getQuietHours().map(([s, e]) => `${s}-${e}`),
|
|
226
232
|
});
|
|
227
233
|
};
|
|
228
234
|
if (!useTui) {
|
|
@@ -587,8 +593,11 @@ async function main() {
|
|
|
587
593
|
for (const s of sorted) {
|
|
588
594
|
const up = firstSeen.has(s.id) ? formatUptime(now - firstSeen.get(s.id)) : "?";
|
|
589
595
|
const errCount = errors.get(s.id) ?? 0;
|
|
590
|
-
const
|
|
596
|
+
const errTs = tui.getSessionErrorTimestamps(s.id);
|
|
597
|
+
const errTrend = errTs.length > 0 ? (computeErrorTrend(errTs, now) === "rising" ? "↑" : computeErrorTrend(errTs, now) === "falling" ? "↓" : "") : "";
|
|
598
|
+
const errStr = errCount > 0 ? ` ${errCount}err${errTrend}` : "";
|
|
591
599
|
const ctxStr = s.contextTokens ? ` ${s.contextTokens}` : "";
|
|
600
|
+
const costStr = tui.getSessionCost(s.id) ? ` ${tui.getSessionCost(s.id)}` : "";
|
|
592
601
|
const note = notes.get(s.id);
|
|
593
602
|
const noteStr = note ? ` "${note}"` : "";
|
|
594
603
|
const group = groups.get(s.id);
|
|
@@ -597,7 +606,7 @@ async function main() {
|
|
|
597
606
|
const lastChange = lastChangeAt.get(s.id);
|
|
598
607
|
const idleStr = (lastChange && (s.status === "idle" || s.status === "stopped" || s.status === "done"))
|
|
599
608
|
? ` idle ${formatUptime(now - lastChange)}` : "";
|
|
600
|
-
tui.log("system", ` ${s.title}${groupStr} — ${s.status} ${up}${ctxStr}${errStr}${idleStr}${noteStr}`);
|
|
609
|
+
tui.log("system", ` ${s.title}${groupStr} — ${s.status} ${up}${ctxStr}${costStr}${errStr}${idleStr}${noteStr}`);
|
|
601
610
|
}
|
|
602
611
|
});
|
|
603
612
|
// wire /uptime listing
|
|
@@ -944,7 +953,7 @@ async function main() {
|
|
|
944
953
|
input.onExportStats(() => {
|
|
945
954
|
const sessions = tui.getSessions();
|
|
946
955
|
const now = Date.now();
|
|
947
|
-
const entries = buildSessionStats(sessions, tui.getSessionErrorCounts(), tui.getAllBurnRates(now), tui.getAllFirstSeen(), tui.getAllLastChangeAt(), tui.getAllHealthScores(now), tui.getAllSessionAliases(), now);
|
|
956
|
+
const entries = buildSessionStats(sessions, tui.getSessionErrorCounts(), tui.getAllBurnRates(now), tui.getAllFirstSeen(), tui.getAllLastChangeAt(), tui.getAllHealthScores(now), tui.getAllSessionAliases(), now, new Map(sessions.map((s) => [s.id, tui.getSessionErrorTimestamps(s.id)])), tui.getAllSessionCosts());
|
|
948
957
|
const ts = new Date(now).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
949
958
|
const dir = join(homedir(), ".aoaoe");
|
|
950
959
|
const path = join(dir, `stats-${ts}.json`);
|
|
@@ -957,6 +966,87 @@ async function main() {
|
|
|
957
966
|
tui.log("error", `export-stats failed: ${err}`);
|
|
958
967
|
}
|
|
959
968
|
});
|
|
969
|
+
// wire /duplicate — clone a session
|
|
970
|
+
input.onDuplicate((target, newTitle) => {
|
|
971
|
+
const num = /^\d+$/.test(target) ? parseInt(target, 10) : undefined;
|
|
972
|
+
const args = tui.getDuplicateArgs(num ?? target, newTitle || undefined);
|
|
973
|
+
if (!args) {
|
|
974
|
+
tui.log("system", `duplicate failed: session "${target}" not found or has no path/tool`);
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
if (executor) {
|
|
978
|
+
executor.execute([{ action: "create_agent", path: args.path, title: args.title, tool: args.tool }], [])
|
|
979
|
+
.then(() => tui.log("+ action", `duplicate: spawned "${args.title}" (${args.tool}) at ${args.path}`))
|
|
980
|
+
.catch((err) => tui.log("! action", `duplicate failed: ${err}`));
|
|
981
|
+
}
|
|
982
|
+
else {
|
|
983
|
+
tui.log("system", `[dry-run] duplicate: would spawn "${args.title}" (${args.tool}) at ${args.path}`);
|
|
984
|
+
}
|
|
985
|
+
});
|
|
986
|
+
// wire /color-all
|
|
987
|
+
input.onColorAll((colorName) => {
|
|
988
|
+
if (!colorName) {
|
|
989
|
+
const count = tui.setColorAll(null);
|
|
990
|
+
tui.log("system", `color-all: cleared accent color for ${count} sessions`);
|
|
991
|
+
persistPrefs();
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
const err = validateColorName(colorName);
|
|
995
|
+
if (err) {
|
|
996
|
+
tui.log("system", err);
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
const count = tui.setColorAll(colorName);
|
|
1000
|
+
tui.log("system", `color-all: set ${colorName} for ${count} sessions`);
|
|
1001
|
+
persistPrefs();
|
|
1002
|
+
});
|
|
1003
|
+
// wire /quiet-hours
|
|
1004
|
+
input.onQuietHours((specs) => {
|
|
1005
|
+
if (specs.length === 0) {
|
|
1006
|
+
tui.setQuietHours([]);
|
|
1007
|
+
tui.log("system", "quiet hours: cleared — alerts active at all hours");
|
|
1008
|
+
persistPrefs();
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
const ranges = [];
|
|
1012
|
+
for (const spec of specs) {
|
|
1013
|
+
const r = parseQuietHoursRange(spec);
|
|
1014
|
+
if (!r) {
|
|
1015
|
+
tui.log("system", `quiet hours: invalid range "${spec}" — use HH-HH (e.g. 22-06)`);
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
ranges.push(r);
|
|
1019
|
+
}
|
|
1020
|
+
tui.setQuietHours(ranges);
|
|
1021
|
+
tui.log("system", `quiet hours: ${specs.join(", ")} — watchdog+burn alerts suppressed`);
|
|
1022
|
+
persistPrefs();
|
|
1023
|
+
});
|
|
1024
|
+
// wire /history-stats
|
|
1025
|
+
input.onHistoryStats(() => {
|
|
1026
|
+
const all = loadTuiHistory(100_000);
|
|
1027
|
+
if (all.length === 0) {
|
|
1028
|
+
tui.log("system", "history-stats: no history entries found");
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
const stats = computeHistoryStats(all);
|
|
1032
|
+
const oldest = stats.oldestTs ? new Date(stats.oldestTs).toLocaleDateString() : "?";
|
|
1033
|
+
const newest = stats.newestTs ? new Date(stats.newestTs).toLocaleDateString() : "?";
|
|
1034
|
+
tui.log("system", `history-stats: ${stats.totalEntries} entries over ${stats.spanDays} day(s) (${oldest} → ${newest})`);
|
|
1035
|
+
const topTags = Object.entries(stats.tagCounts).slice(0, 5);
|
|
1036
|
+
for (const [tag, count] of topTags) {
|
|
1037
|
+
tui.log("system", ` ${tag}: ${count}`);
|
|
1038
|
+
}
|
|
1039
|
+
});
|
|
1040
|
+
// wire /clear-history
|
|
1041
|
+
input.onClearHistory(() => {
|
|
1042
|
+
try {
|
|
1043
|
+
writeFileSync(TUI_HISTORY_FILE, "", "utf-8");
|
|
1044
|
+
tui.log("system", "history cleared — tui-history.jsonl truncated");
|
|
1045
|
+
}
|
|
1046
|
+
catch (err) {
|
|
1047
|
+
tui.log("error", `clear-history failed: ${err}`);
|
|
1048
|
+
}
|
|
1049
|
+
});
|
|
960
1050
|
// wire /recall — search persisted history
|
|
961
1051
|
input.onRecall((keyword, maxResults) => {
|
|
962
1052
|
const matches = searchHistory(keyword, maxResults);
|
|
@@ -977,7 +1067,7 @@ async function main() {
|
|
|
977
1067
|
return;
|
|
978
1068
|
}
|
|
979
1069
|
const now = Date.now();
|
|
980
|
-
const entries = buildSessionStats(sessions, tui.getSessionErrorCounts(), tui.getAllBurnRates(now), tui.getAllFirstSeen(), tui.getAllLastChangeAt(), tui.getAllHealthScores(now), tui.getAllSessionAliases(), now);
|
|
1070
|
+
const entries = buildSessionStats(sessions, tui.getSessionErrorCounts(), tui.getAllBurnRates(now), tui.getAllFirstSeen(), tui.getAllLastChangeAt(), tui.getAllHealthScores(now), tui.getAllSessionAliases(), now, new Map(sessions.map((s) => [s.id, tui.getSessionErrorTimestamps(s.id)])), tui.getAllSessionCosts());
|
|
981
1071
|
tui.log("system", `/stats — ${entries.length} session${entries.length !== 1 ? "s" : ""}:`);
|
|
982
1072
|
for (const line of formatSessionStatsLines(entries)) {
|
|
983
1073
|
tui.log("system", line);
|
package/dist/input.d.ts
CHANGED
|
@@ -47,6 +47,11 @@ export type FindHandler = (text: string) => void;
|
|
|
47
47
|
export type ResetHealthHandler = (target: string) => void;
|
|
48
48
|
export type TimelineHandler = (target: string, count: number) => void;
|
|
49
49
|
export type ColorHandler = (target: string, colorName: string) => void;
|
|
50
|
+
export type ClearHistoryHandler = () => void;
|
|
51
|
+
export type DuplicateHandler = (target: string, newTitle: string) => void;
|
|
52
|
+
export type ColorAllHandler = (colorName: string) => void;
|
|
53
|
+
export type QuietHoursHandler = (specs: string[]) => void;
|
|
54
|
+
export type HistoryStatsHandler = () => void;
|
|
50
55
|
export interface MouseEvent {
|
|
51
56
|
button: number;
|
|
52
57
|
col: number;
|
|
@@ -116,6 +121,11 @@ export declare class InputReader {
|
|
|
116
121
|
private resetHealthHandler;
|
|
117
122
|
private timelineHandler;
|
|
118
123
|
private colorHandler;
|
|
124
|
+
private clearHistoryHandler;
|
|
125
|
+
private duplicateHandler;
|
|
126
|
+
private colorAllHandler;
|
|
127
|
+
private quietHoursHandler;
|
|
128
|
+
private historyStatsHandler;
|
|
119
129
|
private aliases;
|
|
120
130
|
private mouseDataListener;
|
|
121
131
|
onScroll(handler: (dir: ScrollDirection) => void): void;
|
|
@@ -168,6 +178,11 @@ export declare class InputReader {
|
|
|
168
178
|
onResetHealth(handler: ResetHealthHandler): void;
|
|
169
179
|
onTimeline(handler: TimelineHandler): void;
|
|
170
180
|
onColor(handler: ColorHandler): void;
|
|
181
|
+
onClearHistory(handler: ClearHistoryHandler): void;
|
|
182
|
+
onDuplicate(handler: DuplicateHandler): void;
|
|
183
|
+
onColorAll(handler: ColorAllHandler): void;
|
|
184
|
+
onQuietHours(handler: QuietHoursHandler): void;
|
|
185
|
+
onHistoryStats(handler: HistoryStatsHandler): void;
|
|
171
186
|
/** Set aliases from persisted prefs. */
|
|
172
187
|
setAliases(aliases: Record<string, string>): void;
|
|
173
188
|
/** Get current aliases as a plain object. */
|
package/dist/input.js
CHANGED
|
@@ -80,6 +80,11 @@ export class InputReader {
|
|
|
80
80
|
resetHealthHandler = null;
|
|
81
81
|
timelineHandler = null;
|
|
82
82
|
colorHandler = null;
|
|
83
|
+
clearHistoryHandler = null;
|
|
84
|
+
duplicateHandler = null;
|
|
85
|
+
colorAllHandler = null;
|
|
86
|
+
quietHoursHandler = null;
|
|
87
|
+
historyStatsHandler = null;
|
|
83
88
|
aliases = new Map(); // /shortcut → /full command
|
|
84
89
|
mouseDataListener = null;
|
|
85
90
|
// register a callback for scroll key events (PgUp/PgDn/Home/End)
|
|
@@ -282,6 +287,14 @@ export class InputReader {
|
|
|
282
287
|
onColor(handler) {
|
|
283
288
|
this.colorHandler = handler;
|
|
284
289
|
}
|
|
290
|
+
// register a callback for /clear-history
|
|
291
|
+
onClearHistory(handler) {
|
|
292
|
+
this.clearHistoryHandler = handler;
|
|
293
|
+
}
|
|
294
|
+
onDuplicate(handler) { this.duplicateHandler = handler; }
|
|
295
|
+
onColorAll(handler) { this.colorAllHandler = handler; }
|
|
296
|
+
onQuietHours(handler) { this.quietHoursHandler = handler; }
|
|
297
|
+
onHistoryStats(handler) { this.historyStatsHandler = handler; }
|
|
285
298
|
/** Set aliases from persisted prefs. */
|
|
286
299
|
setAliases(aliases) {
|
|
287
300
|
this.aliases.clear();
|
|
@@ -539,6 +552,11 @@ ${BOLD}navigation:${RESET}
|
|
|
539
552
|
/reset-health N clear error counts + context history for a session
|
|
540
553
|
/timeline N [n] show last n activity entries for session N (default 30)
|
|
541
554
|
/color N [color] set accent color for session (lime/amber/rose/teal/sky/slate; no color = clear)
|
|
555
|
+
/clear-history truncate persisted activity history (tui-history.jsonl)
|
|
556
|
+
/duplicate N [t] spawn a new session cloned from session N (same tool/path)
|
|
557
|
+
/color-all [c] set accent color for all sessions (no color = clear all)
|
|
558
|
+
/quiet-hours [H-H] suppress watchdog+burn alerts during hours (e.g. 22-06; no arg = clear)
|
|
559
|
+
/history-stats show aggregate statistics from persisted activity history
|
|
542
560
|
/clip [N] copy last N activity entries to clipboard (default 20)
|
|
543
561
|
/diff N show activity since bookmark N
|
|
544
562
|
/mark bookmark current activity position
|
|
@@ -1045,6 +1063,60 @@ ${BOLD}other:${RESET}
|
|
|
1045
1063
|
}
|
|
1046
1064
|
break;
|
|
1047
1065
|
}
|
|
1066
|
+
case "/duplicate": {
|
|
1067
|
+
const dupArg = line.slice("/duplicate".length).trim();
|
|
1068
|
+
if (!dupArg) {
|
|
1069
|
+
console.error(`${DIM}usage: /duplicate <N|name> [new-title]${RESET}`);
|
|
1070
|
+
break;
|
|
1071
|
+
}
|
|
1072
|
+
if (this.duplicateHandler) {
|
|
1073
|
+
const spaceIdx = dupArg.indexOf(" ");
|
|
1074
|
+
const target = spaceIdx > 0 ? dupArg.slice(0, spaceIdx) : dupArg;
|
|
1075
|
+
const newTitle = spaceIdx > 0 ? dupArg.slice(spaceIdx + 1).trim() : "";
|
|
1076
|
+
this.duplicateHandler(target, newTitle);
|
|
1077
|
+
}
|
|
1078
|
+
else {
|
|
1079
|
+
console.error(`${DIM}duplicate not available (no TUI)${RESET}`);
|
|
1080
|
+
}
|
|
1081
|
+
break;
|
|
1082
|
+
}
|
|
1083
|
+
case "/color-all": {
|
|
1084
|
+
const caArg = line.slice("/color-all".length).trim().toLowerCase() || "";
|
|
1085
|
+
if (this.colorAllHandler) {
|
|
1086
|
+
this.colorAllHandler(caArg);
|
|
1087
|
+
}
|
|
1088
|
+
else {
|
|
1089
|
+
console.error(`${DIM}color-all not available (no TUI)${RESET}`);
|
|
1090
|
+
}
|
|
1091
|
+
break;
|
|
1092
|
+
}
|
|
1093
|
+
case "/quiet-hours": {
|
|
1094
|
+
const qhArg = line.slice("/quiet-hours".length).trim();
|
|
1095
|
+
if (this.quietHoursHandler) {
|
|
1096
|
+
const specs = qhArg ? qhArg.split(/[\s,]+/).filter(Boolean) : [];
|
|
1097
|
+
this.quietHoursHandler(specs);
|
|
1098
|
+
}
|
|
1099
|
+
else {
|
|
1100
|
+
console.error(`${DIM}quiet-hours not available (no TUI)${RESET}`);
|
|
1101
|
+
}
|
|
1102
|
+
break;
|
|
1103
|
+
}
|
|
1104
|
+
case "/history-stats":
|
|
1105
|
+
if (this.historyStatsHandler) {
|
|
1106
|
+
this.historyStatsHandler();
|
|
1107
|
+
}
|
|
1108
|
+
else {
|
|
1109
|
+
console.error(`${DIM}history-stats not available (no TUI)${RESET}`);
|
|
1110
|
+
}
|
|
1111
|
+
break;
|
|
1112
|
+
case "/clear-history":
|
|
1113
|
+
if (this.clearHistoryHandler) {
|
|
1114
|
+
this.clearHistoryHandler();
|
|
1115
|
+
}
|
|
1116
|
+
else {
|
|
1117
|
+
console.error(`${DIM}clear-history not available (no TUI)${RESET}`);
|
|
1118
|
+
}
|
|
1119
|
+
break;
|
|
1048
1120
|
case "/reset-health": {
|
|
1049
1121
|
const rhArg = line.slice("/reset-health".length).trim();
|
|
1050
1122
|
if (!rhArg) {
|
package/dist/tui-history.d.ts
CHANGED
|
@@ -25,6 +25,20 @@ export declare function loadTuiHistory(maxEntries?: number, filePath?: string, m
|
|
|
25
25
|
export declare function rotateTuiHistory(filePath?: string, maxSize?: number): boolean;
|
|
26
26
|
/** Default history file path (for wiring in index.ts) */
|
|
27
27
|
export declare const TUI_HISTORY_FILE: string;
|
|
28
|
+
export interface HistoryStats {
|
|
29
|
+
totalEntries: number;
|
|
30
|
+
uniqueTags: string[];
|
|
31
|
+
tagCounts: Record<string, number>;
|
|
32
|
+
entriesPerDay: Record<string, number>;
|
|
33
|
+
oldestTs: number | null;
|
|
34
|
+
newestTs: number | null;
|
|
35
|
+
spanDays: number;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Compute aggregate statistics from a list of history entries.
|
|
39
|
+
* Pure function — no file I/O.
|
|
40
|
+
*/
|
|
41
|
+
export declare function computeHistoryStats(entries: readonly HistoryEntry[]): HistoryStats;
|
|
28
42
|
/**
|
|
29
43
|
* Search history entries by keyword (case-insensitive substring match on text and tag).
|
|
30
44
|
* Returns up to `maxResults` most recent matching entries (newest last).
|
package/dist/tui-history.js
CHANGED
|
@@ -88,6 +88,39 @@ function isValidEntry(val) {
|
|
|
88
88
|
}
|
|
89
89
|
/** Default history file path (for wiring in index.ts) */
|
|
90
90
|
export const TUI_HISTORY_FILE = HISTORY_FILE;
|
|
91
|
+
/**
|
|
92
|
+
* Compute aggregate statistics from a list of history entries.
|
|
93
|
+
* Pure function — no file I/O.
|
|
94
|
+
*/
|
|
95
|
+
export function computeHistoryStats(entries) {
|
|
96
|
+
if (entries.length === 0) {
|
|
97
|
+
return { totalEntries: 0, uniqueTags: [], tagCounts: {}, entriesPerDay: {}, oldestTs: null, newestTs: null, spanDays: 0 };
|
|
98
|
+
}
|
|
99
|
+
const tagCounts = {};
|
|
100
|
+
const dayMap = {};
|
|
101
|
+
let oldest = Infinity, newest = -Infinity;
|
|
102
|
+
for (const e of entries) {
|
|
103
|
+
tagCounts[e.tag] = (tagCounts[e.tag] ?? 0) + 1;
|
|
104
|
+
const day = new Date(e.ts).toISOString().slice(0, 10);
|
|
105
|
+
dayMap[day] = (dayMap[day] ?? 0) + 1;
|
|
106
|
+
if (e.ts < oldest)
|
|
107
|
+
oldest = e.ts;
|
|
108
|
+
if (e.ts > newest)
|
|
109
|
+
newest = e.ts;
|
|
110
|
+
}
|
|
111
|
+
// sort tagCounts by count desc
|
|
112
|
+
const sortedTagCounts = Object.fromEntries(Object.entries(tagCounts).sort(([, a], [, b]) => b - a));
|
|
113
|
+
const spanDays = oldest === Infinity ? 0 : Math.max(1, Math.round((newest - oldest) / 86400000));
|
|
114
|
+
return {
|
|
115
|
+
totalEntries: entries.length,
|
|
116
|
+
uniqueTags: Object.keys(sortedTagCounts),
|
|
117
|
+
tagCounts: sortedTagCounts,
|
|
118
|
+
entriesPerDay: dayMap,
|
|
119
|
+
oldestTs: oldest === Infinity ? null : oldest,
|
|
120
|
+
newestTs: newest === -Infinity ? null : newest,
|
|
121
|
+
spanDays,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
91
124
|
/**
|
|
92
125
|
* Search history entries by keyword (case-insensitive substring match on text and tag).
|
|
93
126
|
* Returns up to `maxResults` most recent matching entries (newest last).
|
package/dist/tui.d.ts
CHANGED
|
@@ -94,6 +94,16 @@ export declare function formatUptime(ms: number): string;
|
|
|
94
94
|
* Returns empty string if under threshold (< thresholdMs, default 2 min — not worth showing).
|
|
95
95
|
*/
|
|
96
96
|
export declare function formatIdleSince(ms: number, thresholdMs?: number): string;
|
|
97
|
+
/** Error trend: compare recent half of window vs older half. */
|
|
98
|
+
export type ErrorTrend = "rising" | "stable" | "falling";
|
|
99
|
+
/**
|
|
100
|
+
* Compute error trend from a list of recent error timestamps.
|
|
101
|
+
* Splits the window in half; if recent half has significantly more → rising, fewer → falling.
|
|
102
|
+
* "Significant" = difference of at least 1 error.
|
|
103
|
+
*/
|
|
104
|
+
export declare function computeErrorTrend(timestamps: readonly number[], now?: number, windowMs?: number): ErrorTrend;
|
|
105
|
+
/** Format error trend as a directional arrow. */
|
|
106
|
+
export declare function formatErrorTrend(trend: ErrorTrend): string;
|
|
97
107
|
/** Default number of entries shown by /timeline. */
|
|
98
108
|
export declare const TIMELINE_DEFAULT_COUNT = 30;
|
|
99
109
|
/**
|
|
@@ -101,6 +111,26 @@ export declare const TIMELINE_DEFAULT_COUNT = 30;
|
|
|
101
111
|
* Returns up to `count` entries.
|
|
102
112
|
*/
|
|
103
113
|
export declare function filterSessionTimeline(buffer: readonly ActivityEntry[], sessionId: string, count?: number): ActivityEntry[];
|
|
114
|
+
/**
|
|
115
|
+
* Build args for duplicating a session: given a source session, return
|
|
116
|
+
* { path, tool, title } for use in a create_agent action.
|
|
117
|
+
* Returns null if source session not found or path/tool missing.
|
|
118
|
+
*/
|
|
119
|
+
export declare function buildDuplicateArgs(sessions: readonly DaemonSessionState[], sessionIdOrIndex: string | number, newTitle?: string): {
|
|
120
|
+
path: string;
|
|
121
|
+
tool: string;
|
|
122
|
+
title: string;
|
|
123
|
+
} | null;
|
|
124
|
+
/**
|
|
125
|
+
* Check whether a given hour (0-23) falls within any quiet-hour range.
|
|
126
|
+
* Ranges are inclusive, e.g. "22-06" wraps midnight.
|
|
127
|
+
*/
|
|
128
|
+
export declare function isQuietHour(hour: number, ranges: ReadonlyArray<[number, number]>): boolean;
|
|
129
|
+
/**
|
|
130
|
+
* Parse a quiet-hours string like "22-06" or "09-17" into a [start, end] tuple.
|
|
131
|
+
* Returns null if invalid.
|
|
132
|
+
*/
|
|
133
|
+
export declare function parseQuietHoursRange(spec: string): [number, number] | null;
|
|
104
134
|
/** Supported accent color names for /color command. */
|
|
105
135
|
export declare const SESSION_COLOR_NAMES: readonly ["lime", "amber", "rose", "teal", "sky", "slate", "indigo", "cyan"];
|
|
106
136
|
export type SessionColorName = typeof SESSION_COLOR_NAMES[number];
|
|
@@ -131,15 +161,17 @@ export interface SessionStatEntry {
|
|
|
131
161
|
status: string;
|
|
132
162
|
health: number;
|
|
133
163
|
errors: number;
|
|
164
|
+
errorTrend?: ErrorTrend;
|
|
134
165
|
burnRatePerMin: number | null;
|
|
135
166
|
contextPct: number | null;
|
|
167
|
+
costStr?: string;
|
|
136
168
|
uptimeMs: number | null;
|
|
137
169
|
idleSinceMs: number | null;
|
|
138
170
|
}
|
|
139
171
|
/**
|
|
140
172
|
* Build stats entries for all sessions — pure, testable, no side effects.
|
|
141
173
|
*/
|
|
142
|
-
export declare function buildSessionStats(sessions: readonly DaemonSessionState[], errorCounts: ReadonlyMap<string, number>, burnRates: ReadonlyMap<string, number | null>, firstSeen: ReadonlyMap<string, number>, lastChangeAt: ReadonlyMap<string, number>, healthScores: ReadonlyMap<string, number>, sessionAliases: ReadonlyMap<string, string>, now?: number): SessionStatEntry[];
|
|
174
|
+
export declare function buildSessionStats(sessions: readonly DaemonSessionState[], errorCounts: ReadonlyMap<string, number>, burnRates: ReadonlyMap<string, number | null>, firstSeen: ReadonlyMap<string, number>, lastChangeAt: ReadonlyMap<string, number>, healthScores: ReadonlyMap<string, number>, sessionAliases: ReadonlyMap<string, string>, now?: number, errorTimestamps?: ReadonlyMap<string, readonly number[]>, sessionCosts?: ReadonlyMap<string, string>): SessionStatEntry[];
|
|
143
175
|
/**
|
|
144
176
|
* Format session stats entries as a multi-line activity-log-friendly string.
|
|
145
177
|
* Each line is one session summary.
|
|
@@ -246,6 +278,7 @@ export interface TuiPrefs {
|
|
|
246
278
|
sessionAliases?: Record<string, string>;
|
|
247
279
|
sessionTags?: Record<string, string[]>;
|
|
248
280
|
sessionColors?: Record<string, string>;
|
|
281
|
+
quietHours?: string[];
|
|
249
282
|
}
|
|
250
283
|
/** Load persisted TUI preferences. Returns empty object on any error. */
|
|
251
284
|
export declare function loadTuiPrefs(): TuiPrefs;
|
|
@@ -301,6 +334,8 @@ export declare class TUI {
|
|
|
301
334
|
private sessionTags;
|
|
302
335
|
private tagFilter2;
|
|
303
336
|
private sessionColors;
|
|
337
|
+
private sessionCosts;
|
|
338
|
+
private quietHoursRanges;
|
|
304
339
|
private viewMode;
|
|
305
340
|
private drilldownSessionId;
|
|
306
341
|
private sessionOutputs;
|
|
@@ -418,6 +453,24 @@ export declare class TUI {
|
|
|
418
453
|
getSessionColor(id: string): string | undefined;
|
|
419
454
|
getAllSessionColors(): ReadonlyMap<string, string>;
|
|
420
455
|
restoreSessionColors(colors: Record<string, string>): void;
|
|
456
|
+
/** Set the same accent color on all currently visible sessions (or clear all). */
|
|
457
|
+
setColorAll(colorName: string | null): number;
|
|
458
|
+
/** Set quiet-hours ranges (suppresses watchdog/burn-rate alerts). */
|
|
459
|
+
setQuietHours(ranges: Array<[number, number]>): void;
|
|
460
|
+
/** Get current quiet-hours ranges. */
|
|
461
|
+
getQuietHours(): ReadonlyArray<[number, number]>;
|
|
462
|
+
/** Check if current time is in a quiet-hours window. */
|
|
463
|
+
isCurrentlyQuiet(now?: Date): boolean;
|
|
464
|
+
/** Return the duplicate args for a session (for /duplicate wiring). */
|
|
465
|
+
getDuplicateArgs(sessionIdOrIndex: string | number, newTitle?: string): {
|
|
466
|
+
path: string;
|
|
467
|
+
tool: string;
|
|
468
|
+
title: string;
|
|
469
|
+
} | null;
|
|
470
|
+
/** Get the latest cost string for a session (or undefined). */
|
|
471
|
+
getSessionCost(id: string): string | undefined;
|
|
472
|
+
/** Return all session costs (for /stats). */
|
|
473
|
+
getAllSessionCosts(): ReadonlyMap<string, string>;
|
|
421
474
|
getSessionTimeline(sessionIdOrIndex: string | number, count?: number): ActivityEntry[] | null;
|
|
422
475
|
/** Set or clear the freeform tag filter (filters session panel). */
|
|
423
476
|
setTagFilter2(tag: string | null): void;
|
package/dist/tui.js
CHANGED
|
@@ -312,6 +312,41 @@ export function formatIdleSince(ms, thresholdMs = 2 * 60_000) {
|
|
|
312
312
|
return "";
|
|
313
313
|
return `idle ${formatUptime(ms)}`;
|
|
314
314
|
}
|
|
315
|
+
/**
|
|
316
|
+
* Compute error trend from a list of recent error timestamps.
|
|
317
|
+
* Splits the window in half; if recent half has significantly more → rising, fewer → falling.
|
|
318
|
+
* "Significant" = difference of at least 1 error.
|
|
319
|
+
*/
|
|
320
|
+
export function computeErrorTrend(timestamps, now, windowMs = 10 * 60_000) {
|
|
321
|
+
if (timestamps.length === 0)
|
|
322
|
+
return "stable";
|
|
323
|
+
const nowMs = now ?? Date.now();
|
|
324
|
+
const cutoff = nowMs - windowMs;
|
|
325
|
+
const halfMs = windowMs / 2;
|
|
326
|
+
const midpoint = nowMs - halfMs;
|
|
327
|
+
let older = 0, newer = 0;
|
|
328
|
+
for (const ts of timestamps) {
|
|
329
|
+
if (ts < cutoff)
|
|
330
|
+
continue;
|
|
331
|
+
if (ts < midpoint)
|
|
332
|
+
older++;
|
|
333
|
+
else
|
|
334
|
+
newer++;
|
|
335
|
+
}
|
|
336
|
+
if (newer > older)
|
|
337
|
+
return "rising";
|
|
338
|
+
if (older > newer)
|
|
339
|
+
return "falling";
|
|
340
|
+
return "stable";
|
|
341
|
+
}
|
|
342
|
+
/** Format error trend as a directional arrow. */
|
|
343
|
+
export function formatErrorTrend(trend) {
|
|
344
|
+
switch (trend) {
|
|
345
|
+
case "rising": return `${ROSE}↑${RESET}`;
|
|
346
|
+
case "falling": return `${LIME}↓${RESET}`;
|
|
347
|
+
default: return `${SLATE}→${RESET}`;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
315
350
|
// ── Session timeline (pure, exported for testing) ────────────────────────────
|
|
316
351
|
/** Default number of entries shown by /timeline. */
|
|
317
352
|
export const TIMELINE_DEFAULT_COUNT = 30;
|
|
@@ -323,6 +358,62 @@ export function filterSessionTimeline(buffer, sessionId, count = TIMELINE_DEFAUL
|
|
|
323
358
|
const matching = buffer.filter((e) => e.sessionId === sessionId);
|
|
324
359
|
return matching.slice(-count);
|
|
325
360
|
}
|
|
361
|
+
// ── Duplicate session helpers (pure, exported for testing) ───────────────────
|
|
362
|
+
/**
|
|
363
|
+
* Build args for duplicating a session: given a source session, return
|
|
364
|
+
* { path, tool, title } for use in a create_agent action.
|
|
365
|
+
* Returns null if source session not found or path/tool missing.
|
|
366
|
+
*/
|
|
367
|
+
export function buildDuplicateArgs(sessions, sessionIdOrIndex, newTitle) {
|
|
368
|
+
let s;
|
|
369
|
+
if (typeof sessionIdOrIndex === "number") {
|
|
370
|
+
s = sessions[sessionIdOrIndex - 1];
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
const needle = sessionIdOrIndex.toLowerCase();
|
|
374
|
+
s = sessions.find((x) => x.id === sessionIdOrIndex || x.id.startsWith(needle) || x.title.toLowerCase() === needle);
|
|
375
|
+
}
|
|
376
|
+
if (!s || !s.path || !s.tool)
|
|
377
|
+
return null;
|
|
378
|
+
return {
|
|
379
|
+
path: s.path,
|
|
380
|
+
tool: s.tool,
|
|
381
|
+
title: newTitle?.trim() || `${s.title}-copy`,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
// ── Quiet hours (pure, exported for testing) ──────────────────────────────────
|
|
385
|
+
/**
|
|
386
|
+
* Check whether a given hour (0-23) falls within any quiet-hour range.
|
|
387
|
+
* Ranges are inclusive, e.g. "22-06" wraps midnight.
|
|
388
|
+
*/
|
|
389
|
+
export function isQuietHour(hour, ranges) {
|
|
390
|
+
for (const [start, end] of ranges) {
|
|
391
|
+
if (start <= end) {
|
|
392
|
+
if (hour >= start && hour <= end)
|
|
393
|
+
return true;
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
// wraps midnight
|
|
397
|
+
if (hour >= start || hour <= end)
|
|
398
|
+
return true;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Parse a quiet-hours string like "22-06" or "09-17" into a [start, end] tuple.
|
|
405
|
+
* Returns null if invalid.
|
|
406
|
+
*/
|
|
407
|
+
export function parseQuietHoursRange(spec) {
|
|
408
|
+
const m = spec.trim().match(/^(\d{1,2})-(\d{1,2})$/);
|
|
409
|
+
if (!m)
|
|
410
|
+
return null;
|
|
411
|
+
const start = parseInt(m[1], 10);
|
|
412
|
+
const end = parseInt(m[2], 10);
|
|
413
|
+
if (start < 0 || start > 23 || end < 0 || end > 23)
|
|
414
|
+
return null;
|
|
415
|
+
return [start, end];
|
|
416
|
+
}
|
|
326
417
|
// ── Session accent colors (pure, exported for testing) ────────────────────────
|
|
327
418
|
/** Supported accent color names for /color command. */
|
|
328
419
|
export const SESSION_COLOR_NAMES = ["lime", "amber", "rose", "teal", "sky", "slate", "indigo", "cyan"];
|
|
@@ -393,21 +484,25 @@ export function formatSessionTagsBadge(tags) {
|
|
|
393
484
|
/**
|
|
394
485
|
* Build stats entries for all sessions — pure, testable, no side effects.
|
|
395
486
|
*/
|
|
396
|
-
export function buildSessionStats(sessions, errorCounts, burnRates, firstSeen, lastChangeAt, healthScores, sessionAliases, now) {
|
|
487
|
+
export function buildSessionStats(sessions, errorCounts, burnRates, firstSeen, lastChangeAt, healthScores, sessionAliases, now, errorTimestamps, sessionCosts) {
|
|
397
488
|
const nowMs = now ?? Date.now();
|
|
398
489
|
return sessions.map((s) => {
|
|
399
490
|
const ceiling = parseContextCeiling(s.contextTokens);
|
|
400
491
|
const ctxPct = ceiling ? Math.round((ceiling.current / ceiling.max) * 100) : null;
|
|
401
492
|
const fs = firstSeen.get(s.id);
|
|
402
493
|
const lc = lastChangeAt.get(s.id);
|
|
494
|
+
const errTs = errorTimestamps?.get(s.id);
|
|
495
|
+
const errTrend = errTs ? computeErrorTrend(errTs, nowMs) : undefined;
|
|
403
496
|
return {
|
|
404
497
|
title: s.title,
|
|
405
498
|
displayName: sessionAliases.get(s.id),
|
|
406
499
|
status: s.status,
|
|
407
500
|
health: healthScores.get(s.id) ?? 100,
|
|
408
501
|
errors: errorCounts.get(s.id) ?? 0,
|
|
502
|
+
errorTrend: errTrend,
|
|
409
503
|
burnRatePerMin: burnRates.get(s.id) ?? null,
|
|
410
504
|
contextPct: ctxPct,
|
|
505
|
+
costStr: sessionCosts?.get(s.id),
|
|
411
506
|
uptimeMs: fs !== undefined ? nowMs - fs : null,
|
|
412
507
|
idleSinceMs: lc !== undefined ? nowMs - lc : null,
|
|
413
508
|
};
|
|
@@ -423,13 +518,15 @@ export function formatSessionStatsLines(entries) {
|
|
|
423
518
|
return entries.map((e) => {
|
|
424
519
|
const label = e.displayName ? `${e.displayName} (${e.title})` : e.title;
|
|
425
520
|
const healthStr = `⬡${e.health}`;
|
|
426
|
-
const
|
|
521
|
+
const trendStr = e.errorTrend ? ` ${e.errorTrend === "rising" ? "↑" : e.errorTrend === "falling" ? "↓" : "→"}` : "";
|
|
522
|
+
const errStr = e.errors > 0 ? ` ${e.errors}err${trendStr}` : "";
|
|
427
523
|
const burnStr = e.burnRatePerMin !== null && e.burnRatePerMin > 0
|
|
428
524
|
? ` ${Math.round(e.burnRatePerMin / 100) * 100}tok/min` : "";
|
|
429
525
|
const ctxStr = e.contextPct !== null ? ` ctx:${e.contextPct}%` : "";
|
|
526
|
+
const costStr = e.costStr ? ` ${e.costStr}` : "";
|
|
430
527
|
const upStr = e.uptimeMs !== null ? ` up:${formatUptime(e.uptimeMs)}` : "";
|
|
431
528
|
const idleStr = e.idleSinceMs !== null ? ` ${formatIdleSince(e.idleSinceMs)}` : "";
|
|
432
|
-
return ` ${label} [${e.status}] ${healthStr}${errStr}${burnStr}${ctxStr}${upStr}${idleStr}`;
|
|
529
|
+
return ` ${label} [${e.status}] ${healthStr}${errStr}${burnStr}${ctxStr}${costStr}${upStr}${idleStr}`;
|
|
433
530
|
});
|
|
434
531
|
}
|
|
435
532
|
/** Format session stats entries as a JSON object for export. */
|
|
@@ -463,7 +560,8 @@ export const BUILTIN_COMMANDS = new Set([
|
|
|
463
560
|
"/uptime", "/auto-pin", "/note", "/notes", "/clip", "/diff", "/mark",
|
|
464
561
|
"/jump", "/marks", "/search", "/alias", "/insist", "/task", "/tasks",
|
|
465
562
|
"/group", "/groups", "/group-filter", "/burn-rate", "/snapshot", "/broadcast", "/watchdog", "/top", "/ceiling", "/rename", "/copy", "/stats", "/recall", "/pin-all-errors", "/export-stats",
|
|
466
|
-
"/mute-errors", "/prev-goal", "/tag", "/tags", "/tag-filter", "/find", "/reset-health", "/timeline", "/color",
|
|
563
|
+
"/mute-errors", "/prev-goal", "/tag", "/tags", "/tag-filter", "/find", "/reset-health", "/timeline", "/color", "/clear-history",
|
|
564
|
+
"/duplicate", "/color-all", "/quiet-hours", "/history-stats",
|
|
467
565
|
]);
|
|
468
566
|
/** Resolve a slash command through the alias map. Returns the expanded command or the original. */
|
|
469
567
|
export function resolveAlias(line, aliases) {
|
|
@@ -733,6 +831,8 @@ export class TUI {
|
|
|
733
831
|
sessionTags = new Map(); // session ID → freeform tag set
|
|
734
832
|
tagFilter2 = null; // active freeform tag filter on session panel
|
|
735
833
|
sessionColors = new Map(); // session ID → accent color name
|
|
834
|
+
sessionCosts = new Map(); // session ID → latest cost string ("$N.NN")
|
|
835
|
+
quietHoursRanges = []; // quiet-hour start/end pairs
|
|
736
836
|
// drill-down mode: show a single session's full output
|
|
737
837
|
viewMode = "overview";
|
|
738
838
|
drilldownSessionId = null;
|
|
@@ -1168,7 +1268,51 @@ export class TUI {
|
|
|
1168
1268
|
for (const [id, c] of Object.entries(colors))
|
|
1169
1269
|
this.sessionColors.set(id, c);
|
|
1170
1270
|
}
|
|
1271
|
+
/** Set the same accent color on all currently visible sessions (or clear all). */
|
|
1272
|
+
setColorAll(colorName) {
|
|
1273
|
+
let count = 0;
|
|
1274
|
+
for (const s of this.sessions) {
|
|
1275
|
+
if (!colorName) {
|
|
1276
|
+
this.sessionColors.delete(s.id);
|
|
1277
|
+
}
|
|
1278
|
+
else {
|
|
1279
|
+
this.sessionColors.set(s.id, colorName);
|
|
1280
|
+
}
|
|
1281
|
+
count++;
|
|
1282
|
+
}
|
|
1283
|
+
if (count > 0 && this.active)
|
|
1284
|
+
this.paintSessions();
|
|
1285
|
+
return count;
|
|
1286
|
+
}
|
|
1287
|
+
// ── Quiet hours ──────────────────────────────────────────────────────────
|
|
1288
|
+
/** Set quiet-hours ranges (suppresses watchdog/burn-rate alerts). */
|
|
1289
|
+
setQuietHours(ranges) {
|
|
1290
|
+
this.quietHoursRanges = ranges;
|
|
1291
|
+
}
|
|
1292
|
+
/** Get current quiet-hours ranges. */
|
|
1293
|
+
getQuietHours() {
|
|
1294
|
+
return this.quietHoursRanges;
|
|
1295
|
+
}
|
|
1296
|
+
/** Check if current time is in a quiet-hours window. */
|
|
1297
|
+
isCurrentlyQuiet(now) {
|
|
1298
|
+
if (this.quietHoursRanges.length === 0)
|
|
1299
|
+
return false;
|
|
1300
|
+
const hour = (now ?? new Date()).getHours();
|
|
1301
|
+
return isQuietHour(hour, this.quietHoursRanges);
|
|
1302
|
+
}
|
|
1303
|
+
/** Return the duplicate args for a session (for /duplicate wiring). */
|
|
1304
|
+
getDuplicateArgs(sessionIdOrIndex, newTitle) {
|
|
1305
|
+
return buildDuplicateArgs(this.sessions, sessionIdOrIndex, newTitle);
|
|
1306
|
+
}
|
|
1171
1307
|
// ── Session timeline ─────────────────────────────────────────────────────
|
|
1308
|
+
/** Get the latest cost string for a session (or undefined). */
|
|
1309
|
+
getSessionCost(id) {
|
|
1310
|
+
return this.sessionCosts.get(id);
|
|
1311
|
+
}
|
|
1312
|
+
/** Return all session costs (for /stats). */
|
|
1313
|
+
getAllSessionCosts() {
|
|
1314
|
+
return this.sessionCosts;
|
|
1315
|
+
}
|
|
1172
1316
|
getSessionTimeline(sessionIdOrIndex, count = TIMELINE_DEFAULT_COUNT) {
|
|
1173
1317
|
let sessionId;
|
|
1174
1318
|
if (typeof sessionIdOrIndex === "number") {
|
|
@@ -1475,6 +1619,7 @@ export class TUI {
|
|
|
1475
1619
|
// track activity changes for sort-by-activity + first-seen for uptime
|
|
1476
1620
|
const now = Date.now();
|
|
1477
1621
|
const BURN_ALERT_COOLDOWN_MS = 5 * 60 * 1000; // max one alert per session per 5 minutes
|
|
1622
|
+
const quietNow = this.isCurrentlyQuiet(new Date(now));
|
|
1478
1623
|
for (const s of opts.sessions) {
|
|
1479
1624
|
if (!this.sessionFirstSeen.has(s.id))
|
|
1480
1625
|
this.sessionFirstSeen.set(s.id, now);
|
|
@@ -1484,6 +1629,9 @@ export class TUI {
|
|
|
1484
1629
|
}
|
|
1485
1630
|
if (s.lastActivity !== undefined)
|
|
1486
1631
|
this.prevLastActivity.set(s.id, s.lastActivity);
|
|
1632
|
+
// track cost string
|
|
1633
|
+
if (s.costStr)
|
|
1634
|
+
this.sessionCosts.set(s.id, s.costStr);
|
|
1487
1635
|
// track context token history for burn-rate alerts
|
|
1488
1636
|
const tokens = parseContextTokenNumber(s.contextTokens);
|
|
1489
1637
|
if (tokens !== null) {
|
|
@@ -1494,7 +1642,7 @@ export class TUI {
|
|
|
1494
1642
|
this.sessionContextHistory.set(s.id, hist);
|
|
1495
1643
|
// check burn rate and emit alert if above threshold (with cooldown)
|
|
1496
1644
|
const burnRate = computeContextBurnRate(hist, now);
|
|
1497
|
-
if (burnRate !== null && burnRate > CONTEXT_BURN_THRESHOLD) {
|
|
1645
|
+
if (burnRate !== null && burnRate > CONTEXT_BURN_THRESHOLD && !quietNow) {
|
|
1498
1646
|
const lastAlert = this.burnRateAlerted.get(s.id) ?? 0;
|
|
1499
1647
|
if (now - lastAlert >= BURN_ALERT_COOLDOWN_MS) {
|
|
1500
1648
|
this.burnRateAlerted.set(s.id, now);
|
|
@@ -1504,7 +1652,7 @@ export class TUI {
|
|
|
1504
1652
|
}
|
|
1505
1653
|
// check context ceiling and emit alert if above threshold (with cooldown)
|
|
1506
1654
|
const ceiling = parseContextCeiling(s.contextTokens);
|
|
1507
|
-
if (ceiling !== null && ceiling.current / ceiling.max >= CONTEXT_CEILING_THRESHOLD) {
|
|
1655
|
+
if (ceiling !== null && ceiling.current / ceiling.max >= CONTEXT_CEILING_THRESHOLD && !quietNow) {
|
|
1508
1656
|
const lastCeiling = this.ceilingAlerted.get(s.id) ?? 0;
|
|
1509
1657
|
if (now - lastCeiling >= BURN_ALERT_COOLDOWN_MS) {
|
|
1510
1658
|
this.ceilingAlerted.set(s.id, now);
|
|
@@ -1512,8 +1660,8 @@ export class TUI {
|
|
|
1512
1660
|
}
|
|
1513
1661
|
}
|
|
1514
1662
|
}
|
|
1515
|
-
// watchdog: alert if session has been idle longer than threshold
|
|
1516
|
-
if (this.watchdogThresholdMs !== null) {
|
|
1663
|
+
// watchdog: alert if session has been idle longer than threshold (skip during quiet hours)
|
|
1664
|
+
if (this.watchdogThresholdMs !== null && !quietNow) {
|
|
1517
1665
|
for (const s of opts.sessions) {
|
|
1518
1666
|
const lastChange = this.lastChangeAt.get(s.id);
|
|
1519
1667
|
if (lastChange === undefined)
|
package/dist/types.d.ts
CHANGED