@hydra-acp/cli 0.1.2 → 0.1.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 +4 -2
- package/dist/cli.js +1195 -374
- package/dist/index.d.ts +12 -1
- package/dist/index.js +125 -21
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -17,6 +17,11 @@ function hydraHome() {
|
|
|
17
17
|
if (override && override.length > 0) {
|
|
18
18
|
return path.resolve(override);
|
|
19
19
|
}
|
|
20
|
+
if (process.env.VITEST) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
"HYDRA_ACP_HOME is unset under VITEST; vitest.setup.ts must run first"
|
|
23
|
+
);
|
|
24
|
+
}
|
|
20
25
|
return path.join(os.homedir(), ".hydra-acp");
|
|
21
26
|
}
|
|
22
27
|
var ROOT_ENV, paths;
|
|
@@ -43,7 +48,8 @@ var init_paths = __esm({
|
|
|
43
48
|
extensionsDir: () => path.join(hydraHome(), "extensions"),
|
|
44
49
|
extensionLogFile: (name) => path.join(hydraHome(), "extensions", `${name}.log`),
|
|
45
50
|
extensionPidFile: (name) => path.join(hydraHome(), "extensions", `${name}.pid`),
|
|
46
|
-
tuiHistoryFile: () => path.join(hydraHome(), "
|
|
51
|
+
tuiHistoryFile: (id) => path.join(hydraHome(), "sessions", id, "prompt-history"),
|
|
52
|
+
tuiLogFile: () => path.join(hydraHome(), "tui.log")
|
|
47
53
|
};
|
|
48
54
|
}
|
|
49
55
|
});
|
|
@@ -157,7 +163,11 @@ var init_config = __esm({
|
|
|
157
163
|
// /clear, ^L, resize — bypass this throttle. Default 1000 (1 Hz) keeps
|
|
158
164
|
// CPU low during heavy streaming; bump to 250 for 4 Hz, 100 for ~10 Hz,
|
|
159
165
|
// or 0 to disable throttling entirely.
|
|
160
|
-
repaintThrottleMs: z.number().int().nonnegative().default(1e3)
|
|
166
|
+
repaintThrottleMs: z.number().int().nonnegative().default(1e3),
|
|
167
|
+
// Cap on logical lines retained in the in-memory scrollback render
|
|
168
|
+
// buffer. Oldest lines are dropped on overflow. The on-disk session
|
|
169
|
+
// history is unaffected; this only bounds the TUI's local view buffer.
|
|
170
|
+
maxScrollbackLines: z.number().int().positive().default(1e4)
|
|
161
171
|
});
|
|
162
172
|
ExtensionName = z.string().min(1).regex(/^[A-Za-z0-9._-]+$/, "extension name must be filename-safe");
|
|
163
173
|
ExtensionBody = z.object({
|
|
@@ -182,7 +192,7 @@ var init_config = __esm({
|
|
|
182
192
|
// recency and truncated to this count. `--all` overrides in the CLI.
|
|
183
193
|
sessionListColdLimit: z.number().int().nonnegative().default(20),
|
|
184
194
|
extensions: z.record(ExtensionName, ExtensionBody).default({}),
|
|
185
|
-
tui: TuiConfig.default({ repaintThrottleMs: 1e3 })
|
|
195
|
+
tui: TuiConfig.default({ repaintThrottleMs: 1e3, maxScrollbackLines: 1e4 })
|
|
186
196
|
});
|
|
187
197
|
}
|
|
188
198
|
});
|
|
@@ -226,6 +236,9 @@ function extractHydraMeta(meta) {
|
|
|
226
236
|
if (typeof obj.currentMode === "string") {
|
|
227
237
|
out.currentMode = obj.currentMode;
|
|
228
238
|
}
|
|
239
|
+
if (typeof obj.turnStartedAt === "number" && obj.turnStartedAt > 0) {
|
|
240
|
+
out.turnStartedAt = obj.turnStartedAt;
|
|
241
|
+
}
|
|
229
242
|
if (Array.isArray(obj.availableCommands)) {
|
|
230
243
|
const cmds = [];
|
|
231
244
|
for (const raw of obj.availableCommands) {
|
|
@@ -703,6 +716,9 @@ var init_session = __esm({
|
|
|
703
716
|
}
|
|
704
717
|
this.idleTimeoutMs = init.idleTimeoutMs ?? 0;
|
|
705
718
|
this.spawnReplacementAgent = init.spawnReplacementAgent;
|
|
719
|
+
if (init.firstPromptSeeded) {
|
|
720
|
+
this.firstPromptSeeded = true;
|
|
721
|
+
}
|
|
706
722
|
this.historyStore = init.historyStore;
|
|
707
723
|
if (init.seedHistory && init.seedHistory.length > 0) {
|
|
708
724
|
this.history = [...init.seedHistory];
|
|
@@ -767,6 +783,29 @@ var init_session = __esm({
|
|
|
767
783
|
get attachedCount() {
|
|
768
784
|
return this.clients.size;
|
|
769
785
|
}
|
|
786
|
+
// Wall-clock when the in-flight agent turn began, or undefined when
|
|
787
|
+
// idle. Derived from history: the most recent prompt_received without
|
|
788
|
+
// a later turn_complete is the outstanding turn, and its recordedAt
|
|
789
|
+
// is when the prompt was first broadcast. Used by buildResponseMeta
|
|
790
|
+
// so a fresh client reattaching mid-turn boots up with the busy
|
|
791
|
+
// banner showing real elapsed time.
|
|
792
|
+
get turnStartedAt() {
|
|
793
|
+
for (let i = this.history.length - 1; i >= 0; i--) {
|
|
794
|
+
const entry = this.history[i];
|
|
795
|
+
if (!entry) {
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
const params = entry.params;
|
|
799
|
+
const kind = params?.update?.sessionUpdate;
|
|
800
|
+
if (kind === "turn_complete") {
|
|
801
|
+
return void 0;
|
|
802
|
+
}
|
|
803
|
+
if (kind === "prompt_received") {
|
|
804
|
+
return entry.recordedAt;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
return void 0;
|
|
808
|
+
}
|
|
770
809
|
// Snapshot of the current in-memory replay history. Used by the
|
|
771
810
|
// HTTP history endpoint to deliver the "what's accumulated so far"
|
|
772
811
|
// prefix before optionally tailing with onBroadcast. Returns a copy
|
|
@@ -844,13 +883,19 @@ var init_session = __esm({
|
|
|
844
883
|
this.broadcastPromptReceived(client, params);
|
|
845
884
|
this.maybeSeedTitleFromPrompt(params);
|
|
846
885
|
return this.enqueuePrompt(async () => {
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
886
|
+
let response;
|
|
887
|
+
try {
|
|
888
|
+
response = await this.agent.connection.request(
|
|
889
|
+
"session/prompt",
|
|
890
|
+
{
|
|
891
|
+
...params,
|
|
892
|
+
sessionId: this.upstreamSessionId
|
|
893
|
+
}
|
|
894
|
+
);
|
|
895
|
+
} catch (err) {
|
|
896
|
+
this.broadcastTurnComplete(client.clientId, { stopReason: "error" });
|
|
897
|
+
throw err;
|
|
898
|
+
}
|
|
854
899
|
this.broadcastTurnComplete(client.clientId, response);
|
|
855
900
|
return response;
|
|
856
901
|
});
|
|
@@ -938,6 +983,13 @@ var init_session = __esm({
|
|
|
938
983
|
return;
|
|
939
984
|
}
|
|
940
985
|
this.cancelIdleTimer();
|
|
986
|
+
if (opts.regenTitle && this.firstPromptSeeded) {
|
|
987
|
+
const timeoutMs = opts.regenTitleTimeoutMs ?? 5e3;
|
|
988
|
+
await Promise.race([
|
|
989
|
+
this.runTitleRegen().catch(() => void 0),
|
|
990
|
+
new Promise((r) => setTimeout(r, timeoutMs).unref?.())
|
|
991
|
+
]);
|
|
992
|
+
}
|
|
941
993
|
await this.agent.kill().catch(() => void 0);
|
|
942
994
|
this.markClosed({ deleteRecord: opts.deleteRecord ?? false });
|
|
943
995
|
}
|
|
@@ -986,7 +1038,7 @@ var init_session = __esm({
|
|
|
986
1038
|
}
|
|
987
1039
|
const promptParams = params ?? {};
|
|
988
1040
|
const text = extractPromptText(promptParams.prompt);
|
|
989
|
-
const seed = firstLine(text,
|
|
1041
|
+
const seed = firstLine(text, 200);
|
|
990
1042
|
if (!seed) {
|
|
991
1043
|
return;
|
|
992
1044
|
}
|
|
@@ -1365,7 +1417,8 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
1365
1417
|
}
|
|
1366
1418
|
this.idleTimer = setTimeout(() => {
|
|
1367
1419
|
this.idleTimer = void 0;
|
|
1368
|
-
|
|
1420
|
+
const opts = this.firstPromptSeeded ? { deleteRecord: false, regenTitle: true } : { deleteRecord: true };
|
|
1421
|
+
void this.close(opts).catch(() => void 0);
|
|
1369
1422
|
}, this.idleTimeoutMs);
|
|
1370
1423
|
if (typeof this.idleTimer.unref === "function") {
|
|
1371
1424
|
this.idleTimer.unref();
|
|
@@ -1650,6 +1703,147 @@ var init_daemon_bootstrap = __esm({
|
|
|
1650
1703
|
}
|
|
1651
1704
|
});
|
|
1652
1705
|
|
|
1706
|
+
// src/cli/session-row.ts
|
|
1707
|
+
function toRow(s, now = Date.now()) {
|
|
1708
|
+
return {
|
|
1709
|
+
session: stripHydraSessionPrefix(s.sessionId),
|
|
1710
|
+
upstream: s.upstreamSessionId ?? "-",
|
|
1711
|
+
status: (s.status ?? "live").toUpperCase(),
|
|
1712
|
+
clients: s.status === "cold" ? "-" : String(s.attachedClients),
|
|
1713
|
+
agent: s.agentId ?? "?",
|
|
1714
|
+
age: formatRelativeAge(s.updatedAt, now),
|
|
1715
|
+
title: s.title ?? "-",
|
|
1716
|
+
cwd: s.cwd
|
|
1717
|
+
};
|
|
1718
|
+
}
|
|
1719
|
+
function computeWidths(rows) {
|
|
1720
|
+
return {
|
|
1721
|
+
session: maxLen(HEADER.session, rows.map((r) => r.session)),
|
|
1722
|
+
upstream: maxLen(HEADER.upstream, rows.map((r) => r.upstream)),
|
|
1723
|
+
status: maxLen(HEADER.status, rows.map((r) => r.status)),
|
|
1724
|
+
clients: maxLen(HEADER.clients, rows.map((r) => r.clients)),
|
|
1725
|
+
agent: maxLen(HEADER.agent, rows.map((r) => r.agent)),
|
|
1726
|
+
age: maxLen(HEADER.age, rows.map((r) => r.age)),
|
|
1727
|
+
title: maxLen(HEADER.title, rows.map((r) => r.title))
|
|
1728
|
+
};
|
|
1729
|
+
}
|
|
1730
|
+
function formatRelativeAge(iso, now) {
|
|
1731
|
+
if (!iso) {
|
|
1732
|
+
return "?";
|
|
1733
|
+
}
|
|
1734
|
+
const t = Date.parse(iso);
|
|
1735
|
+
if (Number.isNaN(t)) {
|
|
1736
|
+
return "?";
|
|
1737
|
+
}
|
|
1738
|
+
const diff = Math.max(0, now - t);
|
|
1739
|
+
const sec = Math.floor(diff / 1e3);
|
|
1740
|
+
if (sec < 60) {
|
|
1741
|
+
return "<1m";
|
|
1742
|
+
}
|
|
1743
|
+
const min = Math.floor(sec / 60);
|
|
1744
|
+
if (min < 60) {
|
|
1745
|
+
return `${min}m`;
|
|
1746
|
+
}
|
|
1747
|
+
const hr = Math.floor(min / 60);
|
|
1748
|
+
if (hr < 24) {
|
|
1749
|
+
return `${hr}h`;
|
|
1750
|
+
}
|
|
1751
|
+
const day = Math.floor(hr / 24);
|
|
1752
|
+
if (day < 14) {
|
|
1753
|
+
return `${day}d`;
|
|
1754
|
+
}
|
|
1755
|
+
const week = Math.floor(day / 7);
|
|
1756
|
+
if (week < 9) {
|
|
1757
|
+
return `${week}w`;
|
|
1758
|
+
}
|
|
1759
|
+
const month = Math.floor(day / 30);
|
|
1760
|
+
if (month < 12) {
|
|
1761
|
+
return `${month}mo`;
|
|
1762
|
+
}
|
|
1763
|
+
const year = Math.floor(day / 365);
|
|
1764
|
+
return `${year}y`;
|
|
1765
|
+
}
|
|
1766
|
+
function maxLen(headerCell, values) {
|
|
1767
|
+
let max = headerCell.length;
|
|
1768
|
+
for (const v of values) {
|
|
1769
|
+
if (v.length > max) {
|
|
1770
|
+
max = v.length;
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
return max;
|
|
1774
|
+
}
|
|
1775
|
+
function formatRow(r, w, maxWidth) {
|
|
1776
|
+
const fixed = [
|
|
1777
|
+
r.session.padEnd(w.session),
|
|
1778
|
+
r.upstream.padEnd(w.upstream),
|
|
1779
|
+
r.status.padEnd(w.status),
|
|
1780
|
+
r.clients.padStart(w.clients),
|
|
1781
|
+
r.agent.padEnd(w.agent),
|
|
1782
|
+
r.age.padStart(w.age)
|
|
1783
|
+
].join(SEP);
|
|
1784
|
+
if (maxWidth === void 0) {
|
|
1785
|
+
return [fixed, r.title.padEnd(w.title), r.cwd].join(SEP);
|
|
1786
|
+
}
|
|
1787
|
+
const titleCap = Math.min(w.title, TITLE_MAX_WIDTH);
|
|
1788
|
+
const budget = maxWidth - fixed.length - SEP.length;
|
|
1789
|
+
if (budget <= 0) {
|
|
1790
|
+
return fixed.slice(0, maxWidth);
|
|
1791
|
+
}
|
|
1792
|
+
const titleNatural = Math.min(r.title.length, titleCap);
|
|
1793
|
+
let titleAlloc = titleNatural + SEP.length + MIN_CWD <= budget ? titleCap : Math.max(0, budget - SEP.length - MIN_CWD);
|
|
1794
|
+
titleAlloc = Math.min(titleAlloc, Math.max(0, budget - SEP.length - 1));
|
|
1795
|
+
const titleCell = truncateRight(r.title, titleAlloc).padEnd(titleAlloc);
|
|
1796
|
+
const cwdBudget = Math.max(0, budget - titleAlloc - SEP.length);
|
|
1797
|
+
const cwdCell = truncateMiddle(r.cwd, cwdBudget);
|
|
1798
|
+
return [fixed, titleCell, cwdCell].join(SEP);
|
|
1799
|
+
}
|
|
1800
|
+
function truncateRight(s, max) {
|
|
1801
|
+
if (max <= 0) {
|
|
1802
|
+
return "";
|
|
1803
|
+
}
|
|
1804
|
+
if (s.length <= max) {
|
|
1805
|
+
return s;
|
|
1806
|
+
}
|
|
1807
|
+
if (max === 1) {
|
|
1808
|
+
return "\u2026";
|
|
1809
|
+
}
|
|
1810
|
+
return s.slice(0, max - 1) + "\u2026";
|
|
1811
|
+
}
|
|
1812
|
+
function truncateMiddle(s, max) {
|
|
1813
|
+
if (max <= 0) {
|
|
1814
|
+
return "";
|
|
1815
|
+
}
|
|
1816
|
+
if (s.length <= max) {
|
|
1817
|
+
return s;
|
|
1818
|
+
}
|
|
1819
|
+
if (max === 1) {
|
|
1820
|
+
return "\u2026";
|
|
1821
|
+
}
|
|
1822
|
+
const head = Math.ceil((max - 1) / 2);
|
|
1823
|
+
const tail = max - 1 - head;
|
|
1824
|
+
return s.slice(0, head) + "\u2026" + s.slice(s.length - tail);
|
|
1825
|
+
}
|
|
1826
|
+
var HEADER, SEP, MIN_CWD, TITLE_MAX_WIDTH;
|
|
1827
|
+
var init_session_row = __esm({
|
|
1828
|
+
"src/cli/session-row.ts"() {
|
|
1829
|
+
"use strict";
|
|
1830
|
+
init_session();
|
|
1831
|
+
HEADER = {
|
|
1832
|
+
session: "SESSION",
|
|
1833
|
+
upstream: "UPSTREAM",
|
|
1834
|
+
status: "STATUS",
|
|
1835
|
+
clients: "CLIENTS",
|
|
1836
|
+
agent: "AGENT",
|
|
1837
|
+
age: "AGE",
|
|
1838
|
+
title: "TITLE",
|
|
1839
|
+
cwd: "CWD"
|
|
1840
|
+
};
|
|
1841
|
+
SEP = " ";
|
|
1842
|
+
MIN_CWD = 8;
|
|
1843
|
+
TITLE_MAX_WIDTH = 40;
|
|
1844
|
+
}
|
|
1845
|
+
});
|
|
1846
|
+
|
|
1653
1847
|
// src/cli/commands/sessions.ts
|
|
1654
1848
|
async function runSessionsList(opts = {}) {
|
|
1655
1849
|
const config = await loadConfig();
|
|
@@ -1685,44 +1879,13 @@ async function runSessionsList(opts = {}) {
|
|
|
1685
1879
|
visible = [...sorted.slice(0, liveCount), ...coldSlice];
|
|
1686
1880
|
truncated = hiddenCold;
|
|
1687
1881
|
}
|
|
1688
|
-
const
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
agent: s.agentId ?? "?",
|
|
1694
|
-
title: s.title ?? "-",
|
|
1695
|
-
cwd: s.cwd
|
|
1696
|
-
}));
|
|
1697
|
-
const header = {
|
|
1698
|
-
session: "SESSION",
|
|
1699
|
-
upstream: "UPSTREAM",
|
|
1700
|
-
status: "STATUS",
|
|
1701
|
-
clients: "CLIENTS",
|
|
1702
|
-
agent: "AGENT",
|
|
1703
|
-
title: "TITLE",
|
|
1704
|
-
cwd: "CWD"
|
|
1705
|
-
};
|
|
1706
|
-
const widths = {
|
|
1707
|
-
session: maxLen(header.session, rows.map((r) => r.session)),
|
|
1708
|
-
upstream: maxLen(header.upstream, rows.map((r) => r.upstream)),
|
|
1709
|
-
status: maxLen(header.status, rows.map((r) => r.status)),
|
|
1710
|
-
clients: maxLen(header.clients, rows.map((r) => r.clients)),
|
|
1711
|
-
agent: maxLen(header.agent, rows.map((r) => r.agent)),
|
|
1712
|
-
title: maxLen(header.title, rows.map((r) => r.title))
|
|
1713
|
-
};
|
|
1714
|
-
const formatRow2 = (r) => [
|
|
1715
|
-
r.session.padEnd(widths.session),
|
|
1716
|
-
r.upstream.padEnd(widths.upstream),
|
|
1717
|
-
r.status.padEnd(widths.status),
|
|
1718
|
-
r.clients.padStart(widths.clients),
|
|
1719
|
-
r.agent.padEnd(widths.agent),
|
|
1720
|
-
r.title.padEnd(widths.title),
|
|
1721
|
-
r.cwd
|
|
1722
|
-
].join(" ");
|
|
1723
|
-
process.stdout.write(formatRow2(header) + "\n");
|
|
1882
|
+
const now = Date.now();
|
|
1883
|
+
const rows = visible.map((s) => toRow(s, now));
|
|
1884
|
+
const widths = computeWidths(rows);
|
|
1885
|
+
const maxWidth = process.stdout.isTTY ? process.stdout.columns : void 0;
|
|
1886
|
+
process.stdout.write(formatRow(HEADER, widths, maxWidth) + "\n");
|
|
1724
1887
|
for (const r of rows) {
|
|
1725
|
-
process.stdout.write(
|
|
1888
|
+
process.stdout.write(formatRow(r, widths, maxWidth) + "\n");
|
|
1726
1889
|
}
|
|
1727
1890
|
if (truncated > 0) {
|
|
1728
1891
|
process.stdout.write(
|
|
@@ -1732,15 +1895,6 @@ async function runSessionsList(opts = {}) {
|
|
|
1732
1895
|
);
|
|
1733
1896
|
}
|
|
1734
1897
|
}
|
|
1735
|
-
function maxLen(headerCell, values) {
|
|
1736
|
-
let max = headerCell.length;
|
|
1737
|
-
for (const v of values) {
|
|
1738
|
-
if (v.length > max) {
|
|
1739
|
-
max = v.length;
|
|
1740
|
-
}
|
|
1741
|
-
}
|
|
1742
|
-
return max;
|
|
1743
|
-
}
|
|
1744
1898
|
async function runSessionsKill(id) {
|
|
1745
1899
|
if (!id) {
|
|
1746
1900
|
process.stderr.write("Usage: hydra-acp sessions kill <session-id>\n");
|
|
@@ -1748,6 +1902,25 @@ async function runSessionsKill(id) {
|
|
|
1748
1902
|
}
|
|
1749
1903
|
const config = await loadConfig();
|
|
1750
1904
|
const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
1905
|
+
const response = await fetch(`${baseUrl}/v1/sessions/${id}/kill`, {
|
|
1906
|
+
method: "POST",
|
|
1907
|
+
headers: { Authorization: `Bearer ${config.daemon.authToken}` }
|
|
1908
|
+
});
|
|
1909
|
+
if (!response.ok && response.status !== 204) {
|
|
1910
|
+
process.stderr.write(`Daemon returned HTTP ${response.status}
|
|
1911
|
+
`);
|
|
1912
|
+
process.exit(1);
|
|
1913
|
+
}
|
|
1914
|
+
process.stdout.write(`Killed ${id}
|
|
1915
|
+
`);
|
|
1916
|
+
}
|
|
1917
|
+
async function runSessionsRm(id) {
|
|
1918
|
+
if (!id) {
|
|
1919
|
+
process.stderr.write("Usage: hydra-acp sessions rm <session-id>\n");
|
|
1920
|
+
process.exit(2);
|
|
1921
|
+
}
|
|
1922
|
+
const config = await loadConfig();
|
|
1923
|
+
const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
1751
1924
|
const response = await fetch(`${baseUrl}/v1/sessions/${id}`, {
|
|
1752
1925
|
method: "DELETE",
|
|
1753
1926
|
headers: { Authorization: `Bearer ${config.daemon.authToken}` }
|
|
@@ -1757,7 +1930,7 @@ async function runSessionsKill(id) {
|
|
|
1757
1930
|
`);
|
|
1758
1931
|
process.exit(1);
|
|
1759
1932
|
}
|
|
1760
|
-
process.stdout.write(`
|
|
1933
|
+
process.stdout.write(`Removed ${id}
|
|
1761
1934
|
`);
|
|
1762
1935
|
}
|
|
1763
1936
|
function httpBase(host, port, tls) {
|
|
@@ -1768,7 +1941,7 @@ var init_sessions = __esm({
|
|
|
1768
1941
|
"src/cli/commands/sessions.ts"() {
|
|
1769
1942
|
"use strict";
|
|
1770
1943
|
init_config();
|
|
1771
|
-
|
|
1944
|
+
init_session_row();
|
|
1772
1945
|
}
|
|
1773
1946
|
});
|
|
1774
1947
|
|
|
@@ -2008,12 +2181,12 @@ var init_resilient_ws = __esm({
|
|
|
2008
2181
|
});
|
|
2009
2182
|
|
|
2010
2183
|
// src/tui/history.ts
|
|
2011
|
-
import { promises as
|
|
2184
|
+
import { promises as fs10 } from "fs";
|
|
2012
2185
|
import * as path4 from "path";
|
|
2013
2186
|
async function loadHistory(file) {
|
|
2014
2187
|
let text;
|
|
2015
2188
|
try {
|
|
2016
|
-
text = await
|
|
2189
|
+
text = await fs10.readFile(file, "utf8");
|
|
2017
2190
|
} catch (err) {
|
|
2018
2191
|
if (err.code === "ENOENT") {
|
|
2019
2192
|
return [];
|
|
@@ -2053,9 +2226,9 @@ function appendEntry(history, entry) {
|
|
|
2053
2226
|
return out;
|
|
2054
2227
|
}
|
|
2055
2228
|
async function saveHistory(file, history) {
|
|
2056
|
-
await
|
|
2229
|
+
await fs10.mkdir(path4.dirname(file), { recursive: true });
|
|
2057
2230
|
const lines = history.map((entry) => JSON.stringify(entry));
|
|
2058
|
-
await
|
|
2231
|
+
await fs10.writeFile(file, lines.length > 0 ? lines.join("\n") + "\n" : "");
|
|
2059
2232
|
}
|
|
2060
2233
|
var HISTORY_CAP;
|
|
2061
2234
|
var init_history = __esm({
|
|
@@ -2096,6 +2269,26 @@ async function listSessions(config, opts = {}, fetchImpl = fetch) {
|
|
|
2096
2269
|
title: s.title
|
|
2097
2270
|
}));
|
|
2098
2271
|
}
|
|
2272
|
+
async function killSession(config, id, fetchImpl = fetch) {
|
|
2273
|
+
const base = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
2274
|
+
const response = await fetchImpl(`${base}/v1/sessions/${id}/kill`, {
|
|
2275
|
+
method: "POST",
|
|
2276
|
+
headers: { Authorization: `Bearer ${config.daemon.authToken}` }
|
|
2277
|
+
});
|
|
2278
|
+
if (!response.ok && response.status !== 204 && response.status !== 404) {
|
|
2279
|
+
throw new Error(`daemon returned HTTP ${response.status}`);
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
async function deleteSession(config, id, fetchImpl = fetch) {
|
|
2283
|
+
const base = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
2284
|
+
const response = await fetchImpl(`${base}/v1/sessions/${id}`, {
|
|
2285
|
+
method: "DELETE",
|
|
2286
|
+
headers: { Authorization: `Bearer ${config.daemon.authToken}` }
|
|
2287
|
+
});
|
|
2288
|
+
if (!response.ok && response.status !== 204 && response.status !== 404) {
|
|
2289
|
+
throw new Error(`daemon returned HTTP ${response.status}`);
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2099
2292
|
function pickMostRecent(sessions, cwd) {
|
|
2100
2293
|
const matching = sessions.filter((s) => s.cwd === cwd);
|
|
2101
2294
|
if (matching.length === 0) {
|
|
@@ -2123,147 +2316,401 @@ async function pickSession(term, opts) {
|
|
|
2123
2316
|
if (opts.sessions.length === 0) {
|
|
2124
2317
|
return { kind: "new" };
|
|
2125
2318
|
}
|
|
2126
|
-
const
|
|
2127
|
-
|
|
2128
|
-
|
|
2319
|
+
const sortSessions = (sessions) => {
|
|
2320
|
+
const score = (s) => {
|
|
2321
|
+
if (s.status !== "live") {
|
|
2322
|
+
return 0;
|
|
2323
|
+
}
|
|
2324
|
+
return s.cwd === opts.cwd ? 2 : 1;
|
|
2325
|
+
};
|
|
2326
|
+
return [...sessions].sort((a, b) => {
|
|
2327
|
+
const tier = score(b) - score(a);
|
|
2328
|
+
if (tier !== 0) {
|
|
2329
|
+
return tier;
|
|
2330
|
+
}
|
|
2331
|
+
return b.updatedAt.localeCompare(a.updatedAt);
|
|
2332
|
+
});
|
|
2333
|
+
};
|
|
2334
|
+
let visible = sortSessions(opts.sessions);
|
|
2335
|
+
let rows = visible.map((s) => toRow(s, Date.now()));
|
|
2336
|
+
let widths = computeWidths(rows);
|
|
2337
|
+
let total = 1 + visible.length;
|
|
2338
|
+
let selectedIdx = 0;
|
|
2339
|
+
let scrollOffset = 0;
|
|
2340
|
+
let mode = "normal";
|
|
2341
|
+
let pendingAction = null;
|
|
2342
|
+
let transientStatus = null;
|
|
2343
|
+
let termHeight = readTermHeight(term);
|
|
2344
|
+
let termWidth = readTermWidth(term);
|
|
2345
|
+
let viewportSize = 0;
|
|
2346
|
+
let newSessionLabel = "";
|
|
2347
|
+
let headerLine = "";
|
|
2348
|
+
let sessionLines = [];
|
|
2349
|
+
let startRow = 1;
|
|
2350
|
+
const computeLayout = () => {
|
|
2351
|
+
termHeight = readTermHeight(term);
|
|
2352
|
+
termWidth = readTermWidth(term);
|
|
2353
|
+
const maxViewportRows = Math.max(3, termHeight - 6);
|
|
2354
|
+
viewportSize = Math.min(visible.length, maxViewportRows);
|
|
2355
|
+
const rowMaxWidth = Math.max(10, termWidth - ROW_PREFIX_WIDTH);
|
|
2356
|
+
newSessionLabel = formatNewSessionLabel(opts.cwd, rowMaxWidth);
|
|
2357
|
+
headerLine = formatRow(HEADER, widths, rowMaxWidth);
|
|
2358
|
+
sessionLines = rows.map((r) => formatRow(r, widths, rowMaxWidth));
|
|
2359
|
+
};
|
|
2360
|
+
const rebuildRows = () => {
|
|
2361
|
+
rows = visible.map((s) => toRow(s, Date.now()));
|
|
2362
|
+
widths = computeWidths(rows);
|
|
2363
|
+
total = 1 + visible.length;
|
|
2364
|
+
computeLayout();
|
|
2365
|
+
};
|
|
2366
|
+
const adjustScroll = () => {
|
|
2367
|
+
if (selectedIdx === 0) {
|
|
2368
|
+
return;
|
|
2369
|
+
}
|
|
2370
|
+
const sessionIdx = selectedIdx - 1;
|
|
2371
|
+
if (sessionIdx < scrollOffset) {
|
|
2372
|
+
scrollOffset = sessionIdx;
|
|
2373
|
+
} else if (sessionIdx >= scrollOffset + viewportSize) {
|
|
2374
|
+
scrollOffset = sessionIdx - viewportSize + 1;
|
|
2375
|
+
} else if (scrollOffset + viewportSize > visible.length) {
|
|
2376
|
+
scrollOffset = Math.max(0, visible.length - viewportSize);
|
|
2129
2377
|
}
|
|
2130
|
-
return s.cwd === opts.cwd ? 2 : 1;
|
|
2131
2378
|
};
|
|
2132
|
-
const
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2379
|
+
const paintNewItem = () => {
|
|
2380
|
+
if (selectedIdx === 0) {
|
|
2381
|
+
term.brightWhite.bgBlue.noFormat(`\u276F ${newSessionLabel}`);
|
|
2382
|
+
} else {
|
|
2383
|
+
term.noFormat(` ${newSessionLabel}`);
|
|
2136
2384
|
}
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
const
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2385
|
+
};
|
|
2386
|
+
const paintSessionRow = (sessionIdx) => {
|
|
2387
|
+
const label = sessionLines[sessionIdx] ?? "";
|
|
2388
|
+
if (selectedIdx === sessionIdx + 1) {
|
|
2389
|
+
term.brightWhite.bgBlue.noFormat(`\u276F ${label}`);
|
|
2390
|
+
} else {
|
|
2391
|
+
term.noFormat(` ${label}`);
|
|
2392
|
+
}
|
|
2393
|
+
};
|
|
2394
|
+
const formatIndicator = () => {
|
|
2395
|
+
const above = scrollOffset;
|
|
2396
|
+
const below = Math.max(0, visible.length - scrollOffset - viewportSize);
|
|
2397
|
+
if (above === 0 && below === 0) {
|
|
2398
|
+
return "";
|
|
2399
|
+
}
|
|
2400
|
+
const parts = [];
|
|
2401
|
+
if (above > 0) {
|
|
2402
|
+
parts.push(`\u2191 ${above} above`);
|
|
2403
|
+
}
|
|
2404
|
+
if (below > 0) {
|
|
2405
|
+
parts.push(`\u2193 ${below} below`);
|
|
2406
|
+
}
|
|
2407
|
+
return ` ${parts.join(" \xB7 ")}`;
|
|
2408
|
+
};
|
|
2409
|
+
const shortId2 = (sessionId) => stripHydraSessionPrefix(sessionId);
|
|
2410
|
+
const paintIndicator = () => {
|
|
2411
|
+
term.moveTo(1, indicatorRow()).eraseLineAfter();
|
|
2412
|
+
if (mode === "confirm-kill" && pendingAction) {
|
|
2413
|
+
term.brightYellow.noFormat(` kill ${shortId2(pendingAction.sessionId)}? [y/N]`);
|
|
2414
|
+
return;
|
|
2415
|
+
}
|
|
2416
|
+
if (mode === "confirm-delete" && pendingAction) {
|
|
2417
|
+
term.brightRed.noFormat(` delete ${shortId2(pendingAction.sessionId)}? [y/N]`);
|
|
2418
|
+
return;
|
|
2419
|
+
}
|
|
2420
|
+
if (mode === "busy" && pendingAction) {
|
|
2421
|
+
term.dim.noFormat(` working on ${shortId2(pendingAction.sessionId)}\u2026`);
|
|
2422
|
+
return;
|
|
2423
|
+
}
|
|
2424
|
+
if (transientStatus !== null) {
|
|
2425
|
+
term.dim.noFormat(` ${transientStatus}`);
|
|
2426
|
+
return;
|
|
2427
|
+
}
|
|
2428
|
+
term.dim.noFormat(formatIndicator());
|
|
2429
|
+
};
|
|
2430
|
+
const indicatorRow = () => startRow + 3 + viewportSize;
|
|
2431
|
+
const sessionRow = (sessionIdx) => startRow + 3 + (sessionIdx - scrollOffset);
|
|
2432
|
+
const renderFromScratch = () => {
|
|
2433
|
+
computeLayout();
|
|
2434
|
+
adjustScroll();
|
|
2435
|
+
startRow = 1;
|
|
2436
|
+
term.moveTo(1, 1).eraseDisplayBelow();
|
|
2437
|
+
paintNewItem();
|
|
2438
|
+
term("\n\n");
|
|
2439
|
+
term.dim.noFormat(` ${headerLine}`)("\n");
|
|
2440
|
+
for (let v = 0; v < viewportSize; v++) {
|
|
2441
|
+
paintSessionRow(scrollOffset + v);
|
|
2157
2442
|
term("\n");
|
|
2158
|
-
process.exit(130);
|
|
2159
2443
|
}
|
|
2444
|
+
paintIndicator();
|
|
2445
|
+
term("\n");
|
|
2160
2446
|
};
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
response = await term.singleColumnMenu(items, {
|
|
2165
|
-
cancelable: true,
|
|
2166
|
-
exitOnUnexpectedKey: false,
|
|
2167
|
-
selectedIndex: 0,
|
|
2168
|
-
style: term.brightWhite,
|
|
2169
|
-
selectedStyle: term.brightWhite.bgBlue,
|
|
2170
|
-
keyBindings: {
|
|
2171
|
-
ENTER: "submit",
|
|
2172
|
-
KP_ENTER: "submit",
|
|
2173
|
-
UP: "previous",
|
|
2174
|
-
DOWN: "next",
|
|
2175
|
-
TAB: "next",
|
|
2176
|
-
SHIFT_TAB: "previous",
|
|
2177
|
-
HOME: "first",
|
|
2178
|
-
END: "last",
|
|
2179
|
-
ESCAPE: "cancel",
|
|
2180
|
-
CTRL_C: "cancel"
|
|
2181
|
-
}
|
|
2182
|
-
}).promise;
|
|
2183
|
-
} finally {
|
|
2184
|
-
term.off("key", onCtrlC);
|
|
2185
|
-
}
|
|
2186
|
-
term("\n");
|
|
2187
|
-
if (response.canceled || response.selectedIndex === void 0) {
|
|
2188
|
-
return { kind: "abort" };
|
|
2189
|
-
}
|
|
2190
|
-
if (response.selectedIndex === 0) {
|
|
2191
|
-
return { kind: "new" };
|
|
2192
|
-
}
|
|
2193
|
-
const session = visible[response.selectedIndex - 1];
|
|
2194
|
-
if (!session) {
|
|
2195
|
-
return { kind: "abort" };
|
|
2196
|
-
}
|
|
2197
|
-
const result = {
|
|
2198
|
-
kind: "attach",
|
|
2199
|
-
sessionId: session.sessionId
|
|
2447
|
+
const repaintNewItem = () => {
|
|
2448
|
+
term.moveTo(1, startRow).eraseLineAfter();
|
|
2449
|
+
paintNewItem();
|
|
2200
2450
|
};
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2451
|
+
const repaintSessionRow = (sessionIdx) => {
|
|
2452
|
+
if (sessionIdx < scrollOffset || sessionIdx >= scrollOffset + viewportSize) {
|
|
2453
|
+
return;
|
|
2454
|
+
}
|
|
2455
|
+
term.moveTo(1, sessionRow(sessionIdx)).eraseLineAfter();
|
|
2456
|
+
paintSessionRow(sessionIdx);
|
|
2457
|
+
};
|
|
2458
|
+
const repaintViewport = () => {
|
|
2459
|
+
for (let v = 0; v < viewportSize; v++) {
|
|
2460
|
+
const row = startRow + 3 + v;
|
|
2461
|
+
term.moveTo(1, row).eraseLineAfter();
|
|
2462
|
+
const sessionIdx = scrollOffset + v;
|
|
2463
|
+
if (sessionIdx < visible.length) {
|
|
2464
|
+
paintSessionRow(sessionIdx);
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
paintIndicator();
|
|
2215
2468
|
};
|
|
2469
|
+
renderFromScratch();
|
|
2470
|
+
term.hideCursor();
|
|
2471
|
+
return await new Promise((resolve2) => {
|
|
2472
|
+
let resolved = false;
|
|
2473
|
+
const onResize = () => {
|
|
2474
|
+
if (resolved) {
|
|
2475
|
+
return;
|
|
2476
|
+
}
|
|
2477
|
+
renderFromScratch();
|
|
2478
|
+
};
|
|
2479
|
+
const cleanup = () => {
|
|
2480
|
+
if (resolved) {
|
|
2481
|
+
return;
|
|
2482
|
+
}
|
|
2483
|
+
resolved = true;
|
|
2484
|
+
term.off("key", onKey);
|
|
2485
|
+
term.off("resize", onResize);
|
|
2486
|
+
term.grabInput(false);
|
|
2487
|
+
term.hideCursor(false);
|
|
2488
|
+
term.moveTo(1, indicatorRow() + 1);
|
|
2489
|
+
term("\n");
|
|
2490
|
+
};
|
|
2491
|
+
const refresh = async (preferredId) => {
|
|
2492
|
+
try {
|
|
2493
|
+
const next = await listSessions(opts.config);
|
|
2494
|
+
visible = sortSessions(next);
|
|
2495
|
+
rebuildRows();
|
|
2496
|
+
if (preferredId !== void 0) {
|
|
2497
|
+
const idx = visible.findIndex((s) => s.sessionId === preferredId);
|
|
2498
|
+
if (idx >= 0) {
|
|
2499
|
+
selectedIdx = idx + 1;
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
if (selectedIdx > total - 1) {
|
|
2503
|
+
selectedIdx = Math.max(0, total - 1);
|
|
2504
|
+
}
|
|
2505
|
+
if (scrollOffset + viewportSize > visible.length) {
|
|
2506
|
+
scrollOffset = Math.max(0, visible.length - viewportSize);
|
|
2507
|
+
}
|
|
2508
|
+
adjustScroll();
|
|
2509
|
+
renderFromScratch();
|
|
2510
|
+
} catch (err) {
|
|
2511
|
+
transientStatus = `refresh failed: ${err.message}`;
|
|
2512
|
+
renderFromScratch();
|
|
2513
|
+
}
|
|
2514
|
+
};
|
|
2515
|
+
const performAction = async (kind) => {
|
|
2516
|
+
if (!pendingAction) {
|
|
2517
|
+
return;
|
|
2518
|
+
}
|
|
2519
|
+
const target = pendingAction;
|
|
2520
|
+
mode = "busy";
|
|
2521
|
+
paintIndicator();
|
|
2522
|
+
try {
|
|
2523
|
+
if (kind === "kill") {
|
|
2524
|
+
await killSession(opts.config, target.sessionId);
|
|
2525
|
+
} else {
|
|
2526
|
+
await deleteSession(opts.config, target.sessionId);
|
|
2527
|
+
}
|
|
2528
|
+
mode = "normal";
|
|
2529
|
+
pendingAction = null;
|
|
2530
|
+
await refresh(kind === "kill" ? target.sessionId : void 0);
|
|
2531
|
+
} catch (err) {
|
|
2532
|
+
mode = "normal";
|
|
2533
|
+
pendingAction = null;
|
|
2534
|
+
transientStatus = `${kind} failed: ${err.message}`;
|
|
2535
|
+
paintIndicator();
|
|
2536
|
+
}
|
|
2537
|
+
};
|
|
2538
|
+
const move = (delta) => {
|
|
2539
|
+
const next = Math.min(total - 1, Math.max(0, selectedIdx + delta));
|
|
2540
|
+
if (next === selectedIdx) {
|
|
2541
|
+
return;
|
|
2542
|
+
}
|
|
2543
|
+
const old = selectedIdx;
|
|
2544
|
+
const oldScroll = scrollOffset;
|
|
2545
|
+
selectedIdx = next;
|
|
2546
|
+
adjustScroll();
|
|
2547
|
+
if (scrollOffset !== oldScroll) {
|
|
2548
|
+
repaintViewport();
|
|
2549
|
+
if (old === 0 || selectedIdx === 0) {
|
|
2550
|
+
repaintNewItem();
|
|
2551
|
+
}
|
|
2552
|
+
return;
|
|
2553
|
+
}
|
|
2554
|
+
if (old === 0) {
|
|
2555
|
+
repaintNewItem();
|
|
2556
|
+
} else {
|
|
2557
|
+
repaintSessionRow(old - 1);
|
|
2558
|
+
}
|
|
2559
|
+
if (selectedIdx === 0) {
|
|
2560
|
+
repaintNewItem();
|
|
2561
|
+
} else {
|
|
2562
|
+
repaintSessionRow(selectedIdx - 1);
|
|
2563
|
+
}
|
|
2564
|
+
};
|
|
2565
|
+
const clearTransient = () => {
|
|
2566
|
+
if (transientStatus === null) {
|
|
2567
|
+
return false;
|
|
2568
|
+
}
|
|
2569
|
+
transientStatus = null;
|
|
2570
|
+
paintIndicator();
|
|
2571
|
+
return true;
|
|
2572
|
+
};
|
|
2573
|
+
const onKey = (name, _matches, data) => {
|
|
2574
|
+
if (mode === "busy") {
|
|
2575
|
+
return;
|
|
2576
|
+
}
|
|
2577
|
+
if (mode === "confirm-kill" || mode === "confirm-delete") {
|
|
2578
|
+
if (data?.isCharacter && (name === "y" || name === "Y")) {
|
|
2579
|
+
const kind = mode === "confirm-kill" ? "kill" : "delete";
|
|
2580
|
+
void performAction(kind);
|
|
2581
|
+
return;
|
|
2582
|
+
}
|
|
2583
|
+
if (name === "ESCAPE" || name === "CTRL_C" || name === "ENTER" || name === "KP_ENTER" || data?.isCharacter && (name === "n" || name === "N")) {
|
|
2584
|
+
mode = "normal";
|
|
2585
|
+
pendingAction = null;
|
|
2586
|
+
paintIndicator();
|
|
2587
|
+
return;
|
|
2588
|
+
}
|
|
2589
|
+
return;
|
|
2590
|
+
}
|
|
2591
|
+
clearTransient();
|
|
2592
|
+
if (data?.isCharacter) {
|
|
2593
|
+
if ((name === "k" || name === "K") && selectedIdx > 0) {
|
|
2594
|
+
const session = visible[selectedIdx - 1];
|
|
2595
|
+
if (!session) {
|
|
2596
|
+
return;
|
|
2597
|
+
}
|
|
2598
|
+
pendingAction = {
|
|
2599
|
+
sessionId: session.sessionId,
|
|
2600
|
+
cwd: session.cwd,
|
|
2601
|
+
status: session.status
|
|
2602
|
+
};
|
|
2603
|
+
mode = "confirm-kill";
|
|
2604
|
+
paintIndicator();
|
|
2605
|
+
return;
|
|
2606
|
+
}
|
|
2607
|
+
if ((name === "d" || name === "D") && selectedIdx > 0) {
|
|
2608
|
+
const session = visible[selectedIdx - 1];
|
|
2609
|
+
if (!session) {
|
|
2610
|
+
return;
|
|
2611
|
+
}
|
|
2612
|
+
if (session.status === "live") {
|
|
2613
|
+
transientStatus = "session is live \u2014 press k to kill it first";
|
|
2614
|
+
paintIndicator();
|
|
2615
|
+
return;
|
|
2616
|
+
}
|
|
2617
|
+
pendingAction = {
|
|
2618
|
+
sessionId: session.sessionId,
|
|
2619
|
+
cwd: session.cwd,
|
|
2620
|
+
status: session.status
|
|
2621
|
+
};
|
|
2622
|
+
mode = "confirm-delete";
|
|
2623
|
+
paintIndicator();
|
|
2624
|
+
return;
|
|
2625
|
+
}
|
|
2626
|
+
return;
|
|
2627
|
+
}
|
|
2628
|
+
switch (name) {
|
|
2629
|
+
case "UP":
|
|
2630
|
+
case "SHIFT_TAB":
|
|
2631
|
+
move(-1);
|
|
2632
|
+
return;
|
|
2633
|
+
case "DOWN":
|
|
2634
|
+
case "TAB":
|
|
2635
|
+
move(1);
|
|
2636
|
+
return;
|
|
2637
|
+
case "PAGE_UP":
|
|
2638
|
+
move(-viewportSize);
|
|
2639
|
+
return;
|
|
2640
|
+
case "PAGE_DOWN":
|
|
2641
|
+
move(viewportSize);
|
|
2642
|
+
return;
|
|
2643
|
+
case "HOME":
|
|
2644
|
+
move(-total);
|
|
2645
|
+
return;
|
|
2646
|
+
case "END":
|
|
2647
|
+
move(total);
|
|
2648
|
+
return;
|
|
2649
|
+
case "ENTER":
|
|
2650
|
+
case "KP_ENTER": {
|
|
2651
|
+
cleanup();
|
|
2652
|
+
if (selectedIdx === 0) {
|
|
2653
|
+
resolve2({ kind: "new" });
|
|
2654
|
+
return;
|
|
2655
|
+
}
|
|
2656
|
+
const session = visible[selectedIdx - 1];
|
|
2657
|
+
if (!session) {
|
|
2658
|
+
resolve2({ kind: "abort" });
|
|
2659
|
+
return;
|
|
2660
|
+
}
|
|
2661
|
+
const result = {
|
|
2662
|
+
kind: "attach",
|
|
2663
|
+
sessionId: session.sessionId
|
|
2664
|
+
};
|
|
2665
|
+
if (session.agentId !== void 0) {
|
|
2666
|
+
result.agentId = session.agentId;
|
|
2667
|
+
}
|
|
2668
|
+
resolve2(result);
|
|
2669
|
+
return;
|
|
2670
|
+
}
|
|
2671
|
+
case "ESCAPE":
|
|
2672
|
+
case "CTRL_C":
|
|
2673
|
+
cleanup();
|
|
2674
|
+
resolve2({ kind: "abort" });
|
|
2675
|
+
return;
|
|
2676
|
+
}
|
|
2677
|
+
};
|
|
2678
|
+
term.grabInput({});
|
|
2679
|
+
term.on("key", onKey);
|
|
2680
|
+
term.on("resize", onResize);
|
|
2681
|
+
});
|
|
2216
2682
|
}
|
|
2217
|
-
function
|
|
2218
|
-
return
|
|
2219
|
-
session: maxLen4(HEADER.session, rows.map((r) => r.session)),
|
|
2220
|
-
upstream: maxLen4(HEADER.upstream, rows.map((r) => r.upstream)),
|
|
2221
|
-
status: maxLen4(HEADER.status, rows.map((r) => r.status)),
|
|
2222
|
-
clients: maxLen4(HEADER.clients, rows.map((r) => r.clients)),
|
|
2223
|
-
agent: maxLen4(HEADER.agent, rows.map((r) => r.agent)),
|
|
2224
|
-
title: maxLen4(HEADER.title, rows.map((r) => r.title))
|
|
2225
|
-
};
|
|
2683
|
+
function readTermHeight(term) {
|
|
2684
|
+
return term.height ?? 24;
|
|
2226
2685
|
}
|
|
2227
|
-
function
|
|
2228
|
-
|
|
2229
|
-
for (const v of values) {
|
|
2230
|
-
if (v.length > max) {
|
|
2231
|
-
max = v.length;
|
|
2232
|
-
}
|
|
2233
|
-
}
|
|
2234
|
-
return max;
|
|
2686
|
+
function readTermWidth(term) {
|
|
2687
|
+
return term.width ?? 80;
|
|
2235
2688
|
}
|
|
2236
|
-
function
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
r.status.padEnd(w.status),
|
|
2241
|
-
r.clients.padStart(w.clients),
|
|
2242
|
-
r.agent.padEnd(w.agent),
|
|
2243
|
-
r.title.padEnd(w.title),
|
|
2244
|
-
r.cwd
|
|
2245
|
-
].join(" ");
|
|
2689
|
+
function formatNewSessionLabel(cwd, maxWidth) {
|
|
2690
|
+
const prefix = "+ New session in ";
|
|
2691
|
+
const budget = Math.max(1, maxWidth - prefix.length);
|
|
2692
|
+
return prefix + truncateMiddle(cwd, budget);
|
|
2246
2693
|
}
|
|
2247
|
-
var
|
|
2694
|
+
var ROW_PREFIX_WIDTH;
|
|
2248
2695
|
var init_picker = __esm({
|
|
2249
2696
|
"src/tui/picker.ts"() {
|
|
2250
2697
|
"use strict";
|
|
2698
|
+
init_session_row();
|
|
2251
2699
|
init_session();
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
upstream: "UPSTREAM",
|
|
2255
|
-
status: "STATUS",
|
|
2256
|
-
clients: "CLIENTS",
|
|
2257
|
-
agent: "AGENT",
|
|
2258
|
-
title: "TITLE",
|
|
2259
|
-
cwd: "CWD"
|
|
2260
|
-
};
|
|
2700
|
+
init_discovery();
|
|
2701
|
+
ROW_PREFIX_WIDTH = 2;
|
|
2261
2702
|
}
|
|
2262
2703
|
});
|
|
2263
2704
|
|
|
2264
2705
|
// src/tui/screen.ts
|
|
2265
2706
|
import stringWidth from "string-width";
|
|
2266
2707
|
import wrapAnsi from "wrap-ansi";
|
|
2708
|
+
function formattedLineSig(zone, width, line) {
|
|
2709
|
+
if (!line) {
|
|
2710
|
+
return `${zone}|${width}|empty`;
|
|
2711
|
+
}
|
|
2712
|
+
return `${zone}|${width}|${line.prefix ?? ""}|${line.prefixStyle ?? ""}|${line.body}|${line.bodyStyle ?? ""}|${line.ansi ? "1" : "0"}|${line.fillRow ? "1" : "0"}`;
|
|
2713
|
+
}
|
|
2267
2714
|
function computePromptVisualRows(buffer, room) {
|
|
2268
2715
|
const rows = [];
|
|
2269
2716
|
for (let i = 0; i < buffer.length; i++) {
|
|
@@ -2348,16 +2795,16 @@ function writeStyled(term, text, style) {
|
|
|
2348
2795
|
term.bold.red.noFormat(text);
|
|
2349
2796
|
return;
|
|
2350
2797
|
case "tool-status-pending":
|
|
2351
|
-
term.dim.
|
|
2798
|
+
term.dim.noFormat(text);
|
|
2352
2799
|
return;
|
|
2353
2800
|
case "tool-status-running":
|
|
2354
|
-
term.
|
|
2801
|
+
term.brightYellow.noFormat(text);
|
|
2355
2802
|
return;
|
|
2356
2803
|
case "tool-status-cancelled":
|
|
2357
2804
|
term.dim.noFormat(text);
|
|
2358
2805
|
return;
|
|
2359
2806
|
case "plan":
|
|
2360
|
-
term.
|
|
2807
|
+
term.brightYellow.noFormat(text);
|
|
2361
2808
|
return;
|
|
2362
2809
|
case "plan-done":
|
|
2363
2810
|
term.green.noFormat(text);
|
|
@@ -2542,13 +2989,15 @@ function mapKeyName(name) {
|
|
|
2542
2989
|
return "ctrl-u";
|
|
2543
2990
|
case "CTRL_W":
|
|
2544
2991
|
return "ctrl-w";
|
|
2992
|
+
case "CTRL_Y":
|
|
2993
|
+
return "ctrl-y";
|
|
2545
2994
|
case "ESCAPE":
|
|
2546
2995
|
return "escape";
|
|
2547
2996
|
default:
|
|
2548
2997
|
return null;
|
|
2549
2998
|
}
|
|
2550
2999
|
}
|
|
2551
|
-
var HEADER_ROWS, BANNER_ROWS, SEPARATOR_ROWS, MAX_PROMPT_ROWS, MAX_QUEUED_ROWS, MAX_PERMISSION_ROWS, MAX_COMPLETION_ROWS, CONFIRM_PROMPT_ROWS, DEFAULT_CONTENT_REPAINT_THROTTLE_MS, Screen, shortId;
|
|
3000
|
+
var HEADER_ROWS, BANNER_ROWS, SEPARATOR_ROWS, MAX_PROMPT_ROWS, MAX_QUEUED_ROWS, MAX_PERMISSION_ROWS, MAX_COMPLETION_ROWS, CONFIRM_PROMPT_ROWS, DEFAULT_CONTENT_REPAINT_THROTTLE_MS, DEFAULT_MAX_SCROLLBACK_LINES, Screen, shortId;
|
|
2552
3001
|
var init_screen = __esm({
|
|
2553
3002
|
"src/tui/screen.ts"() {
|
|
2554
3003
|
"use strict";
|
|
@@ -2562,6 +3011,7 @@ var init_screen = __esm({
|
|
|
2562
3011
|
MAX_COMPLETION_ROWS = 6;
|
|
2563
3012
|
CONFIRM_PROMPT_ROWS = 2;
|
|
2564
3013
|
DEFAULT_CONTENT_REPAINT_THROTTLE_MS = 1e3;
|
|
3014
|
+
DEFAULT_MAX_SCROLLBACK_LINES = 1e4;
|
|
2565
3015
|
Screen = class {
|
|
2566
3016
|
term;
|
|
2567
3017
|
dispatcher;
|
|
@@ -2582,6 +3032,27 @@ var init_screen = __esm({
|
|
|
2582
3032
|
lastRepaintAt = 0;
|
|
2583
3033
|
throttledRepaintTimer = null;
|
|
2584
3034
|
contentRepaintThrottleMs;
|
|
3035
|
+
maxScrollbackLines;
|
|
3036
|
+
// Wrap memoization: each FormattedLine that lands in this.lines gets a
|
|
3037
|
+
// monotonic id assigned via trackLine(); wrapCache holds the pre-wrapped
|
|
3038
|
+
// FormattedLine[] for that id at wrapCacheWidth. Width changes flush the
|
|
3039
|
+
// whole cache; in-place body mutation (streaming) and splices invalidate
|
|
3040
|
+
// affected ids. Result: steady-state repaints only wrap newly-appended
|
|
3041
|
+
// lines, not the entire history.
|
|
3042
|
+
nextLineId = 1;
|
|
3043
|
+
lineIds = /* @__PURE__ */ new WeakMap();
|
|
3044
|
+
wrapCache = /* @__PURE__ */ new Map();
|
|
3045
|
+
wrapCacheWidth = 0;
|
|
3046
|
+
// Per-row signature of what was painted to each terminal row on the
|
|
3047
|
+
// previous repaint. drawX methods funnel through paintRow(), which
|
|
3048
|
+
// skips the moveTo+eraseLineAfter+write sequence when the new
|
|
3049
|
+
// signature matches the previous frame. Eliminates flicker during
|
|
3050
|
+
// the 1Hz busy-tick: only rows whose content actually changed
|
|
3051
|
+
// (banner elapsed, tools-block summary) get re-emitted instead of
|
|
3052
|
+
// every visible row. Cleared on dimension change.
|
|
3053
|
+
lastFrameRows = /* @__PURE__ */ new Map();
|
|
3054
|
+
lastFrameW = 0;
|
|
3055
|
+
lastFrameH = 0;
|
|
2585
3056
|
permissionPrompt = null;
|
|
2586
3057
|
confirmPrompt = null;
|
|
2587
3058
|
completions = [];
|
|
@@ -2596,6 +3067,7 @@ var init_screen = __esm({
|
|
|
2596
3067
|
queued: 0
|
|
2597
3068
|
};
|
|
2598
3069
|
header = { agent: "?", cwd: "?", sessionId: "?" };
|
|
3070
|
+
lastWindowTitle = null;
|
|
2599
3071
|
resizeHandler;
|
|
2600
3072
|
keyHandler;
|
|
2601
3073
|
mouseHandler;
|
|
@@ -2616,6 +3088,7 @@ var init_screen = __esm({
|
|
|
2616
3088
|
this.dispatcher = opts.dispatcher;
|
|
2617
3089
|
this.onKey = opts.onKey;
|
|
2618
3090
|
this.contentRepaintThrottleMs = opts.repaintThrottleMs ?? DEFAULT_CONTENT_REPAINT_THROTTLE_MS;
|
|
3091
|
+
this.maxScrollbackLines = opts.maxScrollbackLines ?? DEFAULT_MAX_SCROLLBACK_LINES;
|
|
2619
3092
|
this.resizeHandler = () => this.repaint();
|
|
2620
3093
|
this.keyHandler = (name, _matches, data) => this.handleKey(name, data);
|
|
2621
3094
|
this.mouseHandler = (name) => this.handleMouse(name);
|
|
@@ -2716,13 +3189,17 @@ var init_screen = __esm({
|
|
|
2716
3189
|
}
|
|
2717
3190
|
this.streamingActive = false;
|
|
2718
3191
|
this.lines.push(...lines);
|
|
3192
|
+
this.trackLines(lines);
|
|
2719
3193
|
this.adjustScrollForLineChange(lines.length);
|
|
3194
|
+
this.trimScrollback();
|
|
2720
3195
|
this.scheduleRepaint();
|
|
2721
3196
|
}
|
|
2722
3197
|
appendLine(line) {
|
|
2723
3198
|
this.streamingActive = false;
|
|
2724
3199
|
this.lines.push(line);
|
|
3200
|
+
this.trackLine(line);
|
|
2725
3201
|
this.adjustScrollForLineChange(1);
|
|
3202
|
+
this.trimScrollback();
|
|
2726
3203
|
this.scheduleRepaint();
|
|
2727
3204
|
}
|
|
2728
3205
|
// When scrolled away from the bottom, shift scrollOffset to keep the
|
|
@@ -2734,6 +3211,40 @@ var init_screen = __esm({
|
|
|
2734
3211
|
this.scrollOffset = Math.max(0, this.scrollOffset + delta);
|
|
2735
3212
|
}
|
|
2736
3213
|
}
|
|
3214
|
+
trackLine(line) {
|
|
3215
|
+
this.lineIds.set(line, this.nextLineId++);
|
|
3216
|
+
}
|
|
3217
|
+
trackLines(lines) {
|
|
3218
|
+
for (const line of lines) {
|
|
3219
|
+
this.trackLine(line);
|
|
3220
|
+
}
|
|
3221
|
+
}
|
|
3222
|
+
forgetLine(line) {
|
|
3223
|
+
const id = this.lineIds.get(line);
|
|
3224
|
+
if (id !== void 0) {
|
|
3225
|
+
this.wrapCache.delete(id);
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
// Drop oldest lines once scrollback exceeds the configured cap. Removes
|
|
3229
|
+
// their wrap-cache entries and shifts keyedBlocks indices in sync;
|
|
3230
|
+
// blocks whose lines fully fell off the head are dropped (a later
|
|
3231
|
+
// upsert for that key will start a fresh block at the bottom).
|
|
3232
|
+
trimScrollback() {
|
|
3233
|
+
const overflow = this.lines.length - this.maxScrollbackLines;
|
|
3234
|
+
if (overflow <= 0) {
|
|
3235
|
+
return;
|
|
3236
|
+
}
|
|
3237
|
+
const removed = this.lines.splice(0, overflow);
|
|
3238
|
+
for (const line of removed) {
|
|
3239
|
+
this.forgetLine(line);
|
|
3240
|
+
}
|
|
3241
|
+
for (const [key, range] of [...this.keyedBlocks.entries()]) {
|
|
3242
|
+
range.start -= overflow;
|
|
3243
|
+
if (range.start < 0) {
|
|
3244
|
+
this.keyedBlocks.delete(key);
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3247
|
+
}
|
|
2737
3248
|
// Append-or-replace a single-line block keyed by `key`. Thin wrapper
|
|
2738
3249
|
// around upsertLines for the common one-row case (tool calls).
|
|
2739
3250
|
upsertLine(key, line) {
|
|
@@ -2755,7 +3266,15 @@ var init_screen = __esm({
|
|
|
2755
3266
|
touchesEnd = oldEnd >= this.lines.length;
|
|
2756
3267
|
const delta = newLines.length - existing.count;
|
|
2757
3268
|
scrollDelta = delta;
|
|
2758
|
-
this.lines.splice(
|
|
3269
|
+
const removed = this.lines.splice(
|
|
3270
|
+
existing.start,
|
|
3271
|
+
existing.count,
|
|
3272
|
+
...newLines
|
|
3273
|
+
);
|
|
3274
|
+
for (const line of removed) {
|
|
3275
|
+
this.forgetLine(line);
|
|
3276
|
+
}
|
|
3277
|
+
this.trackLines(newLines);
|
|
2759
3278
|
existing.count = newLines.length;
|
|
2760
3279
|
if (delta !== 0) {
|
|
2761
3280
|
for (const [k, range] of this.keyedBlocks) {
|
|
@@ -2772,11 +3291,13 @@ var init_screen = __esm({
|
|
|
2772
3291
|
count: newLines.length
|
|
2773
3292
|
});
|
|
2774
3293
|
this.lines.push(...newLines);
|
|
3294
|
+
this.trackLines(newLines);
|
|
2775
3295
|
}
|
|
2776
3296
|
if (touchesEnd) {
|
|
2777
3297
|
this.streamingActive = false;
|
|
2778
3298
|
}
|
|
2779
3299
|
this.adjustScrollForLineChange(scrollDelta);
|
|
3300
|
+
this.trimScrollback();
|
|
2780
3301
|
this.scheduleRepaint();
|
|
2781
3302
|
}
|
|
2782
3303
|
// Append fragments of a streaming message (e.g. agent_message_chunk). The
|
|
@@ -2793,6 +3314,7 @@ var init_screen = __esm({
|
|
|
2793
3314
|
if (this.streamingActive && this.lines.length > 0) {
|
|
2794
3315
|
const last = this.lines[this.lines.length - 1];
|
|
2795
3316
|
if (last) {
|
|
3317
|
+
this.forgetLine(last);
|
|
2796
3318
|
last.body += first ?? "";
|
|
2797
3319
|
}
|
|
2798
3320
|
} else {
|
|
@@ -2800,7 +3322,9 @@ var init_screen = __esm({
|
|
|
2800
3322
|
const last = this.lines[this.lines.length - 1];
|
|
2801
3323
|
const isBlank = last && last.body === "" && (!last.prefix || last.prefix === "");
|
|
2802
3324
|
if (!isBlank) {
|
|
2803
|
-
|
|
3325
|
+
const sep = { body: "" };
|
|
3326
|
+
this.lines.push(sep);
|
|
3327
|
+
this.trackLine(sep);
|
|
2804
3328
|
added += 1;
|
|
2805
3329
|
}
|
|
2806
3330
|
}
|
|
@@ -2813,25 +3337,48 @@ var init_screen = __esm({
|
|
|
2813
3337
|
initial.prefixStyle = prefixStyle;
|
|
2814
3338
|
}
|
|
2815
3339
|
this.lines.push(initial);
|
|
3340
|
+
this.trackLine(initial);
|
|
2816
3341
|
added += 1;
|
|
2817
3342
|
}
|
|
2818
3343
|
const continuationPrefix = " ".repeat(prefix.length);
|
|
2819
3344
|
for (const piece of rest) {
|
|
2820
|
-
|
|
3345
|
+
const cont = {
|
|
2821
3346
|
prefix: continuationPrefix,
|
|
2822
3347
|
body: piece,
|
|
2823
3348
|
bodyStyle
|
|
2824
|
-
}
|
|
3349
|
+
};
|
|
3350
|
+
this.lines.push(cont);
|
|
3351
|
+
this.trackLine(cont);
|
|
2825
3352
|
added += 1;
|
|
2826
3353
|
}
|
|
2827
3354
|
this.streamingActive = true;
|
|
2828
3355
|
this.adjustScrollForLineChange(added);
|
|
3356
|
+
this.trimScrollback();
|
|
2829
3357
|
this.scheduleRepaint();
|
|
2830
3358
|
}
|
|
2831
3359
|
setHeader(header) {
|
|
2832
3360
|
this.header = { ...this.header, ...header };
|
|
3361
|
+
this.syncWindowTitle();
|
|
2833
3362
|
this.repaint();
|
|
2834
3363
|
}
|
|
3364
|
+
// Push the current session title (or short session id, as fallback) to
|
|
3365
|
+
// the host terminal via OSC 2. Supported by xterm/foot/iTerm2/Alacritty/
|
|
3366
|
+
// most modern emulators; ignored harmlessly elsewhere.
|
|
3367
|
+
syncWindowTitle() {
|
|
3368
|
+
const title = this.header.title?.trim();
|
|
3369
|
+
const fallback = shortId(this.header.sessionId) || "hydra";
|
|
3370
|
+
const raw = title && title.length > 0 ? title : fallback;
|
|
3371
|
+
const clean = raw.replace(/[\x00-\x1f\x7f]/g, "").slice(0, 200);
|
|
3372
|
+
if (clean === this.lastWindowTitle) {
|
|
3373
|
+
return;
|
|
3374
|
+
}
|
|
3375
|
+
this.lastWindowTitle = clean;
|
|
3376
|
+
process.stdout.write(`\x1B]2;${clean}\x1B\\`);
|
|
3377
|
+
}
|
|
3378
|
+
clearWindowTitle() {
|
|
3379
|
+
this.lastWindowTitle = null;
|
|
3380
|
+
process.stdout.write("\x1B]2;\x1B\\");
|
|
3381
|
+
}
|
|
2835
3382
|
setBanner(banner) {
|
|
2836
3383
|
this.banner = { ...this.banner, ...banner };
|
|
2837
3384
|
this.drawBanner();
|
|
@@ -2840,6 +3387,8 @@ var init_screen = __esm({
|
|
|
2840
3387
|
clearScrollback() {
|
|
2841
3388
|
this.lines = [];
|
|
2842
3389
|
this.keyedBlocks.clear();
|
|
3390
|
+
this.wrapCache.clear();
|
|
3391
|
+
this.wrapCacheWidth = 0;
|
|
2843
3392
|
this.streamingActive = false;
|
|
2844
3393
|
this.scrollOffset = 0;
|
|
2845
3394
|
this.repaint();
|
|
@@ -2862,7 +3411,10 @@ var init_screen = __esm({
|
|
|
2862
3411
|
return;
|
|
2863
3412
|
}
|
|
2864
3413
|
const touchesEnd = existing.start + existing.count >= this.lines.length;
|
|
2865
|
-
this.lines.splice(existing.start, existing.count);
|
|
3414
|
+
const removed = this.lines.splice(existing.start, existing.count);
|
|
3415
|
+
for (const line of removed) {
|
|
3416
|
+
this.forgetLine(line);
|
|
3417
|
+
}
|
|
2866
3418
|
this.keyedBlocks.delete(key);
|
|
2867
3419
|
for (const [, range] of this.keyedBlocks) {
|
|
2868
3420
|
if (range.start > existing.start) {
|
|
@@ -2936,9 +3488,12 @@ var init_screen = __esm({
|
|
|
2936
3488
|
if (last && last.body === "" && (last.prefix === void 0 || last.prefix === "")) {
|
|
2937
3489
|
return;
|
|
2938
3490
|
}
|
|
2939
|
-
|
|
3491
|
+
const sep = { body: "" };
|
|
3492
|
+
this.lines.push(sep);
|
|
3493
|
+
this.trackLine(sep);
|
|
2940
3494
|
this.streamingActive = false;
|
|
2941
3495
|
this.adjustScrollForLineChange(1);
|
|
3496
|
+
this.trimScrollback();
|
|
2942
3497
|
this.scheduleRepaint();
|
|
2943
3498
|
}
|
|
2944
3499
|
// The dispatcher is the source of truth for prompt state. If the prompt
|
|
@@ -3010,9 +3565,11 @@ var init_screen = __esm({
|
|
|
3010
3565
|
return Math.max(0, bottom - top + 1);
|
|
3011
3566
|
}
|
|
3012
3567
|
maxScrollOffset() {
|
|
3013
|
-
const
|
|
3014
|
-
|
|
3015
|
-
|
|
3568
|
+
const { rows } = this.wrapTail(
|
|
3569
|
+
this.term.width,
|
|
3570
|
+
Number.POSITIVE_INFINITY
|
|
3571
|
+
);
|
|
3572
|
+
return Math.max(0, rows.length - this.scrollbackVisibleRows());
|
|
3016
3573
|
}
|
|
3017
3574
|
// Used by content mutators to coalesce rapid updates. Repaints fire
|
|
3018
3575
|
// at most once per contentRepaintThrottleMs; if a paint happened
|
|
@@ -3045,6 +3602,22 @@ var init_screen = __esm({
|
|
|
3045
3602
|
this.repaint();
|
|
3046
3603
|
}, this.contentRepaintThrottleMs - elapsed);
|
|
3047
3604
|
}
|
|
3605
|
+
// Funnel for every row that any drawX method renders. Skips emitting
|
|
3606
|
+
// moveTo+eraseLineAfter+paint when the row's signature matches the
|
|
3607
|
+
// previous frame's. The signature must capture everything that affects
|
|
3608
|
+
// visible output for that row (width, FormattedLine fields, banner
|
|
3609
|
+
// state, etc.) so identical sigs guarantee identical bytes.
|
|
3610
|
+
paintRow(row, signature, paint) {
|
|
3611
|
+
if (row < 1 || row > this.term.height) {
|
|
3612
|
+
return;
|
|
3613
|
+
}
|
|
3614
|
+
if (this.lastFrameRows.get(row) === signature) {
|
|
3615
|
+
return;
|
|
3616
|
+
}
|
|
3617
|
+
this.lastFrameRows.set(row, signature);
|
|
3618
|
+
this.term.moveTo(1, row).eraseLineAfter();
|
|
3619
|
+
paint();
|
|
3620
|
+
}
|
|
3048
3621
|
repaint() {
|
|
3049
3622
|
if (this.repaintPaused > 0) {
|
|
3050
3623
|
this.repaintPending = true;
|
|
@@ -3060,6 +3633,11 @@ var init_screen = __esm({
|
|
|
3060
3633
|
if (w < 20 || h < 8) {
|
|
3061
3634
|
return;
|
|
3062
3635
|
}
|
|
3636
|
+
if (w !== this.lastFrameW || h !== this.lastFrameH) {
|
|
3637
|
+
this.lastFrameRows.clear();
|
|
3638
|
+
this.lastFrameW = w;
|
|
3639
|
+
this.lastFrameH = h;
|
|
3640
|
+
}
|
|
3063
3641
|
this.drawHeader();
|
|
3064
3642
|
this.drawSeparator(HEADER_ROWS);
|
|
3065
3643
|
this.drawScrollback();
|
|
@@ -3075,38 +3653,40 @@ var init_screen = __esm({
|
|
|
3075
3653
|
}
|
|
3076
3654
|
drawHeader() {
|
|
3077
3655
|
const w = this.term.width;
|
|
3078
|
-
this.term.moveTo(1, 1).eraseLineAfter();
|
|
3079
3656
|
const usage = formatUsage(this.header.usage);
|
|
3080
3657
|
const sid = shortId(this.header.sessionId);
|
|
3081
3658
|
const title = this.header.title?.trim();
|
|
3082
|
-
const
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3659
|
+
const sig = `hdr|${w}|${this.header.agent}|${this.header.cwd}|${sid}|${title ?? ""}|${usage ?? ""}`;
|
|
3660
|
+
this.paintRow(1, sig, () => {
|
|
3661
|
+
const fixed = "hydra \xB7 ".length + this.header.agent.length + " \xB7 ".length + " \xB7 ".length + sid.length + (title ? " \xB7 ".length : 0) + (usage ? usage.length + 3 : 0);
|
|
3662
|
+
const variableRoom = Math.max(8, w - fixed);
|
|
3663
|
+
let cwdRoom;
|
|
3664
|
+
let titleRoom;
|
|
3665
|
+
if (title) {
|
|
3666
|
+
const cwdMin = Math.min(this.header.cwd.length, 12);
|
|
3667
|
+
const titleCap = Math.max(8, variableRoom - cwdMin);
|
|
3668
|
+
titleRoom = Math.min(title.length, titleCap);
|
|
3669
|
+
cwdRoom = Math.max(8, variableRoom - titleRoom);
|
|
3670
|
+
} else {
|
|
3671
|
+
titleRoom = 0;
|
|
3672
|
+
cwdRoom = variableRoom;
|
|
3673
|
+
}
|
|
3674
|
+
this.term.bold("hydra")(" \xB7 ").cyan.noFormat(this.header.agent)(" \xB7 ").dim.noFormat(truncate(this.header.cwd, cwdRoom))(" \xB7 ").yellow(sid);
|
|
3675
|
+
if (title) {
|
|
3676
|
+
this.term(" \xB7 ").bold.noFormat(truncate(title, titleRoom));
|
|
3677
|
+
}
|
|
3678
|
+
if (usage) {
|
|
3679
|
+
const col = Math.max(1, w - usage.length + 1);
|
|
3680
|
+
this.term.moveTo(col, 1);
|
|
3681
|
+
this.term.dim(usage);
|
|
3682
|
+
}
|
|
3683
|
+
});
|
|
3103
3684
|
}
|
|
3104
3685
|
drawSeparator(row) {
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
this.term.dim("\u2500".repeat(this.term.width));
|
|
3686
|
+
const w = this.term.width;
|
|
3687
|
+
this.paintRow(row, `sep|${w}`, () => {
|
|
3688
|
+
this.term.dim("\u2500".repeat(w));
|
|
3689
|
+
});
|
|
3110
3690
|
}
|
|
3111
3691
|
drawScrollback() {
|
|
3112
3692
|
const w = this.term.width;
|
|
@@ -3115,21 +3695,30 @@ var init_screen = __esm({
|
|
|
3115
3695
|
if (visibleRows <= 0) {
|
|
3116
3696
|
return;
|
|
3117
3697
|
}
|
|
3118
|
-
const wrapped = this.
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3698
|
+
const { rows: wrapped, exhausted } = this.wrapTail(
|
|
3699
|
+
w,
|
|
3700
|
+
visibleRows + this.scrollOffset
|
|
3701
|
+
);
|
|
3702
|
+
if (exhausted) {
|
|
3703
|
+
const max = Math.max(0, wrapped.length - visibleRows);
|
|
3704
|
+
if (this.scrollOffset > max) {
|
|
3705
|
+
this.scrollOffset = max;
|
|
3706
|
+
}
|
|
3122
3707
|
}
|
|
3123
3708
|
const end = wrapped.length - this.scrollOffset;
|
|
3124
3709
|
const start = Math.max(0, end - visibleRows);
|
|
3125
3710
|
const slice = wrapped.slice(start, end);
|
|
3711
|
+
const padTop = Math.max(0, visibleRows - slice.length);
|
|
3126
3712
|
for (let i = 0; i < visibleRows; i++) {
|
|
3127
3713
|
const row = top + i;
|
|
3128
|
-
|
|
3129
|
-
const line = slice[
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3714
|
+
const sliceIdx = i - padTop;
|
|
3715
|
+
const line = sliceIdx >= 0 ? slice[sliceIdx] : void 0;
|
|
3716
|
+
const sig = formattedLineSig("sb", w, line);
|
|
3717
|
+
this.paintRow(row, sig, () => {
|
|
3718
|
+
if (line) {
|
|
3719
|
+
this.writeFormattedLine(line, w);
|
|
3720
|
+
}
|
|
3721
|
+
});
|
|
3133
3722
|
}
|
|
3134
3723
|
}
|
|
3135
3724
|
queuedRows() {
|
|
@@ -3160,26 +3749,27 @@ var init_screen = __esm({
|
|
|
3160
3749
|
}
|
|
3161
3750
|
for (let i = 0; i < rows; i++) {
|
|
3162
3751
|
const row = completionTop + i;
|
|
3163
|
-
this.term.moveTo(1, row).eraseLineAfter();
|
|
3164
3752
|
const item = this.completions[i];
|
|
3165
|
-
if (!item) {
|
|
3166
|
-
continue;
|
|
3167
|
-
}
|
|
3168
3753
|
const isLast = i === rows - 1 && this.completions.length > MAX_COMPLETION_ROWS;
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
)
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3754
|
+
const overflow = this.completions.length - MAX_COMPLETION_ROWS + 1;
|
|
3755
|
+
const sig = item ? isLast ? `comp|${w}|overflow|${overflow}` : `comp|${w}|${nameWidth}|${item.name}|${item.description ?? ""}` : `comp|${w}|empty`;
|
|
3756
|
+
this.paintRow(row, sig, () => {
|
|
3757
|
+
if (!item) {
|
|
3758
|
+
return;
|
|
3759
|
+
}
|
|
3760
|
+
if (isLast) {
|
|
3761
|
+
this.term.dim(` + ${overflow} more match(es)`);
|
|
3762
|
+
return;
|
|
3763
|
+
}
|
|
3764
|
+
const namePadded = item.name.padEnd(nameWidth);
|
|
3765
|
+
const desc = item.description ?? "";
|
|
3766
|
+
const remaining = w - namePadded.length - 4;
|
|
3767
|
+
const truncated = remaining > 0 ? truncate(desc, remaining) : "";
|
|
3768
|
+
this.term(" ").brightCyan(namePadded);
|
|
3769
|
+
if (truncated.length > 0) {
|
|
3770
|
+
this.term(" ").dim(truncated);
|
|
3771
|
+
}
|
|
3772
|
+
});
|
|
3183
3773
|
}
|
|
3184
3774
|
}
|
|
3185
3775
|
drawQueuedZone() {
|
|
@@ -3194,17 +3784,19 @@ var init_screen = __esm({
|
|
|
3194
3784
|
const queuedTop = queuedBottom - rows + 1;
|
|
3195
3785
|
for (let i = 0; i < rows; i++) {
|
|
3196
3786
|
const row = queuedTop + i;
|
|
3197
|
-
this.term.moveTo(1, row).eraseLineAfter();
|
|
3198
3787
|
const text = this.queuedTexts[i];
|
|
3199
|
-
if (text === void 0) {
|
|
3200
|
-
continue;
|
|
3201
|
-
}
|
|
3202
3788
|
const isLast = i === rows - 1 && this.queuedTexts.length > MAX_QUEUED_ROWS;
|
|
3203
3789
|
const overflow = this.queuedTexts.length - MAX_QUEUED_ROWS;
|
|
3204
|
-
const summary = isLast ? `+ ${overflow + 1} more queued` : truncate(firstLine2(text), w - 4);
|
|
3205
|
-
const
|
|
3206
|
-
|
|
3207
|
-
|
|
3790
|
+
const summary = text === void 0 ? "" : isLast ? `+ ${overflow + 1} more queued` : truncate(firstLine2(text), w - 4);
|
|
3791
|
+
const sig = text === void 0 ? `queued|${w}|empty` : `queued|${w}|${isLast ? "ovf" : "row"}|${summary}`;
|
|
3792
|
+
this.paintRow(row, sig, () => {
|
|
3793
|
+
if (text === void 0) {
|
|
3794
|
+
return;
|
|
3795
|
+
}
|
|
3796
|
+
const display = ` \u23F3 ${summary}`;
|
|
3797
|
+
const padded = display + " ".repeat(Math.max(0, w - display.length));
|
|
3798
|
+
this.term.bgBlue.brightWhite.noFormat(padded);
|
|
3799
|
+
});
|
|
3208
3800
|
}
|
|
3209
3801
|
}
|
|
3210
3802
|
drawPrompt() {
|
|
@@ -3225,19 +3817,30 @@ var init_screen = __esm({
|
|
|
3225
3817
|
for (let i = 0; i < layout.rendered; i++) {
|
|
3226
3818
|
const vr = visualRows[layout.windowStart + i];
|
|
3227
3819
|
const row = top + i;
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
this.term(" ");
|
|
3820
|
+
let gutter = "wrap";
|
|
3821
|
+
let slice = "";
|
|
3822
|
+
if (vr) {
|
|
3823
|
+
if (vr.bufferIdx === 0 && vr.startCol === 0) {
|
|
3824
|
+
gutter = "first";
|
|
3825
|
+
} else if (vr.startCol === 0) {
|
|
3826
|
+
gutter = "newline";
|
|
3827
|
+
}
|
|
3828
|
+
slice = (state.buffer[vr.bufferIdx] ?? "").slice(vr.startCol, vr.endCol);
|
|
3238
3829
|
}
|
|
3239
|
-
const
|
|
3240
|
-
this.
|
|
3830
|
+
const sig = vr ? `prompt|${this.term.width}|${gutter}|${slice}` : `prompt|${this.term.width}|empty`;
|
|
3831
|
+
this.paintRow(row, sig, () => {
|
|
3832
|
+
if (!vr) {
|
|
3833
|
+
return;
|
|
3834
|
+
}
|
|
3835
|
+
if (gutter === "first") {
|
|
3836
|
+
this.term.brightWhite("> ");
|
|
3837
|
+
} else if (gutter === "newline") {
|
|
3838
|
+
this.term.dim("\xB7 ");
|
|
3839
|
+
} else {
|
|
3840
|
+
this.term(" ");
|
|
3841
|
+
}
|
|
3842
|
+
this.term.noFormat(slice);
|
|
3843
|
+
});
|
|
3241
3844
|
}
|
|
3242
3845
|
}
|
|
3243
3846
|
drawConfirmPrompt() {
|
|
@@ -3247,10 +3850,12 @@ var init_screen = __esm({
|
|
|
3247
3850
|
}
|
|
3248
3851
|
const w = this.term.width;
|
|
3249
3852
|
const top = this.term.height - CONFIRM_PROMPT_ROWS - BANNER_ROWS + 1;
|
|
3250
|
-
this.
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
this.
|
|
3853
|
+
this.paintRow(top, `confirm|q|${w}|${spec.question}`, () => {
|
|
3854
|
+
this.term.brightYellow(` ? ${truncate(spec.question, w - 4)}`);
|
|
3855
|
+
});
|
|
3856
|
+
this.paintRow(top + 1, `confirm|h|${w}|${spec.hint}`, () => {
|
|
3857
|
+
this.term.dim(` ${truncate(spec.hint, w - 2)}`);
|
|
3858
|
+
});
|
|
3254
3859
|
}
|
|
3255
3860
|
drawPermissionPrompt() {
|
|
3256
3861
|
const spec = this.permissionPrompt;
|
|
@@ -3261,21 +3866,20 @@ var init_screen = __esm({
|
|
|
3261
3866
|
const rows = this.permissionRows();
|
|
3262
3867
|
const top = this.term.height - rows - BANNER_ROWS + 1;
|
|
3263
3868
|
let row = top;
|
|
3264
|
-
const writeRow = (paint) => {
|
|
3869
|
+
const writeRow = (sig, paint) => {
|
|
3265
3870
|
if (row >= top + rows) {
|
|
3266
3871
|
return;
|
|
3267
3872
|
}
|
|
3268
|
-
this.
|
|
3269
|
-
paint();
|
|
3873
|
+
this.paintRow(row, sig, paint);
|
|
3270
3874
|
row += 1;
|
|
3271
3875
|
};
|
|
3272
|
-
writeRow(() => {
|
|
3876
|
+
writeRow(`perm|t|${w}|${spec.title}`, () => {
|
|
3273
3877
|
this.term.brightYellow(` \u{1F512} ${truncate(spec.title, w - 5)}`);
|
|
3274
3878
|
});
|
|
3275
|
-
writeRow(() => {
|
|
3879
|
+
writeRow(`perm|sub|${w}`, () => {
|
|
3276
3880
|
this.term.dim(" This action requires approval");
|
|
3277
3881
|
});
|
|
3278
|
-
writeRow(() => {
|
|
3882
|
+
writeRow(`perm|q|${w}`, () => {
|
|
3279
3883
|
this.term(" Do you want to proceed?");
|
|
3280
3884
|
});
|
|
3281
3885
|
for (let i = 0; i < spec.options.length; i++) {
|
|
@@ -3289,7 +3893,7 @@ var init_screen = __esm({
|
|
|
3289
3893
|
const isSel = i === spec.selectedIndex;
|
|
3290
3894
|
const marker = isSel ? "\u276F" : " ";
|
|
3291
3895
|
const body = ` ${marker} ${i + 1}. ${truncate(opt.label, w - 8)}`;
|
|
3292
|
-
writeRow(() => {
|
|
3896
|
+
writeRow(`perm|o|${w}|${i}|${isSel ? "1" : "0"}|${opt.label}`, () => {
|
|
3293
3897
|
if (isSel) {
|
|
3294
3898
|
this.term.brightCyan(body);
|
|
3295
3899
|
} else {
|
|
@@ -3297,38 +3901,42 @@ var init_screen = __esm({
|
|
|
3297
3901
|
}
|
|
3298
3902
|
});
|
|
3299
3903
|
}
|
|
3300
|
-
writeRow(() => {
|
|
3904
|
+
writeRow(`perm|hint|${w}`, () => {
|
|
3301
3905
|
this.term.dim(" \u2191/\u2193 choose \xB7 Enter submit \xB7 Esc cancel \xB7 1\u20139 quick-pick");
|
|
3302
3906
|
});
|
|
3303
3907
|
}
|
|
3304
3908
|
drawBanner() {
|
|
3305
3909
|
const row = this.term.height;
|
|
3306
|
-
this.term.
|
|
3307
|
-
const
|
|
3308
|
-
const
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3910
|
+
const w = this.term.width;
|
|
3911
|
+
const elapsedStr = this.banner.status === "busy" && this.banner.elapsedMs !== void 0 && this.banner.elapsedMs >= 1e3 ? formatElapsed(this.banner.elapsedMs) : "";
|
|
3912
|
+
const sig = `bnr|${w}|${this.banner.status}|${elapsedStr}|${this.banner.queued}|${this.scrollOffset}|${this.banner.planMode ? "1" : "0"}|${this.banner.hint}`;
|
|
3913
|
+
this.paintRow(row, sig, () => {
|
|
3914
|
+
const dot = this.banner.status === "busy" ? "\u25CF" : "\u25CB";
|
|
3915
|
+
const planLabel = this.banner.planMode ? "plan: ON " : "plan: off";
|
|
3916
|
+
if (this.banner.status === "busy") {
|
|
3917
|
+
this.term.brightYellow(`${dot} ${this.banner.status}`);
|
|
3918
|
+
if (elapsedStr) {
|
|
3919
|
+
this.term(" ").dim(elapsedStr);
|
|
3920
|
+
}
|
|
3921
|
+
} else if (this.banner.status === "disconnected") {
|
|
3922
|
+
this.term.brightRed(`${dot} ${this.banner.status}`);
|
|
3923
|
+
} else {
|
|
3924
|
+
this.term.brightGreen(`${dot} ${this.banner.status}`);
|
|
3313
3925
|
}
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
this.
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
this.term(" \xB7 ")
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
} else {
|
|
3329
|
-
this.term.dim(planLabel);
|
|
3330
|
-
}
|
|
3331
|
-
this.term(" \xB7 ").dim(this.banner.hint);
|
|
3926
|
+
if (this.banner.queued > 0) {
|
|
3927
|
+
this.term(" \xB7 ").brightYellow(`${this.banner.queued} queued`);
|
|
3928
|
+
}
|
|
3929
|
+
if (this.scrollOffset > 0) {
|
|
3930
|
+
this.term(" \xB7 ").brightCyan(`\u2191 ${this.scrollOffset}`);
|
|
3931
|
+
}
|
|
3932
|
+
this.term(" \xB7 ");
|
|
3933
|
+
if (this.banner.planMode) {
|
|
3934
|
+
this.term.brightYellow(planLabel);
|
|
3935
|
+
} else {
|
|
3936
|
+
this.term.dim(planLabel);
|
|
3937
|
+
}
|
|
3938
|
+
this.term(" \xB7 ").dim(this.banner.hint);
|
|
3939
|
+
});
|
|
3332
3940
|
}
|
|
3333
3941
|
placeCursor() {
|
|
3334
3942
|
if (this.permissionPrompt) {
|
|
@@ -3378,37 +3986,83 @@ var init_screen = __esm({
|
|
|
3378
3986
|
4 + this.permissionPrompt.options.length
|
|
3379
3987
|
);
|
|
3380
3988
|
}
|
|
3381
|
-
|
|
3989
|
+
// Walk this.lines from the tail, accumulating wrapped rows via the
|
|
3990
|
+
// wrap cache, until we have at least `needed` rows or run out. Returns
|
|
3991
|
+
// the collected rows in original (top-down) order plus an `exhausted`
|
|
3992
|
+
// flag that's true iff we reached the head of this.lines. The hot path
|
|
3993
|
+
// (drawScrollback) only ever asks for `visibleRows + scrollOffset`
|
|
3994
|
+
// rows, so a 10k-line scrollback costs ~50 cache hits per repaint
|
|
3995
|
+
// instead of 10k. With `needed = Infinity` this walks everything and
|
|
3996
|
+
// doubles as a total-row counter for maxScrollOffset.
|
|
3997
|
+
wrapTail(width, needed) {
|
|
3382
3998
|
if (width <= 4) {
|
|
3383
|
-
|
|
3999
|
+
const take = Math.min(needed, this.lines.length);
|
|
4000
|
+
return {
|
|
4001
|
+
rows: this.lines.slice(this.lines.length - take),
|
|
4002
|
+
exhausted: needed >= this.lines.length
|
|
4003
|
+
};
|
|
3384
4004
|
}
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
4005
|
+
if (this.wrapCacheWidth !== width) {
|
|
4006
|
+
this.wrapCache.clear();
|
|
4007
|
+
this.wrapCacheWidth = width;
|
|
4008
|
+
}
|
|
4009
|
+
if (needed <= 0 || this.lines.length === 0) {
|
|
4010
|
+
return { rows: [], exhausted: true };
|
|
4011
|
+
}
|
|
4012
|
+
const batches = [];
|
|
4013
|
+
let total = 0;
|
|
4014
|
+
let stoppedAt = 0;
|
|
4015
|
+
for (let i = this.lines.length - 1; i >= 0; i--) {
|
|
4016
|
+
const wrapped = this.wrapOne(this.lines[i], width);
|
|
4017
|
+
batches.push(wrapped);
|
|
4018
|
+
total += wrapped.length;
|
|
4019
|
+
stoppedAt = i;
|
|
4020
|
+
if (total >= needed) {
|
|
4021
|
+
break;
|
|
4022
|
+
}
|
|
4023
|
+
}
|
|
4024
|
+
const rows = [];
|
|
4025
|
+
for (let i = batches.length - 1; i >= 0; i--) {
|
|
4026
|
+
rows.push(...batches[i]);
|
|
4027
|
+
}
|
|
4028
|
+
return { rows, exhausted: stoppedAt === 0 };
|
|
4029
|
+
}
|
|
4030
|
+
wrapOne(line, width) {
|
|
4031
|
+
const id = this.lineIds.get(line);
|
|
4032
|
+
if (id !== void 0) {
|
|
4033
|
+
const cached = this.wrapCache.get(id);
|
|
4034
|
+
if (cached) {
|
|
4035
|
+
return cached;
|
|
4036
|
+
}
|
|
4037
|
+
}
|
|
4038
|
+
const prefix = line.prefix ?? "";
|
|
4039
|
+
const room = Math.max(1, width - prefix.length);
|
|
4040
|
+
const chunks = line.ansi ? wrapAnsiBody(line.body, room) : wrap(line.body, room);
|
|
4041
|
+
const wrapped = [];
|
|
4042
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
4043
|
+
const chunk = chunks[i] ?? "";
|
|
4044
|
+
const wrappedLine = {
|
|
4045
|
+
prefix: i === 0 ? line.prefix : " ".repeat(prefix.length),
|
|
4046
|
+
body: chunk
|
|
4047
|
+
};
|
|
4048
|
+
if (line.prefixStyle !== void 0) {
|
|
4049
|
+
wrappedLine.prefixStyle = line.prefixStyle;
|
|
3409
4050
|
}
|
|
4051
|
+
if (line.bodyStyle !== void 0) {
|
|
4052
|
+
wrappedLine.bodyStyle = line.bodyStyle;
|
|
4053
|
+
}
|
|
4054
|
+
if (line.fillRow) {
|
|
4055
|
+
wrappedLine.fillRow = true;
|
|
4056
|
+
}
|
|
4057
|
+
if (line.ansi) {
|
|
4058
|
+
wrappedLine.ansi = true;
|
|
4059
|
+
}
|
|
4060
|
+
wrapped.push(wrappedLine);
|
|
4061
|
+
}
|
|
4062
|
+
if (id !== void 0) {
|
|
4063
|
+
this.wrapCache.set(id, wrapped);
|
|
3410
4064
|
}
|
|
3411
|
-
return
|
|
4065
|
+
return wrapped;
|
|
3412
4066
|
}
|
|
3413
4067
|
writeFormattedLine(line, width) {
|
|
3414
4068
|
if (line.prefix) {
|
|
@@ -3447,6 +4101,10 @@ var init_input = __esm({
|
|
|
3447
4101
|
savedDraft = null;
|
|
3448
4102
|
history = [];
|
|
3449
4103
|
turnRunning = false;
|
|
4104
|
+
// Single-slot kill ring. The most recent killed text (^U, ^K, ^W) lands
|
|
4105
|
+
// here so ^Y can yank it back. Standard readline keeps a stack; we
|
|
4106
|
+
// only keep one slot because that's what 99% of yank uses look like.
|
|
4107
|
+
killBuffer = "";
|
|
3450
4108
|
constructor(opts = {}) {
|
|
3451
4109
|
this.history = [...opts.history ?? []];
|
|
3452
4110
|
this.planMode = opts.planMode ?? false;
|
|
@@ -3555,6 +4213,9 @@ var init_input = __esm({
|
|
|
3555
4213
|
case "ctrl-w":
|
|
3556
4214
|
this.killWord();
|
|
3557
4215
|
return [];
|
|
4216
|
+
case "ctrl-y":
|
|
4217
|
+
this.yank();
|
|
4218
|
+
return [];
|
|
3558
4219
|
case "escape":
|
|
3559
4220
|
return [];
|
|
3560
4221
|
}
|
|
@@ -3648,11 +4309,19 @@ var init_input = __esm({
|
|
|
3648
4309
|
}
|
|
3649
4310
|
killLine() {
|
|
3650
4311
|
const line = this.currentLine();
|
|
4312
|
+
const killed = line.slice(0, this.col);
|
|
4313
|
+
if (killed.length > 0) {
|
|
4314
|
+
this.killBuffer = killed;
|
|
4315
|
+
}
|
|
3651
4316
|
this.setCurrentLine(line.slice(this.col));
|
|
3652
4317
|
this.col = 0;
|
|
3653
4318
|
}
|
|
3654
4319
|
killToEnd() {
|
|
3655
4320
|
const line = this.currentLine();
|
|
4321
|
+
const killed = line.slice(this.col);
|
|
4322
|
+
if (killed.length > 0) {
|
|
4323
|
+
this.killBuffer = killed;
|
|
4324
|
+
}
|
|
3656
4325
|
this.setCurrentLine(line.slice(0, this.col));
|
|
3657
4326
|
}
|
|
3658
4327
|
killWord() {
|
|
@@ -3668,9 +4337,19 @@ var init_input = __esm({
|
|
|
3668
4337
|
while (i > 0 && !/\s/.test(line[i - 1] ?? "")) {
|
|
3669
4338
|
i -= 1;
|
|
3670
4339
|
}
|
|
4340
|
+
const killed = line.slice(i, this.col);
|
|
4341
|
+
if (killed.length > 0) {
|
|
4342
|
+
this.killBuffer = killed;
|
|
4343
|
+
}
|
|
3671
4344
|
this.setCurrentLine(line.slice(0, i) + line.slice(this.col));
|
|
3672
4345
|
this.col = i;
|
|
3673
4346
|
}
|
|
4347
|
+
yank() {
|
|
4348
|
+
if (this.killBuffer.length === 0) {
|
|
4349
|
+
return;
|
|
4350
|
+
}
|
|
4351
|
+
this.insertText(this.killBuffer);
|
|
4352
|
+
}
|
|
3674
4353
|
moveLeft() {
|
|
3675
4354
|
if (this.col > 0) {
|
|
3676
4355
|
this.col -= 1;
|
|
@@ -4333,6 +5012,7 @@ var init_format = __esm({
|
|
|
4333
5012
|
});
|
|
4334
5013
|
|
|
4335
5014
|
// src/tui/app.ts
|
|
5015
|
+
import { appendFileSync, statSync, renameSync } from "fs";
|
|
4336
5016
|
import { nanoid as nanoid3 } from "nanoid";
|
|
4337
5017
|
import termkit from "terminal-kit";
|
|
4338
5018
|
async function runTuiApp(opts) {
|
|
@@ -4350,6 +5030,8 @@ async function runSession(term, config, opts) {
|
|
|
4350
5030
|
term.grabInput(false);
|
|
4351
5031
|
process.exit(0);
|
|
4352
5032
|
}
|
|
5033
|
+
const launchLabel = ctx.sessionId === "__new__" ? "Starting new session\u2026" : "Resuming session\u2026";
|
|
5034
|
+
term.cyan(launchLabel)("\n");
|
|
4353
5035
|
const protocol = config.daemon.tls ? "wss" : "ws";
|
|
4354
5036
|
const wsUrl = `${protocol}://${config.daemon.host}:${config.daemon.port}/acp`;
|
|
4355
5037
|
const subprotocols = ["acp.v1", `hydra-acp-token.${config.daemon.authToken}`];
|
|
@@ -4396,8 +5078,9 @@ async function runSession(term, config, opts) {
|
|
|
4396
5078
|
const screenReady = typeof screenRef !== "undefined" && screenRef !== null;
|
|
4397
5079
|
if (before === 0 && pendingTurns > 0) {
|
|
4398
5080
|
sessionBusySince = Date.now();
|
|
5081
|
+
dispatcherRef?.setTurnRunning(true);
|
|
4399
5082
|
if (screenReady) {
|
|
4400
|
-
screenRef.setBanner({ status: "
|
|
5083
|
+
screenRef.setBanner({ status: "busy", elapsedMs: 0 });
|
|
4401
5084
|
}
|
|
4402
5085
|
if (sessionElapsedTimer === null && screenReady) {
|
|
4403
5086
|
sessionElapsedTimer = setInterval(() => {
|
|
@@ -4406,10 +5089,11 @@ async function runSession(term, config, opts) {
|
|
|
4406
5089
|
}
|
|
4407
5090
|
screenRef.setBanner({ elapsedMs: Date.now() - sessionBusySince });
|
|
4408
5091
|
renderToolsBlock();
|
|
4409
|
-
},
|
|
5092
|
+
}, 1e3);
|
|
4410
5093
|
}
|
|
4411
5094
|
} else if (before > 0 && pendingTurns === 0) {
|
|
4412
5095
|
sessionBusySince = null;
|
|
5096
|
+
dispatcherRef?.setTurnRunning(false);
|
|
4413
5097
|
if (sessionElapsedTimer !== null) {
|
|
4414
5098
|
clearInterval(sessionElapsedTimer);
|
|
4415
5099
|
sessionElapsedTimer = null;
|
|
@@ -4423,9 +5107,11 @@ async function runSession(term, config, opts) {
|
|
|
4423
5107
|
}
|
|
4424
5108
|
};
|
|
4425
5109
|
let screenRef = null;
|
|
5110
|
+
let dispatcherRef = null;
|
|
4426
5111
|
conn.onNotification("session/update", (params) => {
|
|
4427
5112
|
const { update } = params ?? {};
|
|
4428
5113
|
const event = mapUpdate(update);
|
|
5114
|
+
debugLogUpdate(update, event);
|
|
4429
5115
|
if (event?.kind === "user-text") {
|
|
4430
5116
|
adjustPendingTurns(1);
|
|
4431
5117
|
} else if (event?.kind === "turn-complete") {
|
|
@@ -4542,6 +5228,7 @@ async function runSession(term, config, opts) {
|
|
|
4542
5228
|
let initialModel;
|
|
4543
5229
|
let initialMode;
|
|
4544
5230
|
let initialCommands;
|
|
5231
|
+
let initialTurnStartedAt;
|
|
4545
5232
|
if (ctx.sessionId === "__new__") {
|
|
4546
5233
|
const created = await conn.request("session/new", {
|
|
4547
5234
|
cwd: ctx.cwd,
|
|
@@ -4562,6 +5249,7 @@ async function runSession(term, config, opts) {
|
|
|
4562
5249
|
}
|
|
4563
5250
|
initialModel = hydraMeta.currentModel;
|
|
4564
5251
|
initialMode = hydraMeta.currentMode;
|
|
5252
|
+
initialTurnStartedAt = hydraMeta.turnStartedAt;
|
|
4565
5253
|
if (hydraMeta.availableCommands) {
|
|
4566
5254
|
initialCommands = hydraMeta.availableCommands.map(
|
|
4567
5255
|
(c) => c.description !== void 0 ? { name: c.name, description: c.description } : { name: c.name }
|
|
@@ -4587,20 +5275,26 @@ async function runSession(term, config, opts) {
|
|
|
4587
5275
|
}
|
|
4588
5276
|
initialModel = hydraMeta.currentModel;
|
|
4589
5277
|
initialMode = hydraMeta.currentMode;
|
|
5278
|
+
initialTurnStartedAt = hydraMeta.turnStartedAt;
|
|
4590
5279
|
if (hydraMeta.availableCommands) {
|
|
4591
5280
|
initialCommands = hydraMeta.availableCommands.map(
|
|
4592
5281
|
(c) => c.description !== void 0 ? { name: c.name, description: c.description } : { name: c.name }
|
|
4593
5282
|
);
|
|
4594
5283
|
}
|
|
4595
5284
|
}
|
|
4596
|
-
const historyFile = paths.tuiHistoryFile();
|
|
5285
|
+
const historyFile = paths.tuiHistoryFile(resolvedSessionId);
|
|
4597
5286
|
let history = await loadHistory(historyFile).catch(() => []);
|
|
4598
5287
|
const dispatcher = new InputDispatcher({ history });
|
|
5288
|
+
dispatcherRef = dispatcher;
|
|
5289
|
+
if (pendingTurns > 0) {
|
|
5290
|
+
dispatcher.setTurnRunning(true);
|
|
5291
|
+
}
|
|
4599
5292
|
let turnInFlight = null;
|
|
4600
5293
|
const screen = new Screen({
|
|
4601
5294
|
term,
|
|
4602
5295
|
dispatcher,
|
|
4603
5296
|
repaintThrottleMs: config.tui.repaintThrottleMs,
|
|
5297
|
+
maxScrollbackLines: config.tui.maxScrollbackLines,
|
|
4604
5298
|
onKey: (events) => {
|
|
4605
5299
|
for (const ev of events) {
|
|
4606
5300
|
if (pendingPermission && tryHandlePermissionKey(ev)) {
|
|
@@ -4767,11 +5461,18 @@ async function runSession(term, config, opts) {
|
|
|
4767
5461
|
const sessionDone = new Promise((resolve2) => {
|
|
4768
5462
|
finishSession = resolve2;
|
|
4769
5463
|
});
|
|
5464
|
+
const cancelRemoteTurn = () => {
|
|
5465
|
+
conn.notify("session/cancel", { sessionId: resolvedSessionId }).catch(() => void 0);
|
|
5466
|
+
};
|
|
4770
5467
|
const sigintHandler = () => {
|
|
4771
5468
|
if (turnInFlight) {
|
|
4772
5469
|
turnInFlight.cancel();
|
|
4773
5470
|
return;
|
|
4774
5471
|
}
|
|
5472
|
+
if (pendingTurns > 0) {
|
|
5473
|
+
cancelRemoteTurn();
|
|
5474
|
+
return;
|
|
5475
|
+
}
|
|
4775
5476
|
void requestExit();
|
|
4776
5477
|
};
|
|
4777
5478
|
let exitConfirmation = null;
|
|
@@ -4846,6 +5547,7 @@ async function runSession(term, config, opts) {
|
|
|
4846
5547
|
};
|
|
4847
5548
|
const teardown = () => {
|
|
4848
5549
|
process.off("SIGINT", sigintHandler);
|
|
5550
|
+
screen.clearWindowTitle();
|
|
4849
5551
|
screen.stop();
|
|
4850
5552
|
saveHistory(historyFile, history).catch(() => void 0);
|
|
4851
5553
|
void stream.close().catch(() => void 0);
|
|
@@ -4861,22 +5563,32 @@ async function runSession(term, config, opts) {
|
|
|
4861
5563
|
}
|
|
4862
5564
|
};
|
|
4863
5565
|
const switchSession = async () => {
|
|
4864
|
-
|
|
4865
|
-
if (!resume) {
|
|
5566
|
+
if (!finishSession) {
|
|
4866
5567
|
return;
|
|
4867
5568
|
}
|
|
4868
|
-
|
|
4869
|
-
|
|
5569
|
+
const pendingDraft = dispatcher.state().buffer.join("\n");
|
|
5570
|
+
if (pendingDraft.replace(/\s+$/, "").length > 0) {
|
|
5571
|
+
history = appendEntry(history, pendingDraft);
|
|
5572
|
+
dispatcher.setHistory(history);
|
|
5573
|
+
}
|
|
5574
|
+
screen.pauseRepaint();
|
|
5575
|
+
screen.stop();
|
|
5576
|
+
saveHistory(historyFile, history).catch(() => void 0);
|
|
4870
5577
|
const sessions = await listSessions(config);
|
|
4871
5578
|
const choice = await pickSession(term, {
|
|
4872
5579
|
cwd: resolvedCwd,
|
|
4873
5580
|
sessions,
|
|
4874
|
-
|
|
5581
|
+
config
|
|
4875
5582
|
});
|
|
4876
5583
|
if (choice.kind === "abort") {
|
|
4877
|
-
|
|
5584
|
+
screen.start();
|
|
5585
|
+
screen.resumeRepaint();
|
|
4878
5586
|
return;
|
|
4879
5587
|
}
|
|
5588
|
+
const resume = finishSession;
|
|
5589
|
+
finishSession = null;
|
|
5590
|
+
process.off("SIGINT", sigintHandler);
|
|
5591
|
+
void stream.close().catch(() => void 0);
|
|
4880
5592
|
if (choice.kind === "new") {
|
|
4881
5593
|
const { sessionId: _drop, ...rest } = opts;
|
|
4882
5594
|
void _drop;
|
|
@@ -4901,6 +5613,8 @@ async function runSession(term, config, opts) {
|
|
|
4901
5613
|
case "cancel":
|
|
4902
5614
|
if (turnInFlight) {
|
|
4903
5615
|
turnInFlight.cancel();
|
|
5616
|
+
} else if (pendingTurns > 0) {
|
|
5617
|
+
cancelRemoteTurn();
|
|
4904
5618
|
}
|
|
4905
5619
|
if (promptQueue.length > (workerActive ? 1 : 0)) {
|
|
4906
5620
|
promptQueue.length = workerActive ? 1 : 0;
|
|
@@ -5088,7 +5802,6 @@ async function runSession(term, config, opts) {
|
|
|
5088
5802
|
const promptArr = planMode ? [{ type: "text", text: PLAN_PREFIX_TEXT }, ...userBlocks] : userBlocks;
|
|
5089
5803
|
adjustPendingTurns(1);
|
|
5090
5804
|
appendRender({ kind: "user-text", text });
|
|
5091
|
-
dispatcher.setTurnRunning(true);
|
|
5092
5805
|
let cancelled = false;
|
|
5093
5806
|
turnInFlight = {
|
|
5094
5807
|
cancel: () => {
|
|
@@ -5118,7 +5831,6 @@ async function runSession(term, config, opts) {
|
|
|
5118
5831
|
});
|
|
5119
5832
|
} finally {
|
|
5120
5833
|
turnInFlight = null;
|
|
5121
|
-
dispatcher.setTurnRunning(false);
|
|
5122
5834
|
adjustPendingTurns(-1);
|
|
5123
5835
|
appendRender(
|
|
5124
5836
|
stopReason !== void 0 ? { kind: "turn-complete", stopReason } : { kind: "turn-complete" }
|
|
@@ -5174,7 +5886,7 @@ async function runSession(term, config, opts) {
|
|
|
5174
5886
|
const elapsed = end - toolsBlockStartedAt;
|
|
5175
5887
|
let summary;
|
|
5176
5888
|
if (total === 0) {
|
|
5177
|
-
summary = inProgress ? `thinking \xB7 ${formatElapsed(elapsed)}` : `
|
|
5889
|
+
summary = inProgress ? `thinking \xB7 ${formatElapsed(elapsed)}` : `thought \xB7 ${formatElapsed(elapsed)}`;
|
|
5178
5890
|
} else {
|
|
5179
5891
|
const noun = total === 1 ? "tool" : "tools";
|
|
5180
5892
|
const timing = inProgress ? formatElapsed(elapsed) : `took ${formatElapsed(elapsed)}`;
|
|
@@ -5277,6 +5989,12 @@ async function runSession(term, config, opts) {
|
|
|
5277
5989
|
if (formatted2.length > 0) {
|
|
5278
5990
|
screen.appendLines(formatted2);
|
|
5279
5991
|
}
|
|
5992
|
+
screen.clearKey("tools");
|
|
5993
|
+
screen.clearKey("plan");
|
|
5994
|
+
toolStates.clear();
|
|
5995
|
+
toolCallOrder.length = 0;
|
|
5996
|
+
toolsExpanded = false;
|
|
5997
|
+
toolsBlockEndedAt = null;
|
|
5280
5998
|
startToolsBlock();
|
|
5281
5999
|
screen.redraw();
|
|
5282
6000
|
return;
|
|
@@ -5317,12 +6035,10 @@ async function runSession(term, config, opts) {
|
|
|
5317
6035
|
if (event.kind === "turn-complete") {
|
|
5318
6036
|
closeAgentText();
|
|
5319
6037
|
screen.clearKey("plan");
|
|
5320
|
-
if (
|
|
6038
|
+
if (toolsBlockStartedAt !== null) {
|
|
5321
6039
|
toolsBlockEndedAt = Date.now();
|
|
5322
6040
|
renderToolsBlock();
|
|
5323
6041
|
screen.clearKey("tools");
|
|
5324
|
-
} else if (toolsBlockStartedAt !== null) {
|
|
5325
|
-
screen.removeBlock("tools");
|
|
5326
6042
|
}
|
|
5327
6043
|
toolStates.clear();
|
|
5328
6044
|
toolCallOrder.length = 0;
|
|
@@ -5342,6 +6058,23 @@ async function runSession(term, config, opts) {
|
|
|
5342
6058
|
} finally {
|
|
5343
6059
|
screen.resumeRepaint();
|
|
5344
6060
|
}
|
|
6061
|
+
if (initialTurnStartedAt !== void 0 && pendingTurns > 0) {
|
|
6062
|
+
sessionBusySince = initialTurnStartedAt;
|
|
6063
|
+
screen.setBanner({
|
|
6064
|
+
status: "busy",
|
|
6065
|
+
elapsedMs: Date.now() - initialTurnStartedAt
|
|
6066
|
+
});
|
|
6067
|
+
if (sessionElapsedTimer === null) {
|
|
6068
|
+
sessionElapsedTimer = setInterval(() => {
|
|
6069
|
+
if (sessionBusySince === null || screenRef === null) {
|
|
6070
|
+
return;
|
|
6071
|
+
}
|
|
6072
|
+
screenRef.setBanner({ elapsedMs: Date.now() - sessionBusySince });
|
|
6073
|
+
renderToolsBlock();
|
|
6074
|
+
}, 1e3);
|
|
6075
|
+
}
|
|
6076
|
+
startToolsBlock();
|
|
6077
|
+
}
|
|
5345
6078
|
const resetInFlightUiState = () => {
|
|
5346
6079
|
if (pendingPermission) {
|
|
5347
6080
|
const resolve2 = pendingPermission.resolve;
|
|
@@ -5351,13 +6084,9 @@ async function runSession(term, config, opts) {
|
|
|
5351
6084
|
}
|
|
5352
6085
|
closeAgentText();
|
|
5353
6086
|
if (toolsBlockStartedAt !== null) {
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
screen.clearKey("tools");
|
|
5358
|
-
} else {
|
|
5359
|
-
screen.removeBlock("tools");
|
|
5360
|
-
}
|
|
6087
|
+
toolsBlockEndedAt = Date.now();
|
|
6088
|
+
renderToolsBlock();
|
|
6089
|
+
screen.clearKey("tools");
|
|
5361
6090
|
toolStates.clear();
|
|
5362
6091
|
toolCallOrder.length = 0;
|
|
5363
6092
|
toolsBlockStartedAt = null;
|
|
@@ -5427,7 +6156,7 @@ async function runSession(term, config, opts) {
|
|
|
5427
6156
|
]);
|
|
5428
6157
|
}
|
|
5429
6158
|
screen.setBanner({
|
|
5430
|
-
status: pendingTurns > 0 ? "
|
|
6159
|
+
status: pendingTurns > 0 ? "busy" : "ready",
|
|
5431
6160
|
elapsedMs: pendingTurns > 0 ? 0 : void 0
|
|
5432
6161
|
});
|
|
5433
6162
|
};
|
|
@@ -5475,7 +6204,7 @@ async function resolveSession(term, config, opts) {
|
|
|
5475
6204
|
const choice = await pickSession(term, {
|
|
5476
6205
|
cwd,
|
|
5477
6206
|
sessions,
|
|
5478
|
-
|
|
6207
|
+
config
|
|
5479
6208
|
});
|
|
5480
6209
|
if (choice.kind === "abort") {
|
|
5481
6210
|
return null;
|
|
@@ -5496,7 +6225,41 @@ function newCtx(opts, cwd, config) {
|
|
|
5496
6225
|
cwd
|
|
5497
6226
|
};
|
|
5498
6227
|
}
|
|
5499
|
-
|
|
6228
|
+
function debugLogUpdate(update, event) {
|
|
6229
|
+
writeDebugLine({
|
|
6230
|
+
src: "session/update",
|
|
6231
|
+
update,
|
|
6232
|
+
event: event === null ? null : { kind: event.kind }
|
|
6233
|
+
});
|
|
6234
|
+
}
|
|
6235
|
+
function writeDebugLine(payload) {
|
|
6236
|
+
const override = process.env.HYDRA_TUI_DEBUG_LOG;
|
|
6237
|
+
const target = override === void 0 ? paths.tuiLogFile() : override;
|
|
6238
|
+
if (target.length === 0) {
|
|
6239
|
+
return;
|
|
6240
|
+
}
|
|
6241
|
+
try {
|
|
6242
|
+
rotateIfBig(target);
|
|
6243
|
+
const line = JSON.stringify({
|
|
6244
|
+
t: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6245
|
+
...payload
|
|
6246
|
+
});
|
|
6247
|
+
appendFileSync(target, `${line}
|
|
6248
|
+
`);
|
|
6249
|
+
} catch {
|
|
6250
|
+
}
|
|
6251
|
+
}
|
|
6252
|
+
function rotateIfBig(target) {
|
|
6253
|
+
try {
|
|
6254
|
+
const stat3 = statSync(target);
|
|
6255
|
+
if (stat3.size < TUI_LOG_MAX_BYTES) {
|
|
6256
|
+
return;
|
|
6257
|
+
}
|
|
6258
|
+
renameSync(target, `${target}.0`);
|
|
6259
|
+
} catch {
|
|
6260
|
+
}
|
|
6261
|
+
}
|
|
6262
|
+
var PLAN_PREFIX_TEXT, TUI_LOG_MAX_BYTES;
|
|
5500
6263
|
var init_app = __esm({
|
|
5501
6264
|
"src/tui/app.ts"() {
|
|
5502
6265
|
"use strict";
|
|
@@ -5514,6 +6277,7 @@ var init_app = __esm({
|
|
|
5514
6277
|
init_render_update();
|
|
5515
6278
|
init_format();
|
|
5516
6279
|
PLAN_PREFIX_TEXT = "Plan mode is on. Outline what you would do without making any changes. Do not edit files, run shell commands, or otherwise execute side effects; produce a plan only.";
|
|
6280
|
+
TUI_LOG_MAX_BYTES = 5 * 1024 * 1024;
|
|
5517
6281
|
}
|
|
5518
6282
|
});
|
|
5519
6283
|
|
|
@@ -5630,7 +6394,7 @@ import { setTimeout as sleep2 } from "timers/promises";
|
|
|
5630
6394
|
|
|
5631
6395
|
// src/daemon/server.ts
|
|
5632
6396
|
init_config();
|
|
5633
|
-
import * as
|
|
6397
|
+
import * as fs8 from "fs";
|
|
5634
6398
|
import * as fsp2 from "fs/promises";
|
|
5635
6399
|
import Fastify from "fastify";
|
|
5636
6400
|
import websocketPlugin from "@fastify/websocket";
|
|
@@ -5802,6 +6566,9 @@ function planSpawn(agent, extraArgs = []) {
|
|
|
5802
6566
|
throw new Error(`Agent ${agent.id} has no usable distribution method.`);
|
|
5803
6567
|
}
|
|
5804
6568
|
|
|
6569
|
+
// src/core/session-manager.ts
|
|
6570
|
+
import * as fs6 from "fs/promises";
|
|
6571
|
+
|
|
5805
6572
|
// src/core/agent-instance.ts
|
|
5806
6573
|
import { spawn } from "child_process";
|
|
5807
6574
|
|
|
@@ -6189,6 +6956,7 @@ var HistoryStore = class {
|
|
|
6189
6956
|
};
|
|
6190
6957
|
|
|
6191
6958
|
// src/core/session-manager.ts
|
|
6959
|
+
init_paths();
|
|
6192
6960
|
init_types();
|
|
6193
6961
|
var SessionManager = class {
|
|
6194
6962
|
constructor(registry, spawner, store, options = {}) {
|
|
@@ -6307,7 +7075,8 @@ var SessionManager = class {
|
|
|
6307
7075
|
seedHistory: params.seedHistory,
|
|
6308
7076
|
currentModel: params.currentModel,
|
|
6309
7077
|
currentMode: params.currentMode,
|
|
6310
|
-
agentCommands: params.agentCommands
|
|
7078
|
+
agentCommands: params.agentCommands,
|
|
7079
|
+
firstPromptSeeded: true
|
|
6311
7080
|
});
|
|
6312
7081
|
await this.attachManagerHooks(session);
|
|
6313
7082
|
return session;
|
|
@@ -6478,13 +7247,14 @@ var SessionManager = class {
|
|
|
6478
7247
|
continue;
|
|
6479
7248
|
}
|
|
6480
7249
|
liveIds.add(session.sessionId);
|
|
7250
|
+
const used = await historyMtimeIso(session.sessionId) ?? new Date(session.updatedAt).toISOString();
|
|
6481
7251
|
entries.push({
|
|
6482
7252
|
sessionId: session.sessionId,
|
|
6483
7253
|
upstreamSessionId: session.upstreamSessionId,
|
|
6484
7254
|
cwd: session.cwd,
|
|
6485
7255
|
title: session.title,
|
|
6486
7256
|
agentId: session.agentId,
|
|
6487
|
-
updatedAt:
|
|
7257
|
+
updatedAt: used,
|
|
6488
7258
|
attachedClients: session.attachedCount,
|
|
6489
7259
|
status: "live"
|
|
6490
7260
|
});
|
|
@@ -6497,13 +7267,14 @@ var SessionManager = class {
|
|
|
6497
7267
|
if (filter.cwd && r.cwd !== filter.cwd) {
|
|
6498
7268
|
continue;
|
|
6499
7269
|
}
|
|
7270
|
+
const used = await historyMtimeIso(r.sessionId) ?? r.updatedAt;
|
|
6500
7271
|
entries.push({
|
|
6501
7272
|
sessionId: r.sessionId,
|
|
6502
7273
|
upstreamSessionId: r.upstreamSessionId,
|
|
6503
7274
|
cwd: r.cwd,
|
|
6504
7275
|
title: r.title,
|
|
6505
7276
|
agentId: r.agentId,
|
|
6506
|
-
updatedAt:
|
|
7277
|
+
updatedAt: used,
|
|
6507
7278
|
attachedClients: 0,
|
|
6508
7279
|
status: "cold"
|
|
6509
7280
|
});
|
|
@@ -6519,6 +7290,10 @@ var SessionManager = class {
|
|
|
6519
7290
|
await this.store.delete(sessionId).catch(() => void 0);
|
|
6520
7291
|
return true;
|
|
6521
7292
|
}
|
|
7293
|
+
async hasRecord(sessionId) {
|
|
7294
|
+
const record = await this.store.read(sessionId).catch(() => void 0);
|
|
7295
|
+
return record !== void 0;
|
|
7296
|
+
}
|
|
6522
7297
|
// Persist a title update from Session.setTitle. The on-disk record
|
|
6523
7298
|
// was written at create time; updating it here keeps the session
|
|
6524
7299
|
// record's title in sync with what was broadcast to clients so a
|
|
@@ -6593,11 +7368,19 @@ var SessionManager = class {
|
|
|
6593
7368
|
this.sessions.clear();
|
|
6594
7369
|
}
|
|
6595
7370
|
};
|
|
7371
|
+
async function historyMtimeIso(sessionId) {
|
|
7372
|
+
try {
|
|
7373
|
+
const st = await fs6.stat(paths.historyFile(sessionId));
|
|
7374
|
+
return new Date(st.mtimeMs).toISOString();
|
|
7375
|
+
} catch {
|
|
7376
|
+
return void 0;
|
|
7377
|
+
}
|
|
7378
|
+
}
|
|
6596
7379
|
|
|
6597
7380
|
// src/core/extensions.ts
|
|
6598
7381
|
init_paths();
|
|
6599
7382
|
import { spawn as spawn2 } from "child_process";
|
|
6600
|
-
import * as
|
|
7383
|
+
import * as fs7 from "fs";
|
|
6601
7384
|
import * as fsp from "fs/promises";
|
|
6602
7385
|
import * as path3 from "path";
|
|
6603
7386
|
var RESTART_BASE_MS = 1e3;
|
|
@@ -6880,7 +7663,7 @@ var ExtensionManager = class {
|
|
|
6880
7663
|
}
|
|
6881
7664
|
const ext = entry.config;
|
|
6882
7665
|
const command = ext.command.length > 0 ? ext.command : [ext.name];
|
|
6883
|
-
const logStream =
|
|
7666
|
+
const logStream = fs7.createWriteStream(paths.extensionLogFile(ext.name), {
|
|
6884
7667
|
flags: "a"
|
|
6885
7668
|
});
|
|
6886
7669
|
logStream.write(
|
|
@@ -6930,7 +7713,7 @@ var ExtensionManager = class {
|
|
|
6930
7713
|
}
|
|
6931
7714
|
if (typeof child.pid === "number") {
|
|
6932
7715
|
try {
|
|
6933
|
-
|
|
7716
|
+
fs7.writeFileSync(paths.extensionPidFile(ext.name), `${child.pid}
|
|
6934
7717
|
`, {
|
|
6935
7718
|
encoding: "utf8",
|
|
6936
7719
|
mode: 384
|
|
@@ -6955,7 +7738,7 @@ var ExtensionManager = class {
|
|
|
6955
7738
|
});
|
|
6956
7739
|
child.on("exit", (code, signal) => {
|
|
6957
7740
|
try {
|
|
6958
|
-
|
|
7741
|
+
fs7.unlinkSync(paths.extensionPidFile(ext.name));
|
|
6959
7742
|
} catch {
|
|
6960
7743
|
}
|
|
6961
7744
|
logStream.write(
|
|
@@ -7093,6 +7876,22 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
7093
7876
|
reply.code(500).send({ error: err.message });
|
|
7094
7877
|
}
|
|
7095
7878
|
});
|
|
7879
|
+
app.post("/v1/sessions/:id/kill", async (request, reply) => {
|
|
7880
|
+
const raw = request.params.id;
|
|
7881
|
+
const id = await manager.resolveCanonicalId(raw) ?? raw;
|
|
7882
|
+
const session = manager.get(id);
|
|
7883
|
+
if (session) {
|
|
7884
|
+
await session.close({ deleteRecord: false });
|
|
7885
|
+
reply.code(204).send();
|
|
7886
|
+
return;
|
|
7887
|
+
}
|
|
7888
|
+
const exists = await manager.hasRecord(id);
|
|
7889
|
+
if (!exists) {
|
|
7890
|
+
reply.code(404).send({ error: "session not found" });
|
|
7891
|
+
return;
|
|
7892
|
+
}
|
|
7893
|
+
reply.code(204).send();
|
|
7894
|
+
});
|
|
7096
7895
|
app.delete("/v1/sessions/:id", async (request, reply) => {
|
|
7097
7896
|
const raw = request.params.id;
|
|
7098
7897
|
const id = await manager.resolveCanonicalId(raw) ?? raw;
|
|
@@ -7300,6 +8099,16 @@ function parseRegisterBody(body) {
|
|
|
7300
8099
|
};
|
|
7301
8100
|
}
|
|
7302
8101
|
|
|
8102
|
+
// src/daemon/routes/config.ts
|
|
8103
|
+
function registerConfigRoutes(app, defaults) {
|
|
8104
|
+
app.get("/v1/config", async () => {
|
|
8105
|
+
return {
|
|
8106
|
+
defaultAgent: defaults.defaultAgent,
|
|
8107
|
+
defaultCwd: defaults.defaultCwd
|
|
8108
|
+
};
|
|
8109
|
+
});
|
|
8110
|
+
}
|
|
8111
|
+
|
|
7303
8112
|
// src/daemon/acp-ws.ts
|
|
7304
8113
|
init_connection();
|
|
7305
8114
|
init_ws_stream();
|
|
@@ -7553,6 +8362,9 @@ function buildResponseMeta(session) {
|
|
|
7553
8362
|
if (commands.length > 0) {
|
|
7554
8363
|
ours.availableCommands = commands;
|
|
7555
8364
|
}
|
|
8365
|
+
if (session.turnStartedAt !== void 0) {
|
|
8366
|
+
ours.turnStartedAt = session.turnStartedAt;
|
|
8367
|
+
}
|
|
7556
8368
|
return mergeMeta(session.agentMeta, ours);
|
|
7557
8369
|
}
|
|
7558
8370
|
function buildInitializeResult() {
|
|
@@ -7638,6 +8450,10 @@ async function startDaemon(config) {
|
|
|
7638
8450
|
});
|
|
7639
8451
|
registerAgentRoutes(app, registry);
|
|
7640
8452
|
registerExtensionRoutes(app, extensions);
|
|
8453
|
+
registerConfigRoutes(app, {
|
|
8454
|
+
defaultAgent: config.defaultAgent,
|
|
8455
|
+
defaultCwd: config.defaultCwd
|
|
8456
|
+
});
|
|
7641
8457
|
registerAcpWsEndpoint(app, {
|
|
7642
8458
|
config,
|
|
7643
8459
|
manager,
|
|
@@ -7673,7 +8489,7 @@ async function startDaemon(config) {
|
|
|
7673
8489
|
await manager.closeAll();
|
|
7674
8490
|
await app.close();
|
|
7675
8491
|
try {
|
|
7676
|
-
|
|
8492
|
+
fs8.unlinkSync(paths.pidFile());
|
|
7677
8493
|
} catch {
|
|
7678
8494
|
}
|
|
7679
8495
|
try {
|
|
@@ -7712,13 +8528,13 @@ function ensureLoopbackOrTls(config) {
|
|
|
7712
8528
|
init_daemon_bootstrap();
|
|
7713
8529
|
|
|
7714
8530
|
// src/cli/commands/log-tail.ts
|
|
7715
|
-
import * as
|
|
8531
|
+
import * as fs9 from "fs";
|
|
7716
8532
|
import * as fsp3 from "fs/promises";
|
|
7717
8533
|
async function runLogTail(logPath, argv, notFoundMessage) {
|
|
7718
8534
|
const opts = parseLogTailFlags(argv);
|
|
7719
|
-
let
|
|
8535
|
+
let stat3;
|
|
7720
8536
|
try {
|
|
7721
|
-
|
|
8537
|
+
stat3 = await fsp3.stat(logPath);
|
|
7722
8538
|
} catch (err) {
|
|
7723
8539
|
const e = err;
|
|
7724
8540
|
if (e.code === "ENOENT") {
|
|
@@ -7729,14 +8545,14 @@ async function runLogTail(logPath, argv, notFoundMessage) {
|
|
|
7729
8545
|
}
|
|
7730
8546
|
throw err;
|
|
7731
8547
|
}
|
|
7732
|
-
let position = await printTail(logPath,
|
|
8548
|
+
let position = await printTail(logPath, stat3.size, opts.tail);
|
|
7733
8549
|
if (!opts.follow) {
|
|
7734
8550
|
return;
|
|
7735
8551
|
}
|
|
7736
8552
|
process.stdout.write(`-- following ${logPath} --
|
|
7737
8553
|
`);
|
|
7738
8554
|
let pending = false;
|
|
7739
|
-
const watcher =
|
|
8555
|
+
const watcher = fs9.watch(logPath, () => {
|
|
7740
8556
|
if (pending) {
|
|
7741
8557
|
return;
|
|
7742
8558
|
}
|
|
@@ -8915,6 +9731,10 @@ async function main() {
|
|
|
8915
9731
|
await runSessionsKill(positional[2]);
|
|
8916
9732
|
return;
|
|
8917
9733
|
}
|
|
9734
|
+
if (sub === "rm") {
|
|
9735
|
+
await runSessionsRm(positional[2]);
|
|
9736
|
+
return;
|
|
9737
|
+
}
|
|
8918
9738
|
process.stderr.write(`Unknown sessions subcommand: ${sub}
|
|
8919
9739
|
`);
|
|
8920
9740
|
process.exit(2);
|
|
@@ -9026,7 +9846,8 @@ function printHelp() {
|
|
|
9026
9846
|
" hydra-acp daemon start|stop|restart|status",
|
|
9027
9847
|
" hydra-acp daemon logs [-f] [-n N] Tail or follow the daemon log",
|
|
9028
9848
|
" hydra-acp sessions [list] [--all] List sessions (live + 20 most-recent cold; --all for everything)",
|
|
9029
|
-
" hydra-acp sessions kill <id>
|
|
9849
|
+
" hydra-acp sessions kill <id> Demote a live session to cold (keeps the on-disk record)",
|
|
9850
|
+
" hydra-acp sessions rm <id> Remove a session entirely (live or cold)",
|
|
9030
9851
|
" hydra-acp extensions list List configured extensions and live state",
|
|
9031
9852
|
" hydra-acp extensions add <name> [opts] Add an extension to config",
|
|
9032
9853
|
" hydra-acp extensions remove <name> Remove an extension from config",
|