aoaoe 0.109.0 → 0.114.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/README.md +28 -5
- package/dist/index.js +70 -2
- package/dist/input.d.ts +9 -0
- package/dist/input.js +59 -0
- package/dist/task-parser.js +6 -3
- package/dist/tui.d.ts +72 -1
- package/dist/tui.js +209 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<a href="https://github.com/Talador12/agent-of-agent-of-empires/actions/workflows/ci.yml"><img src="https://github.com/Talador12/agent-of-agent-of-empires/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
5
5
|
<a href="https://www.npmjs.com/package/aoaoe"><img src="https://img.shields.io/npm/v/aoaoe" alt="npm version"></a>
|
|
6
6
|
<a href="https://github.com/Talador12/agent-of-agent-of-empires/releases"><img src="https://img.shields.io/github/v/release/Talador12/agent-of-agent-of-empires" alt="GitHub release"></a>
|
|
7
|
-
<img src="https://img.shields.io/badge/tests-
|
|
7
|
+
<img src="https://img.shields.io/badge/tests-1739-brightgreen" alt="tests">
|
|
8
8
|
<img src="https://img.shields.io/badge/node-%3E%3D20-blue" alt="Node.js >= 20">
|
|
9
9
|
<img src="https://img.shields.io/badge/runtime%20deps-0-brightgreen" alt="zero runtime dependencies">
|
|
10
10
|
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
|
|
@@ -256,11 +256,18 @@ The daemon runs an interactive TUI with a rich command set. These commands are a
|
|
|
256
256
|
| `/mute [N\|name]` | Mute/unmute a session's activity entries |
|
|
257
257
|
| `/unmute-all` | Unmute all sessions at once |
|
|
258
258
|
| `/filter [tag]` | Filter activity by tag -- presets: `errors`, `actions`, `system` (no arg = clear) |
|
|
259
|
-
| `/who` | Show fleet status
|
|
259
|
+
| `/who` | Show fleet status: status, uptime, idle-since, context, errors, group, note |
|
|
260
260
|
| `/uptime` | Show session uptimes |
|
|
261
|
+
| `/top [mode]` | Rank sessions by `errors` (default), `burn`, or `idle` |
|
|
261
262
|
| `/auto-pin` | Toggle auto-pin on error |
|
|
262
263
|
| `/note N\|name text` | Attach a note to a session (no text = clear) |
|
|
263
264
|
| `/notes` | List all session notes |
|
|
265
|
+
| `/group N\|name tag` | Assign session to a group (lowercase, max 16 chars; no tag = clear) |
|
|
266
|
+
| `/groups` | List all groups and their members |
|
|
267
|
+
| `/group-filter [tag]` | Show only sessions in a group (no arg = clear) |
|
|
268
|
+
| `/rename N\|name [display]` | Set custom TUI display name (no display = clear); persisted |
|
|
269
|
+
| `/watchdog [N]` | Alert if session stalls N minutes (default 10); `/watchdog off` to disable |
|
|
270
|
+
| `/broadcast <msg>` | Send message to all sessions; `/broadcast group:<tag> <msg>` for group |
|
|
264
271
|
| `/clip [N]` | Copy last N activity entries to clipboard (default 20) |
|
|
265
272
|
| `/diff N` | Show activity since bookmark N |
|
|
266
273
|
| `/mark` | Bookmark current activity position |
|
|
@@ -276,7 +283,7 @@ The daemon runs an interactive TUI with a rich command set. These commands are a
|
|
|
276
283
|
|
|
277
284
|
| Command | What it does |
|
|
278
285
|
|---------|-------------|
|
|
279
|
-
| `/status` | Show daemon state |
|
|
286
|
+
| `/status` | Show daemon state (mode, reasoner, poll counts, last cycle) |
|
|
280
287
|
| `/dashboard` | Show full dashboard |
|
|
281
288
|
| `/tasks` | Show task progress table |
|
|
282
289
|
| `/t ...` `/todo ...` `/idea ...` | Aliases for `/task ...` |
|
|
@@ -284,6 +291,10 @@ The daemon runs an interactive TUI with a rich command set. These commands are a
|
|
|
284
291
|
| `/task <session> :: <goal>` | Fast path: update/create task for an existing session and set its goal |
|
|
285
292
|
| `:<goal>` | Fastest path in drill-down: set goal for that session |
|
|
286
293
|
| `just type` (in drill-down) | Default behavior: update goal for the focused session |
|
|
294
|
+
| `/burn-rate` | Show context token burn rates (tokens/min) for all sessions |
|
|
295
|
+
| `/ceiling` | Show context token usage vs limit for all sessions |
|
|
296
|
+
| `/snapshot [md]` | Export session state snapshot to `~/.aoaoe/snapshot-<ts>.json` (or `.md`) |
|
|
297
|
+
| `/alias /x /cmd` | Create command alias (`/x` expands to `/cmd`); no args = list |
|
|
287
298
|
|
|
288
299
|
### Other
|
|
289
300
|
|
|
@@ -296,9 +307,21 @@ The daemon runs an interactive TUI with a rich command set. These commands are a
|
|
|
296
307
|
### TUI Features
|
|
297
308
|
|
|
298
309
|
- **Activity sparkline** -- 10-minute activity rate chart in the separator bar (Unicode blocks with color gradient)
|
|
299
|
-
- **Session cards** -- per-session status with pin `▲`, mute `◌`, note
|
|
300
|
-
- **
|
|
310
|
+
- **Session cards** -- per-session status with pin `▲`, mute `◌`, note `✎`, group `⊹tag`, and health score `⬡N` indicators
|
|
311
|
+
- **Health score** -- composite 0–100 badge per session (errors, burn rate, context ceiling, stall time); LIME ≥80, AMBER ≥60, ROSE <60
|
|
312
|
+
- **Error sparklines** -- ROSE 5-bucket mini-chart of recent error frequency in each card (last 5 min)
|
|
313
|
+
- **Idle-since** -- time since last output change shown in idle/done card status and `/who` output
|
|
314
|
+
- **Session grouping** -- `/group` assigns sessions to named groups; `/group-filter` narrows the panel view
|
|
315
|
+
- **Session rename** -- `/rename` sets a custom TUI display name (bold with original dim alongside); persisted
|
|
316
|
+
- **Watchdog** -- `/watchdog N` fires an alert if a session stalls for N minutes (rate-limited to once per 5 min)
|
|
317
|
+
- **Burn-rate alerts** -- automatic "status" alert when context token usage exceeds 5k tokens/min; `/burn-rate` for on-demand view
|
|
318
|
+
- **Context ceiling warning** -- automatic alert at 90% context usage when "X / Y tokens" format available; `/ceiling` for on-demand view
|
|
319
|
+
- **Snapshot export** -- `/snapshot [md]` exports full session state (status, group, note, uptime, context, errors, burn rate) to `~/.aoaoe/`
|
|
320
|
+
- **Broadcast** -- `/broadcast [group:<tag>] <message>` sends a message to all (or group-filtered) sessions via tmux
|
|
321
|
+
- **Ranked view** -- `/top [errors|burn|idle]` ranks sessions by composite attention score or a single metric
|
|
322
|
+
- **Sticky preferences** -- sort, compact, focus, bell, auto-pin, tag filter, aliases, groups, and renames persist across restarts
|
|
301
323
|
- **Filter pipeline** -- mute, tag filter, and search compose: mute → tag → search
|
|
324
|
+
- **Aliases** -- `/alias /x /cmd` creates command shortcuts; up to 50, persisted
|
|
302
325
|
- **Activity heatmap** -- 24-hour colored block chart via `aoaoe stats`
|
|
303
326
|
- **Bookmarks** -- mark positions in the activity stream, jump back to them, diff since a bookmark
|
|
304
327
|
- **Clipboard export** -- `/clip` copies activity to system clipboard (macOS) or `~/.aoaoe/clip.txt`
|
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 } 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 } 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";
|
|
@@ -188,14 +188,19 @@ async function main() {
|
|
|
188
188
|
tui.setTagFilter(prefs.tagFilter);
|
|
189
189
|
if (prefs.sessionGroups)
|
|
190
190
|
tui.restoreGroups(prefs.sessionGroups);
|
|
191
|
+
if (prefs.sessionAliases)
|
|
192
|
+
tui.restoreSessionAliases(prefs.sessionAliases);
|
|
191
193
|
}
|
|
192
194
|
const persistPrefs = () => {
|
|
193
195
|
if (!tui)
|
|
194
196
|
return;
|
|
195
|
-
// persist session groups (ID → group tag)
|
|
197
|
+
// persist session groups (ID → group tag) and session aliases (ID → display name)
|
|
196
198
|
const groupsObj = {};
|
|
197
199
|
for (const [id, g] of tui.getAllGroups())
|
|
198
200
|
groupsObj[id] = g;
|
|
201
|
+
const aliasesObj = {};
|
|
202
|
+
for (const [id, name] of tui.getAllSessionAliases())
|
|
203
|
+
aliasesObj[id] = name;
|
|
199
204
|
saveTuiPrefs({
|
|
200
205
|
sortMode: tui.getSortMode(),
|
|
201
206
|
compact: tui.isCompact(),
|
|
@@ -205,6 +210,7 @@ async function main() {
|
|
|
205
210
|
tagFilter: tui.getTagFilter(),
|
|
206
211
|
aliases: input.getAliases(),
|
|
207
212
|
sessionGroups: groupsObj,
|
|
213
|
+
sessionAliases: aliasesObj,
|
|
208
214
|
});
|
|
209
215
|
};
|
|
210
216
|
if (!useTui) {
|
|
@@ -751,6 +757,68 @@ async function main() {
|
|
|
751
757
|
if (!any)
|
|
752
758
|
tui.log("system", ` threshold: ${CONTEXT_BURN_THRESHOLD.toLocaleString()} tokens/min`);
|
|
753
759
|
});
|
|
760
|
+
// wire /rename custom display name
|
|
761
|
+
input.onRename((target, displayName) => {
|
|
762
|
+
const num = /^\d+$/.test(target) ? parseInt(target, 10) : undefined;
|
|
763
|
+
const ok = tui.renameSession(num ?? target, displayName || null);
|
|
764
|
+
if (ok) {
|
|
765
|
+
if (displayName) {
|
|
766
|
+
tui.log("system", `renamed ${target} → "${displayName}"`);
|
|
767
|
+
}
|
|
768
|
+
else {
|
|
769
|
+
tui.log("system", `rename cleared for ${target}`);
|
|
770
|
+
}
|
|
771
|
+
persistPrefs();
|
|
772
|
+
}
|
|
773
|
+
else {
|
|
774
|
+
tui.log("system", `session not found: ${target}`);
|
|
775
|
+
}
|
|
776
|
+
});
|
|
777
|
+
// wire /ceiling context usage view
|
|
778
|
+
input.onCeiling(() => {
|
|
779
|
+
const sessions = tui.getSessions();
|
|
780
|
+
if (sessions.length === 0) {
|
|
781
|
+
tui.log("system", "no sessions — context data not available");
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
const ceilings = tui.getAllContextCeilings();
|
|
785
|
+
let any = false;
|
|
786
|
+
for (const s of sessions) {
|
|
787
|
+
const c = ceilings.get(s.id);
|
|
788
|
+
if (!c) {
|
|
789
|
+
tui.log("system", ` ${s.title}: no ceiling data (${s.contextTokens ?? "no tokens"})`);
|
|
790
|
+
}
|
|
791
|
+
else {
|
|
792
|
+
const pct = Math.round((c.current / c.max) * 100);
|
|
793
|
+
const warn = pct >= CONTEXT_CEILING_THRESHOLD * 100 ? " ⚠" : "";
|
|
794
|
+
tui.log("system", ` ${s.title}: ${pct}% — ${c.current.toLocaleString()} / ${c.max.toLocaleString()} tokens${warn}`);
|
|
795
|
+
any = true;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
if (!any)
|
|
799
|
+
tui.log("system", ` tip: context ceiling requires "X / Y tokens" format in session output`);
|
|
800
|
+
});
|
|
801
|
+
// wire /top ranked session view
|
|
802
|
+
input.onTop((modeArg) => {
|
|
803
|
+
const mode = TOP_SORT_MODES.includes(modeArg)
|
|
804
|
+
? modeArg : "default";
|
|
805
|
+
const sessions = tui.getSessions();
|
|
806
|
+
if (sessions.length === 0) {
|
|
807
|
+
tui.log("system", "no sessions");
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
const now = Date.now();
|
|
811
|
+
const entries = rankSessions(sessions, tui.getSessionErrorCounts(), tui.getAllBurnRates(now), tui.getAllLastChangeAt(), mode, now);
|
|
812
|
+
const modeLabel = mode === "default" ? "composite" : mode;
|
|
813
|
+
tui.log("system", `/top (${modeLabel}) — ${entries.length} sessions:`);
|
|
814
|
+
for (const e of entries) {
|
|
815
|
+
const errStr = e.errors > 0 ? ` ${e.errors}err` : "";
|
|
816
|
+
const burnStr = e.burnRatePerMin !== null && e.burnRatePerMin > 0
|
|
817
|
+
? ` ~${Math.round(e.burnRatePerMin / 100) * 100}tok/min` : "";
|
|
818
|
+
const idleStr = e.idleMs !== null ? ` ${formatIdleSince(e.idleMs) || "active"}` : "";
|
|
819
|
+
tui.log("system", ` #${e.rank} ${e.title} [${e.status}]${errStr}${burnStr}${idleStr}`);
|
|
820
|
+
}
|
|
821
|
+
});
|
|
754
822
|
// wire /watchdog stall detection
|
|
755
823
|
input.onWatchdog((thresholdMinutes) => {
|
|
756
824
|
if (thresholdMinutes === null) {
|
package/dist/input.d.ts
CHANGED
|
@@ -30,6 +30,9 @@ export type BurnRateHandler = () => void;
|
|
|
30
30
|
export type SnapshotHandler = (format: "json" | "md") => void;
|
|
31
31
|
export type BroadcastHandler = (message: string, group: string | null) => void;
|
|
32
32
|
export type WatchdogHandler = (thresholdMinutes: number | null) => void;
|
|
33
|
+
export type TopHandler = (mode: string) => void;
|
|
34
|
+
export type CeilingHandler = () => void;
|
|
35
|
+
export type RenameHandler = (target: string, name: string) => void;
|
|
33
36
|
export interface MouseEvent {
|
|
34
37
|
button: number;
|
|
35
38
|
col: number;
|
|
@@ -82,6 +85,9 @@ export declare class InputReader {
|
|
|
82
85
|
private snapshotHandler;
|
|
83
86
|
private broadcastHandler;
|
|
84
87
|
private watchdogHandler;
|
|
88
|
+
private topHandler;
|
|
89
|
+
private ceilingHandler;
|
|
90
|
+
private renameHandler;
|
|
85
91
|
private aliases;
|
|
86
92
|
private mouseDataListener;
|
|
87
93
|
onScroll(handler: (dir: ScrollDirection) => void): void;
|
|
@@ -117,6 +123,9 @@ export declare class InputReader {
|
|
|
117
123
|
onSnapshot(handler: SnapshotHandler): void;
|
|
118
124
|
onBroadcast(handler: BroadcastHandler): void;
|
|
119
125
|
onWatchdog(handler: WatchdogHandler): void;
|
|
126
|
+
onTop(handler: TopHandler): void;
|
|
127
|
+
onCeiling(handler: CeilingHandler): void;
|
|
128
|
+
onRename(handler: RenameHandler): void;
|
|
120
129
|
/** Set aliases from persisted prefs. */
|
|
121
130
|
setAliases(aliases: Record<string, string>): void;
|
|
122
131
|
/** Get current aliases as a plain object. */
|
package/dist/input.js
CHANGED
|
@@ -63,6 +63,9 @@ export class InputReader {
|
|
|
63
63
|
snapshotHandler = null;
|
|
64
64
|
broadcastHandler = null;
|
|
65
65
|
watchdogHandler = null;
|
|
66
|
+
topHandler = null;
|
|
67
|
+
ceilingHandler = null;
|
|
68
|
+
renameHandler = null;
|
|
66
69
|
aliases = new Map(); // /shortcut → /full command
|
|
67
70
|
mouseDataListener = null;
|
|
68
71
|
// register a callback for scroll key events (PgUp/PgDn/Home/End)
|
|
@@ -197,6 +200,18 @@ export class InputReader {
|
|
|
197
200
|
onWatchdog(handler) {
|
|
198
201
|
this.watchdogHandler = handler;
|
|
199
202
|
}
|
|
203
|
+
// register a callback for /top [mode]
|
|
204
|
+
onTop(handler) {
|
|
205
|
+
this.topHandler = handler;
|
|
206
|
+
}
|
|
207
|
+
// register a callback for /ceiling
|
|
208
|
+
onCeiling(handler) {
|
|
209
|
+
this.ceilingHandler = handler;
|
|
210
|
+
}
|
|
211
|
+
// register a callback for /rename <N|name> [display name]
|
|
212
|
+
onRename(handler) {
|
|
213
|
+
this.renameHandler = handler;
|
|
214
|
+
}
|
|
200
215
|
/** Set aliases from persisted prefs. */
|
|
201
216
|
setAliases(aliases) {
|
|
202
217
|
this.aliases.clear();
|
|
@@ -430,6 +445,9 @@ ${BOLD}navigation:${RESET}
|
|
|
430
445
|
/snapshot [md] export session state snapshot to JSON (or Markdown with md)
|
|
431
446
|
/broadcast <msg> send message to all sessions; /broadcast group:<tag> <msg> for group
|
|
432
447
|
/watchdog [N] alert if session stalls N minutes (default 10); /watchdog off to disable
|
|
448
|
+
/top [mode] rank sessions by errors (default), burn, or idle
|
|
449
|
+
/ceiling show context token usage vs limit for all sessions
|
|
450
|
+
/rename N|name [display] set custom display name in TUI (no display = clear)
|
|
433
451
|
/clip [N] copy last N activity entries to clipboard (default 20)
|
|
434
452
|
/diff N show activity since bookmark N
|
|
435
453
|
/mark bookmark current activity position
|
|
@@ -819,6 +837,47 @@ ${BOLD}other:${RESET}
|
|
|
819
837
|
}
|
|
820
838
|
break;
|
|
821
839
|
}
|
|
840
|
+
case "/rename": {
|
|
841
|
+
const renameArg = line.slice("/rename".length).trim();
|
|
842
|
+
if (!renameArg) {
|
|
843
|
+
console.error(`${DIM}usage: /rename <N|name> [display name] — set custom display name (no name = clear)${RESET}`);
|
|
844
|
+
break;
|
|
845
|
+
}
|
|
846
|
+
if (this.renameHandler) {
|
|
847
|
+
const spaceIdx = renameArg.indexOf(" ");
|
|
848
|
+
if (spaceIdx > 0) {
|
|
849
|
+
const target = renameArg.slice(0, spaceIdx);
|
|
850
|
+
const display = renameArg.slice(spaceIdx + 1).trim();
|
|
851
|
+
this.renameHandler(target, display);
|
|
852
|
+
}
|
|
853
|
+
else {
|
|
854
|
+
// target only — clear alias
|
|
855
|
+
this.renameHandler(renameArg, "");
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
else {
|
|
859
|
+
console.error(`${DIM}rename not available (no TUI)${RESET}`);
|
|
860
|
+
}
|
|
861
|
+
break;
|
|
862
|
+
}
|
|
863
|
+
case "/ceiling":
|
|
864
|
+
if (this.ceilingHandler) {
|
|
865
|
+
this.ceilingHandler();
|
|
866
|
+
}
|
|
867
|
+
else {
|
|
868
|
+
console.error(`${DIM}ceiling not available (no TUI)${RESET}`);
|
|
869
|
+
}
|
|
870
|
+
break;
|
|
871
|
+
case "/top": {
|
|
872
|
+
const topArg = line.slice("/top".length).trim().toLowerCase() || "default";
|
|
873
|
+
if (this.topHandler) {
|
|
874
|
+
this.topHandler(topArg);
|
|
875
|
+
}
|
|
876
|
+
else {
|
|
877
|
+
console.error(`${DIM}top not available (no TUI)${RESET}`);
|
|
878
|
+
}
|
|
879
|
+
break;
|
|
880
|
+
}
|
|
822
881
|
case "/watchdog": {
|
|
823
882
|
const wdArg = line.slice("/watchdog".length).trim().toLowerCase();
|
|
824
883
|
if (this.watchdogHandler) {
|
package/dist/task-parser.js
CHANGED
|
@@ -54,9 +54,12 @@ export function parseModel(output) {
|
|
|
54
54
|
}
|
|
55
55
|
// extract context info from OpenCode pane output
|
|
56
56
|
export function parseContext(output) {
|
|
57
|
-
// matches "137,918 tokens" or "
|
|
58
|
-
const
|
|
59
|
-
|
|
57
|
+
// matches "137,918 / 200,000 tokens" or plain "137,918 tokens"
|
|
58
|
+
const full = output.match(/([\d,]+)\s*\/\s*([\d,]+)\s+tokens/);
|
|
59
|
+
if (full?.[1] && full[2])
|
|
60
|
+
return `${full[1]} / ${full[2]} tokens`;
|
|
61
|
+
const simple = output.match(/([\d,]+)\s+tokens/);
|
|
62
|
+
return simple?.[1] ? `${simple[1]} tokens` : undefined;
|
|
60
63
|
}
|
|
61
64
|
// extract cost from OpenCode pane output
|
|
62
65
|
export function parseCost(output) {
|
package/dist/tui.d.ts
CHANGED
|
@@ -74,6 +74,10 @@ export declare function formatUptime(ms: number): string;
|
|
|
74
74
|
* Returns empty string if under threshold (< thresholdMs, default 2 min — not worth showing).
|
|
75
75
|
*/
|
|
76
76
|
export declare function formatIdleSince(ms: number, thresholdMs?: number): string;
|
|
77
|
+
/** Max visible length for a custom session display name. */
|
|
78
|
+
export declare const MAX_RENAME_LEN = 32;
|
|
79
|
+
/** Truncate a custom display name to the max length. */
|
|
80
|
+
export declare function truncateRename(name: string): string;
|
|
77
81
|
/** Default watchdog threshold: 10 minutes of no output change triggers alert. */
|
|
78
82
|
export declare const WATCHDOG_DEFAULT_MINUTES = 10;
|
|
79
83
|
/** Cooldown between watchdog alerts for the same session (5 minutes). */
|
|
@@ -119,6 +123,42 @@ export declare function buildSnapshotData(sessions: readonly DaemonSessionState[
|
|
|
119
123
|
export declare function formatSnapshotJson(data: SnapshotData): string;
|
|
120
124
|
/** Format a SnapshotData as a Markdown report. */
|
|
121
125
|
export declare function formatSnapshotMarkdown(data: SnapshotData): string;
|
|
126
|
+
/** Health score color thresholds. */
|
|
127
|
+
export declare const HEALTH_GOOD = 80;
|
|
128
|
+
export declare const HEALTH_WARN = 60;
|
|
129
|
+
/** Health score badge icon. */
|
|
130
|
+
export declare const HEALTH_ICON = "\u2B21";
|
|
131
|
+
/**
|
|
132
|
+
* Compute a composite health score (0–100) for a session.
|
|
133
|
+
* Higher = healthier. Deductions for: errors, high burn rate, context ceiling proximity, stall.
|
|
134
|
+
*/
|
|
135
|
+
export declare function computeHealthScore(opts: {
|
|
136
|
+
errorCount: number;
|
|
137
|
+
burnRatePerMin: number | null;
|
|
138
|
+
contextFraction: number | null;
|
|
139
|
+
idleMs: number | null;
|
|
140
|
+
watchdogThresholdMs: number | null;
|
|
141
|
+
}): number;
|
|
142
|
+
/**
|
|
143
|
+
* Format a health score as a colored badge string: "⬡83" in LIME/AMBER/ROSE.
|
|
144
|
+
* Returns empty string for score of exactly 100 (no badge clutter on healthy sessions).
|
|
145
|
+
*/
|
|
146
|
+
export declare function formatHealthBadge(score: number): string;
|
|
147
|
+
export type TopSortMode = "errors" | "burn" | "idle" | "default";
|
|
148
|
+
export declare const TOP_SORT_MODES: TopSortMode[];
|
|
149
|
+
export interface TopEntry {
|
|
150
|
+
title: string;
|
|
151
|
+
status: string;
|
|
152
|
+
rank: number;
|
|
153
|
+
errors: number;
|
|
154
|
+
burnRatePerMin: number | null;
|
|
155
|
+
idleMs: number | null;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Rank sessions for /top output. Returns entries sorted by the given mode.
|
|
159
|
+
* "default" = composite: errors first, then burn rate, then idle.
|
|
160
|
+
*/
|
|
161
|
+
export declare function rankSessions(sessions: readonly DaemonSessionState[], errorCounts: ReadonlyMap<string, number>, burnRates: ReadonlyMap<string, number | null>, lastChangeAt: ReadonlyMap<string, number>, mode: TopSortMode, now?: number): TopEntry[];
|
|
122
162
|
/** Format a broadcast summary for the activity log. */
|
|
123
163
|
export declare function formatBroadcastSummary(count: number, group: string | null): string;
|
|
124
164
|
export interface TuiPrefs {
|
|
@@ -130,6 +170,7 @@ export interface TuiPrefs {
|
|
|
130
170
|
tagFilter?: string | null;
|
|
131
171
|
aliases?: Record<string, string>;
|
|
132
172
|
sessionGroups?: Record<string, string>;
|
|
173
|
+
sessionAliases?: Record<string, string>;
|
|
133
174
|
}
|
|
134
175
|
/** Load persisted TUI preferences. Returns empty object on any error. */
|
|
135
176
|
export declare function loadTuiPrefs(): TuiPrefs;
|
|
@@ -174,8 +215,10 @@ export declare class TUI {
|
|
|
174
215
|
private sessionErrorTimestamps;
|
|
175
216
|
private sessionContextHistory;
|
|
176
217
|
private burnRateAlerted;
|
|
218
|
+
private ceilingAlerted;
|
|
177
219
|
private sessionGroups;
|
|
178
220
|
private groupFilter;
|
|
221
|
+
private sessionAliases;
|
|
179
222
|
private watchdogThresholdMs;
|
|
180
223
|
private watchdogAlerted;
|
|
181
224
|
private viewMode;
|
|
@@ -267,6 +310,11 @@ export declare class TUI {
|
|
|
267
310
|
getSessionErrorTimestamps(id: string): readonly number[];
|
|
268
311
|
/** Return context token history for a session (for burn-rate reporting). */
|
|
269
312
|
getSessionContextHistory(id: string): readonly ContextHistoryEntry[];
|
|
313
|
+
/** Return parsed context ceiling for all sessions ({current, max} or null). */
|
|
314
|
+
getAllContextCeilings(): Map<string, {
|
|
315
|
+
current: number;
|
|
316
|
+
max: number;
|
|
317
|
+
} | null>;
|
|
270
318
|
/** Return all sessions with their current burn rates (tokens/min, null if insufficient data). */
|
|
271
319
|
getAllBurnRates(now?: number): Map<string, number | null>;
|
|
272
320
|
/**
|
|
@@ -294,6 +342,17 @@ export declare class TUI {
|
|
|
294
342
|
setGroupFilter(group: string | null): void;
|
|
295
343
|
/** Get the current group filter (or null if none). */
|
|
296
344
|
getGroupFilter(): string | null;
|
|
345
|
+
/**
|
|
346
|
+
* Set or clear a custom display name for a session (by 1-indexed number, ID, ID prefix, or title).
|
|
347
|
+
* Returns true if session found. Pass empty/null to clear.
|
|
348
|
+
*/
|
|
349
|
+
renameSession(sessionIdOrIndex: string | number, displayName: string | null): boolean;
|
|
350
|
+
/** Get the custom display name for a session (or undefined if not renamed). */
|
|
351
|
+
getSessionAlias(id: string): string | undefined;
|
|
352
|
+
/** Return all session aliases (for persistence and listing). */
|
|
353
|
+
getAllSessionAliases(): ReadonlyMap<string, string>;
|
|
354
|
+
/** Restore session aliases from persisted prefs (bulk restore). */
|
|
355
|
+
restoreSessionAliases(aliases: Record<string, string>): void;
|
|
297
356
|
/**
|
|
298
357
|
* Add a bookmark at the current activity position.
|
|
299
358
|
* Returns the bookmark number (1-indexed) or 0 if buffer is empty.
|
|
@@ -365,7 +424,7 @@ export declare class TUI {
|
|
|
365
424
|
private repaintDrilldownContent;
|
|
366
425
|
private paintInputLine;
|
|
367
426
|
}
|
|
368
|
-
declare function formatSessionCard(s: DaemonSessionState, maxWidth: number, errorSparkline?: string, idleSinceMs?: number): string;
|
|
427
|
+
declare function formatSessionCard(s: DaemonSessionState, maxWidth: number, errorSparkline?: string, idleSinceMs?: number, healthBadge?: string, displayName?: string): string;
|
|
369
428
|
declare function formatActivity(entry: ActivityEntry, maxCols: number): string;
|
|
370
429
|
declare function padBoxLine(line: string, totalWidth: number): string;
|
|
371
430
|
declare function padBoxLineHover(line: string, totalWidth: number, hovered: boolean): string;
|
|
@@ -390,6 +449,18 @@ declare function formatDrilldownScrollIndicator(offset: number, totalLines: numb
|
|
|
390
449
|
declare function computeSparkline(timestamps: number[], now?: number, buckets?: number, windowMs?: number): number[];
|
|
391
450
|
/** Format sparkline bucket counts as a colored Unicode block string. Returns empty string if all zeros. */
|
|
392
451
|
declare function formatSparkline(counts: number[]): string;
|
|
452
|
+
/**
|
|
453
|
+
* Parse a context token string that may include a ceiling: "137,918 / 200,000 tokens".
|
|
454
|
+
* Returns { current, max } if both values are present, null otherwise.
|
|
455
|
+
*/
|
|
456
|
+
export declare function parseContextCeiling(contextTokens: string | undefined): {
|
|
457
|
+
current: number;
|
|
458
|
+
max: number;
|
|
459
|
+
} | null;
|
|
460
|
+
/** Alert threshold: fire ceiling warning when context usage exceeds this fraction. */
|
|
461
|
+
export declare const CONTEXT_CEILING_THRESHOLD = 0.9;
|
|
462
|
+
/** Format a context ceiling alert for the activity log. */
|
|
463
|
+
export declare function formatContextCeilingAlert(title: string, current: number, max: number): string;
|
|
393
464
|
/** Parse a context token string like "137,918 tokens" to a raw number, or null if unparseable. */
|
|
394
465
|
export declare function parseContextTokenNumber(contextTokens: string | undefined): number | null;
|
|
395
466
|
/** Context history entry: token count at a timestamp. */
|
package/dist/tui.js
CHANGED
|
@@ -259,6 +259,13 @@ export function formatIdleSince(ms, thresholdMs = 2 * 60_000) {
|
|
|
259
259
|
return "";
|
|
260
260
|
return `idle ${formatUptime(ms)}`;
|
|
261
261
|
}
|
|
262
|
+
// ── Session rename ────────────────────────────────────────────────────────────
|
|
263
|
+
/** Max visible length for a custom session display name. */
|
|
264
|
+
export const MAX_RENAME_LEN = 32;
|
|
265
|
+
/** Truncate a custom display name to the max length. */
|
|
266
|
+
export function truncateRename(name) {
|
|
267
|
+
return name.length > MAX_RENAME_LEN ? name.slice(0, MAX_RENAME_LEN - 2) + ".." : name;
|
|
268
|
+
}
|
|
262
269
|
// ── Watchdog ──────────────────────────────────────────────────────────────────
|
|
263
270
|
/** Default watchdog threshold: 10 minutes of no output change triggers alert. */
|
|
264
271
|
export const WATCHDOG_DEFAULT_MINUTES = 10;
|
|
@@ -274,7 +281,7 @@ export const BUILTIN_COMMANDS = new Set([
|
|
|
274
281
|
"/pin", "/bell", "/focus", "/mute", "/unmute-all", "/filter", "/who",
|
|
275
282
|
"/uptime", "/auto-pin", "/note", "/notes", "/clip", "/diff", "/mark",
|
|
276
283
|
"/jump", "/marks", "/search", "/alias", "/insist", "/task", "/tasks",
|
|
277
|
-
"/group", "/groups", "/group-filter", "/burn-rate", "/snapshot", "/broadcast", "/watchdog",
|
|
284
|
+
"/group", "/groups", "/group-filter", "/burn-rate", "/snapshot", "/broadcast", "/watchdog", "/top", "/ceiling", "/rename",
|
|
278
285
|
]);
|
|
279
286
|
/** Resolve a slash command through the alias map. Returns the expanded command or the original. */
|
|
280
287
|
export function resolveAlias(line, aliases) {
|
|
@@ -391,6 +398,84 @@ export function formatSnapshotMarkdown(data) {
|
|
|
391
398
|
}
|
|
392
399
|
return lines.join("\n");
|
|
393
400
|
}
|
|
401
|
+
// ── Session health score ──────────────────────────────────────────────────────
|
|
402
|
+
/** Health score color thresholds. */
|
|
403
|
+
export const HEALTH_GOOD = 80;
|
|
404
|
+
export const HEALTH_WARN = 60;
|
|
405
|
+
/** Health score badge icon. */
|
|
406
|
+
export const HEALTH_ICON = "⬡";
|
|
407
|
+
/**
|
|
408
|
+
* Compute a composite health score (0–100) for a session.
|
|
409
|
+
* Higher = healthier. Deductions for: errors, high burn rate, context ceiling proximity, stall.
|
|
410
|
+
*/
|
|
411
|
+
export function computeHealthScore(opts) {
|
|
412
|
+
let score = 100;
|
|
413
|
+
// errors: -10 per error, cap deduction at 50
|
|
414
|
+
const errDeduction = Math.min(50, opts.errorCount * 10);
|
|
415
|
+
score -= errDeduction;
|
|
416
|
+
// burn rate: -20 when above CONTEXT_BURN_THRESHOLD
|
|
417
|
+
if (opts.burnRatePerMin !== null && opts.burnRatePerMin > CONTEXT_BURN_THRESHOLD) {
|
|
418
|
+
score -= 20;
|
|
419
|
+
}
|
|
420
|
+
// context ceiling: -10 per 10% above 70% (so 80%→-10, 90%→-20, 100%→-30)
|
|
421
|
+
if (opts.contextFraction !== null) {
|
|
422
|
+
const overPct = Math.max(0, opts.contextFraction - 0.70);
|
|
423
|
+
const tenPctSteps = Math.floor(overPct * 10); // number of 10% increments over 70%
|
|
424
|
+
const ceDeduction = Math.min(30, tenPctSteps * 10);
|
|
425
|
+
score -= ceDeduction;
|
|
426
|
+
}
|
|
427
|
+
// stall: -15 when idle longer than watchdog threshold
|
|
428
|
+
if (opts.idleMs !== null && opts.watchdogThresholdMs !== null && opts.idleMs >= opts.watchdogThresholdMs) {
|
|
429
|
+
score -= 15;
|
|
430
|
+
}
|
|
431
|
+
return Math.max(0, Math.min(100, score));
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Format a health score as a colored badge string: "⬡83" in LIME/AMBER/ROSE.
|
|
435
|
+
* Returns empty string for score of exactly 100 (no badge clutter on healthy sessions).
|
|
436
|
+
*/
|
|
437
|
+
export function formatHealthBadge(score) {
|
|
438
|
+
if (score >= 100)
|
|
439
|
+
return "";
|
|
440
|
+
const color = score >= HEALTH_GOOD ? LIME : score >= HEALTH_WARN ? AMBER : ROSE;
|
|
441
|
+
return `${color}${HEALTH_ICON}${score}${RESET}`;
|
|
442
|
+
}
|
|
443
|
+
export const TOP_SORT_MODES = ["default", "errors", "burn", "idle"];
|
|
444
|
+
/**
|
|
445
|
+
* Rank sessions for /top output. Returns entries sorted by the given mode.
|
|
446
|
+
* "default" = composite: errors first, then burn rate, then idle.
|
|
447
|
+
*/
|
|
448
|
+
export function rankSessions(sessions, errorCounts, burnRates, lastChangeAt, mode, now) {
|
|
449
|
+
const nowMs = now ?? Date.now();
|
|
450
|
+
const entries = sessions.map((s) => ({
|
|
451
|
+
title: s.title,
|
|
452
|
+
status: s.status,
|
|
453
|
+
rank: 0,
|
|
454
|
+
errors: errorCounts.get(s.id) ?? 0,
|
|
455
|
+
burnRatePerMin: burnRates.get(s.id) ?? null,
|
|
456
|
+
idleMs: lastChangeAt.has(s.id) ? nowMs - lastChangeAt.get(s.id) : null,
|
|
457
|
+
}));
|
|
458
|
+
switch (mode) {
|
|
459
|
+
case "errors":
|
|
460
|
+
entries.sort((a, b) => b.errors - a.errors);
|
|
461
|
+
break;
|
|
462
|
+
case "burn":
|
|
463
|
+
entries.sort((a, b) => (b.burnRatePerMin ?? 0) - (a.burnRatePerMin ?? 0));
|
|
464
|
+
break;
|
|
465
|
+
case "idle":
|
|
466
|
+
entries.sort((a, b) => (b.idleMs ?? 0) - (a.idleMs ?? 0));
|
|
467
|
+
break;
|
|
468
|
+
default: {
|
|
469
|
+
// composite: weight errors heavily, then burn rate, then idle
|
|
470
|
+
const score = (e) => e.errors * 10000 +
|
|
471
|
+
(e.burnRatePerMin ?? 0) * 0.01 +
|
|
472
|
+
(e.idleMs ?? 0) * 0.0001;
|
|
473
|
+
entries.sort((a, b) => score(b) - score(a));
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
entries.forEach((e, i) => { e.rank = i + 1; });
|
|
477
|
+
return entries;
|
|
478
|
+
}
|
|
394
479
|
// ── Broadcast helpers ────────────────────────────────────────────────────────
|
|
395
480
|
/** Format a broadcast summary for the activity log. */
|
|
396
481
|
export function formatBroadcastSummary(count, group) {
|
|
@@ -455,8 +540,10 @@ export class TUI {
|
|
|
455
540
|
sessionErrorTimestamps = new Map(); // session ID → recent error epoch ms (last 100)
|
|
456
541
|
sessionContextHistory = new Map(); // session ID → context token history
|
|
457
542
|
burnRateAlerted = new Map(); // session ID → epoch ms of last burn-rate alert (cooldown)
|
|
543
|
+
ceilingAlerted = new Map(); // session ID → epoch ms of last ceiling alert (cooldown)
|
|
458
544
|
sessionGroups = new Map(); // session ID → group tag
|
|
459
545
|
groupFilter = null; // active group filter (null = show all)
|
|
546
|
+
sessionAliases = new Map(); // session ID → custom display name
|
|
460
547
|
watchdogThresholdMs = null; // null = disabled; ms of inactivity before alert
|
|
461
548
|
watchdogAlerted = new Map(); // session ID → epoch ms of last watchdog alert
|
|
462
549
|
// drill-down mode: show a single session's full output
|
|
@@ -754,6 +841,14 @@ export class TUI {
|
|
|
754
841
|
getSessionContextHistory(id) {
|
|
755
842
|
return this.sessionContextHistory.get(id) ?? [];
|
|
756
843
|
}
|
|
844
|
+
/** Return parsed context ceiling for all sessions ({current, max} or null). */
|
|
845
|
+
getAllContextCeilings() {
|
|
846
|
+
const result = new Map();
|
|
847
|
+
for (const s of this.sessions) {
|
|
848
|
+
result.set(s.id, parseContextCeiling(s.contextTokens));
|
|
849
|
+
}
|
|
850
|
+
return result;
|
|
851
|
+
}
|
|
757
852
|
/** Return all sessions with their current burn rates (tokens/min, null if insufficient data). */
|
|
758
853
|
getAllBurnRates(now) {
|
|
759
854
|
const result = new Map();
|
|
@@ -835,6 +930,46 @@ export class TUI {
|
|
|
835
930
|
getGroupFilter() {
|
|
836
931
|
return this.groupFilter;
|
|
837
932
|
}
|
|
933
|
+
/**
|
|
934
|
+
* Set or clear a custom display name for a session (by 1-indexed number, ID, ID prefix, or title).
|
|
935
|
+
* Returns true if session found. Pass empty/null to clear.
|
|
936
|
+
*/
|
|
937
|
+
renameSession(sessionIdOrIndex, displayName) {
|
|
938
|
+
let sessionId;
|
|
939
|
+
if (typeof sessionIdOrIndex === "number") {
|
|
940
|
+
sessionId = this.sessions[sessionIdOrIndex - 1]?.id;
|
|
941
|
+
}
|
|
942
|
+
else {
|
|
943
|
+
const needle = sessionIdOrIndex.toLowerCase();
|
|
944
|
+
const match = this.sessions.find((s) => s.id === sessionIdOrIndex || s.id.startsWith(needle) || s.title.toLowerCase() === needle);
|
|
945
|
+
sessionId = match?.id;
|
|
946
|
+
}
|
|
947
|
+
if (!sessionId)
|
|
948
|
+
return false;
|
|
949
|
+
if (!displayName || displayName.trim() === "") {
|
|
950
|
+
this.sessionAliases.delete(sessionId);
|
|
951
|
+
}
|
|
952
|
+
else {
|
|
953
|
+
this.sessionAliases.set(sessionId, truncateRename(displayName.trim()));
|
|
954
|
+
}
|
|
955
|
+
if (this.active)
|
|
956
|
+
this.paintSessions();
|
|
957
|
+
return true;
|
|
958
|
+
}
|
|
959
|
+
/** Get the custom display name for a session (or undefined if not renamed). */
|
|
960
|
+
getSessionAlias(id) {
|
|
961
|
+
return this.sessionAliases.get(id);
|
|
962
|
+
}
|
|
963
|
+
/** Return all session aliases (for persistence and listing). */
|
|
964
|
+
getAllSessionAliases() {
|
|
965
|
+
return this.sessionAliases;
|
|
966
|
+
}
|
|
967
|
+
/** Restore session aliases from persisted prefs (bulk restore). */
|
|
968
|
+
restoreSessionAliases(aliases) {
|
|
969
|
+
this.sessionAliases.clear();
|
|
970
|
+
for (const [id, name] of Object.entries(aliases))
|
|
971
|
+
this.sessionAliases.set(id, name);
|
|
972
|
+
}
|
|
838
973
|
/**
|
|
839
974
|
* Add a bookmark at the current activity position.
|
|
840
975
|
* Returns the bookmark number (1-indexed) or 0 if buffer is empty.
|
|
@@ -929,6 +1064,15 @@ export class TUI {
|
|
|
929
1064
|
}
|
|
930
1065
|
}
|
|
931
1066
|
}
|
|
1067
|
+
// check context ceiling and emit alert if above threshold (with cooldown)
|
|
1068
|
+
const ceiling = parseContextCeiling(s.contextTokens);
|
|
1069
|
+
if (ceiling !== null && ceiling.current / ceiling.max >= CONTEXT_CEILING_THRESHOLD) {
|
|
1070
|
+
const lastCeiling = this.ceilingAlerted.get(s.id) ?? 0;
|
|
1071
|
+
if (now - lastCeiling >= BURN_ALERT_COOLDOWN_MS) {
|
|
1072
|
+
this.ceilingAlerted.set(s.id, now);
|
|
1073
|
+
this.log("status", formatContextCeilingAlert(s.title, ceiling.current, ceiling.max), s.id);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
932
1076
|
}
|
|
933
1077
|
// watchdog: alert if session has been idle longer than threshold
|
|
934
1078
|
if (this.watchdogThresholdMs !== null) {
|
|
@@ -1400,6 +1544,20 @@ export class TUI {
|
|
|
1400
1544
|
const errSparkline = errTs ? formatSessionErrorSparkline(errTs, nowMs) : "";
|
|
1401
1545
|
const lastChange = this.lastChangeAt.get(s.id);
|
|
1402
1546
|
const idleSinceMs = lastChange !== undefined ? nowMs - lastChange : undefined;
|
|
1547
|
+
// compute health score
|
|
1548
|
+
const ceiling = parseContextCeiling(s.contextTokens);
|
|
1549
|
+
const contextFraction = ceiling ? ceiling.current / ceiling.max : null;
|
|
1550
|
+
const burnHist = this.sessionContextHistory.get(s.id);
|
|
1551
|
+
const burnRate = burnHist ? computeContextBurnRate(burnHist, nowMs) : null;
|
|
1552
|
+
const healthScore = computeHealthScore({
|
|
1553
|
+
errorCount: this.sessionErrorCounts.get(s.id) ?? 0,
|
|
1554
|
+
burnRatePerMin: burnRate,
|
|
1555
|
+
contextFraction,
|
|
1556
|
+
idleMs: idleSinceMs ?? null,
|
|
1557
|
+
watchdogThresholdMs: this.watchdogThresholdMs,
|
|
1558
|
+
});
|
|
1559
|
+
const healthBadge = formatHealthBadge(healthScore);
|
|
1560
|
+
const displayName = this.sessionAliases.get(s.id);
|
|
1403
1561
|
const muteBadge = muted ? formatMuteBadge(this.mutedEntryCounts.get(s.id) ?? 0) : "";
|
|
1404
1562
|
const muteBadgeWidth = muted ? String(Math.min(this.mutedEntryCounts.get(s.id) ?? 0, 9999)).length + 2 : 0; // "(N)" visible chars, 0 when count is 0
|
|
1405
1563
|
const actualBadgeWidth = (this.mutedEntryCounts.get(s.id) ?? 0) > 0 ? muteBadgeWidth + 1 : 0; // +1 for trailing space
|
|
@@ -1411,7 +1569,7 @@ export class TUI {
|
|
|
1411
1569
|
const badgeSuffix = muteBadge ? `${muteBadge} ` : "";
|
|
1412
1570
|
const iconsWidth = (pinned ? 2 : 0) + (muted ? 2 : 0) + (noted ? 2 : 0) + actualBadgeWidth + groupBadgeWidth;
|
|
1413
1571
|
const cardWidth = innerWidth - 1 - iconsWidth;
|
|
1414
|
-
const line = `${bg}${SLATE}${BOX.v}${RESET}${bg} ${pin}${mute}${badgeSuffix}${note}${groupBadge}${formatSessionCard(s, cardWidth, errSparkline || undefined, idleSinceMs)}`;
|
|
1572
|
+
const line = `${bg}${SLATE}${BOX.v}${RESET}${bg} ${pin}${mute}${badgeSuffix}${note}${groupBadge}${formatSessionCard(s, cardWidth, errSparkline || undefined, idleSinceMs, healthBadge || undefined, displayName)}`;
|
|
1415
1573
|
const padded = padBoxLineHover(line, this.cols, isHovered);
|
|
1416
1574
|
process.stderr.write(moveTo(startRow + 1 + i, 1) + CLEAR_LINE + padded);
|
|
1417
1575
|
}
|
|
@@ -1451,6 +1609,19 @@ export class TUI {
|
|
|
1451
1609
|
const errSparkline = errTs ? formatSessionErrorSparkline(errTs, nowMs2) : "";
|
|
1452
1610
|
const lastChange = this.lastChangeAt.get(s.id);
|
|
1453
1611
|
const idleSinceMs = lastChange !== undefined ? nowMs2 - lastChange : undefined;
|
|
1612
|
+
const ceiling2 = parseContextCeiling(s.contextTokens);
|
|
1613
|
+
const contextFraction2 = ceiling2 ? ceiling2.current / ceiling2.max : null;
|
|
1614
|
+
const burnHist2 = this.sessionContextHistory.get(s.id);
|
|
1615
|
+
const burnRate2 = burnHist2 ? computeContextBurnRate(burnHist2, nowMs2) : null;
|
|
1616
|
+
const healthScore2 = computeHealthScore({
|
|
1617
|
+
errorCount: this.sessionErrorCounts.get(s.id) ?? 0,
|
|
1618
|
+
burnRatePerMin: burnRate2,
|
|
1619
|
+
contextFraction: contextFraction2,
|
|
1620
|
+
idleMs: idleSinceMs ?? null,
|
|
1621
|
+
watchdogThresholdMs: this.watchdogThresholdMs,
|
|
1622
|
+
});
|
|
1623
|
+
const healthBadge2 = formatHealthBadge(healthScore2);
|
|
1624
|
+
const displayName2 = this.sessionAliases.get(s.id);
|
|
1454
1625
|
const muteBadge = muted ? formatMuteBadge(this.mutedEntryCounts.get(s.id) ?? 0) : "";
|
|
1455
1626
|
const actualBadgeWidth = (this.mutedEntryCounts.get(s.id) ?? 0) > 0
|
|
1456
1627
|
? String(Math.min(this.mutedEntryCounts.get(s.id) ?? 0, 9999)).length + 3 : 0; // "(N) " visible chars
|
|
@@ -1462,7 +1633,7 @@ export class TUI {
|
|
|
1462
1633
|
const badgeSuffix = muteBadge ? `${muteBadge} ` : "";
|
|
1463
1634
|
const iconsWidth = (pinned ? 2 : 0) + (muted ? 2 : 0) + (noted ? 2 : 0) + actualBadgeWidth + groupBadgeWidth;
|
|
1464
1635
|
const cardWidth = innerWidth - 1 - iconsWidth;
|
|
1465
|
-
const line = `${bg}${SLATE}${BOX.v}${RESET}${bg} ${pin}${mute}${badgeSuffix}${note}${groupBadge}${formatSessionCard(s, cardWidth, errSparkline || undefined, idleSinceMs)}`;
|
|
1636
|
+
const line = `${bg}${SLATE}${BOX.v}${RESET}${bg} ${pin}${mute}${badgeSuffix}${note}${groupBadge}${formatSessionCard(s, cardWidth, errSparkline || undefined, idleSinceMs, healthBadge2 || undefined, displayName2)}`;
|
|
1466
1637
|
const padded = padBoxLineHover(line, this.cols, isHovered);
|
|
1467
1638
|
process.stderr.write(SAVE_CURSOR + moveTo(startRow + 1 + i, 1) + CLEAR_LINE + padded + RESTORE_CURSOR);
|
|
1468
1639
|
}
|
|
@@ -1584,13 +1755,18 @@ export class TUI {
|
|
|
1584
1755
|
// format a session as a card-style line (inside the box)
|
|
1585
1756
|
// errorSparkline: optional pre-formatted ROSE sparkline string (5 chars) for recent errors
|
|
1586
1757
|
// idleSinceMs: optional ms since last activity change (shown when idle/stopped)
|
|
1587
|
-
|
|
1758
|
+
// healthBadge: optional pre-formatted health score badge ("⬡83" colored)
|
|
1759
|
+
// displayName: optional custom name override (from /rename)
|
|
1760
|
+
function formatSessionCard(s, maxWidth, errorSparkline, idleSinceMs, healthBadge, displayName) {
|
|
1588
1761
|
const dot = STATUS_DOT[s.status] ?? `${AMBER}${DOT.filled}${RESET}`;
|
|
1589
|
-
const
|
|
1762
|
+
const title = displayName ?? s.title;
|
|
1763
|
+
const name = displayName ? `${BOLD}${displayName}${DIM} (${s.title})${RESET}` : `${BOLD}${s.title}${RESET}`;
|
|
1590
1764
|
const toolBadge = `${SLATE}${s.tool}${RESET}`;
|
|
1591
1765
|
const contextBadge = s.contextTokens ? ` ${DIM}(${s.contextTokens})${RESET}` : "";
|
|
1592
1766
|
const sparkSuffix = errorSparkline ? ` ${errorSparkline}` : "";
|
|
1593
|
-
|
|
1767
|
+
const healthPrefix = healthBadge ? `${healthBadge} ` : "";
|
|
1768
|
+
const healthPrefixWidth = healthBadge ? stripAnsiForLen(healthBadge) + 1 : 0;
|
|
1769
|
+
// sparkline + health badge take fixed space so status desc gets less room
|
|
1594
1770
|
const sparkWidth = errorSparkline ? SESSION_SPARK_BUCKETS + 1 : 0;
|
|
1595
1771
|
// idle-since label: show when session is idle/stopped and stale > 2min
|
|
1596
1772
|
const idleLabel = (idleSinceMs !== undefined && (s.status === "idle" || s.status === "stopped" || s.status === "done"))
|
|
@@ -1603,7 +1779,7 @@ function formatSessionCard(s, maxWidth, errorSparkline, idleSinceMs) {
|
|
|
1603
1779
|
}
|
|
1604
1780
|
else if (s.status === "working" || s.status === "running") {
|
|
1605
1781
|
desc = s.currentTask
|
|
1606
|
-
? truncatePlain(s.currentTask, Math.max(20, maxWidth -
|
|
1782
|
+
? truncatePlain(s.currentTask, Math.max(20, maxWidth - title.length - s.tool.length - 16 - sparkWidth))
|
|
1607
1783
|
: `${LIME}working${RESET}`;
|
|
1608
1784
|
}
|
|
1609
1785
|
else if (s.status === "idle" || s.status === "stopped") {
|
|
@@ -1621,7 +1797,7 @@ function formatSessionCard(s, maxWidth, errorSparkline, idleSinceMs) {
|
|
|
1621
1797
|
else {
|
|
1622
1798
|
desc = `${SLATE}${s.status}${RESET}`;
|
|
1623
1799
|
}
|
|
1624
|
-
return truncateAnsi(`${dot} ${name} ${toolBadge}${contextBadge} ${SLATE}${BOX.h}${RESET} ${desc}${sparkSuffix}`, maxWidth);
|
|
1800
|
+
return truncateAnsi(`${dot} ${healthPrefix}${name} ${toolBadge}${contextBadge} ${SLATE}${BOX.h}${RESET} ${desc}${sparkSuffix}`, maxWidth);
|
|
1625
1801
|
}
|
|
1626
1802
|
// colorize an activity entry based on its tag
|
|
1627
1803
|
function formatActivity(entry, maxCols) {
|
|
@@ -1824,6 +2000,31 @@ function formatSparkline(counts) {
|
|
|
1824
2000
|
});
|
|
1825
2001
|
return blocks.join("");
|
|
1826
2002
|
}
|
|
2003
|
+
// ── Context ceiling warning (pure, exported for testing) ─────────────────────
|
|
2004
|
+
/**
|
|
2005
|
+
* Parse a context token string that may include a ceiling: "137,918 / 200,000 tokens".
|
|
2006
|
+
* Returns { current, max } if both values are present, null otherwise.
|
|
2007
|
+
*/
|
|
2008
|
+
export function parseContextCeiling(contextTokens) {
|
|
2009
|
+
if (!contextTokens)
|
|
2010
|
+
return null;
|
|
2011
|
+
const stripped = contextTokens.replace(/,/g, "");
|
|
2012
|
+
const match = stripped.match(/(\d+)\s*\/\s*(\d+)/);
|
|
2013
|
+
if (!match)
|
|
2014
|
+
return null;
|
|
2015
|
+
const current = parseInt(match[1], 10);
|
|
2016
|
+
const max = parseInt(match[2], 10);
|
|
2017
|
+
if (isNaN(current) || isNaN(max) || max <= 0)
|
|
2018
|
+
return null;
|
|
2019
|
+
return { current, max };
|
|
2020
|
+
}
|
|
2021
|
+
/** Alert threshold: fire ceiling warning when context usage exceeds this fraction. */
|
|
2022
|
+
export const CONTEXT_CEILING_THRESHOLD = 0.90;
|
|
2023
|
+
/** Format a context ceiling alert for the activity log. */
|
|
2024
|
+
export function formatContextCeilingAlert(title, current, max) {
|
|
2025
|
+
const pct = Math.round((current / max) * 100);
|
|
2026
|
+
return `${title}: context at ${pct}% (${current.toLocaleString()} / ${max.toLocaleString()} tokens) — approaching limit`;
|
|
2027
|
+
}
|
|
1827
2028
|
// ── Context burn-rate tracking (pure, exported for testing) ─────────────────
|
|
1828
2029
|
/** Parse a context token string like "137,918 tokens" to a raw number, or null if unparseable. */
|
|
1829
2030
|
export function parseContextTokenNumber(contextTokens) {
|