agenthud 0.9.3 → 0.10.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 +29 -13
- package/dist/index.js +1 -1
- package/dist/{main-S27FZ2BJ.js → main-HPL3AG6B.js} +536 -168
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,10 +4,16 @@
|
|
|
4
4
|
[](https://github.com/neochoon/agenthud/actions/workflows/ci.yml)
|
|
5
5
|
[](https://codecov.io/gh/neochoon/agenthud)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
An observability layer for [Claude Code](https://github.com/anthropics/claude-code). **See** your live sessions, **export** structured activity logs, and **summarize** a day or a week into an LLM digest — all from one CLI.
|
|
8
8
|
|
|
9
9
|

|
|
10
10
|
|
|
11
|
+
AgentHUD reads Claude Code's session files from `~/.claude/projects/` and gives you three things:
|
|
12
|
+
|
|
13
|
+
- **Live monitor** ([`agenthud`](#live-monitor)) — a split-view TUI showing every project, session, sub-agent, and activity as it happens.
|
|
14
|
+
- **Structured export** ([`agenthud report`](#report)) — print activity for any date as Markdown or JSON for piping to scripts, dashboards, or other LLMs.
|
|
15
|
+
- **LLM digest** ([`agenthud summary`](#summary)) — synthesize a day or a date range into an engineering summary via the `claude` CLI, with caching so weekly digests are cheap to regenerate.
|
|
16
|
+
|
|
11
17
|
## Install
|
|
12
18
|
|
|
13
19
|
Requires Node.js 20+.
|
|
@@ -18,9 +24,9 @@ npx agenthud
|
|
|
18
24
|
|
|
19
25
|
Run this in a separate terminal while using Claude Code. Press `?` inside the TUI any time for in-app help.
|
|
20
26
|
|
|
21
|
-
##
|
|
27
|
+
## Live monitor
|
|
22
28
|
|
|
23
|
-
AgentHUD
|
|
29
|
+
AgentHUD's TUI splits the screen into a project tree and an activity viewer:
|
|
24
30
|
|
|
25
31
|
```
|
|
26
32
|
┌─ Projects ───────────────────────────────────────────────┐
|
|
@@ -37,8 +43,7 @@ AgentHUD reads Claude Code's session files from `~/.claude/projects/` and displa
|
|
|
37
43
|
│ [10:23] ~ Edit src/ui/App.tsx │
|
|
38
44
|
│ [10:23] $ Bash npm test │
|
|
39
45
|
│ [10:23] < Response Tests passed successfully │
|
|
40
|
-
│ [10:25]
|
|
41
|
-
│ › │
|
|
46
|
+
│ [10:25] ⠧ Edit src/auth/oauth.ts ← bold + spinner = live │
|
|
42
47
|
└──────────────────────────────────────────────────────────┘
|
|
43
48
|
```
|
|
44
49
|
|
|
@@ -53,11 +58,11 @@ AgentHUD reads Claude Code's session files from `~/.claude/projects/` and displa
|
|
|
53
58
|
|
|
54
59
|
**Activity viewer (bottom pane)**
|
|
55
60
|
- Real-time feed for the selected session: file reads, edits, bash, responses, thinking, git commits. Newest at the bottom, like `tail -f`.
|
|
56
|
-
-
|
|
61
|
+
- The **newest visible row is rendered "alive"** while in LIVE mode: its icon is replaced with a spinning glyph and the text turns bold. When a new activity arrives, the spinner moves to the new bottom row. Hidden when paused or empty.
|
|
57
62
|
- Press `f` to cycle through filter presets (configurable).
|
|
58
63
|
- Press `↵` on any row to open a scrollable detail view; on a commit row this shows `git show --stat --patch`.
|
|
59
64
|
|
|
60
|
-
|
|
65
|
+
### Session status
|
|
61
66
|
|
|
62
67
|
Each session row carries a colored badge derived from when its JSONL file was last touched:
|
|
63
68
|
|
|
@@ -70,7 +75,7 @@ Each session row carries a colored badge derived from when its JSONL file was la
|
|
|
70
75
|
|
|
71
76
|
Sub-agents use the same scheme. Projects inherit the hottest status of their sessions; a project is treated as "cold" only when all its sessions are cold.
|
|
72
77
|
|
|
73
|
-
|
|
78
|
+
### Activity types
|
|
74
79
|
|
|
75
80
|
| Icon | Type | Description |
|
|
76
81
|
|------|------|-------------|
|
|
@@ -85,11 +90,11 @@ Sub-agents use the same scheme. Projects inherit the hottest status of their ses
|
|
|
85
90
|
| `…` | Thinking | Claude's thinking (requires `showThinkingSummaries: true`) |
|
|
86
91
|
| `◆` | Commit | Git commit in the project (when `--with-git` or in viewer) |
|
|
87
92
|
|
|
88
|
-
|
|
93
|
+
### Keyboard shortcuts
|
|
89
94
|
|
|
90
95
|
Full reference is also available inside the app — press `?`.
|
|
91
96
|
|
|
92
|
-
|
|
97
|
+
#### Project tree focus
|
|
93
98
|
|
|
94
99
|
| Key | Action |
|
|
95
100
|
|-----|--------|
|
|
@@ -100,11 +105,14 @@ Full reference is also available inside the app — press `?`.
|
|
|
100
105
|
| `Tab` | Switch focus to activity viewer |
|
|
101
106
|
| `PgUp` / `Ctrl+B` | Page up |
|
|
102
107
|
| `PgDn` / `Ctrl+F` | Page down |
|
|
108
|
+
| `t` | Track — auto-follow the newest live sub-agent (any nav key turns it off) |
|
|
103
109
|
| `r` | Refresh now |
|
|
104
110
|
| `?` | Help |
|
|
105
111
|
| `q` | Quit |
|
|
106
112
|
|
|
107
|
-
|
|
113
|
+
When tracking is on, the tree panel's title shows `[LIVE ⠧]` and the status bar replaces `t: track` with `TRK ●`. Any explicit selection-changing key (`↑/k`, `↓/j`, `PgUp/PgDn`, `↵`, `h`, or `t` again) turns tracking off.
|
|
114
|
+
|
|
115
|
+
#### Activity viewer focus
|
|
108
116
|
|
|
109
117
|
| Key | Action |
|
|
110
118
|
|-----|--------|
|
|
@@ -122,7 +130,7 @@ Full reference is also available inside the app — press `?`.
|
|
|
122
130
|
| `?` | Help |
|
|
123
131
|
| `q` | Quit |
|
|
124
132
|
|
|
125
|
-
|
|
133
|
+
#### Detail view
|
|
126
134
|
|
|
127
135
|
| Key | Action |
|
|
128
136
|
|-----|--------|
|
|
@@ -134,7 +142,7 @@ Detail view colors the content based on activity type:
|
|
|
134
142
|
- **Git commit detail** (`git show --stat --patch`): added lines green (`+`), removed lines red (`-`), hunk headers cyan (`@@ ... @@`), `commit/Author/Date/diff` metadata dimmed.
|
|
135
143
|
- **Response / thinking / prompt**: text inside triple-backtick code fences renders in cyan so the boundary between prose and code is obvious. No language-specific syntax highlighting — just code-vs-prose separation.
|
|
136
144
|
|
|
137
|
-
|
|
145
|
+
### Behavior
|
|
138
146
|
|
|
139
147
|
- **Alternate screen buffer.** Watch mode uses the alt-screen (like `vim`, `htop`, `btop`), so quitting (`q`) restores the pre-launch shell completely. No TUI residue, no "is it still running?" confusion.
|
|
140
148
|
- **Minimum terminal size.** 80 cols × 20 rows. Smaller terminals show a one-line hint and redraw automatically when you resize.
|
|
@@ -194,6 +202,10 @@ agenthud summary --prompt "Only commits" # override prompt
|
|
|
194
202
|
agenthud summary --last 7d # last 7 days, ending today
|
|
195
203
|
agenthud summary --from 2026-05-10 --to 2026-05-16 # explicit range
|
|
196
204
|
agenthud summary --last 7d -y # skip per-day confirmations
|
|
205
|
+
|
|
206
|
+
# Cheaper model — summarization doesn't need Opus-tier reasoning
|
|
207
|
+
agenthud summary --date today --model sonnet # ~40% cheaper than Opus
|
|
208
|
+
agenthud summary --last 7d --model haiku # ~80% cheaper, 200K context
|
|
197
209
|
```
|
|
198
210
|
|
|
199
211
|
**Daily summaries** are saved to `~/.agenthud/summaries/YYYY-MM-DD.md`. Past dates are cached and returned instantly; today is always regenerated (activity still growing).
|
|
@@ -206,6 +218,10 @@ Each missing daily prompts for confirmation just before generation, so you see c
|
|
|
206
218
|
|
|
207
219
|
**`--date` formats:** `YYYY-MM-DD`, `today`, `yesterday`, or `-Nd` (N days ago).
|
|
208
220
|
|
|
221
|
+
**Model selection:** Summarization is a low-reasoning task (structured input → structured markdown) — Sonnet or Haiku usually beats Opus on cost-per-summary with no quality loss. Pass `--model sonnet`, `--model haiku`, or a full model id (`--model claude-sonnet-4-6`). With no flag, `claude` uses its default model.
|
|
222
|
+
|
|
223
|
+
**Cost warning:** If the day's activity log is large (~300K tokens or more), AgentHUD prints a warning before sending and asks for one more confirmation in interactive mode. `-y` skips the prompt but still prints the warning.
|
|
224
|
+
|
|
209
225
|
**Requires:** [`@anthropic-ai/claude-code`](https://www.npmjs.com/package/@anthropic-ai/claude-code) installed and authenticated.
|
|
210
226
|
|
|
211
227
|
## Configuration
|
package/dist/index.js
CHANGED
|
@@ -53,6 +53,7 @@ var KNOWN_SUMMARY_FLAGS = /* @__PURE__ */ new Set([
|
|
|
53
53
|
"--to",
|
|
54
54
|
"--prompt",
|
|
55
55
|
"--force",
|
|
56
|
+
"--model",
|
|
56
57
|
"-y",
|
|
57
58
|
"--yes"
|
|
58
59
|
]);
|
|
@@ -89,6 +90,7 @@ Commands:
|
|
|
89
90
|
--to YYYY-MM-DD Date range: end date (use with --from)
|
|
90
91
|
--prompt TEXT Override prompt for this run (daily only)
|
|
91
92
|
--force Regenerate even if cached
|
|
93
|
+
--model NAME Pass --model to claude (e.g. "sonnet", "haiku", or a full model ID)
|
|
92
94
|
-y, --yes Skip confirmation prompts for new daily summaries
|
|
93
95
|
|
|
94
96
|
Environment:
|
|
@@ -172,10 +174,18 @@ function parseArgs(args) {
|
|
|
172
174
|
const includeIdx = rest.indexOf("--include");
|
|
173
175
|
if (includeIdx !== -1) {
|
|
174
176
|
const includeStr = rest[includeIdx + 1];
|
|
175
|
-
if (includeStr
|
|
177
|
+
if (!includeStr) {
|
|
178
|
+
reportError = "Invalid --include: missing value.";
|
|
179
|
+
} else if (includeStr === "all") {
|
|
176
180
|
reportInclude = ALL_TYPES;
|
|
177
|
-
} else
|
|
178
|
-
|
|
181
|
+
} else {
|
|
182
|
+
const tokens = includeStr.split(",").map((s) => s.trim()).filter(Boolean);
|
|
183
|
+
const unknown = tokens.filter((t) => !ALL_TYPES.includes(t));
|
|
184
|
+
if (unknown.length > 0) {
|
|
185
|
+
reportError = `Unknown --include type${unknown.length > 1 ? "s" : ""}: ${unknown.map((u) => `"${u}"`).join(", ")}. Valid types: ${ALL_TYPES.join(", ")} (or "all").`;
|
|
186
|
+
} else {
|
|
187
|
+
reportInclude = tokens;
|
|
188
|
+
}
|
|
179
189
|
}
|
|
180
190
|
}
|
|
181
191
|
let reportFormat = "markdown";
|
|
@@ -220,13 +230,15 @@ function parseArgs(args) {
|
|
|
220
230
|
let summaryPrompt;
|
|
221
231
|
let summaryForce = false;
|
|
222
232
|
let summaryAssumeYes = false;
|
|
233
|
+
let summaryModel;
|
|
223
234
|
let summaryError;
|
|
224
235
|
const FLAGS_WITH_VALUE = /* @__PURE__ */ new Set([
|
|
225
236
|
"--date",
|
|
226
237
|
"--last",
|
|
227
238
|
"--from",
|
|
228
239
|
"--to",
|
|
229
|
-
"--prompt"
|
|
240
|
+
"--prompt",
|
|
241
|
+
"--model"
|
|
230
242
|
]);
|
|
231
243
|
for (let i = 0; i < rest.length; i++) {
|
|
232
244
|
const arg = rest[i];
|
|
@@ -318,6 +330,15 @@ function parseArgs(args) {
|
|
|
318
330
|
summaryPrompt = val;
|
|
319
331
|
}
|
|
320
332
|
}
|
|
333
|
+
const modelIdx = rest.indexOf("--model");
|
|
334
|
+
if (modelIdx !== -1) {
|
|
335
|
+
const val = rest[modelIdx + 1];
|
|
336
|
+
if (!val) {
|
|
337
|
+
summaryError = "Invalid --model: missing value (e.g. --model sonnet).";
|
|
338
|
+
} else {
|
|
339
|
+
summaryModel = val;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
321
342
|
if (rest.includes("--force")) summaryForce = true;
|
|
322
343
|
if (rest.includes("-y") || rest.includes("--yes")) summaryAssumeYes = true;
|
|
323
344
|
return {
|
|
@@ -328,6 +349,7 @@ function parseArgs(args) {
|
|
|
328
349
|
summaryPrompt,
|
|
329
350
|
summaryForce,
|
|
330
351
|
summaryAssumeYes,
|
|
352
|
+
summaryModel,
|
|
331
353
|
summaryError
|
|
332
354
|
};
|
|
333
355
|
}
|
|
@@ -663,20 +685,11 @@ function parseGitCommits(projectPath, startDate, endDate) {
|
|
|
663
685
|
// src/data/sessionHistory.ts
|
|
664
686
|
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
|
|
665
687
|
|
|
666
|
-
// src/data/
|
|
688
|
+
// src/data/toolDetails.ts
|
|
667
689
|
import { basename } from "path";
|
|
668
690
|
function stripAnsi(text) {
|
|
669
691
|
return text.replace(/\x1b\[[0-9;]*m/g, "");
|
|
670
692
|
}
|
|
671
|
-
function parseModelName(modelId) {
|
|
672
|
-
const opusMatch = modelId.match(/claude-opus-(\d+)-(\d+)/);
|
|
673
|
-
if (opusMatch) return `opus-${opusMatch[1]}.${opusMatch[2]}`;
|
|
674
|
-
const sonnetMatch = modelId.match(/claude-sonnet-(\d+)/);
|
|
675
|
-
if (sonnetMatch) return `sonnet-${sonnetMatch[1]}`;
|
|
676
|
-
const haikuMatch = modelId.match(/claude-(\d+)-(\d+)-haiku/);
|
|
677
|
-
if (haikuMatch) return `haiku-${haikuMatch[1]}.${haikuMatch[2]}`;
|
|
678
|
-
return modelId.replace(/-\d{8}$/, "");
|
|
679
|
-
}
|
|
680
693
|
function getToolDetail(_toolName, input) {
|
|
681
694
|
if (!input) return "";
|
|
682
695
|
if (input.command) return stripAnsi(input.command);
|
|
@@ -686,11 +699,128 @@ function getToolDetail(_toolName, input) {
|
|
|
686
699
|
if (input.description) return stripAnsi(input.description);
|
|
687
700
|
return "";
|
|
688
701
|
}
|
|
702
|
+
function rangeStr(start, lines) {
|
|
703
|
+
return `L${start}-${start + Math.max(lines, 1) - 1}`;
|
|
704
|
+
}
|
|
705
|
+
function patchSpan(hunks) {
|
|
706
|
+
if (hunks.length === 0) return null;
|
|
707
|
+
const start = Math.min(...hunks.map((h) => h.newStart));
|
|
708
|
+
const end = Math.max(
|
|
709
|
+
...hunks.map((h) => h.newStart + Math.max(h.newLines, 1) - 1)
|
|
710
|
+
);
|
|
711
|
+
return `L${start}-${end}`;
|
|
712
|
+
}
|
|
713
|
+
function countChanges(hunks) {
|
|
714
|
+
let add = 0;
|
|
715
|
+
let del = 0;
|
|
716
|
+
for (const h of hunks) {
|
|
717
|
+
for (const line of h.lines ?? []) {
|
|
718
|
+
if (line.startsWith("+")) add++;
|
|
719
|
+
else if (line.startsWith("-")) del++;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
const parts = [];
|
|
723
|
+
if (add > 0) parts.push(`+${add}`);
|
|
724
|
+
if (del > 0) parts.push(`-${del}`);
|
|
725
|
+
return parts.join(" ");
|
|
726
|
+
}
|
|
727
|
+
function joinParts(...parts) {
|
|
728
|
+
return parts.filter((p) => !!p).join(" ");
|
|
729
|
+
}
|
|
730
|
+
function summarizeToolDetail(name, input, result) {
|
|
731
|
+
const file = input?.file_path ? basename(input.file_path) : "";
|
|
732
|
+
if (name === "Edit" || name === "Write") {
|
|
733
|
+
const hunks = result?.structuredPatch;
|
|
734
|
+
if (hunks && hunks.length > 0) {
|
|
735
|
+
return joinParts(file, patchSpan(hunks), countChanges(hunks));
|
|
736
|
+
}
|
|
737
|
+
if (name === "Write") {
|
|
738
|
+
const content = result?.content ?? input?.content;
|
|
739
|
+
if (content) {
|
|
740
|
+
const n = content.split("\n").length - (content.endsWith("\n") ? 1 : 0);
|
|
741
|
+
return joinParts(file, rangeStr(1, n), `+${n}`);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
return file;
|
|
745
|
+
}
|
|
746
|
+
if (name === "Read") {
|
|
747
|
+
const f = result?.file;
|
|
748
|
+
if (typeof f?.startLine === "number" && typeof f?.numLines === "number") {
|
|
749
|
+
return joinParts(file, rangeStr(f.startLine, f.numLines));
|
|
750
|
+
}
|
|
751
|
+
if (typeof input?.offset === "number" && typeof input?.limit === "number") {
|
|
752
|
+
return joinParts(file, rangeStr(input.offset, input.limit));
|
|
753
|
+
}
|
|
754
|
+
return file;
|
|
755
|
+
}
|
|
756
|
+
if (name === "TaskUpdate") {
|
|
757
|
+
const id = input?.taskId ?? result?.taskId;
|
|
758
|
+
const idStr = id ? `#${id}` : "";
|
|
759
|
+
const sc = result?.statusChange;
|
|
760
|
+
if (sc?.from && sc?.to) return joinParts(idStr, `${sc.from}\u2192${sc.to}`);
|
|
761
|
+
if (result?.updatedFields?.length) {
|
|
762
|
+
return joinParts(idStr, result.updatedFields.join(", "));
|
|
763
|
+
}
|
|
764
|
+
if (input?.status) return joinParts(idStr, input.status);
|
|
765
|
+
return idStr;
|
|
766
|
+
}
|
|
767
|
+
if (name === "TaskCreate") {
|
|
768
|
+
return input?.subject ?? "";
|
|
769
|
+
}
|
|
770
|
+
return getToolDetail(name, input);
|
|
771
|
+
}
|
|
772
|
+
function buildToolDetailBody(name, input, result) {
|
|
773
|
+
if (name === "Write") {
|
|
774
|
+
const content = result?.content ?? input?.content;
|
|
775
|
+
if (content) return { text: content, kind: "code" };
|
|
776
|
+
}
|
|
777
|
+
if (name === "Edit" || name === "Write") {
|
|
778
|
+
const hunks = result?.structuredPatch;
|
|
779
|
+
if (hunks && hunks.length > 0) {
|
|
780
|
+
const text = hunks.map(
|
|
781
|
+
(h) => `@@ -${h.oldStart},${h.oldLines} +${h.newStart},${h.newLines} @@
|
|
782
|
+
${(h.lines ?? []).join("\n")}`
|
|
783
|
+
).join("\n");
|
|
784
|
+
return { text, kind: "diff" };
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
return null;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// src/data/activityParser.ts
|
|
791
|
+
function parseModelName(modelId) {
|
|
792
|
+
const opusMatch = modelId.match(/claude-opus-(\d+)-(\d+)/);
|
|
793
|
+
if (opusMatch) return `opus-${opusMatch[1]}.${opusMatch[2]}`;
|
|
794
|
+
const sonnetMatch = modelId.match(/claude-sonnet-(\d+)/);
|
|
795
|
+
if (sonnetMatch) return `sonnet-${sonnetMatch[1]}`;
|
|
796
|
+
const haikuMatch = modelId.match(/claude-(\d+)-(\d+)-haiku/);
|
|
797
|
+
if (haikuMatch) return `haiku-${haikuMatch[1]}.${haikuMatch[2]}`;
|
|
798
|
+
return modelId.replace(/-\d{8}$/, "");
|
|
799
|
+
}
|
|
689
800
|
function parseActivitiesFromLines(lines) {
|
|
690
801
|
const activities = [];
|
|
691
802
|
let tokenCount = 0;
|
|
692
803
|
let modelName = null;
|
|
693
804
|
let sessionStartTime = null;
|
|
805
|
+
const resultsById = /* @__PURE__ */ new Map();
|
|
806
|
+
for (const line of lines) {
|
|
807
|
+
let entry;
|
|
808
|
+
try {
|
|
809
|
+
entry = JSON.parse(line);
|
|
810
|
+
} catch {
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
if (entry.type !== "user") continue;
|
|
814
|
+
const tur = entry.toolUseResult;
|
|
815
|
+
if (!tur || typeof tur !== "object") continue;
|
|
816
|
+
const content = entry.message?.content;
|
|
817
|
+
if (!Array.isArray(content)) continue;
|
|
818
|
+
for (const b of content) {
|
|
819
|
+
if (b?.type === "tool_result" && typeof b.tool_use_id === "string") {
|
|
820
|
+
resultsById.set(b.tool_use_id, tur);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
694
824
|
for (const line of lines) {
|
|
695
825
|
let entry;
|
|
696
826
|
try {
|
|
@@ -745,19 +875,26 @@ function parseActivitiesFromLines(lines) {
|
|
|
745
875
|
} else if (block.type === "tool_use" && block.name) {
|
|
746
876
|
if (block.name === "TodoWrite") continue;
|
|
747
877
|
const icon = ICONS[block.name] ?? ICONS.Default;
|
|
748
|
-
const
|
|
878
|
+
const result = block.id ? resultsById.get(block.id) : void 0;
|
|
879
|
+
const detail = summarizeToolDetail(block.name, block.input, result);
|
|
880
|
+
const body = buildToolDetailBody(block.name, block.input, result);
|
|
749
881
|
const last = activities[activities.length - 1];
|
|
750
882
|
if (last && last.type === "tool" && last.label === block.name && last.detail === detail) {
|
|
751
883
|
last.count = (last.count ?? 1) + 1;
|
|
752
884
|
last.timestamp = timestamp;
|
|
753
885
|
} else {
|
|
754
|
-
|
|
886
|
+
const entry2 = {
|
|
755
887
|
timestamp,
|
|
756
888
|
type: "tool",
|
|
757
889
|
icon,
|
|
758
890
|
label: block.name,
|
|
759
891
|
detail
|
|
760
|
-
}
|
|
892
|
+
};
|
|
893
|
+
if (body) {
|
|
894
|
+
entry2.detailBody = body.text;
|
|
895
|
+
entry2.detailKind = body.kind;
|
|
896
|
+
}
|
|
897
|
+
activities.push(entry2);
|
|
761
898
|
}
|
|
762
899
|
} else if (block.type === "text" && block.text && block.text.length > 10) {
|
|
763
900
|
activities.push({
|
|
@@ -1004,6 +1141,33 @@ function getDisplayWidth(s) {
|
|
|
1004
1141
|
return w;
|
|
1005
1142
|
}
|
|
1006
1143
|
|
|
1144
|
+
// src/data/sessionLiveness.ts
|
|
1145
|
+
function detectLiveState(tailLines, mtimeMs, now) {
|
|
1146
|
+
if (now - mtimeMs > THIRTY_MINUTES_MS) return null;
|
|
1147
|
+
for (let i = tailLines.length - 1; i >= 0; i--) {
|
|
1148
|
+
const line = tailLines[i];
|
|
1149
|
+
if (!line || !line.trim()) continue;
|
|
1150
|
+
let entry;
|
|
1151
|
+
try {
|
|
1152
|
+
entry = JSON.parse(line);
|
|
1153
|
+
} catch {
|
|
1154
|
+
continue;
|
|
1155
|
+
}
|
|
1156
|
+
if (entry.type === "assistant") {
|
|
1157
|
+
const content = entry.message?.content;
|
|
1158
|
+
const blocks = Array.isArray(content) ? content : [];
|
|
1159
|
+
const toolUses = blocks.filter((b) => b && b.type === "tool_use");
|
|
1160
|
+
if (toolUses.length === 0) return "waiting";
|
|
1161
|
+
if (toolUses.some((b) => b.name === "AskUserQuestion")) return "waiting";
|
|
1162
|
+
return "working";
|
|
1163
|
+
}
|
|
1164
|
+
if (entry.type === "user") {
|
|
1165
|
+
return "working";
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
return null;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1007
1171
|
// src/data/sessions.ts
|
|
1008
1172
|
function getProjectsDir() {
|
|
1009
1173
|
return process.env.CLAUDE_PROJECTS_DIR ?? join4(homedir2(), ".claude", "projects");
|
|
@@ -1057,23 +1221,26 @@ function readSubAgentInfo(filePath) {
|
|
|
1057
1221
|
return { agentId: null, taskDescription: null };
|
|
1058
1222
|
}
|
|
1059
1223
|
}
|
|
1060
|
-
function
|
|
1061
|
-
if (!existsSync3(filePath)) return null;
|
|
1224
|
+
function readSessionTail(filePath, mtimeMs, now) {
|
|
1225
|
+
if (!existsSync3(filePath)) return { modelName: null, liveState: null };
|
|
1062
1226
|
try {
|
|
1063
1227
|
const content = readFileSync4(filePath, "utf-8");
|
|
1064
|
-
const
|
|
1065
|
-
|
|
1228
|
+
const tail = content.trim().split("\n").filter(Boolean).slice(-50);
|
|
1229
|
+
let modelName = null;
|
|
1230
|
+
for (const line of [...tail].reverse()) {
|
|
1066
1231
|
try {
|
|
1067
1232
|
const entry = JSON.parse(line);
|
|
1068
1233
|
if (entry.type === "assistant" && entry.message?.model) {
|
|
1069
|
-
|
|
1234
|
+
modelName = parseModelName(entry.message.model);
|
|
1235
|
+
break;
|
|
1070
1236
|
}
|
|
1071
1237
|
} catch {
|
|
1072
1238
|
}
|
|
1073
1239
|
}
|
|
1240
|
+
return { modelName, liveState: detectLiveState(tail, mtimeMs, now) };
|
|
1074
1241
|
} catch {
|
|
1242
|
+
return { modelName: null, liveState: null };
|
|
1075
1243
|
}
|
|
1076
|
-
return null;
|
|
1077
1244
|
}
|
|
1078
1245
|
var SYSTEM_PREFIXES = [
|
|
1079
1246
|
"<command-name>",
|
|
@@ -1157,6 +1324,11 @@ function buildSubAgents(parentId, projectDir, config, projectName) {
|
|
|
1157
1324
|
try {
|
|
1158
1325
|
const stat = statSync2(filePath);
|
|
1159
1326
|
const { agentId, taskDescription } = readSubAgentInfo(filePath);
|
|
1327
|
+
const { modelName, liveState } = readSessionTail(
|
|
1328
|
+
filePath,
|
|
1329
|
+
stat.mtimeMs,
|
|
1330
|
+
Date.now()
|
|
1331
|
+
);
|
|
1160
1332
|
return {
|
|
1161
1333
|
id,
|
|
1162
1334
|
hideKey,
|
|
@@ -1165,12 +1337,13 @@ function buildSubAgents(parentId, projectDir, config, projectName) {
|
|
|
1165
1337
|
projectName: "",
|
|
1166
1338
|
lastModifiedMs: stat.mtimeMs,
|
|
1167
1339
|
status: getSessionStatus(stat.mtimeMs),
|
|
1168
|
-
modelName
|
|
1340
|
+
modelName,
|
|
1169
1341
|
subAgents: [],
|
|
1170
1342
|
agentId: agentId ?? void 0,
|
|
1171
1343
|
taskDescription: taskDescription ?? void 0,
|
|
1172
1344
|
nonInteractive: false,
|
|
1173
|
-
firstUserPrompt: null
|
|
1345
|
+
firstUserPrompt: null,
|
|
1346
|
+
liveState
|
|
1174
1347
|
};
|
|
1175
1348
|
} catch {
|
|
1176
1349
|
return null;
|
|
@@ -1226,6 +1399,12 @@ function discoverSessions(config) {
|
|
|
1226
1399
|
try {
|
|
1227
1400
|
const stat = statSync2(filePath);
|
|
1228
1401
|
const subAgents = buildSubAgents(id, projectDir, config, projectName);
|
|
1402
|
+
const nonInteractive = readEntrypoint(filePath) === "sdk-cli";
|
|
1403
|
+
const { modelName, liveState } = readSessionTail(
|
|
1404
|
+
filePath,
|
|
1405
|
+
stat.mtimeMs,
|
|
1406
|
+
Date.now()
|
|
1407
|
+
);
|
|
1229
1408
|
allSessions.push({
|
|
1230
1409
|
id,
|
|
1231
1410
|
hideKey,
|
|
@@ -1234,10 +1413,11 @@ function discoverSessions(config) {
|
|
|
1234
1413
|
projectName,
|
|
1235
1414
|
lastModifiedMs: stat.mtimeMs,
|
|
1236
1415
|
status: getSessionStatus(stat.mtimeMs),
|
|
1237
|
-
modelName
|
|
1416
|
+
modelName,
|
|
1238
1417
|
subAgents,
|
|
1239
|
-
nonInteractive
|
|
1240
|
-
firstUserPrompt: readFirstUserPrompt(filePath)
|
|
1418
|
+
nonInteractive,
|
|
1419
|
+
firstUserPrompt: readFirstUserPrompt(filePath),
|
|
1420
|
+
liveState: nonInteractive ? null : liveState
|
|
1241
1421
|
});
|
|
1242
1422
|
} catch {
|
|
1243
1423
|
}
|
|
@@ -1391,16 +1571,18 @@ function formatUsage(u) {
|
|
|
1391
1571
|
}
|
|
1392
1572
|
function spawnClaude(opts) {
|
|
1393
1573
|
return new Promise((resolve2) => {
|
|
1574
|
+
const args = [
|
|
1575
|
+
"-p",
|
|
1576
|
+
"--no-session-persistence",
|
|
1577
|
+
"--output-format",
|
|
1578
|
+
"stream-json",
|
|
1579
|
+
"--verbose"
|
|
1580
|
+
];
|
|
1581
|
+
if (opts.model) args.push("--model", opts.model);
|
|
1582
|
+
args.push(opts.prompt);
|
|
1394
1583
|
const proc = spawn(
|
|
1395
1584
|
"claude",
|
|
1396
|
-
|
|
1397
|
-
"-p",
|
|
1398
|
-
"--no-session-persistence",
|
|
1399
|
-
"--output-format",
|
|
1400
|
-
"stream-json",
|
|
1401
|
-
"--verbose",
|
|
1402
|
-
opts.prompt
|
|
1403
|
-
],
|
|
1585
|
+
args,
|
|
1404
1586
|
{
|
|
1405
1587
|
stdio: ["pipe", "pipe", "pipe"],
|
|
1406
1588
|
cwd: agenthudHomeDir()
|
|
@@ -1513,6 +1695,7 @@ function spawnClaude(opts) {
|
|
|
1513
1695
|
proc.stdin.end(opts.stdin);
|
|
1514
1696
|
});
|
|
1515
1697
|
}
|
|
1698
|
+
var REPORT_TOKEN_WARN_THRESHOLD = 3e5;
|
|
1516
1699
|
async function generateDailySummary(opts) {
|
|
1517
1700
|
ensureUserPromptFile("daily");
|
|
1518
1701
|
const isToday = isSameLocalDay2(opts.date, opts.today);
|
|
@@ -1556,6 +1739,8 @@ async function generateDailySummary(opts) {
|
|
|
1556
1739
|
detailLimit: 0,
|
|
1557
1740
|
withGit: true
|
|
1558
1741
|
});
|
|
1742
|
+
const reportBytes = Buffer.byteLength(reportMarkdown, "utf-8");
|
|
1743
|
+
const estimatedTokens = Math.ceil(reportBytes / 4);
|
|
1559
1744
|
if (opts.announce) {
|
|
1560
1745
|
const reportLines = reportMarkdown.split("\n");
|
|
1561
1746
|
const sessionCount = reportLines.filter((l) => l.startsWith("## ")).length;
|
|
@@ -1565,11 +1750,9 @@ async function generateDailySummary(opts) {
|
|
|
1565
1750
|
const commitCount = reportLines.filter(
|
|
1566
1751
|
(l) => /^\[\d{2}:\d{2}\] ◆/.test(l)
|
|
1567
1752
|
).length;
|
|
1568
|
-
const sizeKb = (
|
|
1569
|
-
1
|
|
1570
|
-
);
|
|
1753
|
+
const sizeKb = (reportBytes / 1024).toFixed(1);
|
|
1571
1754
|
process.stderr.write(
|
|
1572
|
-
`agenthud: input: ${sessionCount} sessions, ${activityCount} activities, ${commitCount} commits (${reportLines.length} lines, ${sizeKb}KB)
|
|
1755
|
+
`agenthud: input: ${sessionCount} sessions, ${activityCount} activities, ${commitCount} commits (${reportLines.length} lines, ${sizeKb}KB \u2248 ${estimatedTokens.toLocaleString()} tokens)
|
|
1573
1756
|
`
|
|
1574
1757
|
);
|
|
1575
1758
|
}
|
|
@@ -1585,6 +1768,29 @@ async function generateDailySummary(opts) {
|
|
|
1585
1768
|
};
|
|
1586
1769
|
}
|
|
1587
1770
|
}
|
|
1771
|
+
if (estimatedTokens > REPORT_TOKEN_WARN_THRESHOLD) {
|
|
1772
|
+
const sizeMb = (reportBytes / (1024 * 1024)).toFixed(1);
|
|
1773
|
+
process.stderr.write(
|
|
1774
|
+
`agenthud: \u26A0 report is large (~${estimatedTokens.toLocaleString()} tokens, ${sizeMb}MB). Cost will be high; very long reports may exceed context.
|
|
1775
|
+
`
|
|
1776
|
+
);
|
|
1777
|
+
if (!opts.assumeYes) {
|
|
1778
|
+
const proceed = await ask("Send anyway? [Y/n] ", true);
|
|
1779
|
+
if (!proceed) {
|
|
1780
|
+
process.stderr.write(
|
|
1781
|
+
`agenthud: ${dateLabel} \u2014 aborted (report too large).
|
|
1782
|
+
`
|
|
1783
|
+
);
|
|
1784
|
+
return {
|
|
1785
|
+
code: 0,
|
|
1786
|
+
markdown: "",
|
|
1787
|
+
fromCache: false,
|
|
1788
|
+
skipped: true,
|
|
1789
|
+
usage: null
|
|
1790
|
+
};
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1588
1794
|
if (opts.announce) {
|
|
1589
1795
|
process.stderr.write(
|
|
1590
1796
|
`agenthud: sending to claude (this may take a minute)...
|
|
@@ -1597,7 +1803,8 @@ async function generateDailySummary(opts) {
|
|
|
1597
1803
|
prompt,
|
|
1598
1804
|
stdin: reportMarkdown,
|
|
1599
1805
|
cachePath: cached,
|
|
1600
|
-
streamToStdout: opts.streamToStdout
|
|
1806
|
+
streamToStdout: opts.streamToStdout,
|
|
1807
|
+
model: opts.model
|
|
1601
1808
|
});
|
|
1602
1809
|
if (opts.announce && result.code === 0) {
|
|
1603
1810
|
process.stderr.write("\n");
|
|
@@ -1623,7 +1830,8 @@ async function runSummary(options2) {
|
|
|
1623
1830
|
force: options2.force,
|
|
1624
1831
|
promptOverride: options2.prompt,
|
|
1625
1832
|
streamToStdout: true,
|
|
1626
|
-
announce: true
|
|
1833
|
+
announce: true,
|
|
1834
|
+
model: options2.model
|
|
1627
1835
|
});
|
|
1628
1836
|
return res.code;
|
|
1629
1837
|
}
|
|
@@ -1686,7 +1894,9 @@ agenthud: --- ${label} ---
|
|
|
1686
1894
|
force: false,
|
|
1687
1895
|
streamToStdout: false,
|
|
1688
1896
|
announce: true,
|
|
1689
|
-
confirmBeforeSpawn: confirmer
|
|
1897
|
+
confirmBeforeSpawn: confirmer,
|
|
1898
|
+
assumeYes: options2.assumeYes,
|
|
1899
|
+
model: options2.model
|
|
1690
1900
|
});
|
|
1691
1901
|
if (res.skipped) {
|
|
1692
1902
|
process.stderr.write(`agenthud: ${label} \u2014 skipped by user.
|
|
@@ -1731,7 +1941,8 @@ agenthud: combining ${dailyMarkdowns.length} daily summaries into range summary.
|
|
|
1731
1941
|
prompt: metaPrompt,
|
|
1732
1942
|
stdin: metaInput,
|
|
1733
1943
|
cachePath: rangeCache,
|
|
1734
|
-
streamToStdout: true
|
|
1944
|
+
streamToStdout: true,
|
|
1945
|
+
model: options2.model
|
|
1735
1946
|
});
|
|
1736
1947
|
if (metaResult.code !== 0) {
|
|
1737
1948
|
return metaResult.code;
|
|
@@ -1753,7 +1964,33 @@ import { useCallback, useEffect as useEffect3, useMemo, useRef, useState as useS
|
|
|
1753
1964
|
|
|
1754
1965
|
// src/ui/ActivityViewerPanel.tsx
|
|
1755
1966
|
import { Box, Text } from "ink";
|
|
1756
|
-
import {
|
|
1967
|
+
import { memo } from "react";
|
|
1968
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
1969
|
+
function brighten(color) {
|
|
1970
|
+
switch (color) {
|
|
1971
|
+
case void 0:
|
|
1972
|
+
case "gray":
|
|
1973
|
+
return "white";
|
|
1974
|
+
case "white":
|
|
1975
|
+
return "whiteBright";
|
|
1976
|
+
case "green":
|
|
1977
|
+
return "greenBright";
|
|
1978
|
+
case "yellow":
|
|
1979
|
+
return "yellowBright";
|
|
1980
|
+
case "magenta":
|
|
1981
|
+
return "magentaBright";
|
|
1982
|
+
case "cyan":
|
|
1983
|
+
return "cyanBright";
|
|
1984
|
+
case "red":
|
|
1985
|
+
return "redBright";
|
|
1986
|
+
case "blue":
|
|
1987
|
+
return "blueBright";
|
|
1988
|
+
case "black":
|
|
1989
|
+
return "blackBright";
|
|
1990
|
+
default:
|
|
1991
|
+
return color;
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1757
1994
|
function getActivityStyle(activity) {
|
|
1758
1995
|
if (activity.type === "user") {
|
|
1759
1996
|
return { color: "white", dimColor: false };
|
|
@@ -1802,6 +2039,76 @@ function truncateDetail2(detail, maxWidth) {
|
|
|
1802
2039
|
}
|
|
1803
2040
|
return `${truncated}\u2026`;
|
|
1804
2041
|
}
|
|
2042
|
+
var ActivityRow = memo(function ActivityRow2({
|
|
2043
|
+
activity,
|
|
2044
|
+
timestamp,
|
|
2045
|
+
width,
|
|
2046
|
+
contentWidth,
|
|
2047
|
+
isCursor,
|
|
2048
|
+
isLiveRow,
|
|
2049
|
+
liveSpinnerFrame,
|
|
2050
|
+
liveTick
|
|
2051
|
+
}) {
|
|
2052
|
+
const style = getActivityStyle(activity);
|
|
2053
|
+
const timestampWidth = timestamp.length;
|
|
2054
|
+
const icon = isLiveRow && liveSpinnerFrame ? liveSpinnerFrame : activity.icon;
|
|
2055
|
+
const iconWidth = getDisplayWidth(icon);
|
|
2056
|
+
const label = activity.label;
|
|
2057
|
+
const detail = activity.detail;
|
|
2058
|
+
const count = activity.count;
|
|
2059
|
+
const countSuffix = count && count > 1 ? ` (\xD7${count})` : "";
|
|
2060
|
+
const countSuffixWidth = countSuffix.length;
|
|
2061
|
+
const prefixWidth = 2 + timestampWidth + iconWidth + 1;
|
|
2062
|
+
const labelPart = detail ? `${label}: ` : label;
|
|
2063
|
+
const labelWidth = labelPart.length;
|
|
2064
|
+
const detailMaxWidth = width - 2 - timestampWidth - iconWidth - 1 - labelWidth - countSuffixWidth - 1;
|
|
2065
|
+
let labelContent;
|
|
2066
|
+
if (detail) {
|
|
2067
|
+
const truncated = truncateDetail2(detail, Math.max(0, detailMaxWidth));
|
|
2068
|
+
labelContent = `${labelPart}${truncated}${countSuffix}`;
|
|
2069
|
+
} else {
|
|
2070
|
+
labelContent = label + countSuffix;
|
|
2071
|
+
}
|
|
2072
|
+
const usedWidth = 1 + 1 + timestampWidth + iconWidth + 1 + getDisplayWidth(labelContent) + 1;
|
|
2073
|
+
const padding = Math.max(0, width - usedWidth);
|
|
2074
|
+
const SWEEP_WIDTH = 10;
|
|
2075
|
+
let labelNode = labelContent;
|
|
2076
|
+
if (isLiveRow && !isCursor && liveTick != null && labelContent.length > 0) {
|
|
2077
|
+
const period = labelContent.length + SWEEP_WIDTH;
|
|
2078
|
+
const offset = liveTick % period - SWEEP_WIDTH;
|
|
2079
|
+
const litStart = Math.max(0, offset);
|
|
2080
|
+
const litEnd = Math.min(labelContent.length, offset + SWEEP_WIDTH);
|
|
2081
|
+
if (litEnd > litStart) {
|
|
2082
|
+
const pre = labelContent.slice(0, litStart);
|
|
2083
|
+
const lit = labelContent.slice(litStart, litEnd);
|
|
2084
|
+
const post = labelContent.slice(litEnd);
|
|
2085
|
+
labelNode = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2086
|
+
pre,
|
|
2087
|
+
/* @__PURE__ */ jsx(Text, { color: brighten(style.color), bold: true, children: lit }),
|
|
2088
|
+
post
|
|
2089
|
+
] });
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
return /* @__PURE__ */ jsxs(Text, { children: [
|
|
2093
|
+
BOX.v,
|
|
2094
|
+
" ",
|
|
2095
|
+
/* @__PURE__ */ jsxs(Text, { backgroundColor: isCursor ? "blue" : void 0, children: [
|
|
2096
|
+
/* @__PURE__ */ jsx(Text, { dimColor: !isCursor && !isLiveRow, children: timestamp }),
|
|
2097
|
+
/* @__PURE__ */ jsx(Text, { color: "cyan", bold: isLiveRow, children: icon }),
|
|
2098
|
+
" ",
|
|
2099
|
+
/* @__PURE__ */ jsx(
|
|
2100
|
+
Text,
|
|
2101
|
+
{
|
|
2102
|
+
color: isCursor ? void 0 : style.color,
|
|
2103
|
+
dimColor: !isCursor && !isLiveRow && style.dimColor,
|
|
2104
|
+
children: labelNode
|
|
2105
|
+
}
|
|
2106
|
+
),
|
|
2107
|
+
" ".repeat(padding)
|
|
2108
|
+
] }),
|
|
2109
|
+
BOX.v
|
|
2110
|
+
] });
|
|
2111
|
+
});
|
|
1805
2112
|
function ActivityViewerPanel({
|
|
1806
2113
|
activities,
|
|
1807
2114
|
sessionName,
|
|
@@ -1809,8 +2116,8 @@ function ActivityViewerPanel({
|
|
|
1809
2116
|
isLive,
|
|
1810
2117
|
newCount,
|
|
1811
2118
|
visibleRows,
|
|
1812
|
-
|
|
1813
|
-
|
|
2119
|
+
liveSpinnerFrame = null,
|
|
2120
|
+
liveTick = null,
|
|
1814
2121
|
width,
|
|
1815
2122
|
cursorLine,
|
|
1816
2123
|
hasFocus,
|
|
@@ -1854,57 +2161,28 @@ function ActivityViewerPanel({
|
|
|
1854
2161
|
} else {
|
|
1855
2162
|
const effectiveCursor = Math.min(cursorLine, visibleActivities.length - 1);
|
|
1856
2163
|
const cursorIndexInSlice = visibleActivities.length - 1 - effectiveCursor;
|
|
2164
|
+
const liveRowIndex = visibleActivities.length - 1;
|
|
2165
|
+
const liveTreatment = isLive && !!liveSpinnerFrame;
|
|
1857
2166
|
for (let i = 0; i < visibleActivities.length; i++) {
|
|
1858
2167
|
const activity = visibleActivities[i];
|
|
1859
|
-
const style = getActivityStyle(activity);
|
|
1860
2168
|
const isCursor = hasFocus && i === cursorIndexInSlice;
|
|
1861
|
-
const
|
|
1862
|
-
const timestamp = `[${
|
|
1863
|
-
const timestampWidth = timestamp.length;
|
|
1864
|
-
const icon = activity.icon;
|
|
1865
|
-
const iconWidth = getDisplayWidth(icon);
|
|
1866
|
-
const label = activity.label;
|
|
1867
|
-
const detail = activity.detail;
|
|
1868
|
-
const count = activity.count;
|
|
1869
|
-
const countSuffix = count && count > 1 ? ` (\xD7${count})` : "";
|
|
1870
|
-
const countSuffixWidth = countSuffix.length;
|
|
1871
|
-
const prefixWidth = 2 + timestampWidth + iconWidth + 1;
|
|
1872
|
-
const labelPart = detail ? `${label}: ` : label;
|
|
1873
|
-
const labelWidth = labelPart.length;
|
|
1874
|
-
const _availableForDetail = contentWidth - prefixWidth - labelWidth - countSuffixWidth + 1;
|
|
1875
|
-
const detailMaxWidth = width - 2 - timestampWidth - iconWidth - 1 - labelWidth - countSuffixWidth - 1;
|
|
1876
|
-
let labelContent;
|
|
1877
|
-
let _displayWidth;
|
|
1878
|
-
if (detail) {
|
|
1879
|
-
const truncated = truncateDetail2(detail, Math.max(0, detailMaxWidth));
|
|
1880
|
-
labelContent = `${labelPart}${truncated}${countSuffix}`;
|
|
1881
|
-
_displayWidth = prefixWidth - 1 + labelWidth + getDisplayWidth(truncated) + countSuffixWidth;
|
|
1882
|
-
} else {
|
|
1883
|
-
labelContent = label + countSuffix;
|
|
1884
|
-
_displayWidth = prefixWidth - 1 + label.length + countSuffixWidth;
|
|
1885
|
-
}
|
|
1886
|
-
const usedWidth = 1 + 1 + timestampWidth + iconWidth + 1 + getDisplayWidth(labelContent) + 1;
|
|
1887
|
-
const padding = Math.max(0, width - usedWidth);
|
|
2169
|
+
const isLiveRow = liveTreatment && i === liveRowIndex;
|
|
2170
|
+
const timestamp = `[${formatActivityTime(activity.timestamp, now)}] `;
|
|
1888
2171
|
lines.push(
|
|
1889
|
-
/* @__PURE__ */
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
),
|
|
1904
|
-
" ".repeat(padding)
|
|
1905
|
-
] }),
|
|
1906
|
-
BOX.v
|
|
1907
|
-
] }, `activity-${i}`)
|
|
2172
|
+
/* @__PURE__ */ jsx(
|
|
2173
|
+
ActivityRow,
|
|
2174
|
+
{
|
|
2175
|
+
activity,
|
|
2176
|
+
timestamp,
|
|
2177
|
+
width,
|
|
2178
|
+
contentWidth,
|
|
2179
|
+
isCursor,
|
|
2180
|
+
isLiveRow,
|
|
2181
|
+
liveSpinnerFrame: isLiveRow ? liveSpinnerFrame ?? void 0 : void 0,
|
|
2182
|
+
liveTick: isLiveRow ? liveTick ?? void 0 : void 0
|
|
2183
|
+
},
|
|
2184
|
+
`activity-${i}`
|
|
2185
|
+
)
|
|
1908
2186
|
);
|
|
1909
2187
|
}
|
|
1910
2188
|
}
|
|
@@ -1914,29 +2192,7 @@ function ActivityViewerPanel({
|
|
|
1914
2192
|
for (let i = 0; i < padCount; i++) {
|
|
1915
2193
|
padded.push(/* @__PURE__ */ jsx(Text, { children: emptyRow }, `pad-${i}`));
|
|
1916
2194
|
}
|
|
1917
|
-
const
|
|
1918
|
-
const trailing = [];
|
|
1919
|
-
for (let i = 0; i < trailingBlankRows; i++) {
|
|
1920
|
-
if (i === 0 && isLive && liveIndicatorPosition != null && hasContent) {
|
|
1921
|
-
const pos = Math.max(0, liveIndicatorPosition);
|
|
1922
|
-
const arrow = "\u203A";
|
|
1923
|
-
const safePos = Math.min(pos, Math.max(0, contentWidth - 1));
|
|
1924
|
-
const padAfter = Math.max(0, contentWidth - safePos - 1);
|
|
1925
|
-
trailing.push(
|
|
1926
|
-
/* @__PURE__ */ jsxs(Text, { children: [
|
|
1927
|
-
BOX.v,
|
|
1928
|
-
" ",
|
|
1929
|
-
" ".repeat(safePos),
|
|
1930
|
-
/* @__PURE__ */ jsx(Text, { color: "cyan", dimColor: true, children: arrow }),
|
|
1931
|
-
" ".repeat(padAfter),
|
|
1932
|
-
BOX.v
|
|
1933
|
-
] }, `trail-${i}`)
|
|
1934
|
-
);
|
|
1935
|
-
} else {
|
|
1936
|
-
trailing.push(/* @__PURE__ */ jsx(Text, { children: emptyRow }, `trail-${i}`));
|
|
1937
|
-
}
|
|
1938
|
-
}
|
|
1939
|
-
const finalLines = [...padded, ...lines, ...trailing];
|
|
2195
|
+
const finalLines = [...padded, ...lines];
|
|
1940
2196
|
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width, children: [
|
|
1941
2197
|
/* @__PURE__ */ jsx(Text, { color: isLive ? void 0 : "yellow", children: createTitleLine(sessionName, titleSuffix, width) }),
|
|
1942
2198
|
finalLines,
|
|
@@ -2036,8 +2292,9 @@ function DetailViewPanel({
|
|
|
2036
2292
|
}) {
|
|
2037
2293
|
const innerWidth = getInnerWidth(width);
|
|
2038
2294
|
const contentWidth = innerWidth - 1;
|
|
2039
|
-
const
|
|
2040
|
-
const
|
|
2295
|
+
const body = activity.detailBody ?? activity.detail;
|
|
2296
|
+
const classifier = activity.detailKind === "diff" ? classifyDiffLines : activity.detailKind === "code" ? classifyCodeFences : activity.type === "commit" ? classifyDiffLines : classifyCodeFences;
|
|
2297
|
+
const allLines = wrapClassified(body, contentWidth, classifier);
|
|
2041
2298
|
const totalLines = allLines.length;
|
|
2042
2299
|
const clampedOffset = Math.min(
|
|
2043
2300
|
scrollOffset,
|
|
@@ -2102,6 +2359,7 @@ var SECTIONS = [
|
|
|
2102
2359
|
["PgDn / Ctrl+F", "Page down"],
|
|
2103
2360
|
["\u21B5", "Expand/collapse project, session, or summary"],
|
|
2104
2361
|
["h", "Hide selected (project/session/sub-agent)"],
|
|
2362
|
+
["t", "Track: auto-follow the newest live sub-agent (any nav key turns it off)"],
|
|
2105
2363
|
["Tab", "Switch focus to activity viewer"],
|
|
2106
2364
|
["r", "Refresh now"]
|
|
2107
2365
|
]
|
|
@@ -2243,7 +2501,9 @@ function useHotkeys({
|
|
|
2243
2501
|
onHelp,
|
|
2244
2502
|
onHelpScroll,
|
|
2245
2503
|
onHelpScrollToTop,
|
|
2246
|
-
|
|
2504
|
+
onToggleTracking,
|
|
2505
|
+
filterLabel,
|
|
2506
|
+
trackingOn = false
|
|
2247
2507
|
}) {
|
|
2248
2508
|
const handleInput = (input, key) => {
|
|
2249
2509
|
if (helpMode) {
|
|
@@ -2318,6 +2578,10 @@ function useHotkeys({
|
|
|
2318
2578
|
onFilter();
|
|
2319
2579
|
return;
|
|
2320
2580
|
}
|
|
2581
|
+
if (input === "t" && !key.ctrl && onToggleTracking) {
|
|
2582
|
+
onToggleTracking();
|
|
2583
|
+
return;
|
|
2584
|
+
}
|
|
2321
2585
|
if (key.pageUp) {
|
|
2322
2586
|
onScrollPageUp();
|
|
2323
2587
|
return;
|
|
@@ -2377,7 +2641,9 @@ function useHotkeys({
|
|
|
2377
2641
|
}
|
|
2378
2642
|
}
|
|
2379
2643
|
};
|
|
2644
|
+
const trackingItems = trackingOn ? ["TRK \u25CF"] : ["t: track"];
|
|
2380
2645
|
const statusBarItems = helpMode ? ["\u2191\u2193/jk: scroll", "PgDn/Space: page", "\u21B5/Esc/q/?: close"] : detailMode ? ["\u2191\u2193/jk: scroll", "\u21B5/Esc: close", "?: help"] : focus === "tree" ? [
|
|
2646
|
+
...trackingItems,
|
|
2381
2647
|
"Tab: viewer",
|
|
2382
2648
|
"\u2191\u2193/jk: select",
|
|
2383
2649
|
"PgUp/Dn: page",
|
|
@@ -2387,6 +2653,7 @@ function useHotkeys({
|
|
|
2387
2653
|
"?: help",
|
|
2388
2654
|
"q: quit"
|
|
2389
2655
|
] : [
|
|
2656
|
+
...trackingItems,
|
|
2390
2657
|
"Tab: projects",
|
|
2391
2658
|
"\u2191\u2193/jk: scroll",
|
|
2392
2659
|
"PgUp/Dn: page",
|
|
@@ -2400,21 +2667,16 @@ function useHotkeys({
|
|
|
2400
2667
|
return { handleInput, statusBarItems };
|
|
2401
2668
|
}
|
|
2402
2669
|
|
|
2403
|
-
// src/ui/hooks/
|
|
2670
|
+
// src/ui/hooks/useTick.ts
|
|
2404
2671
|
import { useEffect, useState } from "react";
|
|
2405
|
-
function
|
|
2406
|
-
const [
|
|
2407
|
-
useEffect(() => {
|
|
2408
|
-
setIndex(0);
|
|
2409
|
-
}, [resetKey]);
|
|
2672
|
+
function useTick(active, intervalMs = 100) {
|
|
2673
|
+
const [n, setN] = useState(0);
|
|
2410
2674
|
useEffect(() => {
|
|
2411
2675
|
if (!active) return;
|
|
2412
|
-
const
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
}, [active, positions, intervalMs]);
|
|
2417
|
-
return index;
|
|
2676
|
+
const id = setInterval(() => setN((x) => x + 1), intervalMs);
|
|
2677
|
+
return () => clearInterval(id);
|
|
2678
|
+
}, [active, intervalMs]);
|
|
2679
|
+
return n;
|
|
2418
2680
|
}
|
|
2419
2681
|
|
|
2420
2682
|
// src/ui/hooks/useSpinner.ts
|
|
@@ -2435,7 +2697,7 @@ function useSpinner(active, intervalMs = 100) {
|
|
|
2435
2697
|
// src/ui/SessionTreePanel.tsx
|
|
2436
2698
|
import { homedir as homedir4 } from "os";
|
|
2437
2699
|
import { Box as Box4, Text as Text4 } from "ink";
|
|
2438
|
-
import { Fragment, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
2700
|
+
import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
2439
2701
|
function formatElapsed(lastModifiedMs, now = Date.now()) {
|
|
2440
2702
|
const elapsed = Math.max(0, now - lastModifiedMs);
|
|
2441
2703
|
const seconds = Math.floor(elapsed / 1e3);
|
|
@@ -2466,6 +2728,13 @@ function getStatusColor(status) {
|
|
|
2466
2728
|
return "gray";
|
|
2467
2729
|
}
|
|
2468
2730
|
}
|
|
2731
|
+
function getBadge(session) {
|
|
2732
|
+
if (session.liveState === "working")
|
|
2733
|
+
return { text: "[working]", color: "green" };
|
|
2734
|
+
if (session.liveState === "waiting")
|
|
2735
|
+
return { text: "[waiting]", color: "magenta" };
|
|
2736
|
+
return { text: `[${session.status}]`, color: getStatusColor(session.status) };
|
|
2737
|
+
}
|
|
2469
2738
|
function formatProjectPath(projectPath) {
|
|
2470
2739
|
const home = homedir4();
|
|
2471
2740
|
const raw = projectPath.startsWith(home) ? `~${projectPath.slice(home.length)}` : projectPath;
|
|
@@ -2479,8 +2748,7 @@ function SessionRow({
|
|
|
2479
2748
|
contentWidth
|
|
2480
2749
|
}) {
|
|
2481
2750
|
const isParent = prefix === " ";
|
|
2482
|
-
const
|
|
2483
|
-
const badge = `[${session.status}]`;
|
|
2751
|
+
const { text: badge, color: badgeColor } = getBadge(session);
|
|
2484
2752
|
const elapsed = formatElapsed(session.lastModifiedMs);
|
|
2485
2753
|
const model = session.modelName ?? "";
|
|
2486
2754
|
const isNonInteractive = session.nonInteractive;
|
|
@@ -2529,7 +2797,7 @@ function SessionRow({
|
|
|
2529
2797
|
/* @__PURE__ */ jsx4(Text4, { bold: !shouldDim, children: rawName }),
|
|
2530
2798
|
shortIdDisplay ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: shortIdDisplay }) : null,
|
|
2531
2799
|
/* @__PURE__ */ jsx4(Text4, { children: " " }),
|
|
2532
|
-
/* @__PURE__ */ jsx4(Text4, { color:
|
|
2800
|
+
/* @__PURE__ */ jsx4(Text4, { color: badgeColor, children: badge }),
|
|
2533
2801
|
middleText ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: middleSection }) : null,
|
|
2534
2802
|
/* @__PURE__ */ jsx4(Text4, { children: gap }),
|
|
2535
2803
|
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: elapsed }),
|
|
@@ -2653,7 +2921,7 @@ function ProjectRow({
|
|
|
2653
2921
|
dimColor: muted,
|
|
2654
2922
|
children: [
|
|
2655
2923
|
nameText,
|
|
2656
|
-
pathText ? /* @__PURE__ */ jsxs4(
|
|
2924
|
+
pathText ? /* @__PURE__ */ jsxs4(Fragment2, { children: [
|
|
2657
2925
|
" ",
|
|
2658
2926
|
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: pathText })
|
|
2659
2927
|
] }) : null,
|
|
@@ -2728,11 +2996,14 @@ function SessionTreePanel({
|
|
|
2728
2996
|
hasFocus,
|
|
2729
2997
|
width = DEFAULT_PANEL_WIDTH,
|
|
2730
2998
|
maxRows,
|
|
2731
|
-
expandedIds = /* @__PURE__ */ new Set()
|
|
2999
|
+
expandedIds = /* @__PURE__ */ new Set(),
|
|
3000
|
+
trackingOn = false,
|
|
3001
|
+
spinner = ""
|
|
2732
3002
|
}) {
|
|
2733
3003
|
const innerWidth = getInnerWidth(width);
|
|
2734
3004
|
const contentWidth = innerWidth - 1;
|
|
2735
|
-
const
|
|
3005
|
+
const titleSuffix = trackingOn ? `[LIVE ${spinner || "\u25BC"}]` : "";
|
|
3006
|
+
const titleLine = createTitleLine("Projects", titleSuffix, width);
|
|
2736
3007
|
const bottomLine = createBottomLine(width);
|
|
2737
3008
|
const totalProjectCount = projects.length + coldProjects.length;
|
|
2738
3009
|
if (totalProjectCount === 0) {
|
|
@@ -2826,7 +3097,7 @@ function SessionTreePanel({
|
|
|
2826
3097
|
}
|
|
2827
3098
|
|
|
2828
3099
|
// src/ui/App.tsx
|
|
2829
|
-
import { Fragment as
|
|
3100
|
+
import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
2830
3101
|
var VIEWER_HEIGHT_FRACTION = 0.55;
|
|
2831
3102
|
function subSummarySentinel(parentId) {
|
|
2832
3103
|
return {
|
|
@@ -2840,7 +3111,8 @@ function subSummarySentinel(parentId) {
|
|
|
2840
3111
|
modelName: null,
|
|
2841
3112
|
subAgents: [],
|
|
2842
3113
|
nonInteractive: false,
|
|
2843
|
-
firstUserPrompt: null
|
|
3114
|
+
firstUserPrompt: null,
|
|
3115
|
+
liveState: null
|
|
2844
3116
|
};
|
|
2845
3117
|
}
|
|
2846
3118
|
function appendSubAgentRows(result, session, expandedIds) {
|
|
@@ -2879,7 +3151,8 @@ function flattenSessions2(tree, expandedIds) {
|
|
|
2879
3151
|
modelName: null,
|
|
2880
3152
|
subAgents: [],
|
|
2881
3153
|
nonInteractive: false,
|
|
2882
|
-
firstUserPrompt: null
|
|
3154
|
+
firstUserPrompt: null,
|
|
3155
|
+
liveState: null
|
|
2883
3156
|
});
|
|
2884
3157
|
const shouldShowSessions = isCold ? expandedIds.has(`__expanded-${sentinelId}`) : !expandedIds.has(`__collapsed-${sentinelId}`);
|
|
2885
3158
|
if (shouldShowSessions) {
|
|
@@ -2904,7 +3177,8 @@ function flattenSessions2(tree, expandedIds) {
|
|
|
2904
3177
|
modelName: null,
|
|
2905
3178
|
subAgents: [],
|
|
2906
3179
|
nonInteractive: false,
|
|
2907
|
-
firstUserPrompt: null
|
|
3180
|
+
firstUserPrompt: null,
|
|
3181
|
+
liveState: null
|
|
2908
3182
|
});
|
|
2909
3183
|
if (expandedIds.has("__cold__")) {
|
|
2910
3184
|
for (const project of tree.coldProjects) {
|
|
@@ -2927,6 +3201,54 @@ function getSelectedActivity(acts, live, scrollOff, rows, cursorLine) {
|
|
|
2927
3201
|
const effectiveCursor = Math.min(cursorLine, visible.length - 1);
|
|
2928
3202
|
return visible[visible.length - 1 - effectiveCursor] ?? null;
|
|
2929
3203
|
}
|
|
3204
|
+
function pickTrackingTarget(tree, selectedId, seen) {
|
|
3205
|
+
const ids = /* @__PURE__ */ new Set();
|
|
3206
|
+
const liveSubs = [];
|
|
3207
|
+
const liveParents = [];
|
|
3208
|
+
for (const p of tree.projects) {
|
|
3209
|
+
for (const s of p.sessions) {
|
|
3210
|
+
ids.add(s.id);
|
|
3211
|
+
if (s.status === "hot" || s.status === "warm") {
|
|
3212
|
+
liveParents.push({
|
|
3213
|
+
id: s.id,
|
|
3214
|
+
mtime: s.lastModifiedMs,
|
|
3215
|
+
isNew: !seen.has(s.id)
|
|
3216
|
+
});
|
|
3217
|
+
}
|
|
3218
|
+
for (const sa of s.subAgents) {
|
|
3219
|
+
ids.add(sa.id);
|
|
3220
|
+
if (sa.status === "hot" || sa.status === "warm") {
|
|
3221
|
+
liveSubs.push({
|
|
3222
|
+
id: sa.id,
|
|
3223
|
+
mtime: sa.lastModifiedMs,
|
|
3224
|
+
isNew: !seen.has(sa.id)
|
|
3225
|
+
});
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3230
|
+
const newest = (xs) => xs.length === 0 ? null : xs.reduce((a, b) => a.mtime > b.mtime ? a : b).id;
|
|
3231
|
+
const newSubTarget = newest(liveSubs.filter((s) => s.isNew));
|
|
3232
|
+
if (newSubTarget) return { target: newSubTarget, ids };
|
|
3233
|
+
const newParentTarget = newest(liveParents.filter((p) => p.isNew));
|
|
3234
|
+
if (newParentTarget) return { target: newParentTarget, ids };
|
|
3235
|
+
const currentIsLive = selectedId != null && (liveSubs.some((s) => s.id === selectedId) || liveParents.some((p) => p.id === selectedId));
|
|
3236
|
+
if (currentIsLive) return { target: null, ids };
|
|
3237
|
+
return {
|
|
3238
|
+
target: newest(liveSubs) ?? newest(liveParents),
|
|
3239
|
+
ids
|
|
3240
|
+
};
|
|
3241
|
+
}
|
|
3242
|
+
function collectAllIds(tree) {
|
|
3243
|
+
const ids = /* @__PURE__ */ new Set();
|
|
3244
|
+
for (const p of tree.projects) {
|
|
3245
|
+
for (const s of p.sessions) {
|
|
3246
|
+
ids.add(s.id);
|
|
3247
|
+
for (const sa of s.subAgents) ids.add(sa.id);
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3250
|
+
return ids;
|
|
3251
|
+
}
|
|
2930
3252
|
function App({ mode }) {
|
|
2931
3253
|
const { exit } = useApp();
|
|
2932
3254
|
const { stdout } = useStdout();
|
|
@@ -2958,6 +3280,8 @@ function App({ mode }) {
|
|
|
2958
3280
|
const [helpMode, setHelpMode] = useState3(false);
|
|
2959
3281
|
const [helpScroll, setHelpScroll] = useState3(0);
|
|
2960
3282
|
const helpTotalLinesRef = useRef(0);
|
|
3283
|
+
const [tracking, setTracking] = useState3(false);
|
|
3284
|
+
const seenIdsRef = useRef(/* @__PURE__ */ new Set());
|
|
2961
3285
|
const allFlat = useMemo(
|
|
2962
3286
|
() => flattenSessions2(sessionTree, expandedIds),
|
|
2963
3287
|
[sessionTree, expandedIds]
|
|
@@ -3042,11 +3366,24 @@ function App({ mode }) {
|
|
|
3042
3366
|
const freshConfig = loadGlobalConfig();
|
|
3043
3367
|
const tree = discoverSessions(freshConfig);
|
|
3044
3368
|
const updatedFlat = flattenSessions2(tree, expandedIds);
|
|
3045
|
-
|
|
3369
|
+
let nextSelected = selectedId;
|
|
3370
|
+
if (tracking) {
|
|
3371
|
+
const { target, ids } = pickTrackingTarget(
|
|
3372
|
+
tree,
|
|
3373
|
+
selectedId,
|
|
3374
|
+
seenIdsRef.current
|
|
3375
|
+
);
|
|
3376
|
+
if (target && target !== selectedId) {
|
|
3377
|
+
setSelectedId(target);
|
|
3378
|
+
nextSelected = target;
|
|
3379
|
+
}
|
|
3380
|
+
seenIdsRef.current = ids;
|
|
3381
|
+
}
|
|
3382
|
+
const node = updatedFlat.find((s) => s.id === nextSelected);
|
|
3046
3383
|
if (!node) {
|
|
3047
3384
|
const allSessions = tree.projects?.flatMap((p) => p.sessions) ?? [];
|
|
3048
3385
|
const parentSession = allSessions.find(
|
|
3049
|
-
(s) => s.subAgents.some((sa) => sa.id ===
|
|
3386
|
+
(s) => s.subAgents.some((sa) => sa.id === nextSelected)
|
|
3050
3387
|
);
|
|
3051
3388
|
if (parentSession) setSelectedId(parentSession.id);
|
|
3052
3389
|
}
|
|
@@ -3059,7 +3396,7 @@ function App({ mode }) {
|
|
|
3059
3396
|
setScrollOffset((o) => o + delta);
|
|
3060
3397
|
setNewCount((n) => n + delta);
|
|
3061
3398
|
}
|
|
3062
|
-
}, [selectedId, isLive, expandedIds]);
|
|
3399
|
+
}, [selectedId, isLive, expandedIds, tracking]);
|
|
3063
3400
|
const refreshRef = useRef(refresh);
|
|
3064
3401
|
useEffect3(() => {
|
|
3065
3402
|
refreshRef.current = refresh;
|
|
@@ -3094,6 +3431,11 @@ function App({ mode }) {
|
|
|
3094
3431
|
if (debounce) clearTimeout(debounce);
|
|
3095
3432
|
};
|
|
3096
3433
|
}, [isWatchMode, config.refreshIntervalMs]);
|
|
3434
|
+
useEffect3(() => {
|
|
3435
|
+
if (!isWatchMode || !tracking) return;
|
|
3436
|
+
const timer = setInterval(() => refreshRef.current(), 1e3);
|
|
3437
|
+
return () => clearInterval(timer);
|
|
3438
|
+
}, [isWatchMode, tracking]);
|
|
3097
3439
|
const filterPresets = config.filterPresets;
|
|
3098
3440
|
const activePreset = useMemo(
|
|
3099
3441
|
() => filterPresets[filterIndex % filterPresets.length] ?? [],
|
|
@@ -3118,26 +3460,18 @@ function App({ mode }) {
|
|
|
3118
3460
|
const maxTreeRows = Math.floor(height * (1 - VIEWER_HEIGHT_FRACTION));
|
|
3119
3461
|
const naturalTreeRows = allFlat.length;
|
|
3120
3462
|
const treeRows = Math.max(1, Math.min(naturalTreeRows, maxTreeRows));
|
|
3121
|
-
const
|
|
3122
|
-
const
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
);
|
|
3126
|
-
const spinner = useSpinner(isWatchMode);
|
|
3127
|
-
const viewerIndicatorWidth = Math.max(1, width - 3);
|
|
3128
|
-
const liveIndicatorPosition = useSlide(
|
|
3129
|
-
isWatchMode,
|
|
3130
|
-
viewerIndicatorWidth,
|
|
3131
|
-
180,
|
|
3132
|
-
// Reset to 0 whenever the viewer's subject changes so each new
|
|
3133
|
-
// session/sub-agent restarts the arrow from the left.
|
|
3134
|
-
selectedId
|
|
3135
|
-
);
|
|
3463
|
+
const viewerRows = Math.max(5, height - 7 - treeRows);
|
|
3464
|
+
const spinner = useSpinner(isWatchMode, 150);
|
|
3465
|
+
const tickActive = isWatchMode && isLive && !helpMode && !detailMode && activities.length > 0;
|
|
3466
|
+
const liveTick = useTick(tickActive, 150);
|
|
3136
3467
|
const helpViewportRows = Math.max(1, height - 3);
|
|
3137
3468
|
const helpScrollStep = (delta) => {
|
|
3138
3469
|
const max = Math.max(0, helpTotalLinesRef.current - helpViewportRows);
|
|
3139
3470
|
setHelpScroll((s) => Math.max(0, Math.min(max, s + delta)));
|
|
3140
3471
|
};
|
|
3472
|
+
const stopTracking = () => {
|
|
3473
|
+
if (tracking) setTracking(false);
|
|
3474
|
+
};
|
|
3141
3475
|
const { handleInput, statusBarItems } = useHotkeys({
|
|
3142
3476
|
focus,
|
|
3143
3477
|
detailMode,
|
|
@@ -3148,12 +3482,29 @@ function App({ mode }) {
|
|
|
3148
3482
|
},
|
|
3149
3483
|
onHelpScroll: helpScrollStep,
|
|
3150
3484
|
onHelpScrollToTop: () => setHelpScroll(0),
|
|
3485
|
+
onToggleTracking: () => {
|
|
3486
|
+
setTracking((on) => {
|
|
3487
|
+
const next = !on;
|
|
3488
|
+
if (next) {
|
|
3489
|
+
seenIdsRef.current = collectAllIds(sessionTree);
|
|
3490
|
+
const { target } = pickTrackingTarget(
|
|
3491
|
+
sessionTree,
|
|
3492
|
+
selectedId,
|
|
3493
|
+
seenIdsRef.current
|
|
3494
|
+
);
|
|
3495
|
+
if (target) setSelectedId(target);
|
|
3496
|
+
}
|
|
3497
|
+
return next;
|
|
3498
|
+
});
|
|
3499
|
+
},
|
|
3500
|
+
trackingOn: tracking,
|
|
3151
3501
|
onSwitchFocus: () => setFocus((f) => f === "tree" ? "viewer" : "tree"),
|
|
3152
3502
|
// cursorLine = "entries back from the newest" (0 = newest = bottom row).
|
|
3153
3503
|
// Up arrow moves visually upward = older direction = cursorLine++.
|
|
3154
3504
|
// Down arrow moves visually downward = newer direction = cursorLine--.
|
|
3155
3505
|
onScrollUp: () => {
|
|
3156
3506
|
if (focus === "tree") {
|
|
3507
|
+
stopTracking();
|
|
3157
3508
|
if (selectedIndex === -1) return;
|
|
3158
3509
|
const prev = Math.max(0, selectedIndex - 1);
|
|
3159
3510
|
setSelectedId(allFlat[prev]?.id ?? selectedId);
|
|
@@ -3170,6 +3521,7 @@ function App({ mode }) {
|
|
|
3170
3521
|
},
|
|
3171
3522
|
onScrollDown: () => {
|
|
3172
3523
|
if (focus === "tree") {
|
|
3524
|
+
stopTracking();
|
|
3173
3525
|
if (selectedIndex === -1) return;
|
|
3174
3526
|
const next = Math.min(allFlat.length - 1, selectedIndex + 1);
|
|
3175
3527
|
setSelectedId(allFlat[next]?.id ?? selectedId);
|
|
@@ -3193,6 +3545,7 @@ function App({ mode }) {
|
|
|
3193
3545
|
// PgDn = visually down = newer direction = scrollOffset--
|
|
3194
3546
|
onScrollPageUp: () => {
|
|
3195
3547
|
if (focus === "tree") {
|
|
3548
|
+
stopTracking();
|
|
3196
3549
|
const prev = Math.max(0, selectedIndex - 5);
|
|
3197
3550
|
setSelectedId(allFlat[prev]?.id ?? selectedId);
|
|
3198
3551
|
} else {
|
|
@@ -3205,6 +3558,7 @@ function App({ mode }) {
|
|
|
3205
3558
|
},
|
|
3206
3559
|
onScrollPageDown: () => {
|
|
3207
3560
|
if (focus === "tree") {
|
|
3561
|
+
stopTracking();
|
|
3208
3562
|
const next = Math.min(allFlat.length - 1, selectedIndex + 5);
|
|
3209
3563
|
setSelectedId(allFlat[next]?.id ?? selectedId);
|
|
3210
3564
|
} else {
|
|
@@ -3221,6 +3575,7 @@ function App({ mode }) {
|
|
|
3221
3575
|
},
|
|
3222
3576
|
onScrollHalfPageUp: () => {
|
|
3223
3577
|
if (focus === "tree") {
|
|
3578
|
+
stopTracking();
|
|
3224
3579
|
const prev = Math.max(0, selectedIndex - Math.ceil(5 / 2));
|
|
3225
3580
|
setSelectedId(allFlat[prev]?.id ?? selectedId);
|
|
3226
3581
|
} else {
|
|
@@ -3236,6 +3591,7 @@ function App({ mode }) {
|
|
|
3236
3591
|
},
|
|
3237
3592
|
onScrollHalfPageDown: () => {
|
|
3238
3593
|
if (focus === "tree") {
|
|
3594
|
+
stopTracking();
|
|
3239
3595
|
const next = Math.min(
|
|
3240
3596
|
allFlat.length - 1,
|
|
3241
3597
|
selectedIndex + Math.ceil(5 / 2)
|
|
@@ -3296,6 +3652,7 @@ function App({ mode }) {
|
|
|
3296
3652
|
return;
|
|
3297
3653
|
}
|
|
3298
3654
|
if (focus !== "tree" || !selectedId) return;
|
|
3655
|
+
stopTracking();
|
|
3299
3656
|
if (selectedId.startsWith("__proj-") && selectedId.endsWith("__")) {
|
|
3300
3657
|
const projectName = selectedId.slice(7, -2);
|
|
3301
3658
|
const isCold = sessionTree.coldProjects.some(
|
|
@@ -3380,6 +3737,7 @@ function App({ mode }) {
|
|
|
3380
3737
|
},
|
|
3381
3738
|
onHide: () => {
|
|
3382
3739
|
if (focus !== "tree" || !selectedId) return;
|
|
3740
|
+
stopTracking();
|
|
3383
3741
|
if (selectedId.startsWith("__proj-") && selectedId.endsWith("__")) {
|
|
3384
3742
|
const projectName = selectedId.slice(7, -2);
|
|
3385
3743
|
hideProject(projectName);
|
|
@@ -3439,7 +3797,13 @@ function App({ mode }) {
|
|
|
3439
3797
|
if (isWatchMode && (width < MIN_WIDTH || height + 1 < MIN_HEIGHT)) {
|
|
3440
3798
|
return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", width, children: [
|
|
3441
3799
|
/* @__PURE__ */ jsx5(Text5, { bold: true, children: "AgentHUD needs a larger terminal." }),
|
|
3442
|
-
/* @__PURE__ */ jsx5(
|
|
3800
|
+
/* @__PURE__ */ jsx5(
|
|
3801
|
+
Text5,
|
|
3802
|
+
{
|
|
3803
|
+
dimColor: true,
|
|
3804
|
+
children: `Minimum: ${MIN_WIDTH} cols \xD7 ${MIN_HEIGHT} rows`
|
|
3805
|
+
}
|
|
3806
|
+
),
|
|
3443
3807
|
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: `Current: ${width} cols \xD7 ${height + 1} rows` }),
|
|
3444
3808
|
/* @__PURE__ */ jsx5(Text5, { children: " " }),
|
|
3445
3809
|
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Resize the window and AgentHUD will redraw automatically." }),
|
|
@@ -3474,7 +3838,7 @@ function App({ mode }) {
|
|
|
3474
3838
|
helpTotalLinesRef.current = n;
|
|
3475
3839
|
}
|
|
3476
3840
|
}
|
|
3477
|
-
) : /* @__PURE__ */ jsxs5(
|
|
3841
|
+
) : /* @__PURE__ */ jsxs5(Fragment3, { children: [
|
|
3478
3842
|
migrationWarning && /* @__PURE__ */ jsx5(Box5, { marginBottom: 1, children: /* @__PURE__ */ jsx5(Text5, { color: "yellow", children: "Config moved to ~/.agenthud/config.yaml" }) }),
|
|
3479
3843
|
/* @__PURE__ */ jsx5(
|
|
3480
3844
|
SessionTreePanel,
|
|
@@ -3485,7 +3849,9 @@ function App({ mode }) {
|
|
|
3485
3849
|
hasFocus: focus === "tree",
|
|
3486
3850
|
width,
|
|
3487
3851
|
maxRows: treeRows,
|
|
3488
|
-
expandedIds
|
|
3852
|
+
expandedIds,
|
|
3853
|
+
trackingOn: tracking,
|
|
3854
|
+
spinner
|
|
3489
3855
|
}
|
|
3490
3856
|
),
|
|
3491
3857
|
/* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: detailMode && detailActivity ? /* @__PURE__ */ jsx5(
|
|
@@ -3506,8 +3872,8 @@ function App({ mode }) {
|
|
|
3506
3872
|
isLive,
|
|
3507
3873
|
newCount,
|
|
3508
3874
|
visibleRows: viewerRows,
|
|
3509
|
-
|
|
3510
|
-
|
|
3875
|
+
liveSpinnerFrame: spinner,
|
|
3876
|
+
liveTick,
|
|
3511
3877
|
width,
|
|
3512
3878
|
cursorLine: viewerCursorLine,
|
|
3513
3879
|
hasFocus: focus === "viewer",
|
|
@@ -3591,7 +3957,8 @@ if (options.mode === "summary") {
|
|
|
3591
3957
|
to: options.summaryTo,
|
|
3592
3958
|
today,
|
|
3593
3959
|
force: options.summaryForce ?? false,
|
|
3594
|
-
assumeYes: options.summaryAssumeYes ?? false
|
|
3960
|
+
assumeYes: options.summaryAssumeYes ?? false,
|
|
3961
|
+
model: options.summaryModel
|
|
3595
3962
|
});
|
|
3596
3963
|
process.exit(exitCode2);
|
|
3597
3964
|
}
|
|
@@ -3599,7 +3966,8 @@ if (options.mode === "summary") {
|
|
|
3599
3966
|
date: options.summaryDate,
|
|
3600
3967
|
prompt: options.summaryPrompt,
|
|
3601
3968
|
force: options.summaryForce ?? false,
|
|
3602
|
-
today
|
|
3969
|
+
today,
|
|
3970
|
+
model: options.summaryModel
|
|
3603
3971
|
});
|
|
3604
3972
|
process.exit(exitCode);
|
|
3605
3973
|
}
|
package/package.json
CHANGED