agenthud 0.9.2 → 0.9.3
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 +41 -25
- package/dist/index.js +1 -1
- package/dist/{main-6SGKXL7E.js → main-S27FZ2BJ.js} +395 -157
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,36 +23,39 @@ Run this in a separate terminal while using Claude Code. Press `?` inside the TU
|
|
|
23
23
|
AgentHUD reads Claude Code's session files from `~/.claude/projects/` and displays them in a split view:
|
|
24
24
|
|
|
25
25
|
```
|
|
26
|
-
┌─ Projects
|
|
27
|
-
│ > agenthud ~/WestbrookAI/agenthud
|
|
28
|
-
│ #864f [hot] Fix the auth bug in login flow
|
|
29
|
-
│ ├─ » code-reviewer
|
|
30
|
-
│ (#398c [warm])
|
|
31
|
-
│ myproject ~/work/myproject
|
|
32
|
-
│ #def4 [hot] Add OAuth support
|
|
33
|
-
│ ... 12 cold projects
|
|
34
|
-
|
|
35
|
-
┌─ Activity · agenthud
|
|
36
|
-
│ [10:23] ○ Read src/ui/App.tsx
|
|
37
|
-
│ [10:23] ~ Edit src/ui/App.tsx
|
|
38
|
-
│ [10:23] $ Bash npm test
|
|
39
|
-
│ [10:23] < Response Tests passed successfully
|
|
40
|
-
│ [10:25] ◆ abc1234 feat: fix auth callback
|
|
41
|
-
|
|
26
|
+
┌─ Projects ───────────────────────────────────────────────┐
|
|
27
|
+
│ > agenthud ~/WestbrookAI/agenthud 13m │
|
|
28
|
+
│ #864f [hot] Fix the auth bug in login flow │
|
|
29
|
+
│ ├─ » code-reviewer │
|
|
30
|
+
│ (#398c [warm]) │
|
|
31
|
+
│ myproject ~/work/myproject 2d │
|
|
32
|
+
│ #def4 [hot] Add OAuth support │
|
|
33
|
+
│ ... 12 cold projects │
|
|
34
|
+
└──────────────────────────────────────────────────────────┘
|
|
35
|
+
┌─ Activity · agenthud ────────────────────────────────────┐
|
|
36
|
+
│ [10:23] ○ Read src/ui/App.tsx │
|
|
37
|
+
│ [10:23] ~ Edit src/ui/App.tsx │
|
|
38
|
+
│ [10:23] $ Bash npm test │
|
|
39
|
+
│ [10:23] < Response Tests passed successfully │
|
|
40
|
+
│ [10:25] ◆ abc1234 feat: fix auth callback │
|
|
41
|
+
│ › │
|
|
42
|
+
└──────────────────────────────────────────────────────────┘
|
|
42
43
|
```
|
|
43
44
|
|
|
44
45
|
**Project tree (top pane)**
|
|
45
46
|
- Sessions grouped under their project (project name + path at the top).
|
|
46
|
-
- Session rows show short ID + first user prompt (the session's "topic").
|
|
47
|
+
- Session rows show short ID + first user prompt (the session's "topic"). Long titles truncate with a `…` suffix.
|
|
48
|
+
- Right edge of each row shows how long ago it was last touched: `42m`, `17h`, `3d`, `2w`, `1mo`, `1y`. Project rows use the most recent session's mtime.
|
|
47
49
|
- Non-interactive sessions (from `claude -p`, SDK, `agenthud summary`) appear in parens and dimmed.
|
|
48
50
|
- Sub-agents nest one level deeper under their parent session.
|
|
49
51
|
- Cold projects collapse under `... N cold projects` at the bottom (press Enter on the line to expand).
|
|
50
52
|
- Press `h` to hide a project, session, or sub-agent (saved to `~/.agenthud/state.yaml`).
|
|
51
53
|
|
|
52
54
|
**Activity viewer (bottom pane)**
|
|
53
|
-
- Real-time feed for the selected session: file reads, edits, bash, responses, thinking, git commits.
|
|
55
|
+
- Real-time feed for the selected session: file reads, edits, bash, responses, thinking, git commits. Newest at the bottom, like `tail -f`.
|
|
56
|
+
- A `›` slides left → right along the bottom row while the viewer is in LIVE mode — visible proof the feed is alive. Hidden when paused (scrolled into history) or when the session has no activity yet.
|
|
54
57
|
- Press `f` to cycle through filter presets (configurable).
|
|
55
|
-
- Press `↵` on any row to open a scrollable detail view; on a commit row this shows `git show --stat`.
|
|
58
|
+
- Press `↵` on any row to open a scrollable detail view; on a commit row this shows `git show --stat --patch`.
|
|
56
59
|
|
|
57
60
|
## Session status
|
|
58
61
|
|
|
@@ -110,10 +113,11 @@ Full reference is also available inside the app — press `?`.
|
|
|
110
113
|
| `PgUp` / `Ctrl+B` | Page up |
|
|
111
114
|
| `PgDn` / `Ctrl+F` | Page down |
|
|
112
115
|
| `Ctrl+U` / `Ctrl+D` | Half page up / down |
|
|
113
|
-
| `g` | Jump to
|
|
114
|
-
| `G` | Jump to
|
|
116
|
+
| `g` | Jump to top (oldest) |
|
|
117
|
+
| `G` | Jump to live (newest, bottom) |
|
|
115
118
|
| `↵` | Open detail view |
|
|
116
119
|
| `f` | Cycle filter preset |
|
|
120
|
+
| `r` | Refresh now |
|
|
117
121
|
| `Tab` | Switch focus to project tree |
|
|
118
122
|
| `?` | Help |
|
|
119
123
|
| `q` | Quit |
|
|
@@ -125,6 +129,17 @@ Full reference is also available inside the app — press `?`.
|
|
|
125
129
|
| `↑` / `k` / `↓` / `j` | Scroll |
|
|
126
130
|
| `↵` / `Esc` / `q` | Close |
|
|
127
131
|
|
|
132
|
+
Detail view colors the content based on activity type:
|
|
133
|
+
|
|
134
|
+
- **Git commit detail** (`git show --stat --patch`): added lines green (`+`), removed lines red (`-`), hunk headers cyan (`@@ ... @@`), `commit/Author/Date/diff` metadata dimmed.
|
|
135
|
+
- **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
|
+
|
|
137
|
+
## Behavior
|
|
138
|
+
|
|
139
|
+
- **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
|
+
- **Minimum terminal size.** 80 cols × 20 rows. Smaller terminals show a one-line hint and redraw automatically when you resize.
|
|
141
|
+
- **Help overlay scrolls.** Press `?` for an in-app reference. The overlay scrolls (`j/k`, `PgUp/PgDn`, `Ctrl+B/F`, `Space`, `g/G`) so the full content is reachable on shorter terminals.
|
|
142
|
+
|
|
128
143
|
## Report
|
|
129
144
|
|
|
130
145
|
Print activity for a date in Markdown or JSON — suitable for piping to scripts or LLMs:
|
|
@@ -155,7 +170,7 @@ Output:
|
|
|
155
170
|
|
|
156
171
|
| Flag | Default | Description |
|
|
157
172
|
|------|---------|-------------|
|
|
158
|
-
| `--date` | today | `YYYY-MM-DD` or `
|
|
173
|
+
| `--date` | today | `YYYY-MM-DD`, `today`, `yesterday`, or `-Nd` (N days ago, local date) |
|
|
159
174
|
| `--include` | `response,bash,edit,thinking` | Comma-separated types or `all` |
|
|
160
175
|
| `--format` | `markdown` | `markdown` or `json` |
|
|
161
176
|
| `--detail-limit` | `120` | Max chars per detail field; `0` = unlimited |
|
|
@@ -202,10 +217,11 @@ Each missing daily prompts for confirmation just before generation, so you see c
|
|
|
202
217
|
refreshInterval: 2s
|
|
203
218
|
|
|
204
219
|
# Activity filter presets (cycle with 'f' key in viewer)
|
|
205
|
-
# Each list is one preset
|
|
220
|
+
# Each list is one preset. Use "all" (or "*") to show everything.
|
|
221
|
+
# Types: response, user, bash, edit, thinking, read, glob, commit
|
|
206
222
|
filterPresets:
|
|
207
|
-
- []
|
|
208
|
-
- ["response"]
|
|
223
|
+
- ["all"]
|
|
224
|
+
- ["response", "user"]
|
|
209
225
|
- ["commit"]
|
|
210
226
|
```
|
|
211
227
|
|
package/dist/index.js
CHANGED
|
@@ -348,6 +348,44 @@ function parseArgs(args) {
|
|
|
348
348
|
return { mode: "watch" };
|
|
349
349
|
}
|
|
350
350
|
|
|
351
|
+
// src/utils/altScreen.ts
|
|
352
|
+
var ENTER = "\x1B[?1049h";
|
|
353
|
+
var LEAVE = "\x1B[?1049l";
|
|
354
|
+
var entered = false;
|
|
355
|
+
var left = false;
|
|
356
|
+
function enterAltScreen() {
|
|
357
|
+
if (entered) return;
|
|
358
|
+
entered = true;
|
|
359
|
+
process.stdout.write(ENTER);
|
|
360
|
+
}
|
|
361
|
+
function leaveAltScreen() {
|
|
362
|
+
if (left || !entered) return;
|
|
363
|
+
left = true;
|
|
364
|
+
process.stdout.write(LEAVE);
|
|
365
|
+
}
|
|
366
|
+
var hooksInstalled = false;
|
|
367
|
+
function installAltScreenCleanup() {
|
|
368
|
+
if (hooksInstalled) return;
|
|
369
|
+
hooksInstalled = true;
|
|
370
|
+
process.on("exit", () => {
|
|
371
|
+
leaveAltScreen();
|
|
372
|
+
});
|
|
373
|
+
process.on("SIGINT", () => {
|
|
374
|
+
leaveAltScreen();
|
|
375
|
+
process.exit(130);
|
|
376
|
+
});
|
|
377
|
+
process.on("SIGTERM", () => {
|
|
378
|
+
leaveAltScreen();
|
|
379
|
+
process.exit(143);
|
|
380
|
+
});
|
|
381
|
+
process.on("uncaughtException", (err) => {
|
|
382
|
+
leaveAltScreen();
|
|
383
|
+
setImmediate(() => {
|
|
384
|
+
throw err;
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
351
389
|
// src/config/globalConfig.ts
|
|
352
390
|
import { existsSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
353
391
|
import { homedir } from "os";
|
|
@@ -359,9 +397,16 @@ var DEFAULT_GLOBAL_CONFIG = {
|
|
|
359
397
|
refreshIntervalMs: 2e3,
|
|
360
398
|
hiddenSessions: [],
|
|
361
399
|
hiddenSubAgents: [],
|
|
362
|
-
|
|
400
|
+
// [] means "show all"; conversation preset bundles assistant + user;
|
|
401
|
+
// commits-only preset filters down to git activity.
|
|
402
|
+
filterPresets: [[], ["response", "user"], ["commit"]],
|
|
363
403
|
hiddenProjects: []
|
|
364
404
|
};
|
|
405
|
+
var ALL_PRESET_KEYWORDS = /* @__PURE__ */ new Set(["all", "*", "any"]);
|
|
406
|
+
function normalizePreset(tokens) {
|
|
407
|
+
if (tokens.some((t) => ALL_PRESET_KEYWORDS.has(t.toLowerCase()))) return [];
|
|
408
|
+
return tokens;
|
|
409
|
+
}
|
|
365
410
|
function parseInterval(value) {
|
|
366
411
|
const match = value.match(/^(\d+)(s|m)$/);
|
|
367
412
|
if (!match) return null;
|
|
@@ -381,10 +426,11 @@ function writeDefaultConfig() {
|
|
|
381
426
|
refreshInterval: 2s
|
|
382
427
|
|
|
383
428
|
# Activity filter presets (cycle with 'f' key in viewer)
|
|
384
|
-
# Each list is one preset
|
|
429
|
+
# Each list is one preset. Use "all" (or "*") to show everything.
|
|
430
|
+
# Types: response, user, bash, edit, thinking, read, glob, commit
|
|
385
431
|
filterPresets:
|
|
386
|
-
- []
|
|
387
|
-
- ["response"]
|
|
432
|
+
- ["all"]
|
|
433
|
+
- ["response", "user"]
|
|
388
434
|
- ["commit"]
|
|
389
435
|
`;
|
|
390
436
|
try {
|
|
@@ -430,9 +476,12 @@ function loadGlobalConfig() {
|
|
|
430
476
|
if (ms !== null) config.refreshIntervalMs = ms;
|
|
431
477
|
}
|
|
432
478
|
if (Array.isArray(configRaw.filterPresets)) {
|
|
433
|
-
const presets = configRaw.filterPresets.filter(Array.isArray).map(
|
|
434
|
-
|
|
435
|
-
|
|
479
|
+
const presets = configRaw.filterPresets.filter(Array.isArray).map((p) => {
|
|
480
|
+
const tokens = p.filter(
|
|
481
|
+
(t) => typeof t === "string"
|
|
482
|
+
);
|
|
483
|
+
return normalizePreset(tokens);
|
|
484
|
+
});
|
|
436
485
|
if (presets.length > 0) config.filterPresets = presets;
|
|
437
486
|
}
|
|
438
487
|
const legacyHidden = {};
|
|
@@ -572,7 +621,7 @@ function getCommitDetail(projectPath, hash) {
|
|
|
572
621
|
if (!projectPath) return null;
|
|
573
622
|
try {
|
|
574
623
|
return execSync(
|
|
575
|
-
`git --git-dir="${projectPath}/.git" show --stat --no-color ${hash}`,
|
|
624
|
+
`git --git-dir="${projectPath}/.git" show --stat --patch --no-color ${hash}`,
|
|
576
625
|
{ encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }
|
|
577
626
|
).trim();
|
|
578
627
|
} catch {
|
|
@@ -931,6 +980,21 @@ function createBottomLine(panelWidth = DEFAULT_PANEL_WIDTH) {
|
|
|
931
980
|
return BOX.bl + BOX.h.repeat(getInnerWidth(panelWidth)) + BOX.br;
|
|
932
981
|
}
|
|
933
982
|
var SEPARATOR = "\u2500".repeat(CONTENT_WIDTH);
|
|
983
|
+
function truncateByWidth(text, maxWidth) {
|
|
984
|
+
if (maxWidth <= 0) return "";
|
|
985
|
+
if (getDisplayWidth(text) <= maxWidth) return text;
|
|
986
|
+
if (maxWidth === 1) return "\u2026";
|
|
987
|
+
const ellipsisWidth = 1;
|
|
988
|
+
let acc = "";
|
|
989
|
+
let used = 0;
|
|
990
|
+
for (const ch of text) {
|
|
991
|
+
const w = getDisplayWidth(ch);
|
|
992
|
+
if (used + w + ellipsisWidth > maxWidth) break;
|
|
993
|
+
acc += ch;
|
|
994
|
+
used += w;
|
|
995
|
+
}
|
|
996
|
+
return `${acc}\u2026`;
|
|
997
|
+
}
|
|
934
998
|
var widthCache = /* @__PURE__ */ new Map();
|
|
935
999
|
function getDisplayWidth(s) {
|
|
936
1000
|
const cached = widthCache.get(s);
|
|
@@ -965,13 +1029,19 @@ function getSessionStatus(mtimeMs) {
|
|
|
965
1029
|
}
|
|
966
1030
|
return "cold";
|
|
967
1031
|
}
|
|
1032
|
+
var MAX_TITLE_LEN = 300;
|
|
1033
|
+
function capWithEllipsis(s, max = MAX_TITLE_LEN) {
|
|
1034
|
+
const trimmed = s.trim();
|
|
1035
|
+
if (trimmed.length <= max) return trimmed;
|
|
1036
|
+
return `${trimmed.slice(0, max - 1)}\u2026`;
|
|
1037
|
+
}
|
|
968
1038
|
function extractTaskDescription(content) {
|
|
969
1039
|
const headerMatch = content.match(/##\s*(Task\s+\d+[:\s].+)/m);
|
|
970
|
-
if (headerMatch) return headerMatch[1]
|
|
1040
|
+
if (headerMatch) return capWithEllipsis(headerMatch[1]);
|
|
971
1041
|
const thisTaskMatch = content.match(/\*\*This Task[^:]+:\*\*\s*(.+)/);
|
|
972
|
-
if (thisTaskMatch) return thisTaskMatch[1]
|
|
1042
|
+
if (thisTaskMatch) return capWithEllipsis(thisTaskMatch[1]);
|
|
973
1043
|
const firstLine = content.split("\n").find((l) => l.trim());
|
|
974
|
-
return (firstLine ?? "")
|
|
1044
|
+
return capWithEllipsis(firstLine ?? "");
|
|
975
1045
|
}
|
|
976
1046
|
function readSubAgentInfo(filePath) {
|
|
977
1047
|
if (!existsSync3(filePath)) return { agentId: null, taskDescription: null };
|
|
@@ -1054,8 +1124,7 @@ function readFirstUserPrompt(filePath) {
|
|
|
1054
1124
|
if (!text || isSystemNoise(text)) continue;
|
|
1055
1125
|
const firstLine = text.split("\n").find((l) => l.trim()) ?? "";
|
|
1056
1126
|
if (!firstLine || isSystemNoise(firstLine)) continue;
|
|
1057
|
-
|
|
1058
|
-
return trimmed.length > 80 ? trimmed.slice(0, 80) : trimmed;
|
|
1127
|
+
return capWithEllipsis(firstLine);
|
|
1059
1128
|
}
|
|
1060
1129
|
return null;
|
|
1061
1130
|
}
|
|
@@ -1680,7 +1749,7 @@ agenthud: combining ${dailyMarkdowns.length} daily summaries into range summary.
|
|
|
1680
1749
|
// src/ui/App.tsx
|
|
1681
1750
|
import { existsSync as existsSync5, watch } from "fs";
|
|
1682
1751
|
import { Box as Box5, Text as Text5, useApp, useInput, useStdout } from "ink";
|
|
1683
|
-
import { useCallback, useEffect as
|
|
1752
|
+
import { useCallback, useEffect as useEffect3, useMemo, useRef, useState as useState3 } from "react";
|
|
1684
1753
|
|
|
1685
1754
|
// src/ui/ActivityViewerPanel.tsx
|
|
1686
1755
|
import { Box, Text } from "ink";
|
|
@@ -1740,6 +1809,8 @@ function ActivityViewerPanel({
|
|
|
1740
1809
|
isLive,
|
|
1741
1810
|
newCount,
|
|
1742
1811
|
visibleRows,
|
|
1812
|
+
trailingBlankRows = 0,
|
|
1813
|
+
liveIndicatorPosition = null,
|
|
1743
1814
|
width,
|
|
1744
1815
|
cursorLine,
|
|
1745
1816
|
hasFocus,
|
|
@@ -1753,18 +1824,18 @@ function ActivityViewerPanel({
|
|
|
1753
1824
|
if (isLive) {
|
|
1754
1825
|
titleSuffix = `[LIVE ${spinner || "\u25BC"}${filterSuffix}]`;
|
|
1755
1826
|
} else {
|
|
1756
|
-
const badge = newCount > 0 ? ` +${newCount}\
|
|
1757
|
-
titleSuffix = `[PAUSED \
|
|
1827
|
+
const badge = newCount > 0 ? ` +${newCount}\u2193` : "";
|
|
1828
|
+
titleSuffix = `[PAUSED \u2191${scrollOffset}${badge}${filterSuffix}]`;
|
|
1758
1829
|
}
|
|
1759
1830
|
let visibleActivities;
|
|
1760
1831
|
if (activities.length === 0) {
|
|
1761
1832
|
visibleActivities = [];
|
|
1762
1833
|
} else if (isLive) {
|
|
1763
|
-
visibleActivities = activities.slice(-visibleRows)
|
|
1834
|
+
visibleActivities = activities.slice(-visibleRows);
|
|
1764
1835
|
} else {
|
|
1765
1836
|
const end = Math.max(0, activities.length - scrollOffset);
|
|
1766
1837
|
const start = Math.max(0, end - visibleRows);
|
|
1767
|
-
visibleActivities = activities.slice(start, end)
|
|
1838
|
+
visibleActivities = activities.slice(start, end);
|
|
1768
1839
|
}
|
|
1769
1840
|
const now = /* @__PURE__ */ new Date();
|
|
1770
1841
|
const lines = [];
|
|
@@ -1782,10 +1853,11 @@ function ActivityViewerPanel({
|
|
|
1782
1853
|
);
|
|
1783
1854
|
} else {
|
|
1784
1855
|
const effectiveCursor = Math.min(cursorLine, visibleActivities.length - 1);
|
|
1856
|
+
const cursorIndexInSlice = visibleActivities.length - 1 - effectiveCursor;
|
|
1785
1857
|
for (let i = 0; i < visibleActivities.length; i++) {
|
|
1786
1858
|
const activity = visibleActivities[i];
|
|
1787
1859
|
const style = getActivityStyle(activity);
|
|
1788
|
-
const isCursor = hasFocus && i ===
|
|
1860
|
+
const isCursor = hasFocus && i === cursorIndexInSlice;
|
|
1789
1861
|
const time = formatActivityTime(activity.timestamp, now);
|
|
1790
1862
|
const timestamp = `[${time}] `;
|
|
1791
1863
|
const timestampWidth = timestamp.length;
|
|
@@ -1837,28 +1909,110 @@ function ActivityViewerPanel({
|
|
|
1837
1909
|
}
|
|
1838
1910
|
}
|
|
1839
1911
|
const emptyRow = `${BOX.v}${" ".repeat(contentWidth + 1)}${BOX.v}`;
|
|
1840
|
-
|
|
1841
|
-
|
|
1912
|
+
const padCount = Math.max(0, visibleRows - lines.length);
|
|
1913
|
+
const padded = [];
|
|
1914
|
+
for (let i = 0; i < padCount; i++) {
|
|
1915
|
+
padded.push(/* @__PURE__ */ jsx(Text, { children: emptyRow }, `pad-${i}`));
|
|
1916
|
+
}
|
|
1917
|
+
const hasContent = visibleActivities.length > 0;
|
|
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
|
+
}
|
|
1842
1938
|
}
|
|
1939
|
+
const finalLines = [...padded, ...lines, ...trailing];
|
|
1843
1940
|
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width, children: [
|
|
1844
1941
|
/* @__PURE__ */ jsx(Text, { color: isLive ? void 0 : "yellow", children: createTitleLine(sessionName, titleSuffix, width) }),
|
|
1845
|
-
|
|
1942
|
+
finalLines,
|
|
1846
1943
|
/* @__PURE__ */ jsx(Text, { children: createBottomLine(width) })
|
|
1847
1944
|
] });
|
|
1848
1945
|
}
|
|
1849
1946
|
|
|
1850
1947
|
// src/ui/DetailViewPanel.tsx
|
|
1851
1948
|
import { Box as Box2, Text as Text2 } from "ink";
|
|
1949
|
+
|
|
1950
|
+
// src/ui/lineColoring.ts
|
|
1951
|
+
var DIFF_META_PREFIXES = [
|
|
1952
|
+
"diff --git",
|
|
1953
|
+
"index ",
|
|
1954
|
+
"commit ",
|
|
1955
|
+
"Author:",
|
|
1956
|
+
"Date:",
|
|
1957
|
+
"Merge:"
|
|
1958
|
+
];
|
|
1959
|
+
function classifyDiffLines(lines) {
|
|
1960
|
+
return lines.map((line) => {
|
|
1961
|
+
if (line.startsWith("+++") || line.startsWith("---")) return "diff-meta";
|
|
1962
|
+
if (DIFF_META_PREFIXES.some((p) => line.startsWith(p))) return "diff-meta";
|
|
1963
|
+
if (line.startsWith("@@")) return "diff-hunk";
|
|
1964
|
+
if (line.startsWith("+")) return "diff-add";
|
|
1965
|
+
if (line.startsWith("-")) return "diff-remove";
|
|
1966
|
+
return "prose";
|
|
1967
|
+
});
|
|
1968
|
+
}
|
|
1969
|
+
function classifyCodeFences(lines) {
|
|
1970
|
+
const out = [];
|
|
1971
|
+
let inCode = false;
|
|
1972
|
+
for (const line of lines) {
|
|
1973
|
+
if (/^\s*```/.test(line)) {
|
|
1974
|
+
out.push("code-fence");
|
|
1975
|
+
inCode = !inCode;
|
|
1976
|
+
} else {
|
|
1977
|
+
out.push(inCode ? "code" : "prose");
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
return out;
|
|
1981
|
+
}
|
|
1982
|
+
function getLineStyle(category) {
|
|
1983
|
+
switch (category) {
|
|
1984
|
+
case "diff-add":
|
|
1985
|
+
return { color: "green" };
|
|
1986
|
+
case "diff-remove":
|
|
1987
|
+
return { color: "red" };
|
|
1988
|
+
case "diff-hunk":
|
|
1989
|
+
return { color: "cyan" };
|
|
1990
|
+
case "diff-meta":
|
|
1991
|
+
return { dimColor: true };
|
|
1992
|
+
case "code-fence":
|
|
1993
|
+
return { color: "cyan", dimColor: true };
|
|
1994
|
+
case "code":
|
|
1995
|
+
return { color: "cyan" };
|
|
1996
|
+
case "prose":
|
|
1997
|
+
return {};
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
// src/ui/DetailViewPanel.tsx
|
|
1852
2002
|
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1853
|
-
function
|
|
1854
|
-
if (!text) return ["(empty)"];
|
|
1855
|
-
const
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
2003
|
+
function wrapClassified(text, maxWidth, classifier) {
|
|
2004
|
+
if (!text) return [{ text: "(empty)", category: "prose" }];
|
|
2005
|
+
const sourceLines = text.split("\n");
|
|
2006
|
+
const categories = classifier(sourceLines);
|
|
2007
|
+
const out = [];
|
|
2008
|
+
for (let i = 0; i < sourceLines.length; i++) {
|
|
2009
|
+
const line = sourceLines[i];
|
|
2010
|
+
const cat = categories[i] ?? "prose";
|
|
2011
|
+
if (!line) {
|
|
2012
|
+
out.push({ text: "", category: cat });
|
|
1859
2013
|
continue;
|
|
1860
2014
|
}
|
|
1861
|
-
const words =
|
|
2015
|
+
const words = line.split(" ");
|
|
1862
2016
|
let current = "";
|
|
1863
2017
|
for (const word of words) {
|
|
1864
2018
|
if (!current) {
|
|
@@ -1866,13 +2020,13 @@ function wrapText(text, maxWidth) {
|
|
|
1866
2020
|
} else if (getDisplayWidth(`${current} ${word}`) <= maxWidth) {
|
|
1867
2021
|
current += ` ${word}`;
|
|
1868
2022
|
} else {
|
|
1869
|
-
|
|
2023
|
+
out.push({ text: current, category: cat });
|
|
1870
2024
|
current = word;
|
|
1871
2025
|
}
|
|
1872
2026
|
}
|
|
1873
|
-
if (current)
|
|
2027
|
+
if (current) out.push({ text: current, category: cat });
|
|
1874
2028
|
}
|
|
1875
|
-
return
|
|
2029
|
+
return out.length > 0 ? out : [{ text: "(empty)", category: "prose" }];
|
|
1876
2030
|
}
|
|
1877
2031
|
function DetailViewPanel({
|
|
1878
2032
|
activity,
|
|
@@ -1882,7 +2036,8 @@ function DetailViewPanel({
|
|
|
1882
2036
|
}) {
|
|
1883
2037
|
const innerWidth = getInnerWidth(width);
|
|
1884
2038
|
const contentWidth = innerWidth - 1;
|
|
1885
|
-
const
|
|
2039
|
+
const classifier = activity.type === "commit" ? classifyDiffLines : classifyCodeFences;
|
|
2040
|
+
const allLines = wrapClassified(activity.detail, contentWidth, classifier);
|
|
1886
2041
|
const totalLines = allLines.length;
|
|
1887
2042
|
const clampedOffset = Math.min(
|
|
1888
2043
|
scrollOffset,
|
|
@@ -1906,13 +2061,14 @@ function DetailViewPanel({
|
|
|
1906
2061
|
const titleRight = `${dashes}${scrollPart}${BOX.tr}`;
|
|
1907
2062
|
const contentRows = [];
|
|
1908
2063
|
for (let i = 0; i < visibleRows; i++) {
|
|
1909
|
-
const
|
|
1910
|
-
const padding = Math.max(0, contentWidth - getDisplayWidth(
|
|
2064
|
+
const entry = visibleSlice[i] ?? { text: "", category: "prose" };
|
|
2065
|
+
const padding = Math.max(0, contentWidth - getDisplayWidth(entry.text));
|
|
2066
|
+
const lineStyle = getLineStyle(entry.category);
|
|
1911
2067
|
contentRows.push(
|
|
1912
2068
|
/* @__PURE__ */ jsxs2(Text2, { children: [
|
|
1913
2069
|
BOX.v,
|
|
1914
2070
|
" ",
|
|
1915
|
-
|
|
2071
|
+
/* @__PURE__ */ jsx2(Text2, { color: lineStyle.color, dimColor: lineStyle.dimColor, children: entry.text }),
|
|
1916
2072
|
" ".repeat(padding),
|
|
1917
2073
|
BOX.v
|
|
1918
2074
|
] }, i)
|
|
@@ -1956,8 +2112,8 @@ var SECTIONS = [
|
|
|
1956
2112
|
["\u2191 \u2193 / k j", "Scroll one line"],
|
|
1957
2113
|
["PgUp/Dn, Ctrl+B/F", "Scroll one page"],
|
|
1958
2114
|
["Ctrl+U / Ctrl+D", "Scroll half page"],
|
|
1959
|
-
["g", "Jump to
|
|
1960
|
-
["G", "Jump to
|
|
2115
|
+
["g", "Jump to top (oldest)"],
|
|
2116
|
+
["G", "Jump to live (newest, bottom)"],
|
|
1961
2117
|
["\u21B5", "Open detail view for selected activity"],
|
|
1962
2118
|
["f", "Cycle filter preset (set in config.yaml)"],
|
|
1963
2119
|
["Tab", "Switch focus to project tree"]
|
|
@@ -2234,8 +2390,8 @@ function useHotkeys({
|
|
|
2234
2390
|
"Tab: projects",
|
|
2235
2391
|
"\u2191\u2193/jk: scroll",
|
|
2236
2392
|
"PgUp/Dn: page",
|
|
2237
|
-
"g:
|
|
2238
|
-
"G:
|
|
2393
|
+
"g: oldest",
|
|
2394
|
+
"G: live",
|
|
2239
2395
|
"\u21B5: detail",
|
|
2240
2396
|
`f: ${filterLabel}`,
|
|
2241
2397
|
"?: help",
|
|
@@ -2244,12 +2400,29 @@ function useHotkeys({
|
|
|
2244
2400
|
return { handleInput, statusBarItems };
|
|
2245
2401
|
}
|
|
2246
2402
|
|
|
2247
|
-
// src/ui/hooks/
|
|
2403
|
+
// src/ui/hooks/useSlide.ts
|
|
2248
2404
|
import { useEffect, useState } from "react";
|
|
2249
|
-
|
|
2250
|
-
function useSpinner(active, intervalMs = 100) {
|
|
2405
|
+
function useSlide(active, positions, intervalMs = 180, resetKey) {
|
|
2251
2406
|
const [index, setIndex] = useState(0);
|
|
2407
|
+
useEffect(() => {
|
|
2408
|
+
setIndex(0);
|
|
2409
|
+
}, [resetKey]);
|
|
2252
2410
|
useEffect(() => {
|
|
2411
|
+
if (!active) return;
|
|
2412
|
+
const timer = setInterval(() => {
|
|
2413
|
+
setIndex((i) => (i + 1) % positions);
|
|
2414
|
+
}, intervalMs);
|
|
2415
|
+
return () => clearInterval(timer);
|
|
2416
|
+
}, [active, positions, intervalMs]);
|
|
2417
|
+
return index;
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2420
|
+
// src/ui/hooks/useSpinner.ts
|
|
2421
|
+
import { useEffect as useEffect2, useState as useState2 } from "react";
|
|
2422
|
+
var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
2423
|
+
function useSpinner(active, intervalMs = 100) {
|
|
2424
|
+
const [index, setIndex] = useState2(0);
|
|
2425
|
+
useEffect2(() => {
|
|
2253
2426
|
if (!active) return;
|
|
2254
2427
|
const timer = setInterval(() => {
|
|
2255
2428
|
setIndex((i) => (i + 1) % FRAMES.length);
|
|
@@ -2263,12 +2436,20 @@ function useSpinner(active, intervalMs = 100) {
|
|
|
2263
2436
|
import { homedir as homedir4 } from "os";
|
|
2264
2437
|
import { Box as Box4, Text as Text4 } from "ink";
|
|
2265
2438
|
import { Fragment, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
2266
|
-
function formatElapsed(lastModifiedMs) {
|
|
2267
|
-
const elapsed =
|
|
2439
|
+
function formatElapsed(lastModifiedMs, now = Date.now()) {
|
|
2440
|
+
const elapsed = Math.max(0, now - lastModifiedMs);
|
|
2268
2441
|
const seconds = Math.floor(elapsed / 1e3);
|
|
2269
2442
|
const minutes = Math.floor(seconds / 60);
|
|
2270
2443
|
const hours = Math.floor(minutes / 60);
|
|
2271
|
-
|
|
2444
|
+
const days = Math.floor(hours / 24);
|
|
2445
|
+
const weeks = Math.floor(days / 7);
|
|
2446
|
+
const months = Math.floor(days / 30);
|
|
2447
|
+
const years = Math.floor(days / 365);
|
|
2448
|
+
if (years >= 1) return `${years}y`;
|
|
2449
|
+
if (months >= 1) return `${months}mo`;
|
|
2450
|
+
if (weeks >= 1) return `${weeks}w`;
|
|
2451
|
+
if (days >= 1) return `${days}d`;
|
|
2452
|
+
if (hours > 0) return `${hours}h`;
|
|
2272
2453
|
if (minutes > 0) return `${minutes}m`;
|
|
2273
2454
|
if (seconds > 0) return `${seconds}s`;
|
|
2274
2455
|
return "<1s";
|
|
@@ -2290,11 +2471,6 @@ function formatProjectPath(projectPath) {
|
|
|
2290
2471
|
const raw = projectPath.startsWith(home) ? `~${projectPath.slice(home.length)}` : projectPath;
|
|
2291
2472
|
return raw;
|
|
2292
2473
|
}
|
|
2293
|
-
function truncatePath(path, maxWidth) {
|
|
2294
|
-
if (getDisplayWidth(path) <= maxWidth) return path;
|
|
2295
|
-
if (maxWidth < 4) return "";
|
|
2296
|
-
return `...${path.slice(-(maxWidth - 3))}`;
|
|
2297
|
-
}
|
|
2298
2474
|
function SessionRow({
|
|
2299
2475
|
session,
|
|
2300
2476
|
isSelected,
|
|
@@ -2316,36 +2492,40 @@ function SessionRow({
|
|
|
2316
2492
|
const leftCoreBase = `${prefix}${rawName}${shortIdDisplay} ${badge}`;
|
|
2317
2493
|
const leftCoreWidth = getDisplayWidth(leftCoreBase);
|
|
2318
2494
|
const rightWidth = getDisplayWidth(rightSide);
|
|
2319
|
-
const
|
|
2495
|
+
const RIGHT_GAP = 3;
|
|
2496
|
+
const middleAvailable = contentWidth - leftCoreWidth - 1 - rightWidth - RIGHT_GAP;
|
|
2320
2497
|
let middleText = "";
|
|
2321
|
-
if (middleAvailable >
|
|
2498
|
+
if (middleAvailable > 1) {
|
|
2322
2499
|
const raw = isParent ? session.firstUserPrompt ?? "" : session.taskDescription ?? "";
|
|
2323
2500
|
if (raw) {
|
|
2324
|
-
const
|
|
2501
|
+
const flat = raw.replace(/[\r\n\t]+/g, " ").trim();
|
|
2502
|
+
const truncated = truncateByWidth(flat, middleAvailable);
|
|
2325
2503
|
if (truncated) middleText = truncated;
|
|
2326
2504
|
}
|
|
2327
2505
|
}
|
|
2328
2506
|
const middleSection = middleText ? ` ${middleText}` : "";
|
|
2329
2507
|
const gapWidth = Math.max(
|
|
2330
|
-
|
|
2508
|
+
RIGHT_GAP,
|
|
2331
2509
|
contentWidth - leftCoreWidth - getDisplayWidth(middleSection) - rightWidth
|
|
2332
2510
|
);
|
|
2333
2511
|
const gap = " ".repeat(gapWidth);
|
|
2334
2512
|
const fullLine = leftCoreBase + middleSection + gap + rightSide;
|
|
2335
2513
|
const linePadding = Math.max(0, contentWidth - getDisplayWidth(fullLine));
|
|
2336
|
-
const
|
|
2337
|
-
const
|
|
2514
|
+
const focused = isSelected && hasFocus;
|
|
2515
|
+
const muted = isSelected && !hasFocus;
|
|
2516
|
+
const showBg = focused || muted;
|
|
2517
|
+
const shouldDim = isNonInteractive || muted;
|
|
2338
2518
|
return /* @__PURE__ */ jsxs4(Text4, { children: [
|
|
2339
2519
|
BOX.v,
|
|
2340
2520
|
" ",
|
|
2341
2521
|
/* @__PURE__ */ jsxs4(
|
|
2342
2522
|
Text4,
|
|
2343
2523
|
{
|
|
2344
|
-
backgroundColor:
|
|
2345
|
-
bold:
|
|
2346
|
-
dimColor: shouldDim && !
|
|
2524
|
+
backgroundColor: showBg ? "blue" : void 0,
|
|
2525
|
+
bold: focused,
|
|
2526
|
+
dimColor: shouldDim && !focused,
|
|
2347
2527
|
children: [
|
|
2348
|
-
/* @__PURE__ */ jsx4(Text4, { dimColor: shouldDim && !
|
|
2528
|
+
/* @__PURE__ */ jsx4(Text4, { dimColor: shouldDim && !focused, children: prefix }),
|
|
2349
2529
|
/* @__PURE__ */ jsx4(Text4, { bold: !shouldDim, children: rawName }),
|
|
2350
2530
|
shortIdDisplay ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: shortIdDisplay }) : null,
|
|
2351
2531
|
/* @__PURE__ */ jsx4(Text4, { children: " " }),
|
|
@@ -2442,23 +2622,47 @@ function ProjectRow({
|
|
|
2442
2622
|
}) {
|
|
2443
2623
|
const nameText = `> ${project.name}`;
|
|
2444
2624
|
const pathText = project.projectPath ? formatProjectPath(project.projectPath) : "";
|
|
2625
|
+
const latestMtime = project.sessions.reduce(
|
|
2626
|
+
(acc, s) => Math.max(acc, s.lastModifiedMs),
|
|
2627
|
+
0
|
|
2628
|
+
);
|
|
2629
|
+
const elapsed = latestMtime > 0 ? formatElapsed(latestMtime) : "";
|
|
2445
2630
|
const nameWidth = getDisplayWidth(nameText);
|
|
2446
2631
|
const pathWidth = pathText ? getDisplayWidth(pathText) : 0;
|
|
2447
|
-
const
|
|
2448
|
-
const
|
|
2632
|
+
const elapsedWidth = elapsed ? getDisplayWidth(elapsed) : 0;
|
|
2633
|
+
const middleGap = pathText ? 2 : 0;
|
|
2634
|
+
const leftWidth = nameWidth + middleGap + pathWidth;
|
|
2635
|
+
const PROJECT_RIGHT_GAP = 3;
|
|
2636
|
+
const rightGap = Math.max(
|
|
2637
|
+
PROJECT_RIGHT_GAP,
|
|
2638
|
+
contentWidth - leftWidth - elapsedWidth
|
|
2639
|
+
);
|
|
2640
|
+
const totalWidth = leftWidth + rightGap + elapsedWidth;
|
|
2449
2641
|
const padding = Math.max(0, contentWidth - totalWidth);
|
|
2450
|
-
const
|
|
2642
|
+
const focused = isSelected && hasFocus;
|
|
2643
|
+
const muted = isSelected && !hasFocus;
|
|
2644
|
+
const showBg = focused || muted;
|
|
2451
2645
|
return /* @__PURE__ */ jsxs4(Text4, { children: [
|
|
2452
2646
|
BOX.v,
|
|
2453
2647
|
" ",
|
|
2454
|
-
/* @__PURE__ */ jsxs4(
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
"
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2648
|
+
/* @__PURE__ */ jsxs4(
|
|
2649
|
+
Text4,
|
|
2650
|
+
{
|
|
2651
|
+
backgroundColor: showBg ? "blue" : void 0,
|
|
2652
|
+
bold: !showBg,
|
|
2653
|
+
dimColor: muted,
|
|
2654
|
+
children: [
|
|
2655
|
+
nameText,
|
|
2656
|
+
pathText ? /* @__PURE__ */ jsxs4(Fragment, { children: [
|
|
2657
|
+
" ",
|
|
2658
|
+
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: pathText })
|
|
2659
|
+
] }) : null,
|
|
2660
|
+
" ".repeat(rightGap),
|
|
2661
|
+
elapsed ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: elapsed }) : null,
|
|
2662
|
+
" ".repeat(padding)
|
|
2663
|
+
]
|
|
2664
|
+
}
|
|
2665
|
+
),
|
|
2462
2666
|
BOX.v
|
|
2463
2667
|
] });
|
|
2464
2668
|
}
|
|
@@ -2478,11 +2682,12 @@ function SubagentSummaryRow({
|
|
|
2478
2682
|
0,
|
|
2479
2683
|
contentWidth - getDisplayWidth(text) - getDisplayWidth(hint)
|
|
2480
2684
|
);
|
|
2481
|
-
const
|
|
2685
|
+
const focused = isSelected && hasFocus;
|
|
2686
|
+
const muted = isSelected && !hasFocus;
|
|
2482
2687
|
return /* @__PURE__ */ jsxs4(Text4, { children: [
|
|
2483
2688
|
BOX.v,
|
|
2484
2689
|
" ",
|
|
2485
|
-
/* @__PURE__ */ jsxs4(Text4, { dimColor: !
|
|
2690
|
+
/* @__PURE__ */ jsxs4(Text4, { dimColor: !focused, inverse: focused || muted, children: [
|
|
2486
2691
|
text,
|
|
2487
2692
|
" ".repeat(padding),
|
|
2488
2693
|
hint
|
|
@@ -2504,13 +2709,14 @@ function ColdProjectsSummaryRow({
|
|
|
2504
2709
|
const dashCount = Math.max(0, innerWidth - 1 - labelWidth - hintWidth);
|
|
2505
2710
|
const dashes = BOX.h.repeat(dashCount);
|
|
2506
2711
|
const line = `${BOX.ml}${BOX.h}${label}${dashes}${hint}${BOX.mr}`;
|
|
2507
|
-
const
|
|
2712
|
+
const focused = isSelected && hasFocus;
|
|
2713
|
+
const muted = isSelected && !hasFocus;
|
|
2508
2714
|
return /* @__PURE__ */ jsx4(
|
|
2509
2715
|
Text4,
|
|
2510
2716
|
{
|
|
2511
|
-
backgroundColor:
|
|
2512
|
-
bold:
|
|
2513
|
-
dimColor: !
|
|
2717
|
+
backgroundColor: focused || muted ? "blue" : void 0,
|
|
2718
|
+
bold: focused,
|
|
2719
|
+
dimColor: !focused,
|
|
2514
2720
|
children: line
|
|
2515
2721
|
}
|
|
2516
2722
|
);
|
|
@@ -2712,14 +2918,14 @@ function getSelectedActivity(acts, live, scrollOff, rows, cursorLine) {
|
|
|
2712
2918
|
if (acts.length === 0) return null;
|
|
2713
2919
|
let visible;
|
|
2714
2920
|
if (live) {
|
|
2715
|
-
visible = acts.slice(-rows)
|
|
2921
|
+
visible = acts.slice(-rows);
|
|
2716
2922
|
} else {
|
|
2717
2923
|
const end = Math.max(0, acts.length - scrollOff);
|
|
2718
2924
|
const start = Math.max(0, end - rows);
|
|
2719
|
-
visible = acts.slice(start, end)
|
|
2925
|
+
visible = acts.slice(start, end);
|
|
2720
2926
|
}
|
|
2721
2927
|
const effectiveCursor = Math.min(cursorLine, visible.length - 1);
|
|
2722
|
-
return visible[effectiveCursor] ?? null;
|
|
2928
|
+
return visible[visible.length - 1 - effectiveCursor] ?? null;
|
|
2723
2929
|
}
|
|
2724
2930
|
function App({ mode }) {
|
|
2725
2931
|
const { exit } = useApp();
|
|
@@ -2727,47 +2933,47 @@ function App({ mode }) {
|
|
|
2727
2933
|
const isWatchMode = mode === "watch";
|
|
2728
2934
|
const config = useMemo(() => loadGlobalConfig(), []);
|
|
2729
2935
|
const migrationWarning = useMemo(() => hasProjectLevelConfig(), []);
|
|
2730
|
-
const [sessionTree, setSessionTree] =
|
|
2936
|
+
const [sessionTree, setSessionTree] = useState3(
|
|
2731
2937
|
() => discoverSessions(config)
|
|
2732
2938
|
);
|
|
2733
|
-
const [selectedId, setSelectedId] =
|
|
2939
|
+
const [selectedId, setSelectedId] = useState3(() => {
|
|
2734
2940
|
const firstProject = sessionTree.projects[0];
|
|
2735
2941
|
if (firstProject) return `__proj-${firstProject.name}__`;
|
|
2736
2942
|
return null;
|
|
2737
2943
|
});
|
|
2738
|
-
const [focus, setFocus] =
|
|
2739
|
-
const [scrollOffset, setScrollOffset] =
|
|
2740
|
-
const [isLive, setIsLive] =
|
|
2741
|
-
const [activities, setActivities] =
|
|
2742
|
-
const [gitActivities, setGitActivities] =
|
|
2743
|
-
const [newCount, setNewCount] =
|
|
2744
|
-
const [expandedIds, setExpandedIds] =
|
|
2745
|
-
const [viewerCursorLine, setViewerCursorLine] =
|
|
2746
|
-
const [detailMode, setDetailMode] =
|
|
2747
|
-
const [detailActivity, setDetailActivity] =
|
|
2944
|
+
const [focus, setFocus] = useState3("tree");
|
|
2945
|
+
const [scrollOffset, setScrollOffset] = useState3(0);
|
|
2946
|
+
const [isLive, setIsLive] = useState3(true);
|
|
2947
|
+
const [activities, setActivities] = useState3([]);
|
|
2948
|
+
const [gitActivities, setGitActivities] = useState3([]);
|
|
2949
|
+
const [newCount, setNewCount] = useState3(0);
|
|
2950
|
+
const [expandedIds, setExpandedIds] = useState3(/* @__PURE__ */ new Set());
|
|
2951
|
+
const [viewerCursorLine, setViewerCursorLine] = useState3(0);
|
|
2952
|
+
const [detailMode, setDetailMode] = useState3(false);
|
|
2953
|
+
const [detailActivity, setDetailActivity] = useState3(
|
|
2748
2954
|
null
|
|
2749
2955
|
);
|
|
2750
|
-
const [detailScrollOffset, setDetailScrollOffset] =
|
|
2751
|
-
const [filterIndex, setFilterIndex] =
|
|
2752
|
-
const [helpMode, setHelpMode] =
|
|
2753
|
-
const [helpScroll, setHelpScroll] =
|
|
2956
|
+
const [detailScrollOffset, setDetailScrollOffset] = useState3(0);
|
|
2957
|
+
const [filterIndex, setFilterIndex] = useState3(0);
|
|
2958
|
+
const [helpMode, setHelpMode] = useState3(false);
|
|
2959
|
+
const [helpScroll, setHelpScroll] = useState3(0);
|
|
2754
2960
|
const helpTotalLinesRef = useRef(0);
|
|
2755
2961
|
const allFlat = useMemo(
|
|
2756
2962
|
() => flattenSessions2(sessionTree, expandedIds),
|
|
2757
2963
|
[sessionTree, expandedIds]
|
|
2758
2964
|
);
|
|
2759
2965
|
const allFlatRef = useRef(allFlat);
|
|
2760
|
-
|
|
2966
|
+
useEffect3(() => {
|
|
2761
2967
|
allFlatRef.current = allFlat;
|
|
2762
2968
|
}, [allFlat]);
|
|
2763
2969
|
const activitiesLengthRef = useRef(0);
|
|
2764
2970
|
const activitiesRef = useRef(activities);
|
|
2765
|
-
|
|
2971
|
+
useEffect3(() => {
|
|
2766
2972
|
activitiesLengthRef.current = activities.length;
|
|
2767
2973
|
activitiesRef.current = activities;
|
|
2768
2974
|
}, [activities]);
|
|
2769
2975
|
const lastLoadedFileRef = useRef(null);
|
|
2770
|
-
|
|
2976
|
+
useEffect3(() => {
|
|
2771
2977
|
let node = allFlatRef.current.find((s) => s.id === selectedId);
|
|
2772
2978
|
if (node && selectedId?.startsWith("__proj-") && selectedId.endsWith("__")) {
|
|
2773
2979
|
const projectName = selectedId.slice(7, -2);
|
|
@@ -2795,12 +3001,12 @@ function App({ mode }) {
|
|
|
2795
3001
|
if (fileChanged) setGitActivities([]);
|
|
2796
3002
|
}
|
|
2797
3003
|
}, [selectedId, sessionTree]);
|
|
2798
|
-
|
|
3004
|
+
useEffect3(() => {
|
|
2799
3005
|
setScrollOffset(0);
|
|
2800
3006
|
setIsLive(true);
|
|
2801
3007
|
setViewerCursorLine(0);
|
|
2802
3008
|
}, [filterIndex]);
|
|
2803
|
-
|
|
3009
|
+
useEffect3(() => {
|
|
2804
3010
|
if (!isWatchMode) return;
|
|
2805
3011
|
const node = allFlatRef.current.find((s) => s.id === selectedId);
|
|
2806
3012
|
if (!node?.projectPath) return;
|
|
@@ -2855,10 +3061,10 @@ function App({ mode }) {
|
|
|
2855
3061
|
}
|
|
2856
3062
|
}, [selectedId, isLive, expandedIds]);
|
|
2857
3063
|
const refreshRef = useRef(refresh);
|
|
2858
|
-
|
|
3064
|
+
useEffect3(() => {
|
|
2859
3065
|
refreshRef.current = refresh;
|
|
2860
3066
|
}, [refresh]);
|
|
2861
|
-
|
|
3067
|
+
useEffect3(() => {
|
|
2862
3068
|
if (!isWatchMode) return;
|
|
2863
3069
|
const projectsDir = getProjectsDir();
|
|
2864
3070
|
const usePolling = process.platform === "linux" || !existsSync5(projectsDir);
|
|
@@ -2912,8 +3118,21 @@ function App({ mode }) {
|
|
|
2912
3118
|
const maxTreeRows = Math.floor(height * (1 - VIEWER_HEIGHT_FRACTION));
|
|
2913
3119
|
const naturalTreeRows = allFlat.length;
|
|
2914
3120
|
const treeRows = Math.max(1, Math.min(naturalTreeRows, maxTreeRows));
|
|
2915
|
-
const
|
|
3121
|
+
const VIEWER_BREATHING_ROWS = 1;
|
|
3122
|
+
const viewerRows = Math.max(
|
|
3123
|
+
5,
|
|
3124
|
+
height - 7 - treeRows - VIEWER_BREATHING_ROWS
|
|
3125
|
+
);
|
|
2916
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
|
+
);
|
|
2917
3136
|
const helpViewportRows = Math.max(1, height - 3);
|
|
2918
3137
|
const helpScrollStep = (delta) => {
|
|
2919
3138
|
const max = Math.max(0, helpTotalLinesRef.current - helpViewportRows);
|
|
@@ -2930,11 +3149,30 @@ function App({ mode }) {
|
|
|
2930
3149
|
onHelpScroll: helpScrollStep,
|
|
2931
3150
|
onHelpScrollToTop: () => setHelpScroll(0),
|
|
2932
3151
|
onSwitchFocus: () => setFocus((f) => f === "tree" ? "viewer" : "tree"),
|
|
3152
|
+
// cursorLine = "entries back from the newest" (0 = newest = bottom row).
|
|
3153
|
+
// Up arrow moves visually upward = older direction = cursorLine++.
|
|
3154
|
+
// Down arrow moves visually downward = newer direction = cursorLine--.
|
|
2933
3155
|
onScrollUp: () => {
|
|
2934
3156
|
if (focus === "tree") {
|
|
2935
3157
|
if (selectedIndex === -1) return;
|
|
2936
3158
|
const prev = Math.max(0, selectedIndex - 1);
|
|
2937
3159
|
setSelectedId(allFlat[prev]?.id ?? selectedId);
|
|
3160
|
+
} else {
|
|
3161
|
+
if (viewerCursorLine < viewerRows - 1) {
|
|
3162
|
+
setViewerCursorLine((c) => c + 1);
|
|
3163
|
+
} else {
|
|
3164
|
+
setIsLive(false);
|
|
3165
|
+
setScrollOffset(
|
|
3166
|
+
(o) => Math.min(o + 1, Math.max(0, activities.length - viewerRows))
|
|
3167
|
+
);
|
|
3168
|
+
}
|
|
3169
|
+
}
|
|
3170
|
+
},
|
|
3171
|
+
onScrollDown: () => {
|
|
3172
|
+
if (focus === "tree") {
|
|
3173
|
+
if (selectedIndex === -1) return;
|
|
3174
|
+
const next = Math.min(allFlat.length - 1, selectedIndex + 1);
|
|
3175
|
+
setSelectedId(allFlat[next]?.id ?? selectedId);
|
|
2938
3176
|
} else {
|
|
2939
3177
|
if (viewerCursorLine > 0) {
|
|
2940
3178
|
setViewerCursorLine((c) => c - 1);
|
|
@@ -2950,26 +3188,25 @@ function App({ mode }) {
|
|
|
2950
3188
|
}
|
|
2951
3189
|
}
|
|
2952
3190
|
},
|
|
2953
|
-
|
|
3191
|
+
// PgUp/PgDn semantics flip to match the bottom-feed layout:
|
|
3192
|
+
// PgUp = visually up = older direction = scrollOffset++
|
|
3193
|
+
// PgDn = visually down = newer direction = scrollOffset--
|
|
3194
|
+
onScrollPageUp: () => {
|
|
2954
3195
|
if (focus === "tree") {
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
setSelectedId(allFlat[next]?.id ?? selectedId);
|
|
3196
|
+
const prev = Math.max(0, selectedIndex - 5);
|
|
3197
|
+
setSelectedId(allFlat[prev]?.id ?? selectedId);
|
|
2958
3198
|
} else {
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
(o) => Math.min(o + 1, Math.max(0, activities.length - viewerRows))
|
|
2965
|
-
);
|
|
2966
|
-
}
|
|
3199
|
+
setViewerCursorLine(0);
|
|
3200
|
+
setIsLive(false);
|
|
3201
|
+
setScrollOffset(
|
|
3202
|
+
(o) => Math.min(o + viewerRows, Math.max(0, activities.length - viewerRows))
|
|
3203
|
+
);
|
|
2967
3204
|
}
|
|
2968
3205
|
},
|
|
2969
|
-
|
|
3206
|
+
onScrollPageDown: () => {
|
|
2970
3207
|
if (focus === "tree") {
|
|
2971
|
-
const
|
|
2972
|
-
setSelectedId(allFlat[
|
|
3208
|
+
const next = Math.min(allFlat.length - 1, selectedIndex + 5);
|
|
3209
|
+
setSelectedId(allFlat[next]?.id ?? selectedId);
|
|
2973
3210
|
} else {
|
|
2974
3211
|
setViewerCursorLine(0);
|
|
2975
3212
|
setScrollOffset((o) => {
|
|
@@ -2982,22 +3219,28 @@ function App({ mode }) {
|
|
|
2982
3219
|
});
|
|
2983
3220
|
}
|
|
2984
3221
|
},
|
|
2985
|
-
|
|
3222
|
+
onScrollHalfPageUp: () => {
|
|
2986
3223
|
if (focus === "tree") {
|
|
2987
|
-
const
|
|
2988
|
-
setSelectedId(allFlat[
|
|
3224
|
+
const prev = Math.max(0, selectedIndex - Math.ceil(5 / 2));
|
|
3225
|
+
setSelectedId(allFlat[prev]?.id ?? selectedId);
|
|
2989
3226
|
} else {
|
|
2990
3227
|
setViewerCursorLine(0);
|
|
2991
3228
|
setIsLive(false);
|
|
2992
3229
|
setScrollOffset(
|
|
2993
|
-
(o) => Math.min(
|
|
3230
|
+
(o) => Math.min(
|
|
3231
|
+
o + Math.floor(viewerRows / 2),
|
|
3232
|
+
Math.max(0, activities.length - viewerRows)
|
|
3233
|
+
)
|
|
2994
3234
|
);
|
|
2995
3235
|
}
|
|
2996
3236
|
},
|
|
2997
|
-
|
|
3237
|
+
onScrollHalfPageDown: () => {
|
|
2998
3238
|
if (focus === "tree") {
|
|
2999
|
-
const
|
|
3000
|
-
|
|
3239
|
+
const next = Math.min(
|
|
3240
|
+
allFlat.length - 1,
|
|
3241
|
+
selectedIndex + Math.ceil(5 / 2)
|
|
3242
|
+
);
|
|
3243
|
+
setSelectedId(allFlat[next]?.id ?? selectedId);
|
|
3001
3244
|
} else {
|
|
3002
3245
|
setViewerCursorLine(0);
|
|
3003
3246
|
setScrollOffset((o) => {
|
|
@@ -3010,35 +3253,17 @@ function App({ mode }) {
|
|
|
3010
3253
|
});
|
|
3011
3254
|
}
|
|
3012
3255
|
},
|
|
3013
|
-
onScrollHalfPageDown: () => {
|
|
3014
|
-
if (focus === "tree") {
|
|
3015
|
-
const next = Math.min(
|
|
3016
|
-
allFlat.length - 1,
|
|
3017
|
-
selectedIndex + Math.ceil(5 / 2)
|
|
3018
|
-
);
|
|
3019
|
-
setSelectedId(allFlat[next]?.id ?? selectedId);
|
|
3020
|
-
} else {
|
|
3021
|
-
setViewerCursorLine(0);
|
|
3022
|
-
setIsLive(false);
|
|
3023
|
-
setScrollOffset(
|
|
3024
|
-
(o) => Math.min(
|
|
3025
|
-
o + Math.floor(viewerRows / 2),
|
|
3026
|
-
Math.max(0, activities.length - viewerRows)
|
|
3027
|
-
)
|
|
3028
|
-
);
|
|
3029
|
-
}
|
|
3030
|
-
},
|
|
3031
3256
|
onScrollTop: () => {
|
|
3257
|
+
setViewerCursorLine(Math.max(0, viewerRows - 1));
|
|
3258
|
+
setIsLive(false);
|
|
3259
|
+
setScrollOffset(Math.max(0, mergedActivities.length - viewerRows));
|
|
3260
|
+
},
|
|
3261
|
+
onScrollBottom: () => {
|
|
3032
3262
|
setViewerCursorLine(0);
|
|
3033
3263
|
setIsLive(true);
|
|
3034
3264
|
setScrollOffset(0);
|
|
3035
3265
|
setNewCount(0);
|
|
3036
3266
|
},
|
|
3037
|
-
onScrollBottom: () => {
|
|
3038
|
-
setViewerCursorLine(0);
|
|
3039
|
-
setIsLive(false);
|
|
3040
|
-
setScrollOffset(Math.max(0, mergedActivities.length - viewerRows));
|
|
3041
|
-
},
|
|
3042
3267
|
onDetailClose: () => {
|
|
3043
3268
|
setDetailMode(false);
|
|
3044
3269
|
},
|
|
@@ -3130,15 +3355,23 @@ function App({ mode }) {
|
|
|
3130
3355
|
const toggleKey = isCold ? `__expanded-session-${selectedId}` : `__collapsed-session-${selectedId}`;
|
|
3131
3356
|
setExpandedIds((prev) => {
|
|
3132
3357
|
const next = new Set(prev);
|
|
3133
|
-
if (
|
|
3134
|
-
next.
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3358
|
+
if (isCold) {
|
|
3359
|
+
if (next.has(toggleKey)) {
|
|
3360
|
+
next.delete(toggleKey);
|
|
3361
|
+
next.delete(selectedId);
|
|
3362
|
+
} else {
|
|
3363
|
+
next.add(toggleKey);
|
|
3139
3364
|
const firstSub = selectedSessionObj.subAgents[0];
|
|
3140
3365
|
if (firstSub) setSelectedId(firstSub.id);
|
|
3141
3366
|
}
|
|
3367
|
+
} else {
|
|
3368
|
+
if (next.has(toggleKey)) {
|
|
3369
|
+
next.delete(toggleKey);
|
|
3370
|
+
} else {
|
|
3371
|
+
next.add(toggleKey);
|
|
3372
|
+
next.delete(selectedId);
|
|
3373
|
+
setSelectedId(selectedId);
|
|
3374
|
+
}
|
|
3142
3375
|
}
|
|
3143
3376
|
return next;
|
|
3144
3377
|
});
|
|
@@ -3273,6 +3506,8 @@ function App({ mode }) {
|
|
|
3273
3506
|
isLive,
|
|
3274
3507
|
newCount,
|
|
3275
3508
|
visibleRows: viewerRows,
|
|
3509
|
+
trailingBlankRows: VIEWER_BREATHING_ROWS,
|
|
3510
|
+
liveIndicatorPosition,
|
|
3276
3511
|
width,
|
|
3277
3512
|
cursorLine: viewerCursorLine,
|
|
3278
3513
|
hasFocus: focus === "viewer",
|
|
@@ -3369,6 +3604,9 @@ if (options.mode === "summary") {
|
|
|
3369
3604
|
process.exit(exitCode);
|
|
3370
3605
|
}
|
|
3371
3606
|
if (options.mode === "watch") {
|
|
3372
|
-
|
|
3607
|
+
installAltScreenCleanup();
|
|
3608
|
+
enterAltScreen();
|
|
3609
|
+
} else {
|
|
3610
|
+
if (options.mode === "once") clearScreen();
|
|
3373
3611
|
}
|
|
3374
3612
|
render(React.createElement(App, { mode: options.mode }));
|
package/package.json
CHANGED