pinomad 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/{chunk-b457q0tm.js → chunk-41q0saz0.js} +11 -8
- package/dist/chunk-42nqtx5r.js +22 -0
- package/dist/{chunk-wb10efyq.js → chunk-445cevpm.js} +1 -1
- package/dist/{chunk-xqycs43g.js → chunk-47ff1d1t.js} +35 -7
- package/dist/{chunk-n1m1tjrk.js → chunk-5qf1grkk.js} +19 -13
- package/dist/{chunk-ncbqa8jk.js → chunk-7mn38n4f.js} +1 -1
- package/dist/{chunk-sd5fkxjj.js → chunk-7wj0jh3t.js} +1 -1
- package/dist/{chunk-4dcs8sbj.js → chunk-agq4tn9r.js} +43 -21
- package/dist/{chunk-as6pyf00.js → chunk-aqj9wfmb.js} +1 -1
- package/dist/{chunk-y127fe1a.js → chunk-g3ahp7b9.js} +11 -0
- package/dist/{chunk-gtdpjqa3.js → chunk-gg2dcqbg.js} +186 -45
- package/dist/{chunk-6d16c2fv.js → chunk-jeqsdcj1.js} +13 -6
- package/dist/{chunk-67gq282t.js → chunk-ksfxqd8s.js} +491 -34
- package/dist/{chunk-9h5vawnm.js → chunk-mnmqyq7d.js} +237 -27
- package/dist/{chunk-jt34g8st.js → chunk-np4jd070.js} +154 -2
- package/dist/{chunk-44pnts98.js → chunk-nppgxh6c.js} +1 -1
- package/dist/chunk-nq57fdnq.js +106 -0
- package/dist/{chunk-vvj9b1gc.js → chunk-t5abqsva.js} +174 -52
- package/dist/{chunk-tj57aqav.js → chunk-vqkb70d7.js} +2 -2
- package/dist/{chunk-h5q2y57m.js → chunk-vx7dy07y.js} +10 -4
- package/dist/pinomad.js +15 -14
- package/package.json +2 -1
- package/web/assets/astro-DOly5024.js +1 -0
- package/web/assets/c-BIGW1oBm.js +1 -0
- package/web/assets/core-DnpzcQzs.js +4 -0
- package/web/assets/cpp-DddhVWhK.js +1 -0
- package/web/assets/csharp-B_TYUkTI.js +1 -0
- package/web/assets/css-Z72CF80r.js +1 -0
- package/web/assets/dart-CtKP3W-j.js +1 -0
- package/web/assets/diff-D97Zzqfu.js +1 -0
- package/web/assets/dockerfile-BcOcwvcX.js +1 -0
- package/web/assets/elixir-DkJLSxtU.js +1 -0
- package/web/assets/engine-javascript-BHPwbUfN.js +141 -0
- package/web/assets/engine-oniguruma-CQjQPVHy.js +1 -0
- package/web/assets/fish-BvzEVeQv.js +1 -0
- package/web/assets/go-C27-OAKa.js +1 -0
- package/web/assets/graphql-ChdNCCLP.js +1 -0
- package/web/assets/haskell-Df6bDoY_.js +1 -0
- package/web/assets/hcl-BWvSN4gD.js +1 -0
- package/web/assets/highlighted-diff-Dn5ifsvK.js +175 -0
- package/web/assets/highlights-DnHv6NBM.js +4 -0
- package/web/assets/highlights-themes-CUdrlZk3.js +1 -0
- package/web/assets/html-BlS82ilI.js +1 -0
- package/web/assets/{index-DoyHMPwf.css → index-BV0ecolo.css} +1 -1
- package/web/assets/index-DNsTTgPE.js +396 -0
- package/web/assets/ini-BEwlwnbL.js +1 -0
- package/web/assets/java-CylS5w8V.js +1 -0
- package/web/assets/javascript-wDzz0qaB.js +1 -0
- package/web/assets/json-Cp-IABpG.js +1 -0
- package/web/assets/jsonc-Des-eS-w.js +1 -0
- package/web/assets/jsx-g9-lgVsj.js +1 -0
- package/web/assets/kotlin-BdnUsdx6.js +1 -0
- package/web/assets/less-B1dDrJ26.js +1 -0
- package/web/assets/lua-BaeVxFsk.js +1 -0
- package/web/assets/make-CHLpvVh8.js +1 -0
- package/web/assets/markdown-Cvjx9yec.js +1 -0
- package/web/assets/nix-CwoSXNpI.js +1 -0
- package/web/assets/objective-c-DXmwc3jG.js +1 -0
- package/web/assets/php-DoruYMF0.js +1 -0
- package/web/assets/pierre-dark-CyvmCCZW.js +1 -0
- package/web/assets/pierre-dark-VKo95ZgA.js +1 -0
- package/web/assets/pierre-light-480U9XYS.js +1 -0
- package/web/assets/pierre-light-BVSW-Xev.js +1 -0
- package/web/assets/postcss-DnzrXTVv.js +1 -0
- package/web/assets/powershell-Dd0kTxKP.js +1 -0
- package/web/assets/prisma-Vru482bI.js +1 -0
- package/web/assets/proto-C7zT0LnQ.js +1 -0
- package/web/assets/python-B6aJPvgy.js +1 -0
- package/web/assets/ruby-B43yHlf9.js +1 -0
- package/web/assets/rust-DPvrY3IQ.js +1 -0
- package/web/assets/scala-CqE71os6.js +1 -0
- package/web/assets/scss-Cw6XMfVs.js +1 -0
- package/web/assets/shellscript-Yzrsuije.js +1 -0
- package/web/assets/shiki-BuPP5HCg.js +2 -0
- package/web/assets/shiki-langs-FJk0v0Ga.js +2 -0
- package/web/assets/shiki-wasm-CgFRq91U.js +1 -0
- package/web/assets/sql-DkvoVjww.js +1 -0
- package/web/assets/stream-tokenizer-Bl6bisIa.js +4 -0
- package/web/assets/svelte-DoIXUgJc.js +1 -0
- package/web/assets/swift-C2oV4EkX.js +1 -0
- package/web/assets/terraform-BETggiCN.js +1 -0
- package/web/assets/theming-themes-BL_CC-Mm.js +1 -0
- package/web/assets/toml-vGWfd6FD.js +1 -0
- package/web/assets/tsx-COt5Ahok.js +1 -0
- package/web/assets/typescript-BPQ3VLAy.js +1 -0
- package/web/assets/vue-CwSeJimo.js +1 -0
- package/web/assets/xml-sdJ4AIDG.js +1 -0
- package/web/assets/xterm-view-6GBZ9nXN.css +32 -0
- package/web/assets/xterm-view-ZsuzJb2C.js +38 -0
- package/web/assets/yaml-Buea-lGh.js +1 -0
- package/web/assets/zig-VOosw3JB.js +1 -0
- package/web/index.html +2 -2
- package/dist/chunk-etf7ef97.js +0 -55
- package/dist/chunk-greb3dp6.js +0 -22
- package/web/assets/index-BhHrKpoo.js +0 -395
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
openHostSettings,
|
|
6
6
|
sameList,
|
|
7
7
|
settingsDefaults
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-nppgxh6c.js";
|
|
9
9
|
import {
|
|
10
10
|
DevicesDoc,
|
|
11
11
|
RevocationsDoc,
|
|
@@ -16,19 +16,21 @@ import {
|
|
|
16
16
|
pairingOffers
|
|
17
17
|
} from "./chunk-t91zt5f0.js";
|
|
18
18
|
import {
|
|
19
|
+
REMOVAL_NOTICE_FILE,
|
|
20
|
+
markRemoved,
|
|
19
21
|
normalizeOrigin,
|
|
20
22
|
readServerBinding,
|
|
21
23
|
readServerConfig,
|
|
22
24
|
resolveRelayOrigin
|
|
23
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-nq57fdnq.js";
|
|
24
26
|
import {
|
|
25
27
|
serveWebClient
|
|
26
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-445cevpm.js";
|
|
27
29
|
import {
|
|
28
30
|
relayHostId,
|
|
29
31
|
relayWsBase,
|
|
30
32
|
startRelayLink
|
|
31
|
-
} from "./chunk-
|
|
33
|
+
} from "./chunk-g3ahp7b9.js";
|
|
32
34
|
import {
|
|
33
35
|
ATTACHMENT_TTL_MS,
|
|
34
36
|
INVALID_FRAME_CLOSE_CODE,
|
|
@@ -54,7 +56,7 @@ import {
|
|
|
54
56
|
rtcCandidateType,
|
|
55
57
|
sdpCandidates,
|
|
56
58
|
withRtcCandidates
|
|
57
|
-
} from "./chunk-
|
|
59
|
+
} from "./chunk-47ff1d1t.js";
|
|
58
60
|
import {
|
|
59
61
|
ENDPOINT_SET_SCHEMA_VERSION,
|
|
60
62
|
MAX_HOST_ENDPOINT_URL_LENGTH,
|
|
@@ -77,13 +79,13 @@ import {
|
|
|
77
79
|
import {
|
|
78
80
|
packagedVersion,
|
|
79
81
|
webRoot
|
|
80
|
-
} from "./chunk-
|
|
82
|
+
} from "./chunk-7wj0jh3t.js";
|
|
81
83
|
import {
|
|
82
84
|
__require
|
|
83
85
|
} from "./chunk-df32avw6.js";
|
|
84
86
|
|
|
85
87
|
// apps/host/src/main.ts
|
|
86
|
-
import { randomUUID as
|
|
88
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
87
89
|
import { mkdir as mkdir6, readFile as readFile7, writeFile as writeFile5 } from "node:fs/promises";
|
|
88
90
|
import { homedir as homedir3 } from "node:os";
|
|
89
91
|
import { join as join12, resolve as resolve7 } from "node:path";
|
|
@@ -712,11 +714,7 @@ async function checkoutInfo(harness, input, context) {
|
|
|
712
714
|
}
|
|
713
715
|
};
|
|
714
716
|
}
|
|
715
|
-
async function
|
|
716
|
-
const index = await harness.snapshot(IndexDoc, context);
|
|
717
|
-
const checkouts = index?.checkouts ?? [];
|
|
718
|
-
if (checkouts.length === 0)
|
|
719
|
-
return [];
|
|
717
|
+
async function conversationSubtree(harness, rootId, context) {
|
|
720
718
|
const all = [];
|
|
721
719
|
let cursor;
|
|
722
720
|
do {
|
|
@@ -743,6 +741,27 @@ async function cleanupArchivedWorktrees(harness, rootId, context) {
|
|
|
743
741
|
}
|
|
744
742
|
}
|
|
745
743
|
}
|
|
744
|
+
return subtree;
|
|
745
|
+
}
|
|
746
|
+
async function familyRoot(harness, id, context) {
|
|
747
|
+
let current = id;
|
|
748
|
+
const seen = new Set;
|
|
749
|
+
while (!seen.has(current)) {
|
|
750
|
+
seen.add(current);
|
|
751
|
+
const record = await harness.commit((tx) => tx.conversation(current), context);
|
|
752
|
+
const parent = record?.parent?.conversationId ?? record?.owner?.conversationId;
|
|
753
|
+
if (parent === undefined)
|
|
754
|
+
return current;
|
|
755
|
+
current = parent;
|
|
756
|
+
}
|
|
757
|
+
return current;
|
|
758
|
+
}
|
|
759
|
+
async function cleanupArchivedWorktrees(harness, rootId, context) {
|
|
760
|
+
const index = await harness.snapshot(IndexDoc, context);
|
|
761
|
+
const checkouts = index?.checkouts ?? [];
|
|
762
|
+
if (checkouts.length === 0)
|
|
763
|
+
return [];
|
|
764
|
+
const subtree = await conversationSubtree(harness, rootId, context);
|
|
746
765
|
const kept = [];
|
|
747
766
|
for (const record of checkouts.filter((checkout) => subtree.has(checkout.conversationId))) {
|
|
748
767
|
const reason = await removeWorktree(record);
|
|
@@ -1451,6 +1470,7 @@ var todo = {
|
|
|
1451
1470
|
};
|
|
1452
1471
|
|
|
1453
1472
|
// apps/host/src/gateway.ts
|
|
1473
|
+
import { stat as stat6 } from "node:fs/promises";
|
|
1454
1474
|
import { createServer } from "node:http";
|
|
1455
1475
|
import { homedir, networkInterfaces } from "node:os";
|
|
1456
1476
|
import { join as join7, sep as sep5 } from "node:path";
|
|
@@ -2056,6 +2076,329 @@ function dataChannelCarrier(channel, timers, log, onClosed) {
|
|
|
2056
2076
|
};
|
|
2057
2077
|
}
|
|
2058
2078
|
|
|
2079
|
+
// apps/host/src/terminals.ts
|
|
2080
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
2081
|
+
import { userInfo } from "node:os";
|
|
2082
|
+
var TERMINAL_LIMIT = 8;
|
|
2083
|
+
var BUFFER_SIZE = 512 * 1024;
|
|
2084
|
+
var FLUSH_MS = 8;
|
|
2085
|
+
var MAX_CHUNK = 16 * 1024;
|
|
2086
|
+
var MAX_TITLE = 1024;
|
|
2087
|
+
var TITLE = /\x1b\][02];([^\x07\x1b]*)(?:\x07|\x1b\\)/g;
|
|
2088
|
+
|
|
2089
|
+
class Terminals {
|
|
2090
|
+
#options;
|
|
2091
|
+
#limit;
|
|
2092
|
+
#terminals = new Map;
|
|
2093
|
+
#numbers = new Map;
|
|
2094
|
+
#watchers = new Map;
|
|
2095
|
+
constructor(options) {
|
|
2096
|
+
this.#options = options;
|
|
2097
|
+
this.#limit = options.limit ?? TERMINAL_LIMIT;
|
|
2098
|
+
}
|
|
2099
|
+
get limit() {
|
|
2100
|
+
return this.#limit;
|
|
2101
|
+
}
|
|
2102
|
+
list(owner) {
|
|
2103
|
+
const terminals = [...this.#terminals.values()].filter((terminal) => terminal.owner === owner).map(info);
|
|
2104
|
+
return { owner, limit: this.#limit, terminals };
|
|
2105
|
+
}
|
|
2106
|
+
watch(owner, listener) {
|
|
2107
|
+
let set = this.#watchers.get(owner);
|
|
2108
|
+
if (set === undefined)
|
|
2109
|
+
this.#watchers.set(owner, set = new Set);
|
|
2110
|
+
set.add(listener);
|
|
2111
|
+
listener(this.list(owner));
|
|
2112
|
+
return () => {
|
|
2113
|
+
set.delete(listener);
|
|
2114
|
+
if (set.size === 0)
|
|
2115
|
+
this.#watchers.delete(owner);
|
|
2116
|
+
};
|
|
2117
|
+
}
|
|
2118
|
+
open(owner, cwd, size) {
|
|
2119
|
+
const count = [...this.#terminals.values()].filter((terminal2) => terminal2.owner === owner).length;
|
|
2120
|
+
if (count >= this.#limit)
|
|
2121
|
+
throw new Error(`Terminal limit reached (${this.#limit})`);
|
|
2122
|
+
const number = (this.#numbers.get(owner) ?? 0) + 1;
|
|
2123
|
+
this.#numbers.set(owner, number);
|
|
2124
|
+
const terminal = {
|
|
2125
|
+
id: randomUUID2(),
|
|
2126
|
+
owner,
|
|
2127
|
+
cwd,
|
|
2128
|
+
number,
|
|
2129
|
+
pty: undefined,
|
|
2130
|
+
generation: 0,
|
|
2131
|
+
cols: size.cols,
|
|
2132
|
+
rows: size.rows,
|
|
2133
|
+
title: undefined,
|
|
2134
|
+
rename: undefined,
|
|
2135
|
+
exit: undefined,
|
|
2136
|
+
buffer: "",
|
|
2137
|
+
start: 0,
|
|
2138
|
+
pending: "",
|
|
2139
|
+
flushTimer: undefined,
|
|
2140
|
+
titleCarry: "",
|
|
2141
|
+
viewers: new Set
|
|
2142
|
+
};
|
|
2143
|
+
this.#terminals.set(terminal.id, terminal);
|
|
2144
|
+
try {
|
|
2145
|
+
this.#start(terminal);
|
|
2146
|
+
} catch (error) {
|
|
2147
|
+
this.#terminals.delete(terminal.id);
|
|
2148
|
+
throw error;
|
|
2149
|
+
}
|
|
2150
|
+
this.#changed(owner);
|
|
2151
|
+
return terminal.id;
|
|
2152
|
+
}
|
|
2153
|
+
input(id, data, size) {
|
|
2154
|
+
const terminal = this.#get(id);
|
|
2155
|
+
if (size !== undefined)
|
|
2156
|
+
this.#resize(terminal, size);
|
|
2157
|
+
terminal.pty?.write(data);
|
|
2158
|
+
}
|
|
2159
|
+
resize(id, size) {
|
|
2160
|
+
this.#resize(this.#get(id), size);
|
|
2161
|
+
}
|
|
2162
|
+
rename(id, name) {
|
|
2163
|
+
const terminal = this.#get(id);
|
|
2164
|
+
const trimmed = name?.trim();
|
|
2165
|
+
terminal.rename = trimmed === undefined || trimmed === "" ? undefined : trimmed;
|
|
2166
|
+
this.#changed(terminal.owner);
|
|
2167
|
+
}
|
|
2168
|
+
restart(id, size) {
|
|
2169
|
+
const terminal = this.#get(id);
|
|
2170
|
+
this.#stop(terminal);
|
|
2171
|
+
terminal.start += terminal.buffer.length + terminal.pending.length + 1;
|
|
2172
|
+
terminal.buffer = "";
|
|
2173
|
+
terminal.pending = "";
|
|
2174
|
+
terminal.titleCarry = "";
|
|
2175
|
+
terminal.title = undefined;
|
|
2176
|
+
terminal.exit = undefined;
|
|
2177
|
+
terminal.cols = size.cols;
|
|
2178
|
+
terminal.rows = size.rows;
|
|
2179
|
+
for (const viewer of terminal.viewers)
|
|
2180
|
+
viewer.position = undefined;
|
|
2181
|
+
try {
|
|
2182
|
+
this.#start(terminal);
|
|
2183
|
+
} catch (error) {
|
|
2184
|
+
terminal.exit = { code: null };
|
|
2185
|
+
this.#changed(terminal.owner);
|
|
2186
|
+
throw error;
|
|
2187
|
+
}
|
|
2188
|
+
this.#changed(terminal.owner);
|
|
2189
|
+
}
|
|
2190
|
+
close(id) {
|
|
2191
|
+
const terminal = this.#get(id);
|
|
2192
|
+
this.#remove(terminal);
|
|
2193
|
+
this.#changed(terminal.owner);
|
|
2194
|
+
}
|
|
2195
|
+
closeOwners(owners) {
|
|
2196
|
+
for (const owner of new Set(owners)) {
|
|
2197
|
+
const owned = [...this.#terminals.values()].filter((terminal) => terminal.owner === owner);
|
|
2198
|
+
if (owned.length === 0)
|
|
2199
|
+
continue;
|
|
2200
|
+
for (const terminal of owned)
|
|
2201
|
+
this.#remove(terminal);
|
|
2202
|
+
this.#changed(owner);
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
closeAll() {
|
|
2206
|
+
this.closeOwners([...this.#terminals.values()].map((terminal) => terminal.owner));
|
|
2207
|
+
}
|
|
2208
|
+
attach(id, since, sink) {
|
|
2209
|
+
const terminal = this.#get(id);
|
|
2210
|
+
const end = terminal.start + terminal.buffer.length;
|
|
2211
|
+
const position = since !== undefined && since >= terminal.start && since <= end ? since : undefined;
|
|
2212
|
+
const viewer = { sink, position, sending: false };
|
|
2213
|
+
terminal.viewers.add(viewer);
|
|
2214
|
+
this.#pump(terminal, viewer);
|
|
2215
|
+
return () => {
|
|
2216
|
+
terminal.viewers.delete(viewer);
|
|
2217
|
+
};
|
|
2218
|
+
}
|
|
2219
|
+
#get(id) {
|
|
2220
|
+
const terminal = this.#terminals.get(id);
|
|
2221
|
+
if (terminal === undefined)
|
|
2222
|
+
throw new Error("Unknown terminal");
|
|
2223
|
+
return terminal;
|
|
2224
|
+
}
|
|
2225
|
+
#start(terminal) {
|
|
2226
|
+
const generation = ++terminal.generation;
|
|
2227
|
+
const { shell, env } = this.#options;
|
|
2228
|
+
const pty = this.#options.spawn(shell.file, [...shell.args], {
|
|
2229
|
+
name: "xterm-256color",
|
|
2230
|
+
cols: terminal.cols,
|
|
2231
|
+
rows: terminal.rows,
|
|
2232
|
+
cwd: terminal.cwd,
|
|
2233
|
+
env: { ...env, TERM: "xterm-256color", COLORTERM: "truecolor" }
|
|
2234
|
+
});
|
|
2235
|
+
terminal.pty = pty;
|
|
2236
|
+
pty.onData((data) => {
|
|
2237
|
+
if (terminal.generation !== generation)
|
|
2238
|
+
return;
|
|
2239
|
+
terminal.pending += data;
|
|
2240
|
+
if (terminal.pending.length >= (this.#options.maxChunk ?? MAX_CHUNK))
|
|
2241
|
+
this.#flush(terminal);
|
|
2242
|
+
else
|
|
2243
|
+
terminal.flushTimer ??= setTimeout(() => this.#flush(terminal), this.#options.flushMs ?? FLUSH_MS);
|
|
2244
|
+
});
|
|
2245
|
+
pty.onExit(({ exitCode, signal }) => {
|
|
2246
|
+
if (terminal.generation !== generation || !this.#terminals.has(terminal.id))
|
|
2247
|
+
return;
|
|
2248
|
+
this.#flush(terminal);
|
|
2249
|
+
terminal.pty = undefined;
|
|
2250
|
+
terminal.exit = signal === undefined || signal === 0 ? { code: exitCode } : { code: exitCode, signal };
|
|
2251
|
+
this.#changed(terminal.owner);
|
|
2252
|
+
});
|
|
2253
|
+
}
|
|
2254
|
+
#stop(terminal) {
|
|
2255
|
+
if (terminal.flushTimer !== undefined)
|
|
2256
|
+
clearTimeout(terminal.flushTimer);
|
|
2257
|
+
terminal.flushTimer = undefined;
|
|
2258
|
+
terminal.generation++;
|
|
2259
|
+
const pty = terminal.pty;
|
|
2260
|
+
terminal.pty = undefined;
|
|
2261
|
+
try {
|
|
2262
|
+
pty?.kill();
|
|
2263
|
+
} catch {}
|
|
2264
|
+
}
|
|
2265
|
+
#remove(terminal) {
|
|
2266
|
+
this.#stop(terminal);
|
|
2267
|
+
this.#terminals.delete(terminal.id);
|
|
2268
|
+
for (const viewer of terminal.viewers)
|
|
2269
|
+
viewer.sink.ended("Terminal closed");
|
|
2270
|
+
terminal.viewers.clear();
|
|
2271
|
+
}
|
|
2272
|
+
#resize(terminal, size) {
|
|
2273
|
+
if (terminal.cols === size.cols && terminal.rows === size.rows)
|
|
2274
|
+
return;
|
|
2275
|
+
terminal.cols = size.cols;
|
|
2276
|
+
terminal.rows = size.rows;
|
|
2277
|
+
try {
|
|
2278
|
+
terminal.pty?.resize(size.cols, size.rows);
|
|
2279
|
+
} catch {}
|
|
2280
|
+
this.#changed(terminal.owner);
|
|
2281
|
+
}
|
|
2282
|
+
#flush(terminal) {
|
|
2283
|
+
if (terminal.flushTimer !== undefined)
|
|
2284
|
+
clearTimeout(terminal.flushTimer);
|
|
2285
|
+
terminal.flushTimer = undefined;
|
|
2286
|
+
const data = terminal.pending;
|
|
2287
|
+
if (data === "")
|
|
2288
|
+
return;
|
|
2289
|
+
terminal.pending = "";
|
|
2290
|
+
terminal.buffer += data;
|
|
2291
|
+
const bufferSize = this.#options.bufferSize ?? BUFFER_SIZE;
|
|
2292
|
+
if (terminal.buffer.length > bufferSize) {
|
|
2293
|
+
let cut = terminal.buffer.length - bufferSize;
|
|
2294
|
+
const line = terminal.buffer.indexOf(`
|
|
2295
|
+
`, cut);
|
|
2296
|
+
if (line !== -1 && line - cut < 1024)
|
|
2297
|
+
cut = line + 1;
|
|
2298
|
+
terminal.buffer = terminal.buffer.slice(cut);
|
|
2299
|
+
terminal.start += cut;
|
|
2300
|
+
}
|
|
2301
|
+
if (this.#readTitle(terminal, data))
|
|
2302
|
+
this.#changed(terminal.owner);
|
|
2303
|
+
for (const viewer of terminal.viewers)
|
|
2304
|
+
this.#pump(terminal, viewer);
|
|
2305
|
+
}
|
|
2306
|
+
#readTitle(terminal, data) {
|
|
2307
|
+
const text = terminal.titleCarry + data;
|
|
2308
|
+
let title;
|
|
2309
|
+
let consumed = 0;
|
|
2310
|
+
for (const match of text.matchAll(TITLE)) {
|
|
2311
|
+
title = match[1];
|
|
2312
|
+
consumed = match.index + match[0].length;
|
|
2313
|
+
}
|
|
2314
|
+
const open = text.lastIndexOf("\x1B]");
|
|
2315
|
+
terminal.titleCarry = open >= consumed && text.length - open <= MAX_TITLE ? text.slice(open) : "";
|
|
2316
|
+
if (title === undefined || title === terminal.title)
|
|
2317
|
+
return false;
|
|
2318
|
+
terminal.title = title;
|
|
2319
|
+
return terminal.rename === undefined;
|
|
2320
|
+
}
|
|
2321
|
+
async#pump(terminal, viewer) {
|
|
2322
|
+
if (viewer.sending)
|
|
2323
|
+
return;
|
|
2324
|
+
viewer.sending = true;
|
|
2325
|
+
try {
|
|
2326
|
+
const maxChunk = this.#options.maxChunk ?? MAX_CHUNK;
|
|
2327
|
+
while (terminal.viewers.has(viewer)) {
|
|
2328
|
+
const end = terminal.start + terminal.buffer.length;
|
|
2329
|
+
const reset = viewer.position === undefined || viewer.position < terminal.start;
|
|
2330
|
+
const from = reset ? terminal.start : viewer.position;
|
|
2331
|
+
if (!reset && from >= end)
|
|
2332
|
+
return;
|
|
2333
|
+
let to = reset ? end : Math.min(end, from + maxChunk);
|
|
2334
|
+
if (to < end && isHighSurrogate(terminal.buffer.charCodeAt(to - terminal.start - 1)))
|
|
2335
|
+
to--;
|
|
2336
|
+
const data = terminal.buffer.slice(from - terminal.start, to - terminal.start);
|
|
2337
|
+
viewer.position = to;
|
|
2338
|
+
await viewer.sink.output(reset ? { seq: to, data, reset: true } : { seq: to, data });
|
|
2339
|
+
}
|
|
2340
|
+
} finally {
|
|
2341
|
+
viewer.sending = false;
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
#changed(owner) {
|
|
2345
|
+
const watchers = this.#watchers.get(owner);
|
|
2346
|
+
if (watchers === undefined)
|
|
2347
|
+
return;
|
|
2348
|
+
const list = this.list(owner);
|
|
2349
|
+
for (const watcher of watchers)
|
|
2350
|
+
watcher(list);
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
async function startTerminals(options = {}) {
|
|
2354
|
+
let spawn3 = options.spawn;
|
|
2355
|
+
if (spawn3 === undefined) {
|
|
2356
|
+
try {
|
|
2357
|
+
const loaded = options.load === undefined ? await import("@lydell/node-pty") : await options.load();
|
|
2358
|
+
if (typeof loaded.spawn !== "function")
|
|
2359
|
+
throw new Error("the module has no spawn()");
|
|
2360
|
+
spawn3 = loaded.spawn;
|
|
2361
|
+
} catch (error) {
|
|
2362
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
2363
|
+
(options.diagnostic ?? console.error)(`Terminals unavailable: cannot load @lydell/node-pty on ${process.platform} (${process.arch}); missing @lydell/node-pty-${process.platform}-${process.arch}. Install without --omit=optional; the host continues without hello.terminals. (${detail})`);
|
|
2364
|
+
return;
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
return new Terminals({ ...options, spawn: spawn3, shell: options.shell ?? loginShell(), env: options.env ?? shellEnv(process.env) });
|
|
2368
|
+
}
|
|
2369
|
+
function loginShell() {
|
|
2370
|
+
let file = process.env.SHELL;
|
|
2371
|
+
if (file === undefined || file === "") {
|
|
2372
|
+
try {
|
|
2373
|
+
file = userInfo().shell ?? undefined;
|
|
2374
|
+
} catch {
|
|
2375
|
+
file = undefined;
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
return { file: file === undefined || file === "" ? "/bin/sh" : file, args: ["-l"] };
|
|
2379
|
+
}
|
|
2380
|
+
function shellEnv(env) {
|
|
2381
|
+
const result2 = {};
|
|
2382
|
+
for (const [key, value] of Object.entries(env)) {
|
|
2383
|
+
if (value !== undefined && !key.startsWith("PINOMAD_"))
|
|
2384
|
+
result2[key] = value;
|
|
2385
|
+
}
|
|
2386
|
+
return result2;
|
|
2387
|
+
}
|
|
2388
|
+
function info(terminal) {
|
|
2389
|
+
return {
|
|
2390
|
+
id: terminal.id,
|
|
2391
|
+
name: terminal.rename ?? terminal.title ?? `Terminal ${terminal.number}`,
|
|
2392
|
+
renamed: terminal.rename !== undefined,
|
|
2393
|
+
cols: terminal.cols,
|
|
2394
|
+
rows: terminal.rows,
|
|
2395
|
+
...terminal.exit === undefined ? {} : { exit: terminal.exit }
|
|
2396
|
+
};
|
|
2397
|
+
}
|
|
2398
|
+
function isHighSurrogate(code) {
|
|
2399
|
+
return code >= 55296 && code <= 56319;
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2059
2402
|
// apps/host/src/folders.ts
|
|
2060
2403
|
import { readdir, realpath as realpath5, stat as stat5 } from "node:fs/promises";
|
|
2061
2404
|
import { isAbsolute, join as join6 } from "node:path";
|
|
@@ -3351,6 +3694,7 @@ async function startGateway(options) {
|
|
|
3351
3694
|
const clients = new Set;
|
|
3352
3695
|
let rtc;
|
|
3353
3696
|
const rtcHello = () => rtc?.hello();
|
|
3697
|
+
const terminals = await startTerminals(options.terminals);
|
|
3354
3698
|
const broadcast = (level, message) => {
|
|
3355
3699
|
for (const client of clients)
|
|
3356
3700
|
client.send({ type: "notice", level, message });
|
|
@@ -3365,10 +3709,12 @@ async function startGateway(options) {
|
|
|
3365
3709
|
}
|
|
3366
3710
|
rtc?.closeDevice(publicKey);
|
|
3367
3711
|
};
|
|
3712
|
+
const reachableRelay = () => relay?.state().status === "removed" ? undefined : relay;
|
|
3368
3713
|
const currentAdvertisement = () => {
|
|
3369
3714
|
const advertiseUrl = remote?.advertiseUrl;
|
|
3715
|
+
const relay2 = reachableRelay();
|
|
3370
3716
|
const endpoints = remoteEndpointCandidates({
|
|
3371
|
-
...
|
|
3717
|
+
...relay2 === undefined ? {} : { relay: { origin: relay2.origin, hostId: relay2.hostId } },
|
|
3372
3718
|
...advertiseUrl === undefined ? {} : { direct: { advertiseUrl } }
|
|
3373
3719
|
});
|
|
3374
3720
|
return { endpoints, ...advertiseUrl === undefined ? {} : { advertiseUrl } };
|
|
@@ -3385,6 +3731,7 @@ async function startGateway(options) {
|
|
|
3385
3731
|
const client = new GatewayClient(connection, options, conversations, fileLister, skillLister, imageVariants, {
|
|
3386
3732
|
devicePublicKey,
|
|
3387
3733
|
rtcHello,
|
|
3734
|
+
terminals,
|
|
3388
3735
|
closed: (self) => {
|
|
3389
3736
|
clients.delete(self);
|
|
3390
3737
|
connections.delete(self);
|
|
@@ -3405,11 +3752,12 @@ async function startGateway(options) {
|
|
|
3405
3752
|
const fragment = (secret2, target2, endpointSet2) => `${pair(secret2)}&url=${encodeURIComponent(target2)}${endpointSet2 === undefined ? "" : `&endpointSet=${encodeEndpointSetParam(endpointSet2)}`}`;
|
|
3406
3753
|
const advertisement = currentAdvertisement();
|
|
3407
3754
|
const endpointSet = checkedEndpointSet(advertisement.endpoints);
|
|
3755
|
+
const relay2 = reachableRelay();
|
|
3408
3756
|
const relayReachable = endpointSet.endpoints.some((endpoint) => endpoint.role === "relay");
|
|
3409
|
-
if (relayReachable &&
|
|
3757
|
+
if (relayReachable && relay2 !== undefined) {
|
|
3410
3758
|
const { secret: secret2, expiresAt: expiresAt2 } = spec.offers.create("qr");
|
|
3411
3759
|
return {
|
|
3412
|
-
url: `${
|
|
3760
|
+
url: `${relay2.origin}/#${fragment(secret2, relaySocketTarget(relay2.origin, relay2.hostId), endpointSet)}`,
|
|
3413
3761
|
expiresAt: expiresAt2
|
|
3414
3762
|
};
|
|
3415
3763
|
}
|
|
@@ -3422,10 +3770,10 @@ async function startGateway(options) {
|
|
|
3422
3770
|
const { secret: secret2, expiresAt: expiresAt2 } = spec.offers.create("qr");
|
|
3423
3771
|
return { url: `${advertise}/#${fragment(secret2, direct.url, endpointSet)}`, expiresAt: expiresAt2 };
|
|
3424
3772
|
}
|
|
3425
|
-
if (
|
|
3773
|
+
if (relay2 !== undefined) {
|
|
3426
3774
|
const { secret: secret2, expiresAt: expiresAt2 } = spec.offers.create("qr");
|
|
3427
3775
|
return {
|
|
3428
|
-
url: `${
|
|
3776
|
+
url: `${relay2.origin}/#${fragment(secret2, relaySocketTarget(relay2.origin, relay2.hostId))}`,
|
|
3429
3777
|
expiresAt: expiresAt2
|
|
3430
3778
|
};
|
|
3431
3779
|
}
|
|
@@ -3519,6 +3867,10 @@ async function startGateway(options) {
|
|
|
3519
3867
|
});
|
|
3520
3868
|
if (secure?.relay !== undefined) {
|
|
3521
3869
|
const relayOrigin = secure.relay.origin;
|
|
3870
|
+
const startConfig = await readServerConfig(options.dataDir).catch(() => {
|
|
3871
|
+
return;
|
|
3872
|
+
});
|
|
3873
|
+
const linkJoin = startConfig !== undefined && normalizeOrigin(startConfig.origin) === normalizeOrigin(relayOrigin) ? startConfig : undefined;
|
|
3522
3874
|
const currentBinding = async () => {
|
|
3523
3875
|
const config = await readServerConfig(options.dataDir).catch(() => {
|
|
3524
3876
|
return;
|
|
@@ -3572,6 +3924,16 @@ async function startGateway(options) {
|
|
|
3572
3924
|
console.log(JSON.stringify({ event: "relay", status: "registered", origin: link.origin, hostId: link.hostId, push }));
|
|
3573
3925
|
} else if (state.status === "connecting") {
|
|
3574
3926
|
console.log(JSON.stringify({ event: "relay", status: "connecting", origin: link.origin }));
|
|
3927
|
+
} else if (state.status === "removed") {
|
|
3928
|
+
broadcast("warning", state.reason);
|
|
3929
|
+
console.error(state.reason);
|
|
3930
|
+
markRemoved(options.dataDir, {
|
|
3931
|
+
origin: relayOrigin,
|
|
3932
|
+
...linkJoin?.binding === undefined ? {} : { binding: linkJoin.binding },
|
|
3933
|
+
...linkJoin?.username === undefined ? {} : { username: linkJoin.username }
|
|
3934
|
+
}).catch((error) => {
|
|
3935
|
+
console.error(`could not write ${REMOVAL_NOTICE_FILE}: ${error instanceof Error ? error.message : String(error)}`);
|
|
3936
|
+
});
|
|
3575
3937
|
} else {
|
|
3576
3938
|
broadcast("warning", state.reason);
|
|
3577
3939
|
console.error(state.reason);
|
|
@@ -3606,6 +3968,7 @@ async function startGateway(options) {
|
|
|
3606
3968
|
detachSettingsWatch();
|
|
3607
3969
|
await detachPush?.close();
|
|
3608
3970
|
conversations.dispose();
|
|
3971
|
+
terminals?.closeAll();
|
|
3609
3972
|
await rtc?.close();
|
|
3610
3973
|
await Promise.all([...connections].map((connection) => connection.dispose()));
|
|
3611
3974
|
for (const socket of server.clients)
|
|
@@ -3956,6 +4319,7 @@ class ConversationList {
|
|
|
3956
4319
|
facts.label = subagent.label;
|
|
3957
4320
|
if (typeof subagent?.role === "string" && subagent.role !== "")
|
|
3958
4321
|
facts.role = subagent.role;
|
|
4322
|
+
facts.model = modelRefOf(subagent?.model);
|
|
3959
4323
|
}
|
|
3960
4324
|
facts.agent = roleNameOf(await harness.snapshot(RoleDoc, record.id, context3));
|
|
3961
4325
|
const named = (await harness.snapshot(TitleDoc, record.id, context3))?.title;
|
|
@@ -4021,6 +4385,7 @@ class ConversationList {
|
|
|
4021
4385
|
const value = change.value;
|
|
4022
4386
|
facts.label = typeof value?.label === "string" && value.label !== "" ? value.label : undefined;
|
|
4023
4387
|
facts.role = typeof value?.role === "string" && value.role !== "" ? value.role : undefined;
|
|
4388
|
+
facts.model = modelRefOf(value?.model);
|
|
4024
4389
|
touched.add(id);
|
|
4025
4390
|
} else if (change.record.kind === TitleDoc.definition.kind) {
|
|
4026
4391
|
const named = change.value?.title;
|
|
@@ -4108,6 +4473,14 @@ function roleNameOf(value) {
|
|
|
4108
4473
|
return;
|
|
4109
4474
|
return typeof value.name === "string" ? value.name : value.name === null ? null : undefined;
|
|
4110
4475
|
}
|
|
4476
|
+
function modelRefOf(value) {
|
|
4477
|
+
if (typeof value !== "string")
|
|
4478
|
+
return;
|
|
4479
|
+
const slash = value.indexOf("/");
|
|
4480
|
+
if (slash <= 0 || slash === value.length - 1)
|
|
4481
|
+
return;
|
|
4482
|
+
return { provider: value.slice(0, slash), modelId: value.slice(slash + 1) };
|
|
4483
|
+
}
|
|
4111
4484
|
function rolesOf(index) {
|
|
4112
4485
|
const roles = new Map;
|
|
4113
4486
|
for (const entry of index?.conversations ?? []) {
|
|
@@ -4138,6 +4511,8 @@ function summaryOf(record, facts, role, currentAgent) {
|
|
|
4138
4511
|
return {
|
|
4139
4512
|
id: record.id,
|
|
4140
4513
|
kind: record.owner !== undefined ? "subagent" : record.parent !== undefined ? "fork" : "conversation",
|
|
4514
|
+
...record.owner === undefined ? {} : { lifecycle: "foreground" },
|
|
4515
|
+
...facts.model === undefined ? {} : { model: facts.model },
|
|
4141
4516
|
...parent === undefined ? {} : { parent },
|
|
4142
4517
|
...record.parent === undefined ? {} : { forkedAt: String(record.parent.at) },
|
|
4143
4518
|
...(facts.named ?? facts.title) === undefined ? {} : { title: facts.named ?? facts.title },
|
|
@@ -4150,7 +4525,7 @@ function summaryOf(record, facts, role, currentAgent) {
|
|
|
4150
4525
|
};
|
|
4151
4526
|
}
|
|
4152
4527
|
function sameSummary(a, b) {
|
|
4153
|
-
return a.id === b.id && a.kind === b.kind && a.parent === b.parent && a.forkedAt === b.forkedAt && a.title === b.title && a.status === b.status && a.updatedAt === b.updatedAt && a.label === b.label && a.role === b.role && a.currentAgent === b.currentAgent && a.activity?.tool === b.activity?.tool && a.activity?.target === b.activity?.target;
|
|
4528
|
+
return a.id === b.id && a.kind === b.kind && a.lifecycle === b.lifecycle && a.model?.provider === b.model?.provider && a.model?.modelId === b.model?.modelId && a.parent === b.parent && a.forkedAt === b.forkedAt && a.title === b.title && a.status === b.status && a.updatedAt === b.updatedAt && a.label === b.label && a.role === b.role && a.currentAgent === b.currentAgent && a.activity?.tool === b.activity?.tool && a.activity?.target === b.activity?.target;
|
|
4154
4529
|
}
|
|
4155
4530
|
function titleOf(entry) {
|
|
4156
4531
|
const message = entry?.model?.[0];
|
|
@@ -4293,6 +4668,7 @@ class GatewayClient {
|
|
|
4293
4668
|
}
|
|
4294
4669
|
#hostValues;
|
|
4295
4670
|
#subscriptions = new Map;
|
|
4671
|
+
#terminalSizes = new Map;
|
|
4296
4672
|
#disposed = false;
|
|
4297
4673
|
constructor(connection, options, conversations, files, skills, imageVariants, hooks) {
|
|
4298
4674
|
this.#connection = connection;
|
|
@@ -4355,7 +4731,8 @@ class GatewayClient {
|
|
|
4355
4731
|
})),
|
|
4356
4732
|
toolPresentations: options.toolPresentations ?? {},
|
|
4357
4733
|
endpointSet: this.#hooks.endpointSet(),
|
|
4358
|
-
...rtc === undefined ? {} : { rtc }
|
|
4734
|
+
...rtc === undefined ? {} : { rtc },
|
|
4735
|
+
...this.#hooks.terminals === undefined ? {} : { terminals: { limit: this.#hooks.terminals.limit } }
|
|
4359
4736
|
});
|
|
4360
4737
|
}
|
|
4361
4738
|
send(frame) {
|
|
@@ -4368,15 +4745,15 @@ class GatewayClient {
|
|
|
4368
4745
|
}
|
|
4369
4746
|
async#receive(frame) {
|
|
4370
4747
|
if (frame.type === "subscribe")
|
|
4371
|
-
await this.#subscribe(frame.stream, frame.images === "ref");
|
|
4748
|
+
await this.#subscribe(frame.stream, frame.images === "ref", frame.since);
|
|
4372
4749
|
else if (frame.type === "unsubscribe")
|
|
4373
4750
|
await this.#unsubscribe(frame.stream);
|
|
4374
4751
|
else
|
|
4375
4752
|
await this.#answer(frame.id, frame.method, frame.args);
|
|
4376
4753
|
}
|
|
4377
|
-
async#subscribe(stream, imageRefs) {
|
|
4754
|
+
async#subscribe(stream, imageRefs, since) {
|
|
4378
4755
|
const previous = this.#subscriptions.get(stream);
|
|
4379
|
-
const state = imageRefs && stream.startsWith("conversation:") ? { images: new ImageRefs } : {};
|
|
4756
|
+
const state = imageRefs && stream.startsWith("conversation:") ? { images: new ImageRefs } : since === undefined ? {} : { since };
|
|
4380
4757
|
this.#subscriptions.set(stream, state);
|
|
4381
4758
|
const current = () => !this.#disposed && this.#subscriptions.get(stream) === state;
|
|
4382
4759
|
const send = (frame) => current() ? this.send(frame) : Promise.resolve();
|
|
@@ -4420,6 +4797,24 @@ class GatewayClient {
|
|
|
4420
4797
|
}
|
|
4421
4798
|
if (stream === "tasks")
|
|
4422
4799
|
return this.#forward(stream, await this.#options.harness.watchTaskGraph(context3), send);
|
|
4800
|
+
if (stream.startsWith("terminals:")) {
|
|
4801
|
+
const terminals = this.#terminals();
|
|
4802
|
+
const conversation2 = await this.#conversation(Number(stream.slice("terminals:".length)));
|
|
4803
|
+
const { owner } = await this.#terminalOwner(conversation2);
|
|
4804
|
+
return { stop: terminals.watch(owner, (value) => void send({ type: "snapshot", stream, value })) };
|
|
4805
|
+
}
|
|
4806
|
+
if (stream.startsWith("terminal:")) {
|
|
4807
|
+
const terminalStream = stream;
|
|
4808
|
+
const detach = this.#terminals().attach(terminalStream.slice("terminal:".length), state?.since, {
|
|
4809
|
+
output: (frame) => send({ type: "output", stream: terminalStream, ...frame }),
|
|
4810
|
+
ended: (reason) => {
|
|
4811
|
+
send({ type: "ended", stream, reason });
|
|
4812
|
+
if (this.#subscriptions.get(stream) === state)
|
|
4813
|
+
this.#subscriptions.delete(stream);
|
|
4814
|
+
}
|
|
4815
|
+
});
|
|
4816
|
+
return { stop: detach };
|
|
4817
|
+
}
|
|
4423
4818
|
if (stream.startsWith("doc:")) {
|
|
4424
4819
|
const at = stream.lastIndexOf(":");
|
|
4425
4820
|
return this.#openDoc(stream, stream.slice("doc:".length, at), Number(stream.slice(at + 1)), send);
|
|
@@ -4569,10 +4964,43 @@ class GatewayClient {
|
|
|
4569
4964
|
await this.send({ type: "result", id, ok: true, value: value ?? null });
|
|
4570
4965
|
} catch (error) {
|
|
4571
4966
|
await this.send({ type: "result", id, ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
4967
|
+
return;
|
|
4968
|
+
}
|
|
4969
|
+
const device = this.devicePublicKey;
|
|
4970
|
+
if (method === "revokeDevice" && device !== undefined && args.publicKey === device) {
|
|
4971
|
+
this.#hooks.closeDevice(device);
|
|
4572
4972
|
}
|
|
4573
4973
|
}
|
|
4574
4974
|
async#call(method, args) {
|
|
4575
4975
|
switch (method) {
|
|
4976
|
+
case "terminalInput": {
|
|
4977
|
+
const input = args;
|
|
4978
|
+
this.#terminals().input(input.terminalId, input.data, this.#terminalSizes.get(input.terminalId));
|
|
4979
|
+
return null;
|
|
4980
|
+
}
|
|
4981
|
+
case "resizeTerminal": {
|
|
4982
|
+
const input = args;
|
|
4983
|
+
const size = { cols: input.cols, rows: input.rows };
|
|
4984
|
+
this.#terminals().resize(input.terminalId, size);
|
|
4985
|
+
this.#terminalSizes.set(input.terminalId, size);
|
|
4986
|
+
return null;
|
|
4987
|
+
}
|
|
4988
|
+
case "renameTerminal": {
|
|
4989
|
+
const input = args;
|
|
4990
|
+
this.#terminals().rename(input.terminalId, input.name);
|
|
4991
|
+
return null;
|
|
4992
|
+
}
|
|
4993
|
+
case "restartTerminal": {
|
|
4994
|
+
const input = args;
|
|
4995
|
+
const size = { cols: input.cols, rows: input.rows };
|
|
4996
|
+
this.#terminals().restart(input.terminalId, size);
|
|
4997
|
+
this.#terminalSizes.set(input.terminalId, size);
|
|
4998
|
+
return null;
|
|
4999
|
+
}
|
|
5000
|
+
case "closeTerminal":
|
|
5001
|
+
this.#terminals().close(args.terminalId);
|
|
5002
|
+
this.#terminalSizes.delete(args.terminalId);
|
|
5003
|
+
return null;
|
|
4576
5004
|
case "addProject":
|
|
4577
5005
|
return addProject(this.#options.harness, args.path, context3);
|
|
4578
5006
|
case "removeProject":
|
|
@@ -4636,6 +5064,7 @@ class GatewayClient {
|
|
|
4636
5064
|
const input = args;
|
|
4637
5065
|
await archive(this.#options.harness, input.conversationId, input.archived, context3);
|
|
4638
5066
|
if (input.archived) {
|
|
5067
|
+
this.#hooks.terminals?.closeOwners(await conversationSubtree(this.#options.harness, input.conversationId, context3));
|
|
4639
5068
|
const kept = await cleanupArchivedWorktrees(this.#options.harness, input.conversationId, context3);
|
|
4640
5069
|
for (const { record, reason } of kept) {
|
|
4641
5070
|
await this.send({ type: "notice", level: "warning", message: `Kept worktree ${record.path} (branch ${record.branch}): ${reason}` });
|
|
@@ -4652,7 +5081,8 @@ class GatewayClient {
|
|
|
4652
5081
|
case "revokeDevice": {
|
|
4653
5082
|
const { publicKey } = args;
|
|
4654
5083
|
await revokeDevice(this.#options.harness, publicKey, context3);
|
|
4655
|
-
this
|
|
5084
|
+
if (publicKey !== this.devicePublicKey)
|
|
5085
|
+
this.#hooks.closeDevice(publicKey);
|
|
4656
5086
|
return null;
|
|
4657
5087
|
}
|
|
4658
5088
|
case "rtcOffer": {
|
|
@@ -4935,8 +5365,34 @@ class GatewayClient {
|
|
|
4935
5365
|
case "changes": {
|
|
4936
5366
|
return await this.#changes(conversation, context3);
|
|
4937
5367
|
}
|
|
5368
|
+
case "openTerminal": {
|
|
5369
|
+
const input = args;
|
|
5370
|
+
const terminals = this.#terminals();
|
|
5371
|
+
const { owner, cwd, record } = await this.#terminalOwner(conversation);
|
|
5372
|
+
const ready = record === undefined || await worktreeSettled(record);
|
|
5373
|
+
if (!ready || !await stat6(cwd).then((info2) => info2.isDirectory(), () => false)) {
|
|
5374
|
+
throw new Error(`The checkout is unavailable: ${cwd}`);
|
|
5375
|
+
}
|
|
5376
|
+
const size = { cols: input.cols, rows: input.rows };
|
|
5377
|
+
const terminalId = terminals.open(owner, cwd, size);
|
|
5378
|
+
this.#terminalSizes.set(terminalId, size);
|
|
5379
|
+
return { terminalId };
|
|
5380
|
+
}
|
|
4938
5381
|
}
|
|
4939
5382
|
}
|
|
5383
|
+
#terminals() {
|
|
5384
|
+
if (this.#hooks.terminals === undefined)
|
|
5385
|
+
throw new Error("Terminals are unavailable on this host");
|
|
5386
|
+
return this.#hooks.terminals;
|
|
5387
|
+
}
|
|
5388
|
+
async#terminalOwner(conversation) {
|
|
5389
|
+
const cwd = (await conversation.agent(context3)).cwd;
|
|
5390
|
+
if (cwd === undefined)
|
|
5391
|
+
throw new Error("No working directory");
|
|
5392
|
+
const record = checkoutAt((await this.#options.harness.snapshot(IndexDoc, context3))?.checkouts, cwd);
|
|
5393
|
+
const owner = record?.conversationId ?? await familyRoot(this.#options.harness, conversation.id, context3);
|
|
5394
|
+
return { owner, cwd, record };
|
|
5395
|
+
}
|
|
4940
5396
|
async#expandSkills(text, scope, bound) {
|
|
4941
5397
|
if (bound === undefined ? !text.includes("$") : bound.length === 0)
|
|
4942
5398
|
return text;
|
|
@@ -5524,7 +5980,7 @@ import {
|
|
|
5524
5980
|
readdir as readdir3,
|
|
5525
5981
|
rename as rename2,
|
|
5526
5982
|
rm as rm3,
|
|
5527
|
-
stat as
|
|
5983
|
+
stat as stat7,
|
|
5528
5984
|
writeFile as writeFile2
|
|
5529
5985
|
} from "node:fs/promises";
|
|
5530
5986
|
import { basename as basename2, join as join9, resolve as resolve6 } from "node:path";
|
|
@@ -5713,7 +6169,7 @@ class AttachmentStore {
|
|
|
5713
6169
|
throw new Error(problem);
|
|
5714
6170
|
const decoded = base64DecodedLength(data);
|
|
5715
6171
|
const path = this.#dataPath(meta);
|
|
5716
|
-
const received = (await
|
|
6172
|
+
const received = (await stat7(path)).size;
|
|
5717
6173
|
if (received === meta.size) {
|
|
5718
6174
|
await chmod(path, 292);
|
|
5719
6175
|
await this.#writeMeta({ ...meta, state: "complete" });
|
|
@@ -5756,13 +6212,13 @@ class AttachmentStore {
|
|
|
5756
6212
|
if (meta.state === "uploading")
|
|
5757
6213
|
throw new Error(`Attachment ${index + 1} is still uploading`);
|
|
5758
6214
|
const path = this.#dataPath(meta);
|
|
5759
|
-
let
|
|
6215
|
+
let info2;
|
|
5760
6216
|
try {
|
|
5761
|
-
|
|
6217
|
+
info2 = await lstat(path);
|
|
5762
6218
|
} catch {
|
|
5763
6219
|
throw new Error(`Attachment ${index + 1} is unavailable; attach it again`);
|
|
5764
6220
|
}
|
|
5765
|
-
if (!
|
|
6221
|
+
if (!info2.isFile() || info2.size !== meta.size) {
|
|
5766
6222
|
throw new Error(`Attachment ${index + 1} is unavailable; attach it again`);
|
|
5767
6223
|
}
|
|
5768
6224
|
checked.push({ meta, path });
|
|
@@ -5805,7 +6261,7 @@ class AttachmentStore {
|
|
|
5805
6261
|
} else {
|
|
5806
6262
|
this.#pending.delete(entry.name);
|
|
5807
6263
|
try {
|
|
5808
|
-
if (now - (await
|
|
6264
|
+
if (now - (await stat7(directory)).mtimeMs <= this.#ttlMs)
|
|
5809
6265
|
continue;
|
|
5810
6266
|
} catch {
|
|
5811
6267
|
continue;
|
|
@@ -6004,6 +6460,7 @@ async function openHost(options) {
|
|
|
6004
6460
|
}
|
|
6005
6461
|
},
|
|
6006
6462
|
...options.rtc === undefined ? {} : { rtc: options.rtc },
|
|
6463
|
+
...options.terminals === undefined ? {} : { terminals: options.terminals },
|
|
6007
6464
|
secure: {
|
|
6008
6465
|
hostKey,
|
|
6009
6466
|
offers,
|
|
@@ -6085,7 +6542,7 @@ async function openHost(options) {
|
|
|
6085
6542
|
}
|
|
6086
6543
|
|
|
6087
6544
|
// apps/host/src/service/log-rotation.ts
|
|
6088
|
-
import { copyFile, rename as rename3, rm as rm4, stat as
|
|
6545
|
+
import { copyFile, rename as rename3, rm as rm4, stat as stat8, truncate } from "node:fs/promises";
|
|
6089
6546
|
import { join as join11 } from "node:path";
|
|
6090
6547
|
var HOST_LOG_MAX_BYTES = 10 * 1024 * 1024;
|
|
6091
6548
|
var HOST_LOG_HISTORY = 3;
|
|
@@ -6099,7 +6556,7 @@ async function rotateHostLog(dataDir, copy = copyFile) {
|
|
|
6099
6556
|
await rm4(temp, { force: true });
|
|
6100
6557
|
let size;
|
|
6101
6558
|
try {
|
|
6102
|
-
size = (await
|
|
6559
|
+
size = (await stat8(log)).size;
|
|
6103
6560
|
} catch (error) {
|
|
6104
6561
|
if (isMissing(error))
|
|
6105
6562
|
return false;
|
|
@@ -6147,7 +6604,7 @@ function startHostLogRotation(dataDir) {
|
|
|
6147
6604
|
}
|
|
6148
6605
|
|
|
6149
6606
|
// apps/host/src/model-auth.ts
|
|
6150
|
-
import { randomUUID as
|
|
6607
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
6151
6608
|
import { mkdir as mkdir5, readFile as readFile6, rename as rename4, writeFile as writeFile4 } from "node:fs/promises";
|
|
6152
6609
|
import { dirname as dirname3 } from "node:path";
|
|
6153
6610
|
import { getBuiltinProviders } from "@earendil-works/pi-ai/providers/all";
|
|
@@ -6451,7 +6908,7 @@ async function openModelAuth(runtime, options) {
|
|
|
6451
6908
|
if ([...logins.values()].some((login2) => login2.flow.providerId === providerId && login2.flow.state === "running")) {
|
|
6452
6909
|
throw new Error(`A login for ${provider.name} is already in progress`);
|
|
6453
6910
|
}
|
|
6454
|
-
const id =
|
|
6911
|
+
const id = randomUUID3();
|
|
6455
6912
|
const login = {
|
|
6456
6913
|
flow: { id, providerId, method, events: [], state: "running" },
|
|
6457
6914
|
controller: new AbortController,
|
|
@@ -6826,7 +7283,7 @@ async function installId(dataDir) {
|
|
|
6826
7283
|
const path = join12(dataDir, "install-id");
|
|
6827
7284
|
let id = (await readFile7(path, "utf8").catch(() => "")).trim();
|
|
6828
7285
|
if (id === "") {
|
|
6829
|
-
id =
|
|
7286
|
+
id = randomUUID4();
|
|
6830
7287
|
await mkdir6(dataDir, { recursive: true });
|
|
6831
7288
|
await writeFile5(path, `${id}
|
|
6832
7289
|
`, { mode: 384 });
|