aoaoe 0.114.0 → 0.122.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 +96 -2
- package/dist/input.d.ts +15 -0
- package/dist/input.js +81 -0
- package/dist/tui-history.d.ts +6 -0
- package/dist/tui-history.js +47 -0
- package/dist/tui.d.ts +60 -2
- package/dist/tui.js +202 -8
- package/package.json +1 -1
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 } 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 } 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 } from "./tui-history.js";
|
|
23
|
+
import { loadTuiHistory, searchHistory } 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";
|
|
@@ -757,6 +757,100 @@ async function main() {
|
|
|
757
757
|
if (!any)
|
|
758
758
|
tui.log("system", ` threshold: ${CONTEXT_BURN_THRESHOLD.toLocaleString()} tokens/min`);
|
|
759
759
|
});
|
|
760
|
+
// wire /pin-all-errors
|
|
761
|
+
input.onPinAllErrors(() => {
|
|
762
|
+
const count = tui.pinAllErrors();
|
|
763
|
+
if (count === 0) {
|
|
764
|
+
tui.log("system", "pin-all-errors: no error sessions to pin");
|
|
765
|
+
}
|
|
766
|
+
else {
|
|
767
|
+
tui.log("system", `pin-all-errors: pinned ${count} session${count !== 1 ? "s" : ""}`);
|
|
768
|
+
persistPrefs();
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
// wire /export-stats
|
|
772
|
+
input.onExportStats(() => {
|
|
773
|
+
const sessions = tui.getSessions();
|
|
774
|
+
const now = Date.now();
|
|
775
|
+
const entries = buildSessionStats(sessions, tui.getSessionErrorCounts(), tui.getAllBurnRates(now), tui.getAllFirstSeen(), tui.getAllLastChangeAt(), tui.getAllHealthScores(now), tui.getAllSessionAliases(), now);
|
|
776
|
+
const ts = new Date(now).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
777
|
+
const dir = join(homedir(), ".aoaoe");
|
|
778
|
+
const path = join(dir, `stats-${ts}.json`);
|
|
779
|
+
try {
|
|
780
|
+
mkdirSync(dir, { recursive: true });
|
|
781
|
+
writeFileSync(path, formatStatsJson(entries, pkg ?? "dev", now), "utf-8");
|
|
782
|
+
tui.log("system", `stats exported: ~/.aoaoe/stats-${ts}.json (${entries.length} sessions)`);
|
|
783
|
+
}
|
|
784
|
+
catch (err) {
|
|
785
|
+
tui.log("error", `export-stats failed: ${err}`);
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
// wire /recall — search persisted history
|
|
789
|
+
input.onRecall((keyword, maxResults) => {
|
|
790
|
+
const matches = searchHistory(keyword, maxResults);
|
|
791
|
+
if (matches.length === 0) {
|
|
792
|
+
tui.log("system", `recall: no matches for "${keyword}" in history`);
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
tui.log("system", `recall: ${matches.length} match${matches.length !== 1 ? "es" : ""} for "${keyword}":`);
|
|
796
|
+
for (const e of matches) {
|
|
797
|
+
tui.log("system", ` ${e.time} ${e.tag} ${e.text}`);
|
|
798
|
+
}
|
|
799
|
+
});
|
|
800
|
+
// wire /stats per-session summary
|
|
801
|
+
input.onStats(() => {
|
|
802
|
+
const sessions = tui.getSessions();
|
|
803
|
+
if (sessions.length === 0) {
|
|
804
|
+
tui.log("system", "no sessions — no stats available");
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
const now = Date.now();
|
|
808
|
+
const entries = buildSessionStats(sessions, tui.getSessionErrorCounts(), tui.getAllBurnRates(now), tui.getAllFirstSeen(), tui.getAllLastChangeAt(), tui.getAllHealthScores(now), tui.getAllSessionAliases(), now);
|
|
809
|
+
tui.log("system", `/stats — ${entries.length} session${entries.length !== 1 ? "s" : ""}:`);
|
|
810
|
+
for (const line of formatSessionStatsLines(entries)) {
|
|
811
|
+
tui.log("system", line);
|
|
812
|
+
}
|
|
813
|
+
});
|
|
814
|
+
// wire /copy session pane output to clipboard
|
|
815
|
+
input.onCopySession((target) => {
|
|
816
|
+
// resolve target: null = current drill-down session
|
|
817
|
+
let lines = null;
|
|
818
|
+
let label = "current session";
|
|
819
|
+
if (target === null) {
|
|
820
|
+
const ddId = tui.getDrilldownId();
|
|
821
|
+
if (!ddId) {
|
|
822
|
+
tui.log("system", "no session in view — use /copy N or drill into a session first");
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
lines = tui.getSessionOutput(ddId);
|
|
826
|
+
const s = tui.getSessions().find((s) => s.id === ddId);
|
|
827
|
+
label = s?.title ?? ddId.slice(0, 8);
|
|
828
|
+
}
|
|
829
|
+
else {
|
|
830
|
+
const num = /^\d+$/.test(target) ? parseInt(target, 10) : undefined;
|
|
831
|
+
lines = tui.getSessionOutput(num ?? target);
|
|
832
|
+
label = target;
|
|
833
|
+
}
|
|
834
|
+
if (!lines || lines.length === 0) {
|
|
835
|
+
tui.log("system", `no output stored for ${label} — session may not have been polled yet`);
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
const text = lines.join("\n") + "\n";
|
|
839
|
+
try {
|
|
840
|
+
execSync("pbcopy", { input: text, timeout: 5000 });
|
|
841
|
+
tui.log("system", `copied ${lines.length} lines from ${label} to clipboard`);
|
|
842
|
+
}
|
|
843
|
+
catch {
|
|
844
|
+
try {
|
|
845
|
+
const copyPath = join(homedir(), ".aoaoe", "copy.txt");
|
|
846
|
+
writeFileSync(copyPath, text, "utf-8");
|
|
847
|
+
tui.log("system", `saved ${lines.length} lines from ${label} to ~/.aoaoe/copy.txt`);
|
|
848
|
+
}
|
|
849
|
+
catch (writeErr) {
|
|
850
|
+
tui.log("error", `copy failed: ${writeErr}`);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
});
|
|
760
854
|
// wire /rename custom display name
|
|
761
855
|
input.onRename((target, displayName) => {
|
|
762
856
|
const num = /^\d+$/.test(target) ? parseInt(target, 10) : undefined;
|
package/dist/input.d.ts
CHANGED
|
@@ -33,6 +33,11 @@ export type WatchdogHandler = (thresholdMinutes: number | null) => void;
|
|
|
33
33
|
export type TopHandler = (mode: string) => void;
|
|
34
34
|
export type CeilingHandler = () => void;
|
|
35
35
|
export type RenameHandler = (target: string, name: string) => void;
|
|
36
|
+
export type CopySessionHandler = (target: string | null) => void;
|
|
37
|
+
export type StatsHandler = () => void;
|
|
38
|
+
export type RecallHandler = (keyword: string, maxResults: number) => void;
|
|
39
|
+
export type PinAllErrorsHandler = () => void;
|
|
40
|
+
export type ExportStatsHandler = () => void;
|
|
36
41
|
export interface MouseEvent {
|
|
37
42
|
button: number;
|
|
38
43
|
col: number;
|
|
@@ -88,6 +93,11 @@ export declare class InputReader {
|
|
|
88
93
|
private topHandler;
|
|
89
94
|
private ceilingHandler;
|
|
90
95
|
private renameHandler;
|
|
96
|
+
private copySessionHandler;
|
|
97
|
+
private statsHandler;
|
|
98
|
+
private recallHandler;
|
|
99
|
+
private pinAllErrorsHandler;
|
|
100
|
+
private exportStatsHandler;
|
|
91
101
|
private aliases;
|
|
92
102
|
private mouseDataListener;
|
|
93
103
|
onScroll(handler: (dir: ScrollDirection) => void): void;
|
|
@@ -126,6 +136,11 @@ export declare class InputReader {
|
|
|
126
136
|
onTop(handler: TopHandler): void;
|
|
127
137
|
onCeiling(handler: CeilingHandler): void;
|
|
128
138
|
onRename(handler: RenameHandler): void;
|
|
139
|
+
onCopySession(handler: CopySessionHandler): void;
|
|
140
|
+
onStats(handler: StatsHandler): void;
|
|
141
|
+
onRecall(handler: RecallHandler): void;
|
|
142
|
+
onPinAllErrors(handler: PinAllErrorsHandler): void;
|
|
143
|
+
onExportStats(handler: ExportStatsHandler): void;
|
|
129
144
|
/** Set aliases from persisted prefs. */
|
|
130
145
|
setAliases(aliases: Record<string, string>): void;
|
|
131
146
|
/** Get current aliases as a plain object. */
|
package/dist/input.js
CHANGED
|
@@ -66,6 +66,11 @@ export class InputReader {
|
|
|
66
66
|
topHandler = null;
|
|
67
67
|
ceilingHandler = null;
|
|
68
68
|
renameHandler = null;
|
|
69
|
+
copySessionHandler = null;
|
|
70
|
+
statsHandler = null;
|
|
71
|
+
recallHandler = null;
|
|
72
|
+
pinAllErrorsHandler = null;
|
|
73
|
+
exportStatsHandler = null;
|
|
69
74
|
aliases = new Map(); // /shortcut → /full command
|
|
70
75
|
mouseDataListener = null;
|
|
71
76
|
// register a callback for scroll key events (PgUp/PgDn/Home/End)
|
|
@@ -212,6 +217,26 @@ export class InputReader {
|
|
|
212
217
|
onRename(handler) {
|
|
213
218
|
this.renameHandler = handler;
|
|
214
219
|
}
|
|
220
|
+
// register a callback for /copy [N|name] — copy session pane output
|
|
221
|
+
onCopySession(handler) {
|
|
222
|
+
this.copySessionHandler = handler;
|
|
223
|
+
}
|
|
224
|
+
// register a callback for /stats — per-session stats summary
|
|
225
|
+
onStats(handler) {
|
|
226
|
+
this.statsHandler = handler;
|
|
227
|
+
}
|
|
228
|
+
// register a callback for /recall <keyword> [N] — search history
|
|
229
|
+
onRecall(handler) {
|
|
230
|
+
this.recallHandler = handler;
|
|
231
|
+
}
|
|
232
|
+
// register a callback for /pin-all-errors — pin all error sessions
|
|
233
|
+
onPinAllErrors(handler) {
|
|
234
|
+
this.pinAllErrorsHandler = handler;
|
|
235
|
+
}
|
|
236
|
+
// register a callback for /export-stats — export stats to JSON file
|
|
237
|
+
onExportStats(handler) {
|
|
238
|
+
this.exportStatsHandler = handler;
|
|
239
|
+
}
|
|
215
240
|
/** Set aliases from persisted prefs. */
|
|
216
241
|
setAliases(aliases) {
|
|
217
242
|
this.aliases.clear();
|
|
@@ -448,6 +473,11 @@ ${BOLD}navigation:${RESET}
|
|
|
448
473
|
/top [mode] rank sessions by errors (default), burn, or idle
|
|
449
474
|
/ceiling show context token usage vs limit for all sessions
|
|
450
475
|
/rename N|name [display] set custom display name in TUI (no display = clear)
|
|
476
|
+
/copy [N|name] copy session's current pane output to clipboard (default: current drill-down)
|
|
477
|
+
/stats show per-session health, errors, burn rate, context %, uptime
|
|
478
|
+
/recall <keyword> search persisted activity history (last 7 days) for keyword
|
|
479
|
+
/pin-all-errors pin every session currently in error status
|
|
480
|
+
/export-stats export /stats output as JSON to ~/.aoaoe/stats-<ts>.json
|
|
451
481
|
/clip [N] copy last N activity entries to clipboard (default 20)
|
|
452
482
|
/diff N show activity since bookmark N
|
|
453
483
|
/mark bookmark current activity position
|
|
@@ -837,6 +867,57 @@ ${BOLD}other:${RESET}
|
|
|
837
867
|
}
|
|
838
868
|
break;
|
|
839
869
|
}
|
|
870
|
+
case "/pin-all-errors":
|
|
871
|
+
if (this.pinAllErrorsHandler) {
|
|
872
|
+
this.pinAllErrorsHandler();
|
|
873
|
+
}
|
|
874
|
+
else {
|
|
875
|
+
console.error(`${DIM}pin-all-errors not available (no TUI)${RESET}`);
|
|
876
|
+
}
|
|
877
|
+
break;
|
|
878
|
+
case "/export-stats":
|
|
879
|
+
if (this.exportStatsHandler) {
|
|
880
|
+
this.exportStatsHandler();
|
|
881
|
+
}
|
|
882
|
+
else {
|
|
883
|
+
console.error(`${DIM}export-stats not available (no TUI)${RESET}`);
|
|
884
|
+
}
|
|
885
|
+
break;
|
|
886
|
+
case "/recall": {
|
|
887
|
+
const recallArgs = line.slice("/recall".length).trim().split(/\s+/);
|
|
888
|
+
const keyword = recallArgs[0] ?? "";
|
|
889
|
+
if (!keyword) {
|
|
890
|
+
console.error(`${DIM}usage: /recall <keyword> [N] — search activity history (default: last 50 matches)${RESET}`);
|
|
891
|
+
break;
|
|
892
|
+
}
|
|
893
|
+
const maxN = recallArgs[1] ? parseInt(recallArgs[1], 10) : 50;
|
|
894
|
+
const limit = isNaN(maxN) || maxN < 1 ? 50 : Math.min(maxN, 500);
|
|
895
|
+
if (this.recallHandler) {
|
|
896
|
+
this.recallHandler(keyword, limit);
|
|
897
|
+
}
|
|
898
|
+
else {
|
|
899
|
+
console.error(`${DIM}recall not available (no TUI)${RESET}`);
|
|
900
|
+
}
|
|
901
|
+
break;
|
|
902
|
+
}
|
|
903
|
+
case "/stats":
|
|
904
|
+
if (this.statsHandler) {
|
|
905
|
+
this.statsHandler();
|
|
906
|
+
}
|
|
907
|
+
else {
|
|
908
|
+
console.error(`${DIM}stats not available (no TUI)${RESET}`);
|
|
909
|
+
}
|
|
910
|
+
break;
|
|
911
|
+
case "/copy": {
|
|
912
|
+
const copyArg = line.slice("/copy".length).trim() || null;
|
|
913
|
+
if (this.copySessionHandler) {
|
|
914
|
+
this.copySessionHandler(copyArg);
|
|
915
|
+
}
|
|
916
|
+
else {
|
|
917
|
+
console.error(`${DIM}copy not available (no TUI)${RESET}`);
|
|
918
|
+
}
|
|
919
|
+
break;
|
|
920
|
+
}
|
|
840
921
|
case "/rename": {
|
|
841
922
|
const renameArg = line.slice("/rename".length).trim();
|
|
842
923
|
if (!renameArg) {
|
package/dist/tui-history.d.ts
CHANGED
|
@@ -25,4 +25,10 @@ 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
|
+
/**
|
|
29
|
+
* Search history entries by keyword (case-insensitive substring match on text and tag).
|
|
30
|
+
* Returns up to `maxResults` most recent matching entries (newest last).
|
|
31
|
+
* Searches both the current and .old history file.
|
|
32
|
+
*/
|
|
33
|
+
export declare function searchHistory(keyword: string, maxResults?: number, filePath?: string, maxAgeMs?: number): HistoryEntry[];
|
|
28
34
|
//# sourceMappingURL=tui-history.d.ts.map
|
package/dist/tui-history.js
CHANGED
|
@@ -88,4 +88,51 @@ 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
|
+
* Search history entries by keyword (case-insensitive substring match on text and tag).
|
|
93
|
+
* Returns up to `maxResults` most recent matching entries (newest last).
|
|
94
|
+
* Searches both the current and .old history file.
|
|
95
|
+
*/
|
|
96
|
+
export function searchHistory(keyword, maxResults = 50, filePath = HISTORY_FILE, maxAgeMs = 7 * 24 * 60 * 60 * 1000) {
|
|
97
|
+
const lower = keyword.toLowerCase();
|
|
98
|
+
const results = [];
|
|
99
|
+
// helper: collect matches from one file
|
|
100
|
+
const collectFrom = (fp) => {
|
|
101
|
+
try {
|
|
102
|
+
if (!existsSync(fp))
|
|
103
|
+
return;
|
|
104
|
+
const content = readFileSync(fp, "utf-8");
|
|
105
|
+
const cutoff = Date.now() - maxAgeMs;
|
|
106
|
+
for (const line of content.split("\n")) {
|
|
107
|
+
if (!line.trim())
|
|
108
|
+
continue;
|
|
109
|
+
try {
|
|
110
|
+
const e = JSON.parse(line);
|
|
111
|
+
if (!isValidEntry(e) || e.ts < cutoff)
|
|
112
|
+
continue;
|
|
113
|
+
if (e.text.toLowerCase().includes(lower) || e.tag.toLowerCase().includes(lower)) {
|
|
114
|
+
results.push(e);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch { /* skip malformed */ }
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch { /* ignore unreadable file */ }
|
|
121
|
+
};
|
|
122
|
+
// search .old first (older), then current (newer)
|
|
123
|
+
collectFrom(filePath + ".old");
|
|
124
|
+
collectFrom(filePath);
|
|
125
|
+
// deduplicate by ts+text, sort newest last, cap to maxResults
|
|
126
|
+
const seen = new Set();
|
|
127
|
+
const deduped = [];
|
|
128
|
+
for (const e of results) {
|
|
129
|
+
const key = `${e.ts}:${e.text}`;
|
|
130
|
+
if (!seen.has(key)) {
|
|
131
|
+
seen.add(key);
|
|
132
|
+
deduped.push(e);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
deduped.sort((a, b) => a.ts - b.ts);
|
|
136
|
+
return deduped.slice(-maxResults);
|
|
137
|
+
}
|
|
91
138
|
//# sourceMappingURL=tui-history.js.map
|
package/dist/tui.d.ts
CHANGED
|
@@ -28,12 +28,32 @@ declare const COMPACT_NAME_LEN = 10;
|
|
|
28
28
|
declare const PIN_ICON = "\u25B2";
|
|
29
29
|
/**
|
|
30
30
|
* Format sessions as inline compact tokens, wrapped to fit maxWidth.
|
|
31
|
-
* Each token: "{idx}{pin?}{mute?}{dot}{name}" — e.g. "1▲●Alpha" for pinned, "2◌●Bravo" for muted.
|
|
31
|
+
* Each token: "{idx}{pin?}{mute?}{dot}{name}{health?}" — e.g. "1▲●Alpha" for pinned, "2◌●Bravo" for muted.
|
|
32
32
|
* Returns array of formatted row strings (one per display row).
|
|
33
33
|
*/
|
|
34
|
-
declare function formatCompactRows(sessions: DaemonSessionState[], maxWidth: number, pinnedIds?: Set<string>, mutedIds?: Set<string>, noteIds?: Set<string>): string[];
|
|
34
|
+
declare function formatCompactRows(sessions: DaemonSessionState[], maxWidth: number, pinnedIds?: Set<string>, mutedIds?: Set<string>, noteIds?: Set<string>, healthScores?: Map<string, number>, activityRates?: Map<string, number>): string[];
|
|
35
35
|
/** Compute how many display rows compact mode needs (minimum 1). */
|
|
36
36
|
declare function computeCompactRowCount(sessions: DaemonSessionState[], maxWidth: number): number;
|
|
37
|
+
/** Window for computing per-session activity rate (5 minutes). */
|
|
38
|
+
export declare const ACTIVITY_RATE_WINDOW_MS: number;
|
|
39
|
+
/**
|
|
40
|
+
* Compute messages-per-minute for a session from the activity buffer.
|
|
41
|
+
* Only counts entries within the last ACTIVITY_RATE_WINDOW_MS.
|
|
42
|
+
* Returns 0 when no activity in window.
|
|
43
|
+
*/
|
|
44
|
+
export declare function computeSessionActivityRate(buffer: readonly {
|
|
45
|
+
sessionId?: string;
|
|
46
|
+
}[], timestamps: readonly number[], sessionId: string, now?: number, windowMs?: number): number;
|
|
47
|
+
/**
|
|
48
|
+
* Format activity rate as a compact badge for compact mode tokens.
|
|
49
|
+
* Returns empty string when rate is 0 (no clutter for quiet sessions).
|
|
50
|
+
* Format: "3/m" (rounded to nearest integer messages/min).
|
|
51
|
+
*/
|
|
52
|
+
export declare function formatActivityRateBadge(rate: number): string;
|
|
53
|
+
/** Format watchdog status tag for the header (empty string when disabled). */
|
|
54
|
+
export declare function formatWatchdogTag(thresholdMs: number | null): string;
|
|
55
|
+
/** Format group filter tag for the header (empty string when no filter). */
|
|
56
|
+
export declare function formatGroupFilterTag(groupFilter: string | null): string;
|
|
37
57
|
declare function phaseDisplay(phase: DaemonPhase, paused: boolean, spinnerFrame: number): string;
|
|
38
58
|
export interface ActivityEntry {
|
|
39
59
|
time: string;
|
|
@@ -74,6 +94,28 @@ export declare function formatUptime(ms: number): string;
|
|
|
74
94
|
* Returns empty string if under threshold (< thresholdMs, default 2 min — not worth showing).
|
|
75
95
|
*/
|
|
76
96
|
export declare function formatIdleSince(ms: number, thresholdMs?: number): string;
|
|
97
|
+
export interface SessionStatEntry {
|
|
98
|
+
title: string;
|
|
99
|
+
displayName?: string;
|
|
100
|
+
status: string;
|
|
101
|
+
health: number;
|
|
102
|
+
errors: number;
|
|
103
|
+
burnRatePerMin: number | null;
|
|
104
|
+
contextPct: number | null;
|
|
105
|
+
uptimeMs: number | null;
|
|
106
|
+
idleSinceMs: number | null;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Build stats entries for all sessions — pure, testable, no side effects.
|
|
110
|
+
*/
|
|
111
|
+
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[];
|
|
112
|
+
/**
|
|
113
|
+
* Format session stats entries as a multi-line activity-log-friendly string.
|
|
114
|
+
* Each line is one session summary.
|
|
115
|
+
*/
|
|
116
|
+
export declare function formatSessionStatsLines(entries: SessionStatEntry[]): string[];
|
|
117
|
+
/** Format session stats entries as a JSON object for export. */
|
|
118
|
+
export declare function formatStatsJson(entries: SessionStatEntry[], version: string, now?: number): string;
|
|
77
119
|
/** Max visible length for a custom session display name. */
|
|
78
120
|
export declare const MAX_RENAME_LEN = 32;
|
|
79
121
|
/** Truncate a custom display name to the max length. */
|
|
@@ -251,6 +293,11 @@ export declare class TUI {
|
|
|
251
293
|
* Pinned sessions always sort to the top. Returns true if session found.
|
|
252
294
|
*/
|
|
253
295
|
togglePin(sessionIdOrIndex: string | number): boolean;
|
|
296
|
+
/**
|
|
297
|
+
* Pin all sessions currently in "error" status (or with any cumulative errors).
|
|
298
|
+
* Returns the count of newly pinned sessions.
|
|
299
|
+
*/
|
|
300
|
+
pinAllErrors(): number;
|
|
254
301
|
/** Check if a session ID is pinned. */
|
|
255
302
|
isPinned(id: string): boolean;
|
|
256
303
|
/** Return count of pinned sessions. */
|
|
@@ -304,6 +351,15 @@ export declare class TUI {
|
|
|
304
351
|
getAllFirstSeen(): ReadonlyMap<string, number>;
|
|
305
352
|
/** Return the activity buffer (for /clip export). */
|
|
306
353
|
getActivityBuffer(): readonly ActivityEntry[];
|
|
354
|
+
/** Return the activity timestamps (epoch ms per entry, parallel to activityBuffer). */
|
|
355
|
+
getActivityTimestamps(): readonly number[];
|
|
356
|
+
/**
|
|
357
|
+
* Return the stored pane output lines for a session (by 1-indexed number, ID, prefix, or title).
|
|
358
|
+
* Returns null if session not found or no output stored.
|
|
359
|
+
*/
|
|
360
|
+
getSessionOutput(sessionIdOrIndex: string | number): string[] | null;
|
|
361
|
+
/** Return the current drill-down session ID (for /copy default target). */
|
|
362
|
+
getDrilldownId(): string | null;
|
|
307
363
|
/** Return per-session error counts (for /who). */
|
|
308
364
|
getSessionErrorCounts(): ReadonlyMap<string, number>;
|
|
309
365
|
/** Return recent error timestamps for a session (for sparkline rendering). */
|
|
@@ -315,6 +371,8 @@ export declare class TUI {
|
|
|
315
371
|
current: number;
|
|
316
372
|
max: number;
|
|
317
373
|
} | null>;
|
|
374
|
+
/** Compute health scores for all sessions and return as a map (id → score). */
|
|
375
|
+
getAllHealthScores(now?: number): Map<string, number>;
|
|
318
376
|
/** Return all sessions with their current burn rates (tokens/min, null if insufficient data). */
|
|
319
377
|
getAllBurnRates(now?: number): Map<string, number | null>;
|
|
320
378
|
/**
|
package/dist/tui.js
CHANGED
|
@@ -100,10 +100,10 @@ const COMPACT_NAME_LEN = 10;
|
|
|
100
100
|
const PIN_ICON = "▲";
|
|
101
101
|
/**
|
|
102
102
|
* Format sessions as inline compact tokens, wrapped to fit maxWidth.
|
|
103
|
-
* Each token: "{idx}{pin?}{mute?}{dot}{name}" — e.g. "1▲●Alpha" for pinned, "2◌●Bravo" for muted.
|
|
103
|
+
* Each token: "{idx}{pin?}{mute?}{dot}{name}{health?}" — e.g. "1▲●Alpha" for pinned, "2◌●Bravo" for muted.
|
|
104
104
|
* Returns array of formatted row strings (one per display row).
|
|
105
105
|
*/
|
|
106
|
-
function formatCompactRows(sessions, maxWidth, pinnedIds, mutedIds, noteIds) {
|
|
106
|
+
function formatCompactRows(sessions, maxWidth, pinnedIds, mutedIds, noteIds, healthScores, activityRates) {
|
|
107
107
|
if (sessions.length === 0)
|
|
108
108
|
return [`${DIM}no agents connected${RESET}`];
|
|
109
109
|
const tokens = [];
|
|
@@ -119,8 +119,17 @@ function formatCompactRows(sessions, maxWidth, pinnedIds, mutedIds, noteIds) {
|
|
|
119
119
|
const muteIcon = muted ? `${DIM}${MUTE_ICON}${RESET}` : "";
|
|
120
120
|
const noteIcon = noted ? `${TEAL}${NOTE_ICON}${RESET}` : "";
|
|
121
121
|
const name = truncatePlain(s.title, COMPACT_NAME_LEN);
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
// health indicator: single ⬡ glyph when score < HEALTH_GOOD, colored by severity
|
|
123
|
+
const score = healthScores?.get(s.id);
|
|
124
|
+
const healthGlyph = (score !== undefined && score < HEALTH_GOOD)
|
|
125
|
+
? `${score < HEALTH_WARN ? ROSE : AMBER}${HEALTH_ICON}${RESET}` : "";
|
|
126
|
+
const healthWidth = (score !== undefined && score < HEALTH_GOOD) ? 1 : 0;
|
|
127
|
+
// activity rate badge: "3/m" when rate > 0
|
|
128
|
+
const rate = activityRates?.get(s.id) ?? 0;
|
|
129
|
+
const rateBadge = formatActivityRateBadge(rate);
|
|
130
|
+
const rateVisible = rateBadge ? stripAnsiForLen(rateBadge) : 0;
|
|
131
|
+
tokens.push(`${SLATE}${idx}${RESET}${pin}${muteIcon}${noteIcon}${dot}${BOLD}${name}${RESET}${healthGlyph}${rateBadge}`);
|
|
132
|
+
widths.push(idx.length + (pinned ? 1 : 0) + (muted ? 1 : 0) + (noted ? 1 : 0) + 1 + name.length + healthWidth + rateVisible);
|
|
124
133
|
}
|
|
125
134
|
const rows = [];
|
|
126
135
|
let currentRow = "";
|
|
@@ -145,6 +154,49 @@ function formatCompactRows(sessions, maxWidth, pinnedIds, mutedIds, noteIds) {
|
|
|
145
154
|
function computeCompactRowCount(sessions, maxWidth) {
|
|
146
155
|
return Math.max(1, formatCompactRows(sessions, maxWidth).length);
|
|
147
156
|
}
|
|
157
|
+
// ── Activity rate helpers (pure, exported for testing) ───────────────────────
|
|
158
|
+
/** Window for computing per-session activity rate (5 minutes). */
|
|
159
|
+
export const ACTIVITY_RATE_WINDOW_MS = 5 * 60_000;
|
|
160
|
+
/**
|
|
161
|
+
* Compute messages-per-minute for a session from the activity buffer.
|
|
162
|
+
* Only counts entries within the last ACTIVITY_RATE_WINDOW_MS.
|
|
163
|
+
* Returns 0 when no activity in window.
|
|
164
|
+
*/
|
|
165
|
+
export function computeSessionActivityRate(buffer, timestamps, sessionId, now, windowMs = ACTIVITY_RATE_WINDOW_MS) {
|
|
166
|
+
const nowMs = now ?? Date.now();
|
|
167
|
+
const cutoff = nowMs - windowMs;
|
|
168
|
+
let count = 0;
|
|
169
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
170
|
+
if (buffer[i].sessionId === sessionId && (timestamps[i] ?? 0) >= cutoff)
|
|
171
|
+
count++;
|
|
172
|
+
}
|
|
173
|
+
return count === 0 ? 0 : (count / windowMs) * 60_000;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Format activity rate as a compact badge for compact mode tokens.
|
|
177
|
+
* Returns empty string when rate is 0 (no clutter for quiet sessions).
|
|
178
|
+
* Format: "3/m" (rounded to nearest integer messages/min).
|
|
179
|
+
*/
|
|
180
|
+
export function formatActivityRateBadge(rate) {
|
|
181
|
+
if (rate <= 0)
|
|
182
|
+
return "";
|
|
183
|
+
const rounded = Math.max(1, Math.round(rate));
|
|
184
|
+
return `${DIM}${rounded}/m${RESET}`;
|
|
185
|
+
}
|
|
186
|
+
// ── Header status tag helpers (pure, exported for testing) ──────────────────
|
|
187
|
+
/** Format watchdog status tag for the header (empty string when disabled). */
|
|
188
|
+
export function formatWatchdogTag(thresholdMs) {
|
|
189
|
+
if (thresholdMs === null)
|
|
190
|
+
return "";
|
|
191
|
+
const mins = Math.round(thresholdMs / 60_000);
|
|
192
|
+
return `⊛${mins}m`;
|
|
193
|
+
}
|
|
194
|
+
/** Format group filter tag for the header (empty string when no filter). */
|
|
195
|
+
export function formatGroupFilterTag(groupFilter) {
|
|
196
|
+
if (!groupFilter)
|
|
197
|
+
return "";
|
|
198
|
+
return `${GROUP_ICON}${groupFilter}`;
|
|
199
|
+
}
|
|
148
200
|
// ── Status rendering ────────────────────────────────────────────────────────
|
|
149
201
|
const STATUS_DOT = {
|
|
150
202
|
working: `${LIME}${DOT.filled}${RESET}`,
|
|
@@ -259,6 +311,56 @@ export function formatIdleSince(ms, thresholdMs = 2 * 60_000) {
|
|
|
259
311
|
return "";
|
|
260
312
|
return `idle ${formatUptime(ms)}`;
|
|
261
313
|
}
|
|
314
|
+
/**
|
|
315
|
+
* Build stats entries for all sessions — pure, testable, no side effects.
|
|
316
|
+
*/
|
|
317
|
+
export function buildSessionStats(sessions, errorCounts, burnRates, firstSeen, lastChangeAt, healthScores, sessionAliases, now) {
|
|
318
|
+
const nowMs = now ?? Date.now();
|
|
319
|
+
return sessions.map((s) => {
|
|
320
|
+
const ceiling = parseContextCeiling(s.contextTokens);
|
|
321
|
+
const ctxPct = ceiling ? Math.round((ceiling.current / ceiling.max) * 100) : null;
|
|
322
|
+
const fs = firstSeen.get(s.id);
|
|
323
|
+
const lc = lastChangeAt.get(s.id);
|
|
324
|
+
return {
|
|
325
|
+
title: s.title,
|
|
326
|
+
displayName: sessionAliases.get(s.id),
|
|
327
|
+
status: s.status,
|
|
328
|
+
health: healthScores.get(s.id) ?? 100,
|
|
329
|
+
errors: errorCounts.get(s.id) ?? 0,
|
|
330
|
+
burnRatePerMin: burnRates.get(s.id) ?? null,
|
|
331
|
+
contextPct: ctxPct,
|
|
332
|
+
uptimeMs: fs !== undefined ? nowMs - fs : null,
|
|
333
|
+
idleSinceMs: lc !== undefined ? nowMs - lc : null,
|
|
334
|
+
};
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Format session stats entries as a multi-line activity-log-friendly string.
|
|
339
|
+
* Each line is one session summary.
|
|
340
|
+
*/
|
|
341
|
+
export function formatSessionStatsLines(entries) {
|
|
342
|
+
if (entries.length === 0)
|
|
343
|
+
return [" no sessions"];
|
|
344
|
+
return entries.map((e) => {
|
|
345
|
+
const label = e.displayName ? `${e.displayName} (${e.title})` : e.title;
|
|
346
|
+
const healthStr = `⬡${e.health}`;
|
|
347
|
+
const errStr = e.errors > 0 ? ` ${e.errors}err` : "";
|
|
348
|
+
const burnStr = e.burnRatePerMin !== null && e.burnRatePerMin > 0
|
|
349
|
+
? ` ${Math.round(e.burnRatePerMin / 100) * 100}tok/min` : "";
|
|
350
|
+
const ctxStr = e.contextPct !== null ? ` ctx:${e.contextPct}%` : "";
|
|
351
|
+
const upStr = e.uptimeMs !== null ? ` up:${formatUptime(e.uptimeMs)}` : "";
|
|
352
|
+
const idleStr = e.idleSinceMs !== null ? ` ${formatIdleSince(e.idleSinceMs)}` : "";
|
|
353
|
+
return ` ${label} [${e.status}] ${healthStr}${errStr}${burnStr}${ctxStr}${upStr}${idleStr}`;
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
/** Format session stats entries as a JSON object for export. */
|
|
357
|
+
export function formatStatsJson(entries, version, now) {
|
|
358
|
+
return JSON.stringify({
|
|
359
|
+
version,
|
|
360
|
+
exportedAt: new Date(now ?? Date.now()).toISOString(),
|
|
361
|
+
sessions: entries,
|
|
362
|
+
}, null, 2) + "\n";
|
|
363
|
+
}
|
|
262
364
|
// ── Session rename ────────────────────────────────────────────────────────────
|
|
263
365
|
/** Max visible length for a custom session display name. */
|
|
264
366
|
export const MAX_RENAME_LEN = 32;
|
|
@@ -281,7 +383,7 @@ export const BUILTIN_COMMANDS = new Set([
|
|
|
281
383
|
"/pin", "/bell", "/focus", "/mute", "/unmute-all", "/filter", "/who",
|
|
282
384
|
"/uptime", "/auto-pin", "/note", "/notes", "/clip", "/diff", "/mark",
|
|
283
385
|
"/jump", "/marks", "/search", "/alias", "/insist", "/task", "/tasks",
|
|
284
|
-
"/group", "/groups", "/group-filter", "/burn-rate", "/snapshot", "/broadcast", "/watchdog", "/top", "/ceiling", "/rename",
|
|
386
|
+
"/group", "/groups", "/group-filter", "/burn-rate", "/snapshot", "/broadcast", "/watchdog", "/top", "/ceiling", "/rename", "/copy", "/stats", "/recall", "/pin-all-errors", "/export-stats",
|
|
285
387
|
]);
|
|
286
388
|
/** Resolve a slash command through the alias map. Returns the expanded command or the original. */
|
|
287
389
|
export function resolveAlias(line, aliases) {
|
|
@@ -660,6 +762,25 @@ export class TUI {
|
|
|
660
762
|
}
|
|
661
763
|
return true;
|
|
662
764
|
}
|
|
765
|
+
/**
|
|
766
|
+
* Pin all sessions currently in "error" status (or with any cumulative errors).
|
|
767
|
+
* Returns the count of newly pinned sessions.
|
|
768
|
+
*/
|
|
769
|
+
pinAllErrors() {
|
|
770
|
+
let pinned = 0;
|
|
771
|
+
for (const s of this.sessions) {
|
|
772
|
+
if ((s.status === "error" || (this.sessionErrorCounts.get(s.id) ?? 0) > 0) && !this.pinnedIds.has(s.id)) {
|
|
773
|
+
this.pinnedIds.add(s.id);
|
|
774
|
+
pinned++;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
if (pinned > 0) {
|
|
778
|
+
this.sessions = sortSessions(this.sessions, this.sortMode, this.lastChangeAt, this.pinnedIds);
|
|
779
|
+
if (this.active)
|
|
780
|
+
this.paintSessions();
|
|
781
|
+
}
|
|
782
|
+
return pinned;
|
|
783
|
+
}
|
|
663
784
|
/** Check if a session ID is pinned. */
|
|
664
785
|
isPinned(id) {
|
|
665
786
|
return this.pinnedIds.has(id);
|
|
@@ -829,6 +950,32 @@ export class TUI {
|
|
|
829
950
|
getActivityBuffer() {
|
|
830
951
|
return this.activityBuffer;
|
|
831
952
|
}
|
|
953
|
+
/** Return the activity timestamps (epoch ms per entry, parallel to activityBuffer). */
|
|
954
|
+
getActivityTimestamps() {
|
|
955
|
+
return this.activityTimestamps;
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Return the stored pane output lines for a session (by 1-indexed number, ID, prefix, or title).
|
|
959
|
+
* Returns null if session not found or no output stored.
|
|
960
|
+
*/
|
|
961
|
+
getSessionOutput(sessionIdOrIndex) {
|
|
962
|
+
let sessionId;
|
|
963
|
+
if (typeof sessionIdOrIndex === "number") {
|
|
964
|
+
sessionId = this.sessions[sessionIdOrIndex - 1]?.id;
|
|
965
|
+
}
|
|
966
|
+
else {
|
|
967
|
+
const needle = sessionIdOrIndex.toLowerCase();
|
|
968
|
+
const match = this.sessions.find((s) => s.id === sessionIdOrIndex || s.id.startsWith(needle) || s.title.toLowerCase() === needle);
|
|
969
|
+
sessionId = match?.id;
|
|
970
|
+
}
|
|
971
|
+
if (!sessionId)
|
|
972
|
+
return null;
|
|
973
|
+
return this.sessionOutputs.get(sessionId) ?? null;
|
|
974
|
+
}
|
|
975
|
+
/** Return the current drill-down session ID (for /copy default target). */
|
|
976
|
+
getDrilldownId() {
|
|
977
|
+
return this.drilldownSessionId;
|
|
978
|
+
}
|
|
832
979
|
/** Return per-session error counts (for /who). */
|
|
833
980
|
getSessionErrorCounts() {
|
|
834
981
|
return this.sessionErrorCounts;
|
|
@@ -849,6 +996,27 @@ export class TUI {
|
|
|
849
996
|
}
|
|
850
997
|
return result;
|
|
851
998
|
}
|
|
999
|
+
/** Compute health scores for all sessions and return as a map (id → score). */
|
|
1000
|
+
getAllHealthScores(now) {
|
|
1001
|
+
const nowMs = now ?? Date.now();
|
|
1002
|
+
const result = new Map();
|
|
1003
|
+
for (const s of this.sessions) {
|
|
1004
|
+
const ceiling = parseContextCeiling(s.contextTokens);
|
|
1005
|
+
const cf = ceiling ? ceiling.current / ceiling.max : null;
|
|
1006
|
+
const bh = this.sessionContextHistory.get(s.id);
|
|
1007
|
+
const br = bh ? computeContextBurnRate(bh, nowMs) : null;
|
|
1008
|
+
const lc = this.lastChangeAt.get(s.id);
|
|
1009
|
+
const idle = lc !== undefined ? nowMs - lc : null;
|
|
1010
|
+
result.set(s.id, computeHealthScore({
|
|
1011
|
+
errorCount: this.sessionErrorCounts.get(s.id) ?? 0,
|
|
1012
|
+
burnRatePerMin: br,
|
|
1013
|
+
contextFraction: cf,
|
|
1014
|
+
idleMs: idle,
|
|
1015
|
+
watchdogThresholdMs: this.watchdogThresholdMs,
|
|
1016
|
+
}));
|
|
1017
|
+
}
|
|
1018
|
+
return result;
|
|
1019
|
+
}
|
|
852
1020
|
/** Return all sessions with their current burn rates (tokens/min, null if insufficient data). */
|
|
853
1021
|
getAllBurnRates(now) {
|
|
854
1022
|
const result = new Map();
|
|
@@ -1483,7 +1651,12 @@ export class TUI {
|
|
|
1483
1651
|
}
|
|
1484
1652
|
// reasoner badge
|
|
1485
1653
|
const reasonerTag = this.reasonerName ? ` ${SLATE}│${RESET} ${TEAL}${this.reasonerName}${RESET}` : "";
|
|
1486
|
-
|
|
1654
|
+
// watchdog indicator — show threshold when active
|
|
1655
|
+
const wdMin = this.watchdogThresholdMs !== null ? Math.round(this.watchdogThresholdMs / 60_000) : null;
|
|
1656
|
+
const watchdogTag = wdMin !== null ? ` ${SLATE}│${RESET} ${AMBER}⊛${wdMin}m${RESET}` : "";
|
|
1657
|
+
// group filter indicator
|
|
1658
|
+
const groupFilterTag = this.groupFilter ? ` ${SLATE}│${RESET} ${TEAL}${GROUP_ICON}${this.groupFilter}${RESET}` : "";
|
|
1659
|
+
line = ` ${INDIGO}${BOLD}aoaoe${RESET} ${SLATE}${this.version}${RESET} ${SLATE}│${RESET} #${this.pollCount} ${SLATE}│${RESET} ${sessCount} ${SLATE}│${RESET} ${phaseText}${activeTag}${countdownTag}${watchdogTag}${groupFilterTag}${reasonerTag}`;
|
|
1487
1660
|
}
|
|
1488
1661
|
process.stderr.write(SAVE_CURSOR +
|
|
1489
1662
|
moveTo(1, 1) + CLEAR_LINE + BG_DARK + WHITE + truncateAnsi(line, this.cols) + padToWidth(line, this.cols) + RESET +
|
|
@@ -1521,9 +1694,30 @@ export class TUI {
|
|
|
1521
1694
|
process.stderr.write(moveTo(startRow + 1, 1) + CLEAR_LINE + padded);
|
|
1522
1695
|
}
|
|
1523
1696
|
else if (this.compactMode) {
|
|
1524
|
-
// compact: inline tokens, multiple per row (with pin indicators)
|
|
1697
|
+
// compact: inline tokens, multiple per row (with pin indicators + health glyphs)
|
|
1698
|
+
const nowMsCompact = Date.now();
|
|
1525
1699
|
const noteIdSet = new Set(this.sessionNotes.keys());
|
|
1526
|
-
const
|
|
1700
|
+
const compactHealthScores = new Map();
|
|
1701
|
+
for (const s of visibleSessions) {
|
|
1702
|
+
const ceilingC = parseContextCeiling(s.contextTokens);
|
|
1703
|
+
const cfC = ceilingC ? ceilingC.current / ceilingC.max : null;
|
|
1704
|
+
const bhC = this.sessionContextHistory.get(s.id);
|
|
1705
|
+
const brC = bhC ? computeContextBurnRate(bhC, nowMsCompact) : null;
|
|
1706
|
+
const lcC = this.lastChangeAt.get(s.id);
|
|
1707
|
+
const idleC = lcC !== undefined ? nowMsCompact - lcC : null;
|
|
1708
|
+
compactHealthScores.set(s.id, computeHealthScore({
|
|
1709
|
+
errorCount: this.sessionErrorCounts.get(s.id) ?? 0,
|
|
1710
|
+
burnRatePerMin: brC,
|
|
1711
|
+
contextFraction: cfC,
|
|
1712
|
+
idleMs: idleC,
|
|
1713
|
+
watchdogThresholdMs: this.watchdogThresholdMs,
|
|
1714
|
+
}));
|
|
1715
|
+
}
|
|
1716
|
+
const compactActivityRates = new Map();
|
|
1717
|
+
for (const s of visibleSessions) {
|
|
1718
|
+
compactActivityRates.set(s.id, computeSessionActivityRate(this.activityBuffer, this.activityTimestamps, s.id, nowMsCompact));
|
|
1719
|
+
}
|
|
1720
|
+
const compactRows = formatCompactRows(visibleSessions, innerWidth - 1, this.pinnedIds, this.mutedIds, noteIdSet, compactHealthScores, compactActivityRates);
|
|
1527
1721
|
for (let r = 0; r < compactRows.length; r++) {
|
|
1528
1722
|
const line = `${SLATE}${BOX.v}${RESET} ${compactRows[r]}`;
|
|
1529
1723
|
const padded = padBoxLine(line, this.cols);
|