offsite 0.0.86 → 0.0.88
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/apps/client/dist/assets/XIcon-gbQKQqpC.js +67 -0
- package/apps/client/dist/assets/{diff-panel-C7lMMpep.js → diff-panel-CSL-MBoH.js} +1 -1
- package/apps/client/dist/assets/{editor-dialog-BYriXnE3.js → editor-dialog-BZY0-avL.js} +34 -34
- package/apps/client/dist/assets/{image-viewer-CRV8AWKt.js → image-viewer-CVERQTJS.js} +1 -1
- package/apps/client/dist/assets/{index-CSQLBowu.css → index-D8k0XOju.css} +1 -1
- package/apps/client/dist/assets/index-D_7IXtRu.js +72 -0
- package/apps/client/dist/assets/{preview-dialog-9aPCvV36.js → preview-dialog-sVMONOFM.js} +1 -1
- package/apps/client/dist/assets/{use-editor-shortcut-CXHRRkVK.js → use-editor-shortcut-Bqc1G-T5.js} +26 -26
- package/apps/client/dist/assets/{video-viewer-Be4G_Ych.js → video-viewer-DX49MZpf.js} +1 -1
- package/apps/client/dist/index.html +6 -6
- package/apps/client/dist/sw.js +1 -1
- package/apps/server/dist/main.mjs +416 -261
- package/apps/server/dist/main.mjs.map +1 -1
- package/apps/server/drizzle/20261104195246_session_provider/migration.sql +1 -0
- package/apps/server/drizzle/20261104195246_session_provider/snapshot.json +398 -0
- package/apps/server/drizzle/20261104222101_last_used_models/migration.sql +6 -0
- package/apps/server/drizzle/20261104222101_last_used_models/snapshot.json +451 -0
- package/package.json +1 -1
- package/apps/client/dist/assets/XIcon-C_Axuxgf.js +0 -67
- package/apps/client/dist/assets/index-DGntznWS.js +0 -72
|
@@ -16,7 +16,7 @@ import { lstat, mkdir, mkdtemp, open, readFile, readdir, readlink, rm, rmdir, st
|
|
|
16
16
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
17
17
|
import { OpenCode, isSessionNotFoundError } from "@opencode/client";
|
|
18
18
|
import { ensure, headers } from "@opencode/client/service";
|
|
19
|
-
import { and, eq, gt, inArray, lte, max, sql } from "drizzle-orm";
|
|
19
|
+
import { and, eq, gt, inArray, isNull, lte, max, sql } from "drizzle-orm";
|
|
20
20
|
import { pathToFileURL } from "node:url";
|
|
21
21
|
import { sumBy } from "es-toolkit";
|
|
22
22
|
import { DatabaseSync } from "node:sqlite";
|
|
@@ -154,7 +154,7 @@ const browserLogger = fileLogger("browser.log");
|
|
|
154
154
|
// src/services/usage.ts
|
|
155
155
|
const freshFor = 6e4;
|
|
156
156
|
const forcedFreshFor = 1e4;
|
|
157
|
-
const log$
|
|
157
|
+
const log$20 = logger.withTag("usage");
|
|
158
158
|
const entries = /* @__PURE__ */ new Map();
|
|
159
159
|
const fetching = /* @__PURE__ */ new Map();
|
|
160
160
|
function cachedUsage(accountId) {
|
|
@@ -172,7 +172,7 @@ function recordLimits(accountId, limits) {
|
|
|
172
172
|
limits: [...current.limits.map((limit) => limits.find((item) => item.window === limit.window) ?? limit), ...limits.filter((item) => !current.limits.some((limit) => limit.window === item.window))],
|
|
173
173
|
updatedAt: Date.now()
|
|
174
174
|
});
|
|
175
|
-
log$
|
|
175
|
+
log$20.debug(`account ${accountId} reported ${limits.map((limit) => limit.window).join(", ")} limits from a running session`);
|
|
176
176
|
broadcast({
|
|
177
177
|
type: "limits",
|
|
178
178
|
accountId,
|
|
@@ -182,12 +182,12 @@ function recordLimits(accountId, limits) {
|
|
|
182
182
|
async function readUsage(accountId, force) {
|
|
183
183
|
const current = cachedUsage(accountId);
|
|
184
184
|
if (current.fetchedAt !== null && Date.now() - current.fetchedAt < (force ? forcedFreshFor : freshFor)) {
|
|
185
|
-
log$
|
|
185
|
+
log$20.debug(`account ${accountId} served from cache, ${Math.round((Date.now() - current.fetchedAt) / 1e3)}s old${force ? " (forced)" : ""}`);
|
|
186
186
|
return current;
|
|
187
187
|
}
|
|
188
188
|
const pending = fetching.get(accountId);
|
|
189
189
|
if (pending !== void 0) {
|
|
190
|
-
log$
|
|
190
|
+
log$20.debug(`account ${accountId} joined the fetch in progress`);
|
|
191
191
|
return pending;
|
|
192
192
|
}
|
|
193
193
|
const next = fetchUsage(accountId);
|
|
@@ -206,10 +206,10 @@ async function fetchUsage(accountId) {
|
|
|
206
206
|
if (account === void 0) throw new Error("That account was removed");
|
|
207
207
|
let usage;
|
|
208
208
|
const started = performance.now();
|
|
209
|
-
log$
|
|
209
|
+
log$20.debug(`fetching limits for ${account.name} (${account.provider})`);
|
|
210
210
|
try {
|
|
211
211
|
const limits = await providerLimits(account);
|
|
212
|
-
log$
|
|
212
|
+
log$20.debug(`fetched ${limits.length} limits for ${account.name} in ${Math.round(performance.now() - started)}ms`);
|
|
213
213
|
usage = {
|
|
214
214
|
limits,
|
|
215
215
|
fetchedAt: Date.now(),
|
|
@@ -217,7 +217,7 @@ async function fetchUsage(accountId) {
|
|
|
217
217
|
error: null
|
|
218
218
|
};
|
|
219
219
|
} catch (error) {
|
|
220
|
-
log$
|
|
220
|
+
log$20.warn(`could not read usage limits for ${account.name}:`, error);
|
|
221
221
|
usage = {
|
|
222
222
|
...cachedUsage(accountId),
|
|
223
223
|
fetchedAt: Date.now(),
|
|
@@ -369,7 +369,7 @@ function isRecord(value) {
|
|
|
369
369
|
const home = args.dir;
|
|
370
370
|
const lockFile = path.join(home, "server.pid");
|
|
371
371
|
const savedServer = z.object({ port: z.number() }).nullable();
|
|
372
|
-
const log$
|
|
372
|
+
const log$19 = logger.withTag("storage");
|
|
373
373
|
mkdirSync(home, { recursive: true });
|
|
374
374
|
const scratchAvailable = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], { cwd: home }).status !== 0;
|
|
375
375
|
function listenPort() {
|
|
@@ -402,7 +402,7 @@ function lock() {
|
|
|
402
402
|
const writtenThisBoot = modified > Date.now() - os.uptime() * 1e3;
|
|
403
403
|
if (validPid && writtenThisBoot && pid !== process.pid && isRunning(pid)) {
|
|
404
404
|
console.error(`offsite is already running for ${home} (pid ${pid})`);
|
|
405
|
-
log$
|
|
405
|
+
log$19.error(`offsite is already running for ${home} (pid ${pid})`);
|
|
406
406
|
process.exit(1);
|
|
407
407
|
}
|
|
408
408
|
unlinkSync(lockFile);
|
|
@@ -435,7 +435,7 @@ function readSettings(name, schema, defaults) {
|
|
|
435
435
|
try {
|
|
436
436
|
value = readJson(name);
|
|
437
437
|
} catch (error) {
|
|
438
|
-
log$
|
|
438
|
+
log$19.warn(`invalid ${file}, using the defaults:`, error);
|
|
439
439
|
return structuredClone(defaults);
|
|
440
440
|
}
|
|
441
441
|
if (value === null) return structuredClone(defaults);
|
|
@@ -447,10 +447,10 @@ function readSettings(name, schema, defaults) {
|
|
|
447
447
|
for (const target of targets) {
|
|
448
448
|
const discarded = discard(value, target);
|
|
449
449
|
if (discarded === null) {
|
|
450
|
-
log$
|
|
450
|
+
log$19.warn(`invalid ${file}, using the defaults:\n${z.prettifyError(result.error)}`);
|
|
451
451
|
return structuredClone(defaults);
|
|
452
452
|
}
|
|
453
|
-
log$
|
|
453
|
+
log$19.warn(`${file}: discarded ${discarded.map(String).join(".")} (${issue.message} at ${target.map(String).join(".")})`);
|
|
454
454
|
}
|
|
455
455
|
result = schema.safeParse(value);
|
|
456
456
|
}
|
|
@@ -518,7 +518,7 @@ function quote(value) {
|
|
|
518
518
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
519
519
|
}
|
|
520
520
|
// src/lib/codex/app-server.ts
|
|
521
|
-
const log$
|
|
521
|
+
const log$18 = logger.withTag("codex");
|
|
522
522
|
const IDLE_LIMIT$3 = 18e5;
|
|
523
523
|
const START_TIMEOUT = 15e3;
|
|
524
524
|
const REQUEST_TIMEOUT = 3e4;
|
|
@@ -559,7 +559,7 @@ var AppServer = class {
|
|
|
559
559
|
const executable = findCodex();
|
|
560
560
|
if (executable === null) throw new Error("Codex is not installed or not on the PATH");
|
|
561
561
|
this.home = home;
|
|
562
|
-
log$
|
|
562
|
+
log$18.info(`starting the Codex app server for ${home}`);
|
|
563
563
|
this.child = spawn(executable, ["app-server"], {
|
|
564
564
|
env: {
|
|
565
565
|
...process.env,
|
|
@@ -586,7 +586,7 @@ var AppServer = class {
|
|
|
586
586
|
requestAttestation: false
|
|
587
587
|
}
|
|
588
588
|
}, START_TIMEOUT).then(() => this.write({ method: "initialized" }), (error) => {
|
|
589
|
-
log$
|
|
589
|
+
log$18.warn(`the Codex app server for ${home} did not start:`, error);
|
|
590
590
|
this.child.kill();
|
|
591
591
|
throw error;
|
|
592
592
|
});
|
|
@@ -598,10 +598,10 @@ var AppServer = class {
|
|
|
598
598
|
const started = performance.now();
|
|
599
599
|
try {
|
|
600
600
|
const result = await this.call(method, params, REQUEST_TIMEOUT);
|
|
601
|
-
log$
|
|
601
|
+
log$18.debug(`${method} took ${Math.round(performance.now() - started)}ms`);
|
|
602
602
|
return result;
|
|
603
603
|
} catch (error) {
|
|
604
|
-
log$
|
|
604
|
+
log$18.debug(`${method} failed after ${Math.round(performance.now() - started)}ms: ${error}`);
|
|
605
605
|
throw error;
|
|
606
606
|
}
|
|
607
607
|
}
|
|
@@ -621,7 +621,7 @@ var AppServer = class {
|
|
|
621
621
|
});
|
|
622
622
|
}
|
|
623
623
|
stop() {
|
|
624
|
-
log$
|
|
624
|
+
log$18.info(`stopping the Codex app server for ${this.home}`);
|
|
625
625
|
clearTimeout(this.idleTimer);
|
|
626
626
|
if (servers.get(this.home) === this) servers.delete(this.home);
|
|
627
627
|
this.child.stdin.end();
|
|
@@ -662,7 +662,7 @@ var AppServer = class {
|
|
|
662
662
|
try {
|
|
663
663
|
message = JSON.parse(line);
|
|
664
664
|
} catch {
|
|
665
|
-
log$
|
|
665
|
+
log$18.warn(`ignoring a Codex line that is not JSON: ${line}`);
|
|
666
666
|
return;
|
|
667
667
|
}
|
|
668
668
|
if (message.method === void 0) {
|
|
@@ -712,7 +712,7 @@ var AppServer = class {
|
|
|
712
712
|
exit(error) {
|
|
713
713
|
if (this.exited !== null) return;
|
|
714
714
|
this.exited = error;
|
|
715
|
-
log$
|
|
715
|
+
log$18.info(`the Codex app server for ${this.home} exited:`, error.message);
|
|
716
716
|
clearTimeout(this.idleTimer);
|
|
717
717
|
if (servers.get(this.home) === this) servers.delete(this.home);
|
|
718
718
|
for (const waiter of this.waiting.values()) waiter.reject(error);
|
|
@@ -722,7 +722,7 @@ var AppServer = class {
|
|
|
722
722
|
}
|
|
723
723
|
};
|
|
724
724
|
// src/lib/codex/accounts.ts
|
|
725
|
-
const log$
|
|
725
|
+
const log$17 = logger.withTag("codex");
|
|
726
726
|
const privateEntries = [
|
|
727
727
|
"auth.json",
|
|
728
728
|
"models_cache.json",
|
|
@@ -739,6 +739,7 @@ async function codexAccount(account) {
|
|
|
739
739
|
email: null,
|
|
740
740
|
displayName: null,
|
|
741
741
|
organization: null,
|
|
742
|
+
identity: null,
|
|
742
743
|
plan: null,
|
|
743
744
|
linked
|
|
744
745
|
};
|
|
@@ -753,7 +754,7 @@ async function codexAccount(account) {
|
|
|
753
754
|
case "chatgpt": return {
|
|
754
755
|
...base,
|
|
755
756
|
email: result.account.email,
|
|
756
|
-
|
|
757
|
+
...codexLogin(configDir),
|
|
757
758
|
organization: result.account.workspaceName,
|
|
758
759
|
plan: result.account.planType,
|
|
759
760
|
error: null,
|
|
@@ -825,21 +826,40 @@ function toLimits(snapshot) {
|
|
|
825
826
|
rejected: window.usedPercent >= 100
|
|
826
827
|
}]);
|
|
827
828
|
}
|
|
828
|
-
function
|
|
829
|
+
function codexLogin(home) {
|
|
829
830
|
let raw;
|
|
830
831
|
try {
|
|
831
832
|
raw = JSON.parse(readFileSync(path.join(home, "auth.json"), "utf8"));
|
|
832
833
|
} catch (error) {
|
|
833
|
-
if (error.code !== "ENOENT") log$
|
|
834
|
-
return
|
|
834
|
+
if (error.code !== "ENOENT") log$17.warn(`could not read the Codex account:`, error);
|
|
835
|
+
return {
|
|
836
|
+
displayName: null,
|
|
837
|
+
identity: null
|
|
838
|
+
};
|
|
835
839
|
}
|
|
836
|
-
if (!isRecord(raw) || !isRecord(raw.tokens) || typeof raw.tokens.id_token !== "string") return
|
|
840
|
+
if (!isRecord(raw) || !isRecord(raw.tokens) || typeof raw.tokens.id_token !== "string") return {
|
|
841
|
+
displayName: null,
|
|
842
|
+
identity: null
|
|
843
|
+
};
|
|
837
844
|
const payload = raw.tokens.id_token.split(".").at(1);
|
|
838
|
-
if (payload === void 0) return
|
|
845
|
+
if (payload === void 0) return {
|
|
846
|
+
displayName: null,
|
|
847
|
+
identity: null
|
|
848
|
+
};
|
|
839
849
|
const claims = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
840
|
-
if (!isRecord(claims)
|
|
841
|
-
|
|
842
|
-
|
|
850
|
+
if (!isRecord(claims)) return {
|
|
851
|
+
displayName: null,
|
|
852
|
+
identity: null
|
|
853
|
+
};
|
|
854
|
+
const name = typeof claims.name === "string" ? claims.name.trim() : "";
|
|
855
|
+
const auth = claims["https://api.openai.com/auth"];
|
|
856
|
+
return {
|
|
857
|
+
displayName: name === "" ? null : name,
|
|
858
|
+
identity: isRecord(auth) && typeof auth.chatgpt_user_id === "string" && typeof auth.chatgpt_account_id === "string" ? {
|
|
859
|
+
accountId: auth.chatgpt_user_id,
|
|
860
|
+
organizationId: auth.chatgpt_account_id
|
|
861
|
+
} : null
|
|
862
|
+
};
|
|
843
863
|
}
|
|
844
864
|
function isMirror(dir) {
|
|
845
865
|
try {
|
|
@@ -1126,19 +1146,6 @@ const configSchema = z.strictObject({
|
|
|
1126
1146
|
codex: z.boolean().default(true),
|
|
1127
1147
|
opencode: z.boolean().default(true)
|
|
1128
1148
|
}).prefault({}),
|
|
1129
|
-
newThreads: z.strictObject({
|
|
1130
|
-
provider: z.enum([
|
|
1131
|
-
"claude",
|
|
1132
|
-
"codex",
|
|
1133
|
-
"opencode"
|
|
1134
|
-
]).default("claude"),
|
|
1135
|
-
accountId: z.string().nullable().default(null),
|
|
1136
|
-
models: z.strictObject({
|
|
1137
|
-
claude: z.string().nullable().default(null),
|
|
1138
|
-
codex: z.string().nullable().default(null),
|
|
1139
|
-
opencode: z.string().nullable().default(null)
|
|
1140
|
-
}).prefault({})
|
|
1141
|
-
}).prefault({}),
|
|
1142
1149
|
commitMessage: generationModel,
|
|
1143
1150
|
titleGeneration: generationModel,
|
|
1144
1151
|
models: z.strictObject({
|
|
@@ -1148,7 +1155,11 @@ const configSchema = z.strictObject({
|
|
|
1148
1155
|
}).prefault({}),
|
|
1149
1156
|
delivery: z.enum(["queue", "steer"]).default("steer"),
|
|
1150
1157
|
autoSkipQuestionTimeout: z.number().positive().nullable().default(180),
|
|
1151
|
-
autoPermissionMode: z.enum([
|
|
1158
|
+
autoPermissionMode: z.enum([
|
|
1159
|
+
"bypass",
|
|
1160
|
+
"allow",
|
|
1161
|
+
"deny"
|
|
1162
|
+
]).default("bypass"),
|
|
1152
1163
|
autoPermissionTimeout: z.number().positive().nullable().default(15),
|
|
1153
1164
|
terminal: z.strictObject({
|
|
1154
1165
|
shell: z.strictObject({
|
|
@@ -1210,7 +1221,7 @@ function saveConfig(value) {
|
|
|
1210
1221
|
return result.data;
|
|
1211
1222
|
}
|
|
1212
1223
|
// src/lib/live.ts
|
|
1213
|
-
const log$
|
|
1224
|
+
const log$16 = logger.withTag("runs");
|
|
1214
1225
|
const runs = /* @__PURE__ */ new Map();
|
|
1215
1226
|
const titleJobs = /* @__PURE__ */ new Set();
|
|
1216
1227
|
let states = /* @__PURE__ */ new Map();
|
|
@@ -1237,10 +1248,10 @@ function broadcastActivity() {
|
|
|
1237
1248
|
...run.compacting ? ["compacting"] : []
|
|
1238
1249
|
].join(", ");
|
|
1239
1250
|
next.set(run.sessionId, state);
|
|
1240
|
-
if (!states.has(run.sessionId)) log$
|
|
1241
|
-
if (states.get(run.sessionId) !== state) log$
|
|
1251
|
+
if (!states.has(run.sessionId)) log$16.info(`${run.provider} session ${run.sessionId} opened in ${run.cwd} with account ${run.settings.accountId}`);
|
|
1252
|
+
if (states.get(run.sessionId) !== state) log$16.debug(`session ${run.sessionId} is ${state}`);
|
|
1242
1253
|
}
|
|
1243
|
-
for (const sessionId of states.keys()) if (!next.has(sessionId)) log$
|
|
1254
|
+
for (const sessionId of states.keys()) if (!next.has(sessionId)) log$16.info(`session ${sessionId} closed`);
|
|
1244
1255
|
states = next;
|
|
1245
1256
|
broadcast({
|
|
1246
1257
|
type: "activity",
|
|
@@ -1250,16 +1261,18 @@ function broadcastActivity() {
|
|
|
1250
1261
|
function scheduleAutoResolve(sessionId, request) {
|
|
1251
1262
|
if (request.kind === "plan") return request;
|
|
1252
1263
|
const config = readConfig();
|
|
1253
|
-
const
|
|
1264
|
+
const run = runs.get(sessionId);
|
|
1265
|
+
const immediate = config.autoPermissionMode === "bypass" && (run?.provider === "claude" && run.settings.mode === "bypassPermissions" || run?.provider === "codex" && run.settings.mode === "full-access");
|
|
1266
|
+
const seconds = request.kind === "question" ? config.autoSkipQuestionTimeout : immediate ? 0 : config.autoPermissionTimeout;
|
|
1254
1267
|
if (seconds === null) return request;
|
|
1255
|
-
const decision = request.kind === "question" ? "allow" : config.autoPermissionMode;
|
|
1268
|
+
const decision = request.kind === "question" || config.autoPermissionMode === "bypass" ? "allow" : config.autoPermissionMode;
|
|
1256
1269
|
setTimeout(async () => {
|
|
1257
1270
|
const run = runs.get(sessionId);
|
|
1258
1271
|
if (run === void 0) return;
|
|
1259
1272
|
try {
|
|
1260
1273
|
await run.respond(request.id, decision, request.kind === "question" ? {} : null);
|
|
1261
1274
|
} catch (error) {
|
|
1262
|
-
log$
|
|
1275
|
+
log$16.warn(`could not auto-resolve ${request.id}:`, error);
|
|
1263
1276
|
}
|
|
1264
1277
|
}, seconds * 1e3);
|
|
1265
1278
|
return {
|
|
@@ -1400,7 +1413,7 @@ function reply(method, params, decision, answers) {
|
|
|
1400
1413
|
throw new Error(`offsite cannot answer ${method}`);
|
|
1401
1414
|
}
|
|
1402
1415
|
// src/lib/codex/run.ts
|
|
1403
|
-
const log$
|
|
1416
|
+
const log$15 = logger.withTag("codex");
|
|
1404
1417
|
const IDLE_LIMIT$2 = 18e5;
|
|
1405
1418
|
function codexServer(settings) {
|
|
1406
1419
|
return appServer(storedAccount("codex", settings.accountId).configDir ?? codexBaseDir);
|
|
@@ -1525,10 +1538,10 @@ var CodexRun = class CodexRun {
|
|
|
1525
1538
|
if (runs.get(this.sessionId) === this) runs.delete(this.sessionId);
|
|
1526
1539
|
this.server.threads.delete(this.sessionId);
|
|
1527
1540
|
clearTimeout(this.idleTimer);
|
|
1528
|
-
this.server.request("thread/unsubscribe", { threadId: this.sessionId }).catch((error) => log$
|
|
1541
|
+
this.server.request("thread/unsubscribe", { threadId: this.sessionId }).catch((error) => log$15.warn(`could not unload Codex thread:`, error)).finally(() => this.end());
|
|
1529
1542
|
}
|
|
1530
1543
|
exited() {
|
|
1531
|
-
log$
|
|
1544
|
+
log$15.warn(`session ${this.sessionId} lost its Codex app server`);
|
|
1532
1545
|
this.put(errorItem(randomUUID(), this.pending.size === 0 ? "Codex stopped" : "Codex stopped, its open requests expired"));
|
|
1533
1546
|
if (runs.get(this.sessionId) === this) runs.delete(this.sessionId);
|
|
1534
1547
|
this.end();
|
|
@@ -1732,7 +1745,7 @@ var CodexRun = class CodexRun {
|
|
|
1732
1745
|
});
|
|
1733
1746
|
}
|
|
1734
1747
|
fail(error) {
|
|
1735
|
-
log$
|
|
1748
|
+
log$15.warn(`session ${this.sessionId} failed:`, error);
|
|
1736
1749
|
this.put(errorItem(randomUUID(), error instanceof Error ? error.message : String(error)));
|
|
1737
1750
|
}
|
|
1738
1751
|
armIdleTimer() {
|
|
@@ -2599,7 +2612,7 @@ function sessionSettings(session, settings) {
|
|
|
2599
2612
|
};
|
|
2600
2613
|
}
|
|
2601
2614
|
// src/lib/opencode/run.ts
|
|
2602
|
-
const log$
|
|
2615
|
+
const log$14 = logger.withTag("opencode");
|
|
2603
2616
|
const IDLE_LIMIT$1 = 18e5;
|
|
2604
2617
|
const RECENT = 30;
|
|
2605
2618
|
const replies = {
|
|
@@ -2727,7 +2740,7 @@ var OpenCodeRun = class OpenCodeRun {
|
|
|
2727
2740
|
this.end();
|
|
2728
2741
|
}
|
|
2729
2742
|
exited() {
|
|
2730
|
-
log$
|
|
2743
|
+
log$14.warn(`session ${this.sessionId} lost the connection to OpenCode`);
|
|
2731
2744
|
this.put([errorItem(randomUUID(), this.pending.size === 0 ? "Lost the connection to OpenCode" : "Lost the connection to OpenCode, its open requests expired")]);
|
|
2732
2745
|
this.stop();
|
|
2733
2746
|
}
|
|
@@ -2795,7 +2808,7 @@ var OpenCodeRun = class OpenCodeRun {
|
|
|
2795
2808
|
}
|
|
2796
2809
|
refresh(client) {
|
|
2797
2810
|
clearTimeout(this.refreshTimer);
|
|
2798
|
-
this.loading = this.loading.then(async () => this.load(client ?? await openCode())).catch((error) => log$
|
|
2811
|
+
this.loading = this.loading.then(async () => this.load(client ?? await openCode())).catch((error) => log$14.warn(`could not load OpenCode messages:`, error));
|
|
2799
2812
|
}
|
|
2800
2813
|
async load(client) {
|
|
2801
2814
|
const messages = (await client.message.list({
|
|
@@ -2912,7 +2925,7 @@ var OpenCodeRun = class OpenCodeRun {
|
|
|
2912
2925
|
});
|
|
2913
2926
|
}
|
|
2914
2927
|
fail(error) {
|
|
2915
|
-
log$
|
|
2928
|
+
log$14.warn(`session ${this.sessionId} failed:`, error);
|
|
2916
2929
|
this.put([errorItem(randomUUID(), error instanceof Error ? error.message : String(error))]);
|
|
2917
2930
|
}
|
|
2918
2931
|
armIdleTimer() {
|
|
@@ -2963,6 +2976,11 @@ const projects$1 = snakeCase.table("projects", {
|
|
|
2963
2976
|
});
|
|
2964
2977
|
const sessions$1 = snakeCase.table("sessions", {
|
|
2965
2978
|
sessionId: text().primaryKey(),
|
|
2979
|
+
provider: text({ enum: [
|
|
2980
|
+
"claude",
|
|
2981
|
+
"codex",
|
|
2982
|
+
"opencode"
|
|
2983
|
+
] }),
|
|
2966
2984
|
interactedAt: integer(),
|
|
2967
2985
|
status: text({ enum: [
|
|
2968
2986
|
"active",
|
|
@@ -2998,6 +3016,16 @@ const pushSubscriptions = snakeCase.table("push_subscriptions", {
|
|
|
2998
3016
|
machine: text().notNull(),
|
|
2999
3017
|
server: text().notNull()
|
|
3000
3018
|
});
|
|
3019
|
+
const lastUsedModels = snakeCase.table("last_used_models", {
|
|
3020
|
+
provider: text({ enum: [
|
|
3021
|
+
"claude",
|
|
3022
|
+
"codex",
|
|
3023
|
+
"opencode"
|
|
3024
|
+
] }).primaryKey(),
|
|
3025
|
+
accountId: text(),
|
|
3026
|
+
model: text().notNull(),
|
|
3027
|
+
usedAt: integer().notNull()
|
|
3028
|
+
});
|
|
3001
3029
|
// src/storage/auth.ts
|
|
3002
3030
|
const CODE_LIFETIME = 3e5;
|
|
3003
3031
|
const DEVICE_LIFETIME = 2592e6;
|
|
@@ -4047,11 +4075,145 @@ async function find(provider, paths) {
|
|
|
4047
4075
|
});
|
|
4048
4076
|
}
|
|
4049
4077
|
}
|
|
4078
|
+
// src/services/threads.ts
|
|
4079
|
+
const log$13 = logger.withTag("threads");
|
|
4080
|
+
const allProviders$2 = [
|
|
4081
|
+
"claude",
|
|
4082
|
+
"codex",
|
|
4083
|
+
"opencode"
|
|
4084
|
+
];
|
|
4085
|
+
const threads = {
|
|
4086
|
+
claude: /* @__PURE__ */ new Map(),
|
|
4087
|
+
codex: /* @__PURE__ */ new Map(),
|
|
4088
|
+
opencode: /* @__PURE__ */ new Map()
|
|
4089
|
+
};
|
|
4090
|
+
const changed = {
|
|
4091
|
+
claude: /* @__PURE__ */ new Set(),
|
|
4092
|
+
codex: /* @__PURE__ */ new Set(),
|
|
4093
|
+
opencode: /* @__PURE__ */ new Set()
|
|
4094
|
+
};
|
|
4095
|
+
const failures = /* @__PURE__ */ new Set();
|
|
4096
|
+
let built = Promise.resolve();
|
|
4097
|
+
let builtFor = "";
|
|
4098
|
+
function rebuildThreads() {
|
|
4099
|
+
const config = readConfig();
|
|
4100
|
+
const key = JSON.stringify([
|
|
4101
|
+
projectPaths$1(),
|
|
4102
|
+
config.enabledProviders,
|
|
4103
|
+
config.claudeBaseDir
|
|
4104
|
+
]);
|
|
4105
|
+
if (key === builtFor) return;
|
|
4106
|
+
builtFor = key;
|
|
4107
|
+
built = relistAll();
|
|
4108
|
+
}
|
|
4109
|
+
function threadsReady() {
|
|
4110
|
+
return built;
|
|
4111
|
+
}
|
|
4112
|
+
function threadFailed(provider) {
|
|
4113
|
+
return failures.has(provider);
|
|
4114
|
+
}
|
|
4115
|
+
function knownThreads() {
|
|
4116
|
+
const known = new Set(projects().map((project) => project.id));
|
|
4117
|
+
return allProviders$2.filter((provider) => readConfig().enabledProviders[provider]).flatMap((provider) => [...threads[provider].values()]).filter((item) => known.has(item.projectId));
|
|
4118
|
+
}
|
|
4119
|
+
async function providerThreads(provider) {
|
|
4120
|
+
await built;
|
|
4121
|
+
if (failures.has(provider)) throw new Error(`could not list ${providerName(provider)} sessions`);
|
|
4122
|
+
return knownThreads().filter((item) => item.provider === provider);
|
|
4123
|
+
}
|
|
4124
|
+
function threadOf(provider, id) {
|
|
4125
|
+
return threads[provider].get(id);
|
|
4126
|
+
}
|
|
4127
|
+
function threadProvider(id) {
|
|
4128
|
+
return allProviders$2.find((provider) => threads[provider].has(id));
|
|
4129
|
+
}
|
|
4130
|
+
function markChanged(provider, id) {
|
|
4131
|
+
changed[provider].add(id);
|
|
4132
|
+
}
|
|
4133
|
+
function fileChanged(provider, filename) {
|
|
4134
|
+
if (filename === null) return;
|
|
4135
|
+
if (provider === "claude") {
|
|
4136
|
+
const parts = filename.split(path.sep);
|
|
4137
|
+
if (parts.length === 2 && parts[1].endsWith(".jsonl")) changed.claude.add(path.basename(parts[1], ".jsonl"));
|
|
4138
|
+
return;
|
|
4139
|
+
}
|
|
4140
|
+
const id = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/.exec(filename)?.[1];
|
|
4141
|
+
if (id !== void 0) changed.codex.add(id);
|
|
4142
|
+
}
|
|
4143
|
+
function forgetThread(provider, id) {
|
|
4144
|
+
threads[provider].delete(id);
|
|
4145
|
+
broadcast({ type: "threads" });
|
|
4146
|
+
}
|
|
4147
|
+
async function updateThreads(provider) {
|
|
4148
|
+
if (provider === "opencode") {
|
|
4149
|
+
await relist(provider);
|
|
4150
|
+
return;
|
|
4151
|
+
}
|
|
4152
|
+
const ids = [...changed[provider]];
|
|
4153
|
+
changed[provider].clear();
|
|
4154
|
+
if (ids.length === 0) return;
|
|
4155
|
+
await built;
|
|
4156
|
+
const paths = new Map(projectPaths$1());
|
|
4157
|
+
const results = await Promise.all(ids.map(async (id) => {
|
|
4158
|
+
try {
|
|
4159
|
+
return {
|
|
4160
|
+
id,
|
|
4161
|
+
item: await timeLimit(lookup$1(provider, id, paths), `${providerName(provider)} did not find session ${id} in time`)
|
|
4162
|
+
};
|
|
4163
|
+
} catch (error) {
|
|
4164
|
+
log$13.warn(`could not update ${providerName(provider)} session:`, error);
|
|
4165
|
+
return null;
|
|
4166
|
+
}
|
|
4167
|
+
}));
|
|
4168
|
+
let updated = false;
|
|
4169
|
+
for (const result of results) {
|
|
4170
|
+
if (result === null) continue;
|
|
4171
|
+
const before = threads[provider].get(result.id);
|
|
4172
|
+
if (result.item === null) {
|
|
4173
|
+
updated ||= before !== void 0;
|
|
4174
|
+
threads[provider].delete(result.id);
|
|
4175
|
+
continue;
|
|
4176
|
+
}
|
|
4177
|
+
updated ||= JSON.stringify(before) !== JSON.stringify(result.item);
|
|
4178
|
+
threads[provider].set(result.id, result.item);
|
|
4179
|
+
}
|
|
4180
|
+
if (updated) broadcast({ type: "threads" });
|
|
4181
|
+
}
|
|
4182
|
+
async function lookup$1(provider, id, paths) {
|
|
4183
|
+
const known = threads[provider].get(id);
|
|
4184
|
+
const knownPath = known === void 0 ? void 0 : [...paths].find((entry) => entry[1] === known.projectId);
|
|
4185
|
+
if (knownPath === void 0) return info(provider, id, paths);
|
|
4186
|
+
return await info(provider, id, new Map([knownPath])) ?? info(provider, id, paths);
|
|
4187
|
+
}
|
|
4188
|
+
async function relistAll() {
|
|
4189
|
+
await Promise.all(allProviders$2.map((provider) => relist(provider)));
|
|
4190
|
+
}
|
|
4191
|
+
async function relist(provider) {
|
|
4192
|
+
if (!readConfig().enabledProviders[provider]) {
|
|
4193
|
+
threads[provider] = /* @__PURE__ */ new Map();
|
|
4194
|
+
failures.delete(provider);
|
|
4195
|
+
broadcast({ type: "threads" });
|
|
4196
|
+
return;
|
|
4197
|
+
}
|
|
4198
|
+
try {
|
|
4199
|
+
const found = await timeLimit(find(provider, new Map(projectPaths$1())), `${providerName(provider)} did not list its sessions in time`) ?? [];
|
|
4200
|
+
threads[provider] = new Map(found.map((item) => [item.id, item]));
|
|
4201
|
+
failures.delete(provider);
|
|
4202
|
+
} catch (error) {
|
|
4203
|
+
log$13.warn(`could not list all ${providerName(provider)} sessions:`, error);
|
|
4204
|
+
failures.add(provider);
|
|
4205
|
+
builtFor = "";
|
|
4206
|
+
}
|
|
4207
|
+
broadcast({ type: "threads" });
|
|
4208
|
+
}
|
|
4209
|
+
function projectPaths$1() {
|
|
4210
|
+
return projects().map((project) => [project.path, project.id]);
|
|
4211
|
+
}
|
|
4050
4212
|
// src/services/sessions.ts
|
|
4051
|
-
const log$
|
|
4213
|
+
const log$12 = logger.withTag("sessions");
|
|
4052
4214
|
const day = 864e5;
|
|
4053
4215
|
const REFRESH_DELAY = 1e3;
|
|
4054
|
-
const allProviders = [
|
|
4216
|
+
const allProviders$1 = [
|
|
4055
4217
|
"claude",
|
|
4056
4218
|
"codex",
|
|
4057
4219
|
"opencode"
|
|
@@ -4084,61 +4246,35 @@ const providers = /* @__PURE__ */ new Map();
|
|
|
4084
4246
|
let watchers = [];
|
|
4085
4247
|
function sessions() {
|
|
4086
4248
|
const known = new Set(projects().map((project) => project.id));
|
|
4087
|
-
return withStatus(allProviders.flatMap((provider) => listed[provider]).filter((item) => known.has(item.projectId))).filter((session) => session.status !== "inactive");
|
|
4249
|
+
return withStatus(allProviders$1.flatMap((provider) => listed[provider]).filter((item) => known.has(item.projectId))).filter((session) => session.status !== "inactive");
|
|
4088
4250
|
}
|
|
4089
|
-
async function inactiveSessions(
|
|
4090
|
-
|
|
4251
|
+
async function inactiveSessions(params) {
|
|
4252
|
+
await threadsReady();
|
|
4253
|
+
const scope = new Set(projects().filter((project) => params.projectId === null || project.id === params.projectId || params.projectId === "no-project" && project.scratch).map((project) => project.id));
|
|
4254
|
+
const found = withStatus(knownThreads().filter((item) => scope.has(item.projectId))).filter((session) => session.status === "inactive").sort((a, b) => b.updatedAt - a.updatedAt);
|
|
4255
|
+
return {
|
|
4256
|
+
sessions: found.slice(0, params.limit),
|
|
4257
|
+
total: found.length
|
|
4258
|
+
};
|
|
4091
4259
|
}
|
|
4092
4260
|
async function searchableSessions() {
|
|
4093
|
-
|
|
4094
|
-
const
|
|
4095
|
-
try {
|
|
4096
|
-
return {
|
|
4097
|
-
provider,
|
|
4098
|
-
items: await timeLimit(find(provider, new Map(paths)), `${providerName(provider)} did not list its sessions in time`) ?? [],
|
|
4099
|
-
failed: false
|
|
4100
|
-
};
|
|
4101
|
-
} catch (error) {
|
|
4102
|
-
log$11.warn(`could not search ${providerName(provider)}:`, error);
|
|
4103
|
-
return {
|
|
4104
|
-
provider,
|
|
4105
|
-
items: [],
|
|
4106
|
-
failed: true
|
|
4107
|
-
};
|
|
4108
|
-
}
|
|
4109
|
-
}));
|
|
4110
|
-
const found = lists.flatMap((list) => list.items);
|
|
4111
|
-
for (const item of found) providers.set(item.id, item.provider);
|
|
4261
|
+
await threadsReady();
|
|
4262
|
+
const found = knownThreads();
|
|
4112
4263
|
const opens = new Map(db.select().from(sessionOpens).all().map((record) => [record.sessionId, record]));
|
|
4113
4264
|
return {
|
|
4114
4265
|
sessions: withStatus(found).map((session) => ({
|
|
4115
4266
|
...session,
|
|
4116
4267
|
opens: opens.get(session.id) ?? null
|
|
4117
4268
|
})),
|
|
4118
|
-
failed:
|
|
4269
|
+
failed: enabledProviders().filter((provider) => threadFailed(provider))
|
|
4119
4270
|
};
|
|
4120
4271
|
}
|
|
4121
4272
|
async function findSession(id) {
|
|
4122
|
-
|
|
4273
|
+
if (ownerOf(id) === void 0) await threadsReady();
|
|
4123
4274
|
const owner = ownerOf(id);
|
|
4124
|
-
if (owner
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
}
|
|
4128
|
-
for (const provider of allProviders) {
|
|
4129
|
-
if (!readConfig().enabledProviders[provider]) continue;
|
|
4130
|
-
const item = await info(provider, id, paths).catch(() => null);
|
|
4131
|
-
if (item !== null) {
|
|
4132
|
-
providers.set(id, provider);
|
|
4133
|
-
return withStatus([item])[0];
|
|
4134
|
-
}
|
|
4135
|
-
}
|
|
4136
|
-
return null;
|
|
4137
|
-
}
|
|
4138
|
-
async function everything(paths) {
|
|
4139
|
-
const found = (await Promise.all(enabledProviders().map((provider) => find(provider, new Map(paths))))).flatMap((items) => items ?? []);
|
|
4140
|
-
for (const item of found) providers.set(item.id, item.provider);
|
|
4141
|
-
return withStatus(found);
|
|
4275
|
+
if (owner === void 0) return null;
|
|
4276
|
+
const item = await info(owner, id, new Map(projectPaths()));
|
|
4277
|
+
return item === null ? null : withStatus([item])[0];
|
|
4142
4278
|
}
|
|
4143
4279
|
function withStatus(items) {
|
|
4144
4280
|
const records = new Map(db.select().from(sessions$1).all().map((record) => [record.sessionId, record]));
|
|
@@ -4153,16 +4289,16 @@ function withStatus(items) {
|
|
|
4153
4289
|
});
|
|
4154
4290
|
}
|
|
4155
4291
|
function enabledProviders() {
|
|
4156
|
-
return allProviders.filter((provider) => readConfig().enabledProviders[provider]);
|
|
4292
|
+
return allProviders$1.filter((provider) => readConfig().enabledProviders[provider]);
|
|
4157
4293
|
}
|
|
4158
4294
|
function projectPaths() {
|
|
4159
4295
|
return projects().map((project) => [project.path, project.id]);
|
|
4160
4296
|
}
|
|
4161
4297
|
function ownerOf(sessionId) {
|
|
4162
|
-
return runs.get(sessionId)?.provider ?? providers.get(sessionId);
|
|
4298
|
+
return runs.get(sessionId)?.provider ?? db.select({ provider: sessions$1.provider }).from(sessions$1).where(eq(sessions$1.sessionId, sessionId)).get()?.provider ?? providers.get(sessionId) ?? threadProvider(sessionId);
|
|
4163
4299
|
}
|
|
4164
4300
|
function providerStatuses() {
|
|
4165
|
-
return allProviders.map((provider) => statuses[provider]);
|
|
4301
|
+
return allProviders$1.map((provider) => statuses[provider]);
|
|
4166
4302
|
}
|
|
4167
4303
|
function listedUpdatedAt(provider, sessionId) {
|
|
4168
4304
|
return listed[provider].find((item) => item.id === sessionId)?.updatedAt ?? null;
|
|
@@ -4172,14 +4308,16 @@ function providerOf(sessionId) {
|
|
|
4172
4308
|
if (provider === void 0) throw new Error("offsite does not know this session yet, reload the page");
|
|
4173
4309
|
return provider;
|
|
4174
4310
|
}
|
|
4175
|
-
function recordInteraction(id) {
|
|
4311
|
+
function recordInteraction(id, provider) {
|
|
4176
4312
|
db.insert(sessions$1).values({
|
|
4177
4313
|
sessionId: id,
|
|
4314
|
+
provider,
|
|
4178
4315
|
interactedAt: Date.now(),
|
|
4179
4316
|
status: "active"
|
|
4180
4317
|
}).onConflictDoUpdate({
|
|
4181
4318
|
target: sessions$1.sessionId,
|
|
4182
4319
|
set: {
|
|
4320
|
+
provider,
|
|
4183
4321
|
interactedAt: Date.now(),
|
|
4184
4322
|
status: sql`CASE ${sessions$1.status} WHEN 'pinned' THEN 'pinned' WHEN 'snoozed' THEN 'snoozed' WHEN 'minimized' THEN 'minimized' ELSE 'active' END`
|
|
4185
4323
|
}
|
|
@@ -4202,13 +4340,15 @@ function publishSessions() {
|
|
|
4202
4340
|
});
|
|
4203
4341
|
}
|
|
4204
4342
|
function refreshAll() {
|
|
4205
|
-
|
|
4343
|
+
rebuildThreads();
|
|
4344
|
+
for (const provider of allProviders$1) refresh(provider);
|
|
4206
4345
|
}
|
|
4207
4346
|
function refreshSessions(provider) {
|
|
4208
4347
|
if (timers.has(provider)) return;
|
|
4209
4348
|
timers.set(provider, setTimeout(() => {
|
|
4210
4349
|
timers.delete(provider);
|
|
4211
4350
|
refresh(provider);
|
|
4351
|
+
updateThreads(provider);
|
|
4212
4352
|
}, REFRESH_DELAY));
|
|
4213
4353
|
}
|
|
4214
4354
|
async function refresh(provider) {
|
|
@@ -4257,7 +4397,7 @@ async function list(provider) {
|
|
|
4257
4397
|
}
|
|
4258
4398
|
} catch (error) {
|
|
4259
4399
|
const message = error instanceof Error ? error.message : String(error);
|
|
4260
|
-
log$
|
|
4400
|
+
log$12.warn(`could not list ${providerName(provider)} sessions: ${message}`);
|
|
4261
4401
|
statuses[provider] = {
|
|
4262
4402
|
provider,
|
|
4263
4403
|
state: "error",
|
|
@@ -4273,15 +4413,10 @@ async function list(provider) {
|
|
|
4273
4413
|
}
|
|
4274
4414
|
async function tracked(provider, paths) {
|
|
4275
4415
|
if (provider === "codex" && !codexInstalled() || provider === "opencode" && !openCodeInstalled()) return null;
|
|
4276
|
-
return (await Promise.all(db.select().from(sessions$1).all().map((record) =>
|
|
4277
|
-
const owner = ownerOf(record.sessionId);
|
|
4278
|
-
if (owner === provider) return lookup(provider, record.sessionId, paths);
|
|
4279
|
-
if (owner === void 0) return lookup(provider, record.sessionId, paths).catch(() => null);
|
|
4280
|
-
return null;
|
|
4281
|
-
}))).filter((item) => item !== null);
|
|
4416
|
+
return (await Promise.all(db.select().from(sessions$1).all().filter((record) => record.provider === provider).map((record) => lookup(provider, record.sessionId, paths)))).filter((item) => item !== null);
|
|
4282
4417
|
}
|
|
4283
4418
|
async function lookup(provider, sessionId, paths) {
|
|
4284
|
-
const known = listed[provider].find((item) => item.id === sessionId);
|
|
4419
|
+
const known = threadOf(provider, sessionId) ?? listed[provider].find((item) => item.id === sessionId);
|
|
4285
4420
|
const knownPath = known === void 0 ? void 0 : [...paths].find((entry) => entry[1] === known.projectId);
|
|
4286
4421
|
if (knownPath === void 0) return info(provider, sessionId, paths);
|
|
4287
4422
|
return await info(provider, sessionId, new Map([knownPath])) ?? info(provider, sessionId, paths);
|
|
@@ -4291,13 +4426,16 @@ function watchSessions() {
|
|
|
4291
4426
|
watchers = [["claude", path.join(claudeBaseDir(), "projects")], ["codex", path.join(codexBaseDir, "sessions")]].flatMap(([provider, dir]) => {
|
|
4292
4427
|
if (!readConfig().enabledProviders[provider]) return [];
|
|
4293
4428
|
try {
|
|
4294
|
-
const watcher = watch(dir, { recursive: true }, () =>
|
|
4295
|
-
|
|
4296
|
-
|
|
4429
|
+
const watcher = watch(dir, { recursive: true }, (_, filename) => {
|
|
4430
|
+
fileChanged(provider, filename);
|
|
4431
|
+
refreshSessions(provider);
|
|
4432
|
+
});
|
|
4433
|
+
watcher.on("error", (error) => log$12.warn(`stopped watching ${dir}:`, error));
|
|
4434
|
+
log$12.debug(`watching ${dir}`);
|
|
4297
4435
|
return [watcher];
|
|
4298
4436
|
} catch (error) {
|
|
4299
4437
|
if (error.code !== "ENOENT") throw error;
|
|
4300
|
-
log$
|
|
4438
|
+
log$12.debug(`not watching ${dir}, it does not exist`);
|
|
4301
4439
|
return [];
|
|
4302
4440
|
}
|
|
4303
4441
|
});
|
|
@@ -4331,16 +4469,24 @@ async function updateSession(id, patch) {
|
|
|
4331
4469
|
status: patch.status,
|
|
4332
4470
|
interactedAt: Date.now()
|
|
4333
4471
|
} : { status: patch.status };
|
|
4472
|
+
const provider = providerOf(id);
|
|
4334
4473
|
db.insert(sessions$1).values({
|
|
4335
4474
|
sessionId: id,
|
|
4475
|
+
provider,
|
|
4336
4476
|
...set
|
|
4337
4477
|
}).onConflictDoUpdate({
|
|
4338
4478
|
target: sessions$1.sessionId,
|
|
4339
|
-
set
|
|
4479
|
+
set: {
|
|
4480
|
+
provider,
|
|
4481
|
+
...set
|
|
4482
|
+
}
|
|
4340
4483
|
}).run();
|
|
4341
|
-
await refresh(
|
|
4484
|
+
await refresh(provider);
|
|
4485
|
+
}
|
|
4486
|
+
if (patch.title !== void 0) {
|
|
4487
|
+
markChanged(providerOf(id), id);
|
|
4488
|
+
refreshSessions(providerOf(id));
|
|
4342
4489
|
}
|
|
4343
|
-
if (patch.title !== void 0) refreshSessions(providerOf(id));
|
|
4344
4490
|
}
|
|
4345
4491
|
async function removeSession(id, projectId) {
|
|
4346
4492
|
const provider = providerOf(id);
|
|
@@ -4361,19 +4507,19 @@ async function removeSession(id, projectId) {
|
|
|
4361
4507
|
db.delete(sessions$1).where(eq(sessions$1.sessionId, id)).run();
|
|
4362
4508
|
db.delete(sessionOpens).where(eq(sessionOpens.sessionId, id)).run();
|
|
4363
4509
|
listed[provider] = listed[provider].filter((item) => item.id !== id);
|
|
4510
|
+
forgetThread(provider, id);
|
|
4364
4511
|
const project = projects().find((item) => item.id === projectId);
|
|
4365
4512
|
if (project === void 0 || !project.scratch) return;
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
log$11.warn(`could not check other chats in ${project.path}:`, error);
|
|
4370
|
-
}
|
|
4513
|
+
await threadsReady();
|
|
4514
|
+
if (enabledProviders().some((owner) => threadFailed(owner))) return;
|
|
4515
|
+
if (!knownThreads().some((item) => item.projectId === project.id)) removeProject(project.id);
|
|
4371
4516
|
}
|
|
4372
4517
|
async function fork(sessionId, projectId, messageId) {
|
|
4373
4518
|
const provider = providerOf(sessionId);
|
|
4374
4519
|
const result = await forkWith(provider, sessionId, projectPath(projectId), messageId);
|
|
4375
4520
|
providers.set(result.sessionId, provider);
|
|
4376
|
-
recordInteraction(result.sessionId);
|
|
4521
|
+
recordInteraction(result.sessionId, provider);
|
|
4522
|
+
markChanged(provider, result.sessionId);
|
|
4377
4523
|
refreshSessions(provider);
|
|
4378
4524
|
return result;
|
|
4379
4525
|
}
|
|
@@ -4387,7 +4533,7 @@ async function forkWith(provider, sessionId, cwd, messageId) {
|
|
|
4387
4533
|
})).sessionId };
|
|
4388
4534
|
}
|
|
4389
4535
|
// src/lib/opencode/client.ts
|
|
4390
|
-
const log$
|
|
4536
|
+
const log$11 = logger.withTag("opencode");
|
|
4391
4537
|
const listChanges = /* @__PURE__ */ new Set([
|
|
4392
4538
|
"session.created",
|
|
4393
4539
|
"session.renamed",
|
|
@@ -4433,7 +4579,7 @@ async function start() {
|
|
|
4433
4579
|
}
|
|
4434
4580
|
async function connect() {
|
|
4435
4581
|
const endpoint = await ensure();
|
|
4436
|
-
log$
|
|
4582
|
+
log$11.info(`connected to OpenCode at ${endpoint.url}`);
|
|
4437
4583
|
return OpenCode.make({
|
|
4438
4584
|
baseUrl: endpoint.url,
|
|
4439
4585
|
headers: headers(endpoint)
|
|
@@ -4448,11 +4594,11 @@ async function listen(client) {
|
|
|
4448
4594
|
signal: controller.signal,
|
|
4449
4595
|
onActivity: () => silence.refresh()
|
|
4450
4596
|
})) route(event);
|
|
4451
|
-
log$
|
|
4597
|
+
log$11.warn("the OpenCode event stream ended");
|
|
4452
4598
|
} catch (error) {
|
|
4453
4599
|
if (listener !== controller) return;
|
|
4454
|
-
if (controller.signal.aborted) log$
|
|
4455
|
-
else log$
|
|
4600
|
+
if (controller.signal.aborted) log$11.warn(`the OpenCode event stream was silent for ${SILENCE_LIMIT$2 / 1e3} seconds`);
|
|
4601
|
+
else log$11.warn("lost the OpenCode event stream:", error);
|
|
4456
4602
|
} finally {
|
|
4457
4603
|
clearTimeout(silence);
|
|
4458
4604
|
}
|
|
@@ -4468,15 +4614,15 @@ async function reconnect() {
|
|
|
4468
4614
|
try {
|
|
4469
4615
|
client = await connect();
|
|
4470
4616
|
} catch (error) {
|
|
4471
|
-
log$
|
|
4617
|
+
log$11.debug(`reconnect attempt ${attempt + 1} to OpenCode failed:`, error);
|
|
4472
4618
|
failure = error;
|
|
4473
4619
|
continue;
|
|
4474
4620
|
}
|
|
4475
4621
|
listen(client);
|
|
4476
|
-
await catchUp(client).catch((error) => log$
|
|
4622
|
+
await catchUp(client).catch((error) => log$11.warn(`could not catch up with OpenCode:`, error));
|
|
4477
4623
|
return client;
|
|
4478
4624
|
}
|
|
4479
|
-
log$
|
|
4625
|
+
log$11.warn(`could not reconnect to OpenCode:`, failure);
|
|
4480
4626
|
connection = void 0;
|
|
4481
4627
|
for (const run of openCodeRuns()) run.exited();
|
|
4482
4628
|
refreshSessions("opencode");
|
|
@@ -4507,7 +4653,7 @@ function openCodeRuns() {
|
|
|
4507
4653
|
return [...runs.values()].flatMap((run) => run.provider === "opencode" ? [run] : []);
|
|
4508
4654
|
}
|
|
4509
4655
|
// src/services/accounts.ts
|
|
4510
|
-
const log$
|
|
4656
|
+
const log$10 = logger.withTag("accounts");
|
|
4511
4657
|
const providerNames = {
|
|
4512
4658
|
claude: "Claude",
|
|
4513
4659
|
codex: "Codex",
|
|
@@ -4575,6 +4721,7 @@ function disabledAccount(account) {
|
|
|
4575
4721
|
email: null,
|
|
4576
4722
|
displayName: null,
|
|
4577
4723
|
organization: null,
|
|
4724
|
+
identity: null,
|
|
4578
4725
|
plan: null,
|
|
4579
4726
|
error: null,
|
|
4580
4727
|
linked: true,
|
|
@@ -4588,7 +4735,7 @@ async function publishAccounts() {
|
|
|
4588
4735
|
...await accounts()
|
|
4589
4736
|
});
|
|
4590
4737
|
} catch (error) {
|
|
4591
|
-
log$
|
|
4738
|
+
log$10.warn(`could not list accounts:`, error);
|
|
4592
4739
|
}
|
|
4593
4740
|
}
|
|
4594
4741
|
async function capabilities(provider, accountId, cwd) {
|
|
@@ -4695,7 +4842,7 @@ function validAccount(name, configDir) {
|
|
|
4695
4842
|
throw new Error("Use an absolute config directory or one starting with ~/");
|
|
4696
4843
|
}
|
|
4697
4844
|
// src/services/push.ts
|
|
4698
|
-
const log$
|
|
4845
|
+
const log$9 = logger.withTag("push");
|
|
4699
4846
|
const TITLE_LIMIT = 100;
|
|
4700
4847
|
const BODY_LIMIT = 500;
|
|
4701
4848
|
const TIME_TO_LIVE = 900;
|
|
@@ -4743,12 +4890,12 @@ function subscribe(registration, client) {
|
|
|
4743
4890
|
set: values
|
|
4744
4891
|
}).run();
|
|
4745
4892
|
if (notifier !== null && notifier.client === client) notifier.endpoint = registration.subscription.endpoint;
|
|
4746
|
-
log$
|
|
4893
|
+
log$9.info("a browser subscribed to push notifications");
|
|
4747
4894
|
}
|
|
4748
4895
|
function unsubscribe(endpoint) {
|
|
4749
4896
|
db.delete(pushSubscriptions).where(eq(pushSubscriptions.endpoint, endpoint)).run();
|
|
4750
4897
|
if (notifier !== null && notifier.endpoint === endpoint) notifier.endpoint = null;
|
|
4751
|
-
log$
|
|
4898
|
+
log$9.info("a browser unsubscribed from push notifications");
|
|
4752
4899
|
}
|
|
4753
4900
|
async function sendPush(message) {
|
|
4754
4901
|
const subscriptions = targets();
|
|
@@ -4790,14 +4937,14 @@ async function sendPush(message) {
|
|
|
4790
4937
|
404,
|
|
4791
4938
|
410
|
|
4792
4939
|
].includes(status)) {
|
|
4793
|
-
log$
|
|
4940
|
+
log$9.info(`push subscription ${subscription.endpoint} is gone or was made for another key, removing it`);
|
|
4794
4941
|
gone.push(subscription.endpoint);
|
|
4795
4942
|
return;
|
|
4796
4943
|
}
|
|
4797
|
-
log$
|
|
4944
|
+
log$9.warn(`could not send a push to ${subscription.endpoint}:`, error);
|
|
4798
4945
|
}
|
|
4799
4946
|
}));
|
|
4800
|
-
log$
|
|
4947
|
+
log$9.debug(`sent a push to ${delivered} of ${subscriptions.length} browsers`);
|
|
4801
4948
|
if (gone.length === 0) return delivered > 0;
|
|
4802
4949
|
db.delete(pushSubscriptions).where(and(eq(pushSubscriptions.server, server$1), inArray(pushSubscriptions.endpoint, gone))).run();
|
|
4803
4950
|
return delivered > 0;
|
|
@@ -4807,7 +4954,7 @@ function targets() {
|
|
|
4807
4954
|
return db.select().from(pushSubscriptions).where(and(eq(pushSubscriptions.server, server$1), eq(pushSubscriptions.endpoint, notifier.endpoint))).all();
|
|
4808
4955
|
}
|
|
4809
4956
|
// src/lib/claude/claude.ts
|
|
4810
|
-
const log$
|
|
4957
|
+
const log$8 = logger.withTag("claude");
|
|
4811
4958
|
const efforts = [
|
|
4812
4959
|
"low",
|
|
4813
4960
|
"medium",
|
|
@@ -4946,7 +5093,7 @@ async function claudeAccount(account) {
|
|
|
4946
5093
|
return {
|
|
4947
5094
|
...account,
|
|
4948
5095
|
email: info.email ?? null,
|
|
4949
|
-
|
|
5096
|
+
...claudeLogin(configDir),
|
|
4950
5097
|
organization: info.organization ?? null,
|
|
4951
5098
|
plan: info.subscriptionType ?? null,
|
|
4952
5099
|
error: null,
|
|
@@ -4959,6 +5106,7 @@ async function claudeAccount(account) {
|
|
|
4959
5106
|
email: null,
|
|
4960
5107
|
displayName: null,
|
|
4961
5108
|
organization: null,
|
|
5109
|
+
identity: null,
|
|
4962
5110
|
plan: null,
|
|
4963
5111
|
error: error instanceof Error ? error.message : String(error),
|
|
4964
5112
|
linked,
|
|
@@ -5037,18 +5185,31 @@ async function claudeLimits(account) {
|
|
|
5037
5185
|
probe.close();
|
|
5038
5186
|
}
|
|
5039
5187
|
}
|
|
5040
|
-
function
|
|
5188
|
+
function claudeLogin(configDir) {
|
|
5041
5189
|
const file = path.resolve(configDir) === path.join(os.homedir(), ".claude") ? path.join(os.homedir(), ".claude.json") : path.join(configDir, ".claude.json");
|
|
5042
5190
|
let raw;
|
|
5043
5191
|
try {
|
|
5044
5192
|
raw = JSON.parse(readFileSync(file, "utf8"));
|
|
5045
5193
|
} catch (error) {
|
|
5046
|
-
if (error.code !== "ENOENT") log$
|
|
5047
|
-
return
|
|
5194
|
+
if (error.code !== "ENOENT") log$8.warn(`could not read the Claude account:`, error);
|
|
5195
|
+
return {
|
|
5196
|
+
displayName: null,
|
|
5197
|
+
identity: null
|
|
5198
|
+
};
|
|
5048
5199
|
}
|
|
5049
|
-
if (!isRecord(raw) || !isRecord(raw.oauthAccount)
|
|
5050
|
-
|
|
5051
|
-
|
|
5200
|
+
if (!isRecord(raw) || !isRecord(raw.oauthAccount)) return {
|
|
5201
|
+
displayName: null,
|
|
5202
|
+
identity: null
|
|
5203
|
+
};
|
|
5204
|
+
const login = raw.oauthAccount;
|
|
5205
|
+
const name = typeof login.displayName === "string" ? login.displayName.trim() : "";
|
|
5206
|
+
return {
|
|
5207
|
+
displayName: name === "" ? null : name,
|
|
5208
|
+
identity: typeof login.accountUuid === "string" && typeof login.organizationUuid === "string" ? {
|
|
5209
|
+
accountId: login.accountUuid,
|
|
5210
|
+
organizationId: login.organizationUuid
|
|
5211
|
+
} : null
|
|
5212
|
+
};
|
|
5052
5213
|
}
|
|
5053
5214
|
function isLinked(configDir) {
|
|
5054
5215
|
const base = claudeBaseDir();
|
|
@@ -6258,7 +6419,7 @@ async function submitFirst(client, run, request, projectId) {
|
|
|
6258
6419
|
from: request.projectId
|
|
6259
6420
|
});
|
|
6260
6421
|
await run.submit(request.prompt, request.delivery);
|
|
6261
|
-
recordInteraction(sessionId);
|
|
6422
|
+
recordInteraction(sessionId, run.provider);
|
|
6262
6423
|
}
|
|
6263
6424
|
async function sendExisting(client, sessionId, cwd, request) {
|
|
6264
6425
|
const provider = providerOf(sessionId);
|
|
@@ -6272,7 +6433,7 @@ async function sendExisting(client, sessionId, cwd, request) {
|
|
|
6272
6433
|
if (run === existing) await run.configure(request.settings);
|
|
6273
6434
|
else await open$1(client, sessionId, request.projectId);
|
|
6274
6435
|
await run.submit(request.prompt, request.delivery);
|
|
6275
|
-
recordInteraction(sessionId);
|
|
6436
|
+
recordInteraction(sessionId, provider);
|
|
6276
6437
|
if (emptied.has(sessionId) && request.prompt.text.trim() !== "") {
|
|
6277
6438
|
emptied.delete(sessionId);
|
|
6278
6439
|
await updateSession(sessionId, { title: request.prompt.text.trim() });
|
|
@@ -6445,7 +6606,7 @@ function fenced(language, text) {
|
|
|
6445
6606
|
return `${fence}${language}\n${text}\n${fence}`;
|
|
6446
6607
|
}
|
|
6447
6608
|
// src/services/message-search.ts
|
|
6448
|
-
const log$
|
|
6609
|
+
const log$7 = logger.withTag("search");
|
|
6449
6610
|
const READ_LIMIT = 30;
|
|
6450
6611
|
const CODEX_READ_LIMIT = 10;
|
|
6451
6612
|
const RG_TIME_LIMIT = 5e3;
|
|
@@ -6456,7 +6617,6 @@ async function searchMessages(query) {
|
|
|
6456
6617
|
matches: [],
|
|
6457
6618
|
failed: []
|
|
6458
6619
|
};
|
|
6459
|
-
const paths = new Map(projects().map((project) => [project.path, project.id]));
|
|
6460
6620
|
const folders = new Map(projects().map((project) => [project.id, project.path]));
|
|
6461
6621
|
const results = await Promise.all([
|
|
6462
6622
|
"claude",
|
|
@@ -6466,11 +6626,11 @@ async function searchMessages(query) {
|
|
|
6466
6626
|
try {
|
|
6467
6627
|
return {
|
|
6468
6628
|
provider,
|
|
6469
|
-
matches: await timeLimit(search(provider, query,
|
|
6629
|
+
matches: await timeLimit(search(provider, query, folders), `${providerName(provider)} did not search in time`),
|
|
6470
6630
|
failed: false
|
|
6471
6631
|
};
|
|
6472
6632
|
} catch (error) {
|
|
6473
|
-
log$
|
|
6633
|
+
log$7.warn(`could not search ${providerName(provider)} messages:`, error);
|
|
6474
6634
|
return {
|
|
6475
6635
|
provider,
|
|
6476
6636
|
matches: [],
|
|
@@ -6483,10 +6643,8 @@ async function searchMessages(query) {
|
|
|
6483
6643
|
failed: results.filter((result) => result.failed).map((result) => result.provider)
|
|
6484
6644
|
};
|
|
6485
6645
|
}
|
|
6486
|
-
async function search(provider, query,
|
|
6487
|
-
const
|
|
6488
|
-
if (found === null) return [];
|
|
6489
|
-
const sessions = newestFirst(found);
|
|
6646
|
+
async function search(provider, query, folders) {
|
|
6647
|
+
const sessions = newestFirst(await providerThreads(provider));
|
|
6490
6648
|
switch (provider) {
|
|
6491
6649
|
case "claude": return searchClaude(query, sessions, folders);
|
|
6492
6650
|
case "codex": return searchCodex(query, sessions);
|
|
@@ -6520,7 +6678,7 @@ async function claudeCandidates(query) {
|
|
|
6520
6678
|
const lists = await Promise.all(words.map((word) => sessionsWith(word)));
|
|
6521
6679
|
return new Set(lists[0].filter((id) => lists.every((list) => list.includes(id))));
|
|
6522
6680
|
} catch (error) {
|
|
6523
|
-
log$
|
|
6681
|
+
log$7.warn(`could not narrow Claude sessions with rg:`, error);
|
|
6524
6682
|
return null;
|
|
6525
6683
|
}
|
|
6526
6684
|
}
|
|
@@ -7082,7 +7240,7 @@ function shellIntegrationIssue(command, platform, defaultShell) {
|
|
|
7082
7240
|
return "Available for bash, zsh and fish";
|
|
7083
7241
|
}
|
|
7084
7242
|
// src/services/terminal/shell-integration.ts
|
|
7085
|
-
const log$
|
|
7243
|
+
const log$6 = logger.withTag("terminal");
|
|
7086
7244
|
const scripts = path.join(serverDir, "src/services/terminal/shell-integration");
|
|
7087
7245
|
const folder = path.join(home, "shell-integration");
|
|
7088
7246
|
function prepare(env, command, shell) {
|
|
@@ -7125,7 +7283,7 @@ function zshGlobalsSupported() {
|
|
|
7125
7283
|
contents = readFileSync(global, "utf8");
|
|
7126
7284
|
} catch (error) {
|
|
7127
7285
|
if (error.code === "ENOENT") continue;
|
|
7128
|
-
log$
|
|
7286
|
+
log$6.warn(`could not read ${global}, zsh starts without shell integration:`, error);
|
|
7129
7287
|
return false;
|
|
7130
7288
|
}
|
|
7131
7289
|
for (const rawLine of contents.split("\n")) {
|
|
@@ -7170,12 +7328,12 @@ function writeScript(target, source) {
|
|
|
7170
7328
|
renameSync(`${target}.tmp`, target);
|
|
7171
7329
|
return true;
|
|
7172
7330
|
} catch (error) {
|
|
7173
|
-
log$
|
|
7331
|
+
log$6.warn(`could not write ${target}, the shell starts without shell integration:`, error);
|
|
7174
7332
|
return false;
|
|
7175
7333
|
}
|
|
7176
7334
|
}
|
|
7177
7335
|
// src/services/terminal/process.ts
|
|
7178
|
-
const log$
|
|
7336
|
+
const log$5 = logger.withTag("terminal");
|
|
7179
7337
|
const run = promisify(execFile);
|
|
7180
7338
|
const iterm2Version = "3.6.8";
|
|
7181
7339
|
const ghosttyVersion = "1.3.1";
|
|
@@ -7271,7 +7429,7 @@ async function busyProgram(pid) {
|
|
|
7271
7429
|
try {
|
|
7272
7430
|
return await programName(group);
|
|
7273
7431
|
} catch (error) {
|
|
7274
|
-
log$
|
|
7432
|
+
log$5.warn(`could not read the name of process ${group}:`, error);
|
|
7275
7433
|
return `process ${group}`;
|
|
7276
7434
|
}
|
|
7277
7435
|
}
|
|
@@ -7513,7 +7671,7 @@ var Alerts = class {
|
|
|
7513
7671
|
}
|
|
7514
7672
|
};
|
|
7515
7673
|
// src/services/terminal/terminal.ts
|
|
7516
|
-
const log$
|
|
7674
|
+
const log$4 = logger.withTag("terminal");
|
|
7517
7675
|
const HISTORY_LIMIT = 2097152;
|
|
7518
7676
|
const OUTPUT_LIMIT = 1048576;
|
|
7519
7677
|
const STALL_LIMIT = 1e4;
|
|
@@ -7548,7 +7706,7 @@ async function terminalRequest(client, message) {
|
|
|
7548
7706
|
function terminalInput(client, frame) {
|
|
7549
7707
|
const connection = connections.get(client);
|
|
7550
7708
|
if (connection === void 0) {
|
|
7551
|
-
log$
|
|
7709
|
+
log$4.warn("terminal input from a browser that is not attached");
|
|
7552
7710
|
return;
|
|
7553
7711
|
}
|
|
7554
7712
|
input(connection, frame);
|
|
@@ -7565,7 +7723,7 @@ async function busyTerminal(id) {
|
|
|
7565
7723
|
try {
|
|
7566
7724
|
return { program: await busyProgram(terminal.pty.pid) };
|
|
7567
7725
|
} catch (error) {
|
|
7568
|
-
log$
|
|
7726
|
+
log$4.warn(`could not tell if terminal ${id} is busy:`, error);
|
|
7569
7727
|
return { error: String(error) };
|
|
7570
7728
|
}
|
|
7571
7729
|
}
|
|
@@ -7634,12 +7792,12 @@ function input(connection, bytes) {
|
|
|
7634
7792
|
try {
|
|
7635
7793
|
frame = decodeFrame(bytes);
|
|
7636
7794
|
} catch (error) {
|
|
7637
|
-
log$
|
|
7795
|
+
log$4.warn(`ignoring terminal input:`, error);
|
|
7638
7796
|
return;
|
|
7639
7797
|
}
|
|
7640
7798
|
const terminal = terminals.get(frame.id);
|
|
7641
7799
|
if (terminal === void 0) {
|
|
7642
|
-
log$
|
|
7800
|
+
log$4.warn(`input for terminal ${frame.id}, which does not exist`);
|
|
7643
7801
|
return;
|
|
7644
7802
|
}
|
|
7645
7803
|
if (frame.kind === 1) {
|
|
@@ -7651,7 +7809,7 @@ function input(connection, bytes) {
|
|
|
7651
7809
|
terminal.pty.resize(size.cols, size.rows);
|
|
7652
7810
|
}
|
|
7653
7811
|
} else {
|
|
7654
|
-
log$
|
|
7812
|
+
log$4.warn(`ignoring terminal input of unknown kind ${frame.kind}`);
|
|
7655
7813
|
return;
|
|
7656
7814
|
}
|
|
7657
7815
|
terminal.pty.write(Buffer.from(frame.data));
|
|
@@ -7659,7 +7817,7 @@ function input(connection, bytes) {
|
|
|
7659
7817
|
async function createTab(projectId) {
|
|
7660
7818
|
const cwd = projectId === null ? workingDirectory() : projectPath(projectId);
|
|
7661
7819
|
if (!statSync(cwd, { throwIfNoEntry: false })?.isDirectory()) {
|
|
7662
|
-
log$
|
|
7820
|
+
log$4.error(`${cwd} is not a folder, terminal not created`);
|
|
7663
7821
|
return;
|
|
7664
7822
|
}
|
|
7665
7823
|
const activeId = active.get(projectId ?? "global");
|
|
@@ -7668,7 +7826,7 @@ async function createTab(projectId) {
|
|
|
7668
7826
|
if (current !== void 0) try {
|
|
7669
7827
|
directory = await currentDirectory(current.pty.pid, current.scanner.directory);
|
|
7670
7828
|
} catch (error) {
|
|
7671
|
-
log$
|
|
7829
|
+
log$4.warn(`starting in the project folder, could not read the folder of the active terminal:`, error);
|
|
7672
7830
|
}
|
|
7673
7831
|
nextTab += 1;
|
|
7674
7832
|
const id = nextTab;
|
|
@@ -7676,7 +7834,7 @@ async function createTab(projectId) {
|
|
|
7676
7834
|
try {
|
|
7677
7835
|
pty = startShell(directory);
|
|
7678
7836
|
} catch (error) {
|
|
7679
|
-
log$
|
|
7837
|
+
log$4.error(`could not start terminal ${id}:`, error);
|
|
7680
7838
|
return;
|
|
7681
7839
|
}
|
|
7682
7840
|
const terminal = {
|
|
@@ -7687,7 +7845,7 @@ async function createTab(projectId) {
|
|
|
7687
7845
|
alerts: new Alerts(),
|
|
7688
7846
|
scanner: new Scanner()
|
|
7689
7847
|
};
|
|
7690
|
-
log$
|
|
7848
|
+
log$4.info(`started terminal ${id} with pid ${pty.pid} in ${directory}`);
|
|
7691
7849
|
terminals.set(id, terminal);
|
|
7692
7850
|
tabs.push({
|
|
7693
7851
|
id,
|
|
@@ -7701,7 +7859,7 @@ async function createTab(projectId) {
|
|
|
7701
7859
|
held.add(pty);
|
|
7702
7860
|
}
|
|
7703
7861
|
pty.onExit((event) => {
|
|
7704
|
-
log$
|
|
7862
|
+
log$4.info(`terminal ${id} exited with code ${event.exitCode}`);
|
|
7705
7863
|
terminals.delete(id);
|
|
7706
7864
|
paused.delete(pty);
|
|
7707
7865
|
held.delete(pty);
|
|
@@ -7711,7 +7869,7 @@ async function createTab(projectId) {
|
|
|
7711
7869
|
function closeTab(id) {
|
|
7712
7870
|
const tab = findTab(id);
|
|
7713
7871
|
if (tab === void 0) return false;
|
|
7714
|
-
log$
|
|
7872
|
+
log$4.debug(`closing terminal ${id}`);
|
|
7715
7873
|
release(tab);
|
|
7716
7874
|
tabs.splice(tabs.indexOf(tab), 1);
|
|
7717
7875
|
const terminal = terminals.get(id);
|
|
@@ -7753,7 +7911,7 @@ function drain() {
|
|
|
7753
7911
|
last.queued = queued;
|
|
7754
7912
|
progress.set(connection, last);
|
|
7755
7913
|
if (now - last.at > STALL_LIMIT) {
|
|
7756
|
-
log$
|
|
7914
|
+
log$4.warn("a browser is not reading fast enough, disconnecting it");
|
|
7757
7915
|
connection.client.socket.terminate();
|
|
7758
7916
|
continue;
|
|
7759
7917
|
}
|
|
@@ -7843,6 +8001,36 @@ function send(connection, event) {
|
|
|
7843
8001
|
event
|
|
7844
8002
|
});
|
|
7845
8003
|
}
|
|
8004
|
+
// src/storage/last-used.ts
|
|
8005
|
+
const lastUsedSchema = z.discriminatedUnion("provider", [z.strictObject({
|
|
8006
|
+
provider: z.enum(["claude", "codex"]),
|
|
8007
|
+
accountId: z.string(),
|
|
8008
|
+
model: z.string()
|
|
8009
|
+
}), z.strictObject({
|
|
8010
|
+
provider: z.literal("opencode"),
|
|
8011
|
+
accountId: z.null(),
|
|
8012
|
+
model: z.string()
|
|
8013
|
+
})]);
|
|
8014
|
+
function readLastUsed() {
|
|
8015
|
+
return db.select().from(lastUsedModels).all();
|
|
8016
|
+
}
|
|
8017
|
+
function saveLastUsed(value) {
|
|
8018
|
+
const result = lastUsedSchema.safeParse(value);
|
|
8019
|
+
if (!result.success) throw new Error(z.prettifyError(result.error));
|
|
8020
|
+
const row = {
|
|
8021
|
+
...result.data,
|
|
8022
|
+
usedAt: Date.now()
|
|
8023
|
+
};
|
|
8024
|
+
db.insert(lastUsedModels).values(row).onConflictDoUpdate({
|
|
8025
|
+
target: lastUsedModels.provider,
|
|
8026
|
+
set: row
|
|
8027
|
+
}).run();
|
|
8028
|
+
broadcast({
|
|
8029
|
+
type: "lastUsed",
|
|
8030
|
+
lastUsed: readLastUsed()
|
|
8031
|
+
});
|
|
8032
|
+
return null;
|
|
8033
|
+
}
|
|
7846
8034
|
// ../core/src/app-icons.ts
|
|
7847
8035
|
const appIcons = [
|
|
7848
8036
|
{
|
|
@@ -7913,77 +8101,14 @@ const appIcons = [
|
|
|
7913
8101
|
}
|
|
7914
8102
|
];
|
|
7915
8103
|
// src/storage/state.ts
|
|
7916
|
-
const stateSchema = z.strictObject({
|
|
7917
|
-
composer: z.strictObject({
|
|
7918
|
-
provider: z.enum([
|
|
7919
|
-
"claude",
|
|
7920
|
-
"codex",
|
|
7921
|
-
"opencode"
|
|
7922
|
-
]).default("claude"),
|
|
7923
|
-
claude: z.strictObject({
|
|
7924
|
-
accountId: z.string().nullable().default(null),
|
|
7925
|
-
model: z.string().nullable().default(null),
|
|
7926
|
-
effort: z.enum([
|
|
7927
|
-
"low",
|
|
7928
|
-
"medium",
|
|
7929
|
-
"high",
|
|
7930
|
-
"xhigh",
|
|
7931
|
-
"max"
|
|
7932
|
-
]).nullable().default(null),
|
|
7933
|
-
mode: z.enum([
|
|
7934
|
-
"default",
|
|
7935
|
-
"acceptEdits",
|
|
7936
|
-
"bypassPermissions",
|
|
7937
|
-
"plan",
|
|
7938
|
-
"dontAsk",
|
|
7939
|
-
"auto"
|
|
7940
|
-
]).nullable().default("bypassPermissions"),
|
|
7941
|
-
plan: z.boolean().default(false)
|
|
7942
|
-
}).prefault({}),
|
|
7943
|
-
codex: z.strictObject({
|
|
7944
|
-
accountId: z.string().nullable().default(null),
|
|
7945
|
-
model: z.string().nullable().default(null),
|
|
7946
|
-
effort: z.string().nullable().default(null),
|
|
7947
|
-
mode: z.enum([
|
|
7948
|
-
"read-only",
|
|
7949
|
-
"auto",
|
|
7950
|
-
"full-access"
|
|
7951
|
-
]).nullable().default("auto"),
|
|
7952
|
-
plan: z.boolean().default(false)
|
|
7953
|
-
}).prefault({}),
|
|
7954
|
-
opencode: z.strictObject({
|
|
7955
|
-
accountId: z.null().default(null),
|
|
7956
|
-
model: z.string().nullable().default(null),
|
|
7957
|
-
effort: z.string().nullable().default(null),
|
|
7958
|
-
mode: z.string().nullable().default(null),
|
|
7959
|
-
plan: z.boolean().default(false)
|
|
7960
|
-
}).prefault({})
|
|
7961
|
-
}).prefault({}),
|
|
7962
|
-
appIcon: z.custom((value) => appIcons.some((icon) => icon.id === value)).default("original")
|
|
7963
|
-
});
|
|
8104
|
+
const stateSchema = z.strictObject({ appIcon: z.custom((value) => appIcons.some((icon) => icon.id === value)).default("original") });
|
|
7964
8105
|
const defaults = stateSchema.parse({});
|
|
7965
8106
|
function readState() {
|
|
7966
8107
|
return readSettings("state.json", stateSchema, defaults);
|
|
7967
8108
|
}
|
|
7968
|
-
function readComposer() {
|
|
7969
|
-
return readState().composer;
|
|
7970
|
-
}
|
|
7971
8109
|
function readAppIcon() {
|
|
7972
8110
|
return readState().appIcon;
|
|
7973
8111
|
}
|
|
7974
|
-
function saveComposer(value) {
|
|
7975
|
-
const result = stateSchema.safeParse({
|
|
7976
|
-
...readState(),
|
|
7977
|
-
composer: value
|
|
7978
|
-
});
|
|
7979
|
-
if (!result.success) throw new Error(z.prettifyError(result.error));
|
|
7980
|
-
writeJson("state.json", result.data);
|
|
7981
|
-
broadcast({
|
|
7982
|
-
type: "composer",
|
|
7983
|
-
composer: result.data.composer
|
|
7984
|
-
});
|
|
7985
|
-
return result.data.composer;
|
|
7986
|
-
}
|
|
7987
8112
|
function saveAppIcon(value) {
|
|
7988
8113
|
const result = stateSchema.safeParse({
|
|
7989
8114
|
...readState(),
|
|
@@ -7998,14 +8123,14 @@ function saveAppIcon(value) {
|
|
|
7998
8123
|
return result.data.appIcon;
|
|
7999
8124
|
}
|
|
8000
8125
|
// src/api/socket.ts
|
|
8001
|
-
const log$
|
|
8126
|
+
const log$3 = logger.withTag("socket");
|
|
8002
8127
|
const handlers = {
|
|
8003
8128
|
ping: () => null,
|
|
8004
8129
|
sync: () => {
|
|
8005
8130
|
refreshAll();
|
|
8006
8131
|
return {
|
|
8007
8132
|
...configInfo(),
|
|
8008
|
-
|
|
8133
|
+
lastUsed: readLastUsed(),
|
|
8009
8134
|
hostname: os.hostname(),
|
|
8010
8135
|
projects: projects(),
|
|
8011
8136
|
sessions: sessions(),
|
|
@@ -8016,8 +8141,7 @@ const handlers = {
|
|
|
8016
8141
|
};
|
|
8017
8142
|
},
|
|
8018
8143
|
"config.get": () => configInfo(),
|
|
8019
|
-
"
|
|
8020
|
-
"composer.save": (params) => saveComposer(params),
|
|
8144
|
+
"lastUsed.save": (params) => saveLastUsed(params),
|
|
8021
8145
|
"appIcon.save": (params) => saveAppIcon(params),
|
|
8022
8146
|
"config.save": async (params) => {
|
|
8023
8147
|
const before = readConfig();
|
|
@@ -8073,7 +8197,7 @@ const handlers = {
|
|
|
8073
8197
|
if (params.opened && session !== null) recordOpen(params.sessionId, params.query);
|
|
8074
8198
|
return session;
|
|
8075
8199
|
},
|
|
8076
|
-
"sessions.inactive": (params) => inactiveSessions(params
|
|
8200
|
+
"sessions.inactive": (params) => inactiveSessions(params),
|
|
8077
8201
|
"sessions.searchable": () => searchableSessions(),
|
|
8078
8202
|
"sessions.searchMessages": (params) => searchMessages(params.query),
|
|
8079
8203
|
"sessions.earlier": (params) => earlier(params.sessionId, params.before),
|
|
@@ -8114,7 +8238,7 @@ const handlers = {
|
|
|
8114
8238
|
const run = runs.get(params.sessionId);
|
|
8115
8239
|
if (run === void 0) return null;
|
|
8116
8240
|
await run.respond(params.requestId, params.decision, params.answers);
|
|
8117
|
-
recordInteraction(params.sessionId);
|
|
8241
|
+
recordInteraction(params.sessionId, run.provider);
|
|
8118
8242
|
return null;
|
|
8119
8243
|
},
|
|
8120
8244
|
"sessions.rewind": (params) => rewind(params),
|
|
@@ -8228,7 +8352,7 @@ function connectClient(socket, device, address) {
|
|
|
8228
8352
|
send: (message) => socket.send(JSON.stringify(message))
|
|
8229
8353
|
};
|
|
8230
8354
|
clients.add(client);
|
|
8231
|
-
log$
|
|
8355
|
+
log$3.info(`a browser connected from ${address}`);
|
|
8232
8356
|
socket.on("message", (data, isBinary) => {
|
|
8233
8357
|
client.heardAt = Date.now();
|
|
8234
8358
|
if (isBinary) {
|
|
@@ -8237,9 +8361,9 @@ function connectClient(socket, device, address) {
|
|
|
8237
8361
|
}
|
|
8238
8362
|
answer(client, String(data));
|
|
8239
8363
|
});
|
|
8240
|
-
socket.on("error", (error) => log$
|
|
8364
|
+
socket.on("error", (error) => log$3.warn(`a browser connection from ${address} failed:`, error));
|
|
8241
8365
|
socket.on("close", (code) => {
|
|
8242
|
-
log$
|
|
8366
|
+
log$3.info(`a browser from ${address} disconnected (code ${code})`);
|
|
8243
8367
|
clients.delete(client);
|
|
8244
8368
|
detachTerminal(client);
|
|
8245
8369
|
});
|
|
@@ -8249,7 +8373,7 @@ async function answer(client, text) {
|
|
|
8249
8373
|
try {
|
|
8250
8374
|
call = JSON.parse(text);
|
|
8251
8375
|
} catch (error) {
|
|
8252
|
-
log$
|
|
8376
|
+
log$3.warn(`ignoring a message from ${client.address} that is not JSON:`, error);
|
|
8253
8377
|
return;
|
|
8254
8378
|
}
|
|
8255
8379
|
const started = performance.now();
|
|
@@ -8263,7 +8387,7 @@ async function answer(client, text) {
|
|
|
8263
8387
|
result
|
|
8264
8388
|
});
|
|
8265
8389
|
} catch (error) {
|
|
8266
|
-
log$
|
|
8390
|
+
log$3.warn(`${call.method} from ${client.address} failed:`, error);
|
|
8267
8391
|
client.send({
|
|
8268
8392
|
type: "reply",
|
|
8269
8393
|
id: call.id,
|
|
@@ -8271,7 +8395,7 @@ async function answer(client, text) {
|
|
|
8271
8395
|
});
|
|
8272
8396
|
} finally {
|
|
8273
8397
|
const took = Math.round(performance.now() - started);
|
|
8274
|
-
if (call.method !== "ping") log$
|
|
8398
|
+
if (call.method !== "ping") log$3.debug(`${call.method} from ${client.address} took ${took}ms`);
|
|
8275
8399
|
}
|
|
8276
8400
|
}
|
|
8277
8401
|
// src/lib/proxy/css.ts
|
|
@@ -9444,7 +9568,7 @@ function fail(response, status, message) {
|
|
|
9444
9568
|
response.end(message);
|
|
9445
9569
|
}
|
|
9446
9570
|
// src/services/app-icon.ts
|
|
9447
|
-
const log$
|
|
9571
|
+
const log$2 = logger.withTag("app-icon");
|
|
9448
9572
|
const pngs = /* @__PURE__ */ new Map([
|
|
9449
9573
|
["icon-192.png", {
|
|
9450
9574
|
size: 192,
|
|
@@ -9477,7 +9601,7 @@ async function serveAppIcon(publicDir, pathname, response) {
|
|
|
9477
9601
|
response.setHeader("cache-control", "no-cache");
|
|
9478
9602
|
response.end(icon.body);
|
|
9479
9603
|
} catch (error) {
|
|
9480
|
-
log$
|
|
9604
|
+
log$2.warn(`could not serve the app icon:`, error);
|
|
9481
9605
|
response.statusCode = 500;
|
|
9482
9606
|
response.end("could not render the app icon");
|
|
9483
9607
|
}
|
|
@@ -9509,6 +9633,36 @@ function tile(icon, maskable) {
|
|
|
9509
9633
|
if (icon.tile.shape === "circle") return `<circle cx="256" cy="256" r="256" fill="${icon.tile.color}"/>`;
|
|
9510
9634
|
throw new Error(`unknown tile shape for ${icon.id}`);
|
|
9511
9635
|
}
|
|
9636
|
+
// src/services/backfill-session-providers.ts
|
|
9637
|
+
const log$1 = logger.withTag("backfill");
|
|
9638
|
+
const allProviders = [
|
|
9639
|
+
"claude",
|
|
9640
|
+
"codex",
|
|
9641
|
+
"opencode"
|
|
9642
|
+
];
|
|
9643
|
+
async function backfillSessionProviders() {
|
|
9644
|
+
const rows = db.select().from(sessions$1).where(isNull(sessions$1.provider)).all();
|
|
9645
|
+
if (rows.length === 0) return;
|
|
9646
|
+
const paths = new Map(projects().map((project) => [project.path, project.id]));
|
|
9647
|
+
const enabled = allProviders.filter((provider) => readConfig().enabledProviders[provider]);
|
|
9648
|
+
for (const row of rows) {
|
|
9649
|
+
const answers = await Promise.all(enabled.map(async (provider) => {
|
|
9650
|
+
try {
|
|
9651
|
+
return await timeLimit(info(provider, row.sessionId, paths), `${providerName(provider)} did not answer in time`) !== null;
|
|
9652
|
+
} catch (error) {
|
|
9653
|
+
log$1.debug(`${providerName(provider)} does not know session ${row.sessionId}:`, error);
|
|
9654
|
+
return false;
|
|
9655
|
+
}
|
|
9656
|
+
}));
|
|
9657
|
+
const owners = enabled.filter((_, index) => answers[index]);
|
|
9658
|
+
if (owners.length !== 1) {
|
|
9659
|
+
log$1.warn(`${owners.length} providers know session ${row.sessionId}, it stays without a provider until the next start`);
|
|
9660
|
+
continue;
|
|
9661
|
+
}
|
|
9662
|
+
db.update(sessions$1).set({ provider: owners[0] }).where(eq(sessions$1.sessionId, row.sessionId)).run();
|
|
9663
|
+
log$1.info(`saved ${providerName(owners[0])} as the provider of session ${row.sessionId}`);
|
|
9664
|
+
}
|
|
9665
|
+
}
|
|
9512
9666
|
// src/services/browser-logs.ts
|
|
9513
9667
|
const logsBody = z.array(z.object({
|
|
9514
9668
|
time: z.number(),
|
|
@@ -9609,6 +9763,7 @@ lockHome();
|
|
|
9609
9763
|
log.info(`starting offsite ${args.command} with pid ${process.pid} on ${address}, dir ${args.dir}, auth ${args.auth ? "on" : "off"}`);
|
|
9610
9764
|
process.env.CLAUDE_CONFIG_DIR = claudeBaseDir();
|
|
9611
9765
|
installOpen(args.host, port);
|
|
9766
|
+
await backfillSessionProviders();
|
|
9612
9767
|
watchSessions();
|
|
9613
9768
|
refreshAll();
|
|
9614
9769
|
if (findClaude() === null) {
|