offsite 0.0.94 → 0.0.96

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.
Files changed (30) hide show
  1. package/apps/client/dist/assets/{XIcon-CDJz0EhT.js → XIcon-sNE7wlGT.js} +4 -4
  2. package/apps/client/dist/assets/{audio-player-CEvt4CVB.js → audio-player-DosgEDBw.js} +1 -1
  3. package/apps/client/dist/assets/{button-CWFREsRn.js → button-BnzxfmO3.js} +1 -1
  4. package/apps/client/dist/assets/diff-panel-D9sGyf1Y.js +260 -0
  5. package/apps/client/dist/assets/{dist-C_Y10kCd.js → dist-DNx_j6Py.js} +1 -1
  6. package/apps/client/dist/assets/{editor-dialog-CEMtEtdH.js → editor-dialog-De_ijtFY.js} +7 -7
  7. package/apps/client/dist/assets/{image-viewer-3S4rNjFt.js → image-viewer-DuyADV75.js} +1 -1
  8. package/apps/client/dist/assets/index-9ox1jth4.js +72 -0
  9. package/apps/client/dist/assets/index-dtN3QMc8.css +2 -0
  10. package/apps/client/dist/assets/panel-close-button-yIEA8ujQ.js +1 -0
  11. package/apps/client/dist/assets/{preview-dialog-BuSSuMeQ.js → preview-dialog-CPV2cjGd.js} +2 -2
  12. package/apps/client/dist/assets/{react-BgpkckXP.js → react-aDzERmfM.js} +1 -1
  13. package/apps/client/dist/assets/{tooltip-CKPboHje.js → tooltip-CU6Gh-yj.js} +1 -1
  14. package/apps/client/dist/assets/{use-editor-shortcut-auobREOP.js → use-editor-shortcut-BoixVvhO.js} +26 -26
  15. package/apps/client/dist/assets/{utils-U6QfaJBO.js → utils-Cih_T9uV.js} +1 -1
  16. package/apps/client/dist/assets/{video-viewer-gcKzQe3N.js → video-viewer-DbYvGHLh.js} +1 -1
  17. package/apps/client/dist/assets/zoomable-image-D0X1OvmT.js +1 -0
  18. package/apps/client/dist/assets/{zoomable-image-BD-kkiyn.js → zoomable-image-D6TeXYQ9.js} +1 -1
  19. package/apps/client/dist/index.html +10 -10
  20. package/apps/client/dist/sw.js +1 -1
  21. package/apps/server/dist/main.mjs +582 -150
  22. package/apps/server/dist/main.mjs.map +1 -1
  23. package/apps/server/drizzle/20261108212949_last_efforts/migration.sql +6 -0
  24. package/apps/server/drizzle/20261108212949_last_efforts/snapshot.json +495 -0
  25. package/package.json +1 -1
  26. package/apps/client/dist/assets/diff-panel-CeJqpNi8.js +0 -260
  27. package/apps/client/dist/assets/index-B7o5cqzG.js +0 -72
  28. package/apps/client/dist/assets/index-BwL3keyv.css +0 -2
  29. package/apps/client/dist/assets/panel-close-button-Bf-0TdmN.js +0 -1
  30. package/apps/client/dist/assets/zoomable-image-_HGrW8wW.js +0 -1
@@ -1,5 +1,5 @@
1
1
  import { createHash, randomBytes, randomUUID } from "node:crypto";
2
- import { accessSync, appendFileSync, chmodSync, closeSync, constants, createReadStream, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, watch, writeFileSync, writeSync } from "node:fs";
2
+ import { accessSync, appendFileSync, chmodSync, closeSync, constants, createReadStream, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, symlinkSync, unlinkSync, watch, writeFileSync, writeSync } from "node:fs";
3
3
  import http, { createServer } from "node:http";
4
4
  import path from "node:path";
5
5
  import zlib, { createGzip } from "node:zlib";
@@ -22,7 +22,7 @@ import { sumBy } from "es-toolkit";
22
22
  import { DatabaseSync } from "node:sqlite";
23
23
  import { drizzle } from "drizzle-orm/node-sqlite";
24
24
  import { migrate } from "drizzle-orm/node-sqlite/migrator";
25
- import { integer, real, snakeCase, text } from "drizzle-orm/sqlite-core";
25
+ import { integer, primaryKey, real, snakeCase, text } from "drizzle-orm/sqlite-core";
26
26
  import sharp from "sharp";
27
27
  import webPush from "web-push";
28
28
  import dns from "node:dns";
@@ -302,6 +302,29 @@ function notice(text) {
302
302
  isError: true
303
303
  };
304
304
  }
305
+ function resultNotice(message) {
306
+ if (message.subtype === "success" || message.terminal_reason === "aborted_streaming" || message.terminal_reason === "aborted_tools") return null;
307
+ return notice(message.errors.filter((error) => !error.startsWith("[ede_diagnostic]")).join("\n") || message.subtype);
308
+ }
309
+ const failingReasons = [
310
+ "api_error",
311
+ "malformed_tool_use_exhausted",
312
+ "budget_exhausted",
313
+ "structured_output_retry_exhausted",
314
+ "tool_deferred_unavailable",
315
+ "turn_setup_failed",
316
+ "blocking_limit",
317
+ "rapid_refill_breaker",
318
+ "prompt_too_long",
319
+ "image_error",
320
+ "model_error"
321
+ ];
322
+ function turnFailed(message) {
323
+ if (message.terminal_reason === "aborted_tools" || message.terminal_reason === "aborted_streaming") return false;
324
+ if (message.subtype === "success") return message.api_error_status === 429 || message.api_error_status === 529 || message.is_error && message.api_error_status === 403 || failingReasons.includes(message.terminal_reason);
325
+ const errors = message.errors.join("\n").toLowerCase();
326
+ return !errors.includes("interrupt") && !errors.includes("cancel");
327
+ }
305
328
  function contextTokens(message) {
306
329
  if (!isRecord(message) || !isRecord(message.usage)) return null;
307
330
  const usage = message.usage;
@@ -459,18 +482,26 @@ function readJson(name) {
459
482
  throw error;
460
483
  }
461
484
  }
462
- function readSettings(name, schema, defaults) {
485
+ function readSettings(name, schema, defaults, migrate = (value) => value) {
463
486
  const file = path.join(home, name);
464
- let value;
487
+ let saved;
465
488
  try {
466
- value = readJson(name);
489
+ saved = readJson(name);
467
490
  } catch (error) {
468
491
  log$19.warn(`invalid ${file}, using the defaults:`, error);
469
492
  return structuredClone(defaults);
470
493
  }
471
- if (value === null) return structuredClone(defaults);
494
+ if (saved === null) return structuredClone(defaults);
495
+ const value = migrate(saved);
496
+ if (value !== saved) {
497
+ writeJson(`${name}.bak`, saved);
498
+ log$19.info(`migrated ${file}, the old file is in ${file}.bak`);
499
+ }
472
500
  let result = schema.safeParse(value);
473
- if (result.success) return result.data;
501
+ if (result.success) {
502
+ if (value !== saved) writeJson(name, result.data);
503
+ return result.data;
504
+ }
474
505
  while (!result.success) {
475
506
  const issue = result.error.issues[0];
476
507
  const targets = issue.code === "unrecognized_keys" ? issue.keys.map((key) => [...issue.path, key]) : [issue.path];
@@ -525,11 +556,11 @@ function installOpen(host, port) {
525
556
  "wslview"
526
557
  ]) {
527
558
  const wrapper = path.join(folder, name);
528
- writeFileSync(wrapper, `#!/bin/sh\nexec ${quote(process.execPath)} ${quote(script)} ${name} "$@"\n`);
559
+ writeFileSync(wrapper, `#!/bin/sh\nexec ${quote$1(process.execPath)} ${quote$1(script)} ${name} "$@"\n`);
529
560
  chmodSync(wrapper, 493);
530
561
  }
531
562
  const startup = path.join(folder, "env.sh");
532
- writeFileSync(startup, `export PATH=${quote(folder)}:"$PATH"\n`);
563
+ writeFileSync(startup, `export PATH=${quote$1(folder)}:"$PATH"\n`);
533
564
  env = {
534
565
  PATH: process.env.PATH === void 0 ? folder : `${folder}${path.delimiter}${process.env.PATH}`,
535
566
  BROWSER: path.join(folder, process.platform === "darwin" ? "open" : "xdg-open"),
@@ -544,7 +575,7 @@ function loopback(host) {
544
575
  if (host.includes(":")) return `[${host}]`;
545
576
  return host;
546
577
  }
547
- function quote(value) {
578
+ function quote$1(value) {
548
579
  return `'${value.replaceAll("'", "'\\''")}'`;
549
580
  }
550
581
  // src/lib/codex/app-server.ts
@@ -722,7 +753,11 @@ var AppServer = class {
722
753
  publishCodexLimits(this.home, params.rateLimits);
723
754
  return;
724
755
  }
725
- listener?.notify(message.method, params);
756
+ if (listener === void 0) {
757
+ owner?.notifyAgent?.(message.method, params);
758
+ return;
759
+ }
760
+ listener.notify(message.method, params);
726
761
  }
727
762
  rootOf(threadId) {
728
763
  if (typeof threadId !== "string") return;
@@ -1130,10 +1165,7 @@ function record$1(value) {
1130
1165
  return isRecord(value) ? value : { value };
1131
1166
  }
1132
1167
  // src/storage/config.ts
1133
- const modelSetting = z.strictObject({
1134
- hidden: z.boolean(),
1135
- effort: z.string().nullable()
1136
- });
1168
+ const modelSetting = z.strictObject({ hidden: z.boolean() });
1137
1169
  const generationModel = z.strictObject({
1138
1170
  provider: z.enum([
1139
1171
  "claude",
@@ -1162,27 +1194,31 @@ function accountSchema(provider) {
1162
1194
  enabled: z.boolean().default(true)
1163
1195
  });
1164
1196
  }
1197
+ const modelSettings = z.record(z.string(), modelSetting).default({});
1165
1198
  const configSchema = z.strictObject({
1166
- claudeBaseDir: platformDirs,
1199
+ version: z.literal(2).default(2),
1200
+ claude: z.strictObject({
1201
+ enabled: z.boolean().default(true),
1202
+ baseDir: platformDirs,
1203
+ accounts: z.array(accountSchema("claude")).default([]),
1204
+ models: modelSettings
1205
+ }).prefault({}),
1206
+ codex: z.strictObject({
1207
+ enabled: z.boolean().default(true),
1208
+ accounts: z.array(accountSchema("codex")).default([]),
1209
+ models: modelSettings
1210
+ }).prefault({}),
1211
+ opencode: z.strictObject({
1212
+ enabled: z.boolean().default(true),
1213
+ models: modelSettings
1214
+ }).prefault({}),
1167
1215
  workingDir: z.strictObject({
1168
1216
  macos: workingDir(path.posix.isAbsolute),
1169
1217
  linux: workingDir(path.posix.isAbsolute),
1170
1218
  windows: workingDir(path.win32.isAbsolute)
1171
1219
  }).prefault({}),
1172
- claudeAccounts: z.array(accountSchema("claude")).default([]),
1173
- codexAccounts: z.array(accountSchema("codex")).default([]),
1174
- enabledProviders: z.strictObject({
1175
- claude: z.boolean().default(true),
1176
- codex: z.boolean().default(true),
1177
- opencode: z.boolean().default(true)
1178
- }).prefault({}),
1179
1220
  commitMessage: generationModel,
1180
1221
  titleGeneration: generationModel,
1181
- models: z.strictObject({
1182
- claude: z.record(z.string(), modelSetting).default({}),
1183
- codex: z.record(z.string(), modelSetting).default({}),
1184
- opencode: z.record(z.string(), modelSetting).default({})
1185
- }).prefault({}),
1186
1222
  delivery: z.enum(["queue", "steer"]).default("steer"),
1187
1223
  autoSkipQuestionTimeout: z.number().positive().nullable().default(180),
1188
1224
  autoPermissionMode: z.enum([
@@ -1216,7 +1252,7 @@ function platform() {
1216
1252
  throw new Error(`unsupported platform ${process.platform}`);
1217
1253
  }
1218
1254
  function claudeBaseDir() {
1219
- return readConfig().claudeBaseDir[platform()] ?? path.join(os.homedir(), ".claude");
1255
+ return readConfig().claude.baseDir[platform()] ?? path.join(os.homedir(), ".claude");
1220
1256
  }
1221
1257
  function workingDirectory() {
1222
1258
  const dir = readConfig().workingDir[platform()] ?? "~";
@@ -1234,11 +1270,42 @@ function configInfo() {
1234
1270
  defaultShell: defaultShell(),
1235
1271
  defaultClaudeBaseDir: path.join(os.homedir(), ".claude"),
1236
1272
  homeDir: os.homedir(),
1237
- scratchAvailable
1273
+ scratchAvailable,
1274
+ version: "v0.0.96"
1238
1275
  };
1239
1276
  }
1240
1277
  function readConfig() {
1241
- return readSettings("config.json", configSchema, defaults$1);
1278
+ return readSettings("config.json", configSchema, defaults$1, migrate$1);
1279
+ }
1280
+ function migrate$1(value) {
1281
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return value;
1282
+ if (!("version" in value)) return fromVersion1(value);
1283
+ if (value.version === 2) return value;
1284
+ throw new Error(`config.json has version ${String(value.version)}, which this version of offsite cannot read. Update offsite`);
1285
+ }
1286
+ function fromVersion1(value) {
1287
+ const { claudeBaseDir, claudeAccounts, codexAccounts, enabledProviders, models, ...rest } = value;
1288
+ const enabled = enabledProviders ?? {};
1289
+ const providerModels = models ?? {};
1290
+ return {
1291
+ ...rest,
1292
+ version: 2,
1293
+ claude: {
1294
+ enabled: enabled.claude,
1295
+ baseDir: claudeBaseDir,
1296
+ accounts: claudeAccounts,
1297
+ models: providerModels.claude
1298
+ },
1299
+ codex: {
1300
+ enabled: enabled.codex,
1301
+ accounts: codexAccounts,
1302
+ models: providerModels.codex
1303
+ },
1304
+ opencode: {
1305
+ enabled: enabled.opencode,
1306
+ models: providerModels.opencode
1307
+ }
1308
+ };
1242
1309
  }
1243
1310
  function saveConfig(value) {
1244
1311
  const result = configSchema.safeParse(value);
@@ -1266,6 +1333,7 @@ function activity() {
1266
1333
  prompted: run.prompted,
1267
1334
  compacting: run.compacting,
1268
1335
  backgroundAgent: run.backgroundAgents > 0,
1336
+ holdsCompletion: !run.failed && run.agentTasks > 0,
1269
1337
  tasks: run.tasks
1270
1338
  };
1271
1339
  return result;
@@ -1314,6 +1382,24 @@ function scheduleAutoResolve(sessionId, request) {
1314
1382
  }
1315
1383
  };
1316
1384
  }
1385
+ // src/lib/codex/agents.ts
1386
+ function trackAgents(agents, method, params) {
1387
+ const threadId = params.threadId;
1388
+ if (method === "turn/started" && agents.has(threadId)) agents.set(threadId, true);
1389
+ if (method === "turn/completed" && agents.has(threadId)) agents.set(threadId, false);
1390
+ if (method !== "item/completed") return;
1391
+ const item = params.item;
1392
+ if (item.type === "subAgentActivity") {
1393
+ if (item.kind === "started" && !agents.has(item.agentThreadId)) agents.set(item.agentThreadId, true);
1394
+ if ((item.kind === "interrupted" || item.kind === "completed") && agents.has(item.agentThreadId)) agents.set(item.agentThreadId, false);
1395
+ }
1396
+ if (item.type === "collabAgentToolCall") {
1397
+ if (item.tool === "spawnAgent") {
1398
+ for (const agent of item.receiverThreadIds) if (!agents.has(agent)) agents.set(agent, true);
1399
+ }
1400
+ for (const [agent, state] of Object.entries(item.agentsStates)) if (agents.get(agent) === true) agents.set(agent, state.status === "pendingInit" || state.status === "running");
1401
+ }
1402
+ }
1317
1403
  // src/lib/codex/presets.ts
1318
1404
  const presets = /* @__PURE__ */ new Map([
1319
1405
  ["read-only", {
@@ -1466,11 +1552,13 @@ var CodexRun = class CodexRun {
1466
1552
  compacting = false;
1467
1553
  backgroundTasks = 0;
1468
1554
  backgroundAgents = 0;
1555
+ failed = false;
1469
1556
  tasks = {};
1470
1557
  activeAt = Date.now();
1471
1558
  model;
1472
1559
  close;
1473
1560
  turnId = null;
1561
+ agents = /* @__PURE__ */ new Map();
1474
1562
  idleTimer;
1475
1563
  static async start(cwd, settings) {
1476
1564
  const server = codexServer(settings);
@@ -1579,6 +1667,7 @@ var CodexRun = class CodexRun {
1579
1667
  }
1580
1668
  notify(method, params) {
1581
1669
  this.activeAt = Date.now();
1670
+ this.notifyAgent(method, params);
1582
1671
  switch (method) {
1583
1672
  case "turn/started": {
1584
1673
  const turn = params.turn;
@@ -1592,6 +1681,7 @@ var CodexRun = class CodexRun {
1592
1681
  case "turn/completed": {
1593
1682
  const turn = params.turn;
1594
1683
  if (turn.status === "failed" && turn.error !== null) this.put(errorItem(`${turn.id}:error`, turn.error.message));
1684
+ this.failed = turn.status === "failed" || turn.status === "inProgress";
1595
1685
  this.finishTurn();
1596
1686
  return;
1597
1687
  }
@@ -1657,6 +1747,14 @@ var CodexRun = class CodexRun {
1657
1747
  case "serverRequest/resolved": this.settle(String(params.requestId));
1658
1748
  }
1659
1749
  }
1750
+ notifyAgent(method, params) {
1751
+ const running = this.agentTasks;
1752
+ trackAgents(this.agents, method, params);
1753
+ if (this.agentTasks !== running) broadcastActivity();
1754
+ }
1755
+ get agentTasks() {
1756
+ return [...this.agents.values()].filter((running) => running).length;
1757
+ }
1660
1758
  request(id, method, params) {
1661
1759
  if (method === "mcpServer/elicitation/request") {
1662
1760
  this.server.reply(id, {
@@ -2667,6 +2765,8 @@ var OpenCodeRun = class OpenCodeRun {
2667
2765
  compacting = false;
2668
2766
  backgroundTasks = 0;
2669
2767
  backgroundAgents = 0;
2768
+ agentTasks = 0;
2769
+ failed = false;
2670
2770
  tasks = {};
2671
2771
  close;
2672
2772
  applied;
@@ -3057,6 +3157,15 @@ const lastUsedModels = snakeCase.table("last_used_models", {
3057
3157
  model: text().notNull(),
3058
3158
  usedAt: integer().notNull()
3059
3159
  });
3160
+ const lastEfforts = snakeCase.table("last_efforts", {
3161
+ provider: text({ enum: [
3162
+ "claude",
3163
+ "codex",
3164
+ "opencode"
3165
+ ] }).notNull(),
3166
+ model: text().notNull(),
3167
+ effort: text()
3168
+ }, (table) => [primaryKey({ columns: [table.provider, table.model] })]);
3060
3169
  // src/storage/auth.ts
3061
3170
  const CODE_LIFETIME = 3e5;
3062
3171
  const DEVICE_LIFETIME = 2592e6;
@@ -3443,7 +3552,7 @@ const UNTRACKED_LIMIT = 100;
3443
3552
  const NEW_FILE_LIMIT = 262144;
3444
3553
  const REPO_DEPTH = 3;
3445
3554
  const FETCH_TIMEOUT = 6e4;
3446
- const run$3 = promisify(execFile);
3555
+ const run$4 = promisify(execFile);
3447
3556
  const PULL_REQUEST_INSTRUCTIONS = `You write GitHub pull requests as Markdown.
3448
3557
 
3449
3558
  ## TITLE RULES
@@ -3475,6 +3584,7 @@ Return one Markdown document. The title is the level-1 heading (\`# <title>\`),
3475
3584
  async function gitRepos(cwd) {
3476
3585
  return Promise.all((await projectRepos(cwd)).map(async (repo) => ({
3477
3586
  ...await gitStatus(path.join(cwd, repo)),
3587
+ ...await lineChanges(path.join(cwd, repo)),
3478
3588
  path: repo
3479
3589
  })));
3480
3590
  }
@@ -3535,7 +3645,7 @@ async function sync(cwd) {
3535
3645
  async function fetchProject(cwd) {
3536
3646
  await Promise.all((await projectRepos(cwd)).map(async (repo) => {
3537
3647
  try {
3538
- await run$3("git", ["fetch"], {
3648
+ await run$4("git", ["fetch"], {
3539
3649
  cwd: path.join(cwd, repo),
3540
3650
  env: {
3541
3651
  ...process.env,
@@ -3566,7 +3676,7 @@ async function switchBranch(cwd, branch, create) {
3566
3676
  async function pullRequestBranches(cwd) {
3567
3677
  const head = (await gitStatus(cwd)).branch;
3568
3678
  if (head === null) throw new Error("Not on a branch");
3569
- const base = (await run$3("gh", [
3679
+ const base = (await run$4("gh", [
3570
3680
  "repo",
3571
3681
  "view",
3572
3682
  "--json",
@@ -3591,7 +3701,7 @@ async function preparePullRequest(cwd, base, commitChanges) {
3591
3701
  if (commitChanges) await commit(cwd, "", true);
3592
3702
  const status = await gitStatus(cwd);
3593
3703
  if (status.upstream === null || status.ahead > 0) await push$1(cwd);
3594
- const open = (await run$3("gh", [
3704
+ const open = (await run$4("gh", [
3595
3705
  "pr",
3596
3706
  "list",
3597
3707
  "--head",
@@ -3638,7 +3748,7 @@ async function preparePullRequest(cwd, base, commitChanges) {
3638
3748
  };
3639
3749
  }
3640
3750
  async function createPullRequest(cwd, request) {
3641
- const url = (await run$3("gh", [
3751
+ const url = (await run$4("gh", [
3642
3752
  "pr",
3643
3753
  "create",
3644
3754
  "--title",
@@ -3671,7 +3781,7 @@ async function projectDiff(cwd) {
3671
3781
  return (await Promise.all(repos.map((repo) => repoDiff(path.join(cwd, repo), repo === "" ? "" : `${repo}/`)))).join("");
3672
3782
  }
3673
3783
  function git(cwd, args) {
3674
- return run$3("git", args, {
3784
+ return run$4("git", args, {
3675
3785
  cwd,
3676
3786
  maxBuffer: 67108864
3677
3787
  });
@@ -3708,6 +3818,19 @@ async function gitStatus(cwd) {
3708
3818
  }
3709
3819
  return status;
3710
3820
  }
3821
+ async function lineChanges(repo) {
3822
+ let added = 0;
3823
+ let removed = 0;
3824
+ let hunk = false;
3825
+ for (const line of (await repoDiff(repo, "")).split("\n")) if (line.startsWith("diff --git ")) hunk = false;
3826
+ else if (line.startsWith("@@")) hunk = true;
3827
+ else if (hunk && line.startsWith("+")) added++;
3828
+ else if (hunk && line.startsWith("-")) removed++;
3829
+ return {
3830
+ added,
3831
+ removed
3832
+ };
3833
+ }
3711
3834
  async function rebaseOnUpstream(cwd) {
3712
3835
  try {
3713
3836
  await git(cwd, ["rebase", "@{upstream}"]);
@@ -4130,8 +4253,10 @@ function rebuildThreads() {
4130
4253
  const config = readConfig();
4131
4254
  const key = JSON.stringify([
4132
4255
  projectPaths$1(),
4133
- config.enabledProviders,
4134
- config.claudeBaseDir
4256
+ config.claude.enabled,
4257
+ config.codex.enabled,
4258
+ config.opencode.enabled,
4259
+ config.claude.baseDir
4135
4260
  ]);
4136
4261
  if (key === builtFor) return;
4137
4262
  builtFor = key;
@@ -4145,7 +4270,7 @@ function threadFailed(provider) {
4145
4270
  }
4146
4271
  function knownThreads() {
4147
4272
  const known = new Set(projects().map((project) => project.id));
4148
- return allProviders$2.filter((provider) => readConfig().enabledProviders[provider]).flatMap((provider) => [...threads[provider].values()]).filter((item) => known.has(item.projectId));
4273
+ return allProviders$2.filter((provider) => readConfig()[provider].enabled).flatMap((provider) => [...threads[provider].values()]).filter((item) => known.has(item.projectId));
4149
4274
  }
4150
4275
  async function providerThreads(provider) {
4151
4276
  await built;
@@ -4220,7 +4345,7 @@ async function relistAll() {
4220
4345
  await Promise.all(allProviders$2.map((provider) => relist(provider)));
4221
4346
  }
4222
4347
  async function relist(provider) {
4223
- if (!readConfig().enabledProviders[provider]) {
4348
+ if (!readConfig()[provider].enabled) {
4224
4349
  threads[provider] = /* @__PURE__ */ new Map();
4225
4350
  failures.delete(provider);
4226
4351
  broadcast({ type: "threads" });
@@ -4320,7 +4445,7 @@ function withStatus(items) {
4320
4445
  });
4321
4446
  }
4322
4447
  function enabledProviders() {
4323
- return allProviders$1.filter((provider) => readConfig().enabledProviders[provider]);
4448
+ return allProviders$1.filter((provider) => readConfig()[provider].enabled);
4324
4449
  }
4325
4450
  function projectPaths() {
4326
4451
  return projects().map((project) => [project.path, project.id]);
@@ -4339,18 +4464,18 @@ function providerOf(sessionId) {
4339
4464
  if (provider === void 0) throw new Error("offsite does not know this session yet, reload the page");
4340
4465
  return provider;
4341
4466
  }
4342
- function recordInteraction(id, provider) {
4467
+ function recordInteraction(id, provider, status) {
4343
4468
  db.insert(sessions$1).values({
4344
4469
  sessionId: id,
4345
4470
  provider,
4346
4471
  interactedAt: Date.now(),
4347
- status: "active"
4472
+ status: status ?? "active"
4348
4473
  }).onConflictDoUpdate({
4349
4474
  target: sessions$1.sessionId,
4350
4475
  set: {
4351
4476
  provider,
4352
4477
  interactedAt: Date.now(),
4353
- status: sql`CASE ${sessions$1.status} WHEN 'pinned' THEN 'pinned' WHEN 'snoozed' THEN 'snoozed' WHEN 'minimized' THEN 'minimized' ELSE 'active' END`
4478
+ status: status === null ? sql`CASE ${sessions$1.status} WHEN 'pinned' THEN 'pinned' WHEN 'snoozed' THEN 'snoozed' WHEN 'minimized' THEN 'minimized' ELSE 'active' END` : sql`CASE ${sessions$1.status} WHEN 'pinned' THEN 'pinned' ELSE ${status} END`
4354
4479
  }
4355
4480
  }).run();
4356
4481
  }
@@ -4400,7 +4525,7 @@ async function list(provider) {
4400
4525
  const paths = new Map(projectPaths());
4401
4526
  const before = statuses[provider];
4402
4527
  try {
4403
- if (!readConfig().enabledProviders[provider]) {
4528
+ if (!readConfig()[provider].enabled) {
4404
4529
  listed[provider] = [];
4405
4530
  statuses[provider] = {
4406
4531
  provider,
@@ -4455,7 +4580,7 @@ async function lookup(provider, sessionId, paths) {
4455
4580
  function watchSessions() {
4456
4581
  for (const watcher of watchers) watcher.close();
4457
4582
  watchers = [["claude", path.join(claudeBaseDir(), "projects")], ["codex", path.join(codexBaseDir, "sessions")]].flatMap(([provider, dir]) => {
4458
- if (!readConfig().enabledProviders[provider]) return [];
4583
+ if (!readConfig()[provider].enabled) return [];
4459
4584
  try {
4460
4585
  const watcher = watch(dir, { recursive: true }, (_, filename) => {
4461
4586
  fileChanged(provider, filename);
@@ -4549,7 +4674,7 @@ async function fork(sessionId, projectId, messageId) {
4549
4674
  const provider = providerOf(sessionId);
4550
4675
  const result = await forkWith(provider, sessionId, projectPath(projectId), messageId);
4551
4676
  providers.set(result.sessionId, provider);
4552
- recordInteraction(result.sessionId, provider);
4677
+ recordInteraction(result.sessionId, provider, null);
4553
4678
  markChanged(provider, result.sessionId);
4554
4679
  refreshSessions(provider);
4555
4680
  return result;
@@ -4695,16 +4820,16 @@ function providerName(provider) {
4695
4820
  }
4696
4821
  function storedAccounts() {
4697
4822
  const config = readConfig();
4698
- return [...config.claudeAccounts.map((item) => ({
4823
+ return [...config.claude.accounts.map((item) => ({
4699
4824
  ...item,
4700
4825
  configDir: item.configDir[platform()]
4701
- })), ...config.codexAccounts.map((item) => ({
4826
+ })), ...config.codex.accounts.map((item) => ({
4702
4827
  ...item,
4703
4828
  configDir: item.configDir[platform()]
4704
4829
  }))];
4705
4830
  }
4706
4831
  function assertEnabled(provider, accountId) {
4707
- if (!readConfig().enabledProviders[provider]) throw new Error(`${providerName(provider)} is disabled in Settings`);
4832
+ if (!readConfig()[provider].enabled) throw new Error(`${providerName(provider)} is disabled in Settings`);
4708
4833
  if (provider === "opencode") return;
4709
4834
  if (!storedAccount(provider, accountId).enabled) throw new Error("That account is disabled in Settings");
4710
4835
  }
@@ -4734,7 +4859,7 @@ function storedAccount(provider, accountId) {
4734
4859
  }
4735
4860
  async function accounts() {
4736
4861
  const stored = await Promise.all(storedAccounts().map((account) => {
4737
- if (!account.enabled || !readConfig().enabledProviders[account.provider]) return disabledAccount(account);
4862
+ if (!account.enabled || !readConfig()[account.provider].enabled) return disabledAccount(account);
4738
4863
  switch (account.provider) {
4739
4864
  case "claude": return claudeAccount(account);
4740
4865
  case "codex": return codexAccount(account);
@@ -4743,7 +4868,7 @@ async function accounts() {
4743
4868
  return {
4744
4869
  claudeAccounts: stored.filter((item) => item.provider === "claude"),
4745
4870
  codexAccounts: stored.filter((item) => item.provider === "codex"),
4746
- openCode: { available: openCodeInstalled() && readConfig().enabledProviders.opencode }
4871
+ openCode: { available: openCodeInstalled() && readConfig().opencode.enabled }
4747
4872
  };
4748
4873
  }
4749
4874
  function disabledAccount(account) {
@@ -4772,23 +4897,20 @@ async function publishAccounts() {
4772
4897
  async function capabilities(provider, accountId, cwd) {
4773
4898
  assertEnabled(provider, accountId);
4774
4899
  const result = await providerCapabilities(provider, accountId, cwd);
4775
- const settings = readConfig().models[provider];
4900
+ const settings = readConfig()[provider].models;
4776
4901
  return {
4777
4902
  ...result,
4778
4903
  defaultModel: defaultModels[provider],
4779
4904
  models: result.models.map((model) => {
4780
4905
  const deprecated = deprecatedModels[provider].includes(model.value);
4781
- const setting = settings[model.value] ?? {
4782
- hidden: deprecated,
4783
- effort: null
4784
- };
4906
+ const setting = settings[model.value] ?? { hidden: deprecated };
4785
4907
  const preferred = defaultEfforts[provider];
4786
4908
  const effort = preferred === null ? null : model.efforts.includes(preferred) ? preferred : model.efforts.at(-1) ?? null;
4787
4909
  return {
4788
4910
  ...model,
4789
4911
  deprecated,
4790
4912
  hidden: setting.hidden,
4791
- effort: setting.effort ?? effort
4913
+ effort
4792
4914
  };
4793
4915
  })
4794
4916
  };
@@ -4805,51 +4927,80 @@ function providerCapabilities(provider, accountId, cwd) {
4805
4927
  function addAccount(provider, name, configDir) {
4806
4928
  const account = validAccount(name, configDir);
4807
4929
  const config = readConfig();
4808
- const key = `${provider}Accounts`;
4809
4930
  saveConfig({
4810
4931
  ...config,
4811
- [key]: [...config[key], {
4812
- id: randomUUID(),
4813
- provider,
4814
- name: account.name,
4815
- color: null,
4816
- configDir: { [platform()]: account.configDir },
4817
- enabled: true
4818
- }]
4932
+ [provider]: {
4933
+ ...config[provider],
4934
+ accounts: [...config[provider].accounts, {
4935
+ id: randomUUID(),
4936
+ provider,
4937
+ name: account.name,
4938
+ color: null,
4939
+ configDir: { [platform()]: account.configDir },
4940
+ enabled: true
4941
+ }]
4942
+ }
4819
4943
  });
4944
+ publishAccounts();
4945
+ return null;
4820
4946
  }
4821
4947
  async function updateAccount(update) {
4822
4948
  const before = storedAccounts().find((item) => item.id === update.id);
4823
4949
  if (before === void 0) throw new Error("That account was removed");
4824
4950
  const account = validAccount(update.name, update.configDir);
4825
4951
  const config = readConfig();
4826
- const key = `${before.provider}Accounts`;
4827
4952
  saveConfig({
4828
4953
  ...config,
4829
- [key]: config[key].map((item) => item.id === update.id ? {
4830
- ...item,
4831
- name: account.name,
4832
- color: update.color,
4833
- configDir: {
4834
- ...item.configDir,
4835
- [platform()]: account.configDir
4836
- },
4837
- enabled: update.enabled
4838
- } : item)
4954
+ [before.provider]: {
4955
+ ...config[before.provider],
4956
+ accounts: config[before.provider].accounts.map((item) => item.id === update.id ? {
4957
+ ...item,
4958
+ name: account.name,
4959
+ color: update.color,
4960
+ configDir: {
4961
+ ...item.configDir,
4962
+ [platform()]: account.configDir
4963
+ },
4964
+ enabled: update.enabled
4965
+ } : item)
4966
+ }
4839
4967
  });
4840
4968
  forgetClaude(update.id);
4841
4969
  forgetUsage(update.id);
4842
4970
  if (before.enabled && !update.enabled) await stopAccount(before);
4971
+ publishAccounts();
4972
+ return null;
4973
+ }
4974
+ function accountLinkPlan(id) {
4975
+ return linkPlan(storedAccount("claude", id).configDir ?? claudeBaseDir());
4976
+ }
4977
+ async function linkAccount(id) {
4978
+ const account = storedAccount("claude", id);
4979
+ await stopAccount(account);
4980
+ try {
4981
+ await linkClaude(account.configDir ?? claudeBaseDir());
4982
+ } finally {
4983
+ publishAccounts();
4984
+ }
4985
+ return null;
4843
4986
  }
4844
4987
  function removeAccount(id) {
4845
4988
  const config = readConfig();
4846
4989
  saveConfig({
4847
4990
  ...config,
4848
- claudeAccounts: config.claudeAccounts.filter((item) => item.id !== id),
4849
- codexAccounts: config.codexAccounts.filter((item) => item.id !== id)
4991
+ claude: {
4992
+ ...config.claude,
4993
+ accounts: config.claude.accounts.filter((item) => item.id !== id)
4994
+ },
4995
+ codex: {
4996
+ ...config.codex,
4997
+ accounts: config.codex.accounts.filter((item) => item.id !== id)
4998
+ }
4850
4999
  });
4851
5000
  forgetClaude(id);
4852
5001
  forgetUsage(id);
5002
+ publishAccounts();
5003
+ return null;
4853
5004
  }
4854
5005
  function validAccount(name, configDir) {
4855
5006
  if (name.trim() === "") throw new Error("Give the account a name");
@@ -4984,6 +5135,213 @@ function targets() {
4984
5135
  if (notifier === null || notifier.endpoint === null) return [];
4985
5136
  return db.select().from(pushSubscriptions).where(and(eq(pushSubscriptions.server, server$1), eq(pushSubscriptions.endpoint, notifier.endpoint))).all();
4986
5137
  }
5138
+ // src/lib/claude/links.ts
5139
+ const run$3 = promisify(execFile);
5140
+ const requiredFolders = [
5141
+ "projects",
5142
+ "file-history",
5143
+ "tasks",
5144
+ "todos",
5145
+ "plans",
5146
+ "paste-cache",
5147
+ "session-env",
5148
+ "jobs",
5149
+ "teams",
5150
+ "sessions"
5151
+ ];
5152
+ const sharedFolders = [
5153
+ "skills",
5154
+ "agents",
5155
+ "commands",
5156
+ "output-styles",
5157
+ "plugins",
5158
+ "cache",
5159
+ "state",
5160
+ "shell-snapshots"
5161
+ ];
5162
+ const sharedFiles = [
5163
+ "settings.json",
5164
+ "CLAUDE.md",
5165
+ "keybindings.json",
5166
+ "history.jsonl"
5167
+ ];
5168
+ function isLinked(configDir) {
5169
+ const base = claudeBaseDir();
5170
+ return sameFile(configDir, base) || requiredFolders.every((name) => sameFile(path.join(configDir, name), path.join(base, name)));
5171
+ }
5172
+ function linkPlan(configDir) {
5173
+ const base = path.resolve(claudeBaseDir());
5174
+ const dir = statSync(configDir, { throwIfNoEntry: false });
5175
+ if (dir === void 0 || !dir.isDirectory()) throw new Error(`${configDir} does not exist`);
5176
+ if (sameFile(configDir, base)) throw new Error(`${configDir} is the main Claude folder`);
5177
+ return {
5178
+ configDir,
5179
+ baseDir: base,
5180
+ entries: [
5181
+ ...requiredFolders.map((name) => entry(configDir, base, name, true, true)),
5182
+ ...sharedFolders.map((name) => entry(configDir, base, name, false, true)),
5183
+ ...sharedFiles.map((name) => entry(configDir, base, name, false, false))
5184
+ ]
5185
+ };
5186
+ }
5187
+ async function linkClaude(configDir) {
5188
+ const plan = linkPlan(configDir);
5189
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replaceAll(":", "-");
5190
+ const steps = plan.entries.flatMap((item) => entrySteps(plan, item, stamp));
5191
+ if (steps.length === 0) return;
5192
+ for (const item of plan.entries) if (item.createInBase) mkdirSync(path.join(plan.baseDir, item.name), { recursive: true });
5193
+ if (process.platform === "win32" && !canSymlink()) await runElevated(steps);
5194
+ else for (const step of steps) runStep(step);
5195
+ const missing = plan.entries.filter((item) => item.action !== "skip" && !sameFile(path.join(plan.configDir, item.name), path.join(plan.baseDir, item.name)));
5196
+ if (missing.length > 0) throw new Error(`Could not link ${missing.map((item) => item.name).join(", ")}`);
5197
+ }
5198
+ function entry(configDir, base, name, required, folder) {
5199
+ const target = path.join(configDir, name);
5200
+ const source = path.join(base, name);
5201
+ const sourceExists = statSync(source, { throwIfNoEntry: false }) !== void 0;
5202
+ return {
5203
+ name,
5204
+ required,
5205
+ folder,
5206
+ action: entryAction(target, source, folder, sourceExists),
5207
+ createInBase: folder && !sourceExists
5208
+ };
5209
+ }
5210
+ function entryAction(target, source, folder, sourceExists) {
5211
+ if (!folder && !sourceExists) return "skip";
5212
+ const existing = lstatSync(target, { throwIfNoEntry: false });
5213
+ if (existing === void 0) return "link";
5214
+ if (sameFile(target, source)) return "linked";
5215
+ if (existing.isSymbolicLink()) return "relink";
5216
+ if (existing.isDirectory() && readdirSync(target).length === 0) return "replace";
5217
+ return "backup";
5218
+ }
5219
+ function entrySteps(plan, item, stamp) {
5220
+ const target = path.join(plan.configDir, item.name);
5221
+ const link = {
5222
+ type: "link",
5223
+ path: target,
5224
+ target: path.join(plan.baseDir, item.name),
5225
+ folder: item.folder
5226
+ };
5227
+ switch (item.action) {
5228
+ case "linked":
5229
+ case "skip": return [];
5230
+ case "link": return [link];
5231
+ case "relink": return [{
5232
+ type: "unlink",
5233
+ path: target
5234
+ }, link];
5235
+ case "replace": return [{
5236
+ type: "rmdir",
5237
+ path: target
5238
+ }, link];
5239
+ case "backup": {
5240
+ const backup = `${target}.backup-${stamp}`;
5241
+ if (lstatSync(backup, { throwIfNoEntry: false }) !== void 0) throw new Error(`${backup} already exists`);
5242
+ return [{
5243
+ type: "move",
5244
+ from: target,
5245
+ to: backup
5246
+ }, link];
5247
+ }
5248
+ }
5249
+ }
5250
+ function sameFile(first, second) {
5251
+ const firstStats = statSync(first, {
5252
+ bigint: true,
5253
+ throwIfNoEntry: false
5254
+ });
5255
+ const secondStats = statSync(second, {
5256
+ bigint: true,
5257
+ throwIfNoEntry: false
5258
+ });
5259
+ if (firstStats === void 0 || secondStats === void 0) return false;
5260
+ return firstStats.dev === secondStats.dev && firstStats.ino === secondStats.ino;
5261
+ }
5262
+ function canSymlink() {
5263
+ const dir = mkdtempSync(path.join(os.tmpdir(), "offsite-"));
5264
+ const link = path.join(dir, "link");
5265
+ try {
5266
+ symlinkSync(dir, link, "dir");
5267
+ } catch (error) {
5268
+ rmdirSync(dir);
5269
+ if (error.code === "EPERM") return false;
5270
+ throw error;
5271
+ }
5272
+ unlinkSync(link);
5273
+ rmdirSync(dir);
5274
+ return true;
5275
+ }
5276
+ function runStep(step) {
5277
+ switch (step.type) {
5278
+ case "unlink":
5279
+ unlinkSync(step.path);
5280
+ return;
5281
+ case "rmdir":
5282
+ rmdirSync(step.path);
5283
+ return;
5284
+ case "move":
5285
+ renameSync(step.from, step.to);
5286
+ return;
5287
+ case "link": symlinkSync(step.target, step.path, step.folder ? "dir" : "file");
5288
+ }
5289
+ }
5290
+ async function runElevated(steps) {
5291
+ const user = await run$3("whoami", [
5292
+ "/user",
5293
+ "/fo",
5294
+ "csv",
5295
+ "/nh"
5296
+ ]);
5297
+ const sid = user.stdout.trim().split(",").at(-1)?.replaceAll("\"", "");
5298
+ if (sid === void 0 || !sid.startsWith("S-1-")) throw new Error(`Could not read your Windows user: ${user.stdout}`);
5299
+ const owner = `*${sid}`;
5300
+ const dir = mkdtempSync(path.join(os.tmpdir(), "offsite-"));
5301
+ const errorFile = path.join(dir, "error.txt");
5302
+ const script = `const fs = require('node:fs')
5303
+ const childProcess = require('node:child_process')
5304
+ const data = ${JSON.stringify({
5305
+ steps,
5306
+ owner,
5307
+ errorFile
5308
+ })}
5309
+ try {
5310
+ for (const step of data.steps) {
5311
+ if (step.type === 'unlink') fs.unlinkSync(step.path)
5312
+ if (step.type === 'rmdir') fs.rmdirSync(step.path)
5313
+ if (step.type === 'move') fs.renameSync(step.from, step.to)
5314
+ if (step.type === 'link') {
5315
+ fs.symlinkSync(step.target, step.path, step.folder ? 'dir' : 'file')
5316
+ childProcess.execFileSync('icacls', [step.path, '/setowner', data.owner, '/L', '/Q'])
5317
+ }
5318
+ }
5319
+ } catch (error) {
5320
+ fs.writeFileSync(data.errorFile, error.message)
5321
+ process.exit(1)
5322
+ }`;
5323
+ const code = `eval(Buffer.from('${Buffer.from(script).toString("base64")}','base64').toString())`;
5324
+ try {
5325
+ await run$3("powershell.exe", [
5326
+ "-NoProfile",
5327
+ "-NonInteractive",
5328
+ "-Command",
5329
+ `$ProgressPreference = 'SilentlyContinue'; try { $process = Start-Process -FilePath ${quote(process.execPath)} -ArgumentList '-e',${quote(code)} -Verb RunAs -WindowStyle Hidden -Wait -PassThru -ErrorAction Stop } catch { [Console]::Error.WriteLine($_.Exception.Message); exit 1 }; exit $process.ExitCode`
5330
+ ]);
5331
+ } catch (error) {
5332
+ if (existsSync(errorFile)) throw new Error(readFileSync(errorFile, "utf8"));
5333
+ if (error instanceof Error && "stderr" in error && typeof error.stderr === "string" && error.stderr.trim() !== "") throw new Error(`Windows admin permission failed: ${error.stderr.trim()}`);
5334
+ throw error;
5335
+ } finally {
5336
+ rmSync(dir, {
5337
+ recursive: true,
5338
+ force: true
5339
+ });
5340
+ }
5341
+ }
5342
+ function quote(value) {
5343
+ return `'${value.replaceAll("'", "''")}'`;
5344
+ }
4987
5345
  // src/lib/claude/claude.ts
4988
5346
  const log$8 = logger.withTag("claude");
4989
5347
  const efforts = [
@@ -5093,7 +5451,8 @@ function runOptions(options) {
5093
5451
  ...settings.model === null ? {} : { model: settings.model },
5094
5452
  ...settings.effort === null ? {} : { effort: claudeEffort(settings.effort) },
5095
5453
  allowDangerouslySkipPermissions: true,
5096
- includePartialMessages: true
5454
+ includePartialMessages: true,
5455
+ perTaskStopAffordance: true
5097
5456
  };
5098
5457
  }
5099
5458
  async function claudeCapabilities(account, cwd) {
@@ -5242,16 +5601,6 @@ function claudeLogin(configDir) {
5242
5601
  } : null
5243
5602
  };
5244
5603
  }
5245
- function isLinked(configDir) {
5246
- const base = claudeBaseDir();
5247
- try {
5248
- if (realpathSync(configDir) === realpathSync(base)) return true;
5249
- return realpathSync(path.join(configDir, "projects")) === realpathSync(path.join(base, "projects"));
5250
- } catch (error) {
5251
- if (error.code === "ENOENT") return false;
5252
- throw error;
5253
- }
5254
- }
5255
5604
  function initialization(accountId, cwd) {
5256
5605
  const key = `${accountId}\n${cwd}`;
5257
5606
  const existing = probes.get(key);
@@ -5812,10 +6161,18 @@ async function image(sessionId, uuid, index) {
5812
6161
  data: block.data
5813
6162
  };
5814
6163
  }
6164
+ async function shownMessages(sessionId, cwd) {
6165
+ const [disk, session] = await Promise.all([getSessionMessages(sessionId, { dir: cwd }), loadSession(sessionId)]);
6166
+ const found = runs.get(sessionId);
6167
+ const saved = session?.entries.findLast((entry) => entry.type === "last-prompt" && entry.rewound === true);
6168
+ const cuts = [found?.provider === "claude" ? found.cut : null, saved?.cutUuid];
6169
+ const index = disk.findIndex((item) => cuts.includes(item.uuid));
6170
+ return index < 0 ? disk : disk.slice(0, index);
6171
+ }
5815
6172
  async function history$1(sessionId, cwd) {
5816
6173
  const found = runs.get(sessionId);
5817
6174
  const run = found?.provider === "claude" ? found : void 0;
5818
- const [disk, session] = await Promise.all([getSessionMessages(sessionId, { dir: cwd }), loadSession(sessionId)]);
6175
+ const [disk, session] = await Promise.all([shownMessages(sessionId, cwd), loadSession(sessionId)]);
5819
6176
  const entries = session === null ? [] : session.entries;
5820
6177
  const resumeAt = run?.resumeAt ?? null;
5821
6178
  const kept = resumeAt === null ? disk : disk.slice(0, disk.findIndex((item) => item.uuid === resumeAt) + 1);
@@ -5858,6 +6215,28 @@ async function agentMessages(sessionId, cwd, agentId, parent) {
5858
6215
  parent_tool_use_id: parent
5859
6216
  }, null)]));
5860
6217
  }
6218
+ // src/lib/claude/permission.ts
6219
+ function permissionResult(input, suggestions, decision, answers) {
6220
+ switch (decision) {
6221
+ case "allow": return {
6222
+ behavior: "allow",
6223
+ updatedInput: answers === null ? input : {
6224
+ ...input,
6225
+ answers
6226
+ }
6227
+ };
6228
+ case "always": return {
6229
+ behavior: "allow",
6230
+ updatedInput: input,
6231
+ updatedPermissions: suggestions
6232
+ };
6233
+ case "deny": return {
6234
+ behavior: "deny",
6235
+ message: "The user rejected this action",
6236
+ interrupt: true
6237
+ };
6238
+ }
6239
+ }
5861
6240
  // src/lib/claude/run.ts
5862
6241
  const IDLE_LIMIT = 6e5;
5863
6242
  var Run = class {
@@ -5870,6 +6249,7 @@ var Run = class {
5870
6249
  query;
5871
6250
  closed;
5872
6251
  resumeAt;
6252
+ cut = null;
5873
6253
  queue = [];
5874
6254
  settings;
5875
6255
  context = null;
@@ -5880,6 +6260,8 @@ var Run = class {
5880
6260
  compacting = false;
5881
6261
  backgroundTasks = 0;
5882
6262
  backgroundAgents = 0;
6263
+ agentTasks = 0;
6264
+ failed = false;
5883
6265
  tasks = {};
5884
6266
  inbox = [];
5885
6267
  wake = null;
@@ -5952,30 +6334,7 @@ var Run = class {
5952
6334
  respond(requestId, decision, answers) {
5953
6335
  const pending = this.pending.get(requestId);
5954
6336
  if (pending === void 0) return;
5955
- const input = pending.input;
5956
- switch (decision) {
5957
- case "allow":
5958
- this.settle(requestId, {
5959
- behavior: "allow",
5960
- updatedInput: answers === null ? input : {
5961
- ...input,
5962
- answers
5963
- }
5964
- });
5965
- return;
5966
- case "always":
5967
- this.settle(requestId, {
5968
- behavior: "allow",
5969
- updatedInput: input,
5970
- updatedPermissions: pending.suggestions
5971
- });
5972
- return;
5973
- case "deny": this.settle(requestId, {
5974
- behavior: "deny",
5975
- message: "The user rejected this action",
5976
- interrupt: true
5977
- });
5978
- }
6337
+ this.settle(requestId, permissionResult(pending.input, pending.suggestions, decision, answers));
5979
6338
  }
5980
6339
  stop() {
5981
6340
  this.stopping = true;
@@ -6069,7 +6428,9 @@ var Run = class {
6069
6428
  return;
6070
6429
  }
6071
6430
  case "result": {
6072
- if (message.subtype !== "success" && message.terminal_reason !== "aborted_streaming" && message.terminal_reason !== "aborted_tools") this.add(notice(message.errors.filter((error) => !error.startsWith("[ede_diagnostic]")).join("\n") || message.subtype));
6431
+ const failure = resultNotice(message);
6432
+ if (failure !== null) this.add(failure);
6433
+ this.failed = turnFailed(message);
6073
6434
  this.finishTurn();
6074
6435
  const usage = this.model === null ? void 0 : message.modelUsage[this.model];
6075
6436
  if (this.context !== null && usage !== void 0) this.context = {
@@ -6091,6 +6452,7 @@ var Run = class {
6091
6452
  });
6092
6453
  return;
6093
6454
  case "background_tasks_changed": {
6455
+ this.agentTasks = message.tasks.filter((task) => task.task_type !== "local_bash").length;
6094
6456
  const live = message.tasks.filter((task) => task.ambient !== true);
6095
6457
  this.backgroundTasks = live.length;
6096
6458
  this.backgroundAgents = live.filter((task) => task.task_type === "local_agent").length;
@@ -6237,6 +6599,30 @@ var Run = class {
6237
6599
  });
6238
6600
  }
6239
6601
  };
6602
+ // src/lib/claude/rewind.ts
6603
+ const ATTEMPTS = 10;
6604
+ const answerSchema = z.object({ response: z.discriminatedUnion("rewound", [z.object({ rewound: z.literal(true) }), z.object({
6605
+ rewound: z.literal(false),
6606
+ reason: z.string(),
6607
+ error: z.string()
6608
+ })]) });
6609
+ async function rewindLive(run, messageId, lastSeenId) {
6610
+ const request = Reflect.get(run.query, "request");
6611
+ if (typeof request !== "function") throw new Error("This Claude Code version cannot rewind a live session");
6612
+ for (let attempt = 1;; attempt++) {
6613
+ const answer = answerSchema.parse(await Reflect.apply(request, run.query, [{
6614
+ subtype: "rewind_conversation",
6615
+ target_message_uuid: messageId,
6616
+ last_seen_user_message_uuid: lastSeenId
6617
+ }])).response;
6618
+ if (answer.rewound) break;
6619
+ if (answer.reason !== "turn_running" || attempt === ATTEMPTS) throw new Error(`Claude Code could not rewind: ${answer.error}`);
6620
+ await setTimeout$1(500);
6621
+ }
6622
+ const index = run.messages.findIndex((item) => item.uuid === messageId);
6623
+ run.messages.splice(index < 0 ? 0 : index);
6624
+ run.cut = messageId;
6625
+ }
6240
6626
  // src/lib/proxy/sites.ts
6241
6627
  const sites = /* @__PURE__ */ new Map();
6242
6628
  const siteNames = /* @__PURE__ */ new Map();
@@ -6381,6 +6767,13 @@ function sessionHistory(sessionId, cwd) {
6381
6767
  case "opencode": return openCodeHistory(sessionId, cwd);
6382
6768
  }
6383
6769
  }
6770
+ function agentHistory(sessionId, cwd, agentId, parent) {
6771
+ switch (providerOf(sessionId)) {
6772
+ case "claude": return agentMessages(sessionId, cwd, agentId, parent);
6773
+ case "codex": return codexAgent(agentId, parent);
6774
+ case "opencode": return openCodeAgent(agentId, parent);
6775
+ }
6776
+ }
6384
6777
  function rewind(request) {
6385
6778
  return locked(request.sessionId, () => rewindNow(request));
6386
6779
  }
@@ -6388,7 +6781,6 @@ async function rewindNow(request) {
6388
6781
  const sessionId = request.sessionId;
6389
6782
  const cwd = projectPath(request.projectId);
6390
6783
  if (runs.get(sessionId)?.running === true) throw new Error(`Stop ${providerName(providerOf(sessionId))} before rewinding`);
6391
- if ((runs.get(sessionId)?.backgroundTasks ?? 0) > 0) throw new Error("Wait for background tasks to finish before rewinding");
6392
6784
  const run = await runFor(sessionId, cwd, request.settings);
6393
6785
  if (run.provider === "codex") {
6394
6786
  if (request.code) throw new Error("Codex cannot restore files");
@@ -6418,9 +6810,16 @@ async function rewindNow(request) {
6418
6810
  };
6419
6811
  }
6420
6812
  if (request.dryRun || !request.conversation) return result;
6421
- const disk = await getSessionMessages(sessionId, { dir: cwd });
6813
+ const disk = await shownMessages(sessionId, cwd);
6422
6814
  const index = disk.findIndex((item) => item.uuid === request.messageId);
6423
6815
  if (index < 1) throw new Error("Cannot rewind the conversation to that point");
6816
+ if (run.backgroundTasks > 0) {
6817
+ const lastSeen = disk.findLast((item) => item.type === "user" && item.parent_tool_use_id === null);
6818
+ if (lastSeen === void 0) throw new Error("Cannot rewind the conversation to that point");
6819
+ await rewindLive(run, request.messageId, lastSeen.uuid);
6820
+ publish$2(sessionId, await history$1(sessionId, cwd));
6821
+ return result;
6822
+ }
6424
6823
  run.stop();
6425
6824
  await run.closed;
6426
6825
  new Run({
@@ -6433,6 +6832,24 @@ async function rewindNow(request) {
6433
6832
  publish$2(sessionId, await history$1(sessionId, cwd));
6434
6833
  return result;
6435
6834
  }
6835
+ function restart(sessionId) {
6836
+ return locked(sessionId, async () => {
6837
+ const run = runs.get(sessionId);
6838
+ if (run === void 0) return;
6839
+ if (run.provider !== "claude") throw new Error("Only Claude Code threads can be restarted");
6840
+ if (run.running) await run.interrupt();
6841
+ run.stop();
6842
+ await run.closed;
6843
+ new Run({
6844
+ sessionId,
6845
+ cwd: run.cwd,
6846
+ resume: true,
6847
+ resumeAt: null,
6848
+ settings: run.settings
6849
+ });
6850
+ publish$2(sessionId, await history$1(sessionId, run.cwd));
6851
+ });
6852
+ }
6436
6853
  async function open$1(client, sessionId, projectId) {
6437
6854
  client.opening = sessionId;
6438
6855
  const event = await sessionHistory(sessionId, projectPath(projectId));
@@ -6471,7 +6888,7 @@ async function submitFirst(client, run, request, projectId) {
6471
6888
  from: request.projectId
6472
6889
  });
6473
6890
  await run.submit(request.prompt, request.delivery);
6474
- recordInteraction(sessionId, run.provider);
6891
+ recordInteraction(sessionId, run.provider, request.status);
6475
6892
  }
6476
6893
  async function sendExisting(client, sessionId, cwd, request) {
6477
6894
  const provider = providerOf(sessionId);
@@ -6485,7 +6902,7 @@ async function sendExisting(client, sessionId, cwd, request) {
6485
6902
  if (run === existing) await run.configure(request.settings);
6486
6903
  else await open$1(client, sessionId, request.projectId);
6487
6904
  await run.submit(request.prompt, request.delivery);
6488
- recordInteraction(sessionId, provider);
6905
+ recordInteraction(sessionId, provider, request.status);
6489
6906
  if (emptied.has(sessionId) && request.prompt.text.trim() !== "") {
6490
6907
  emptied.delete(sessionId);
6491
6908
  await updateSession(sessionId, { title: request.prompt.text.trim() });
@@ -6535,7 +6952,7 @@ async function startRun(cwd, settings) {
6535
6952
  }
6536
6953
  async function changedElsewhere(run, cwd) {
6537
6954
  if (run.messages.length === 0) return false;
6538
- const last = (await getSessionMessages(run.sessionId, { dir: cwd })).at(-1)?.uuid;
6955
+ const last = (await shownMessages(run.sessionId, cwd)).at(-1)?.uuid;
6539
6956
  return last !== void 0 && !run.messages.some((item) => item.uuid === last);
6540
6957
  }
6541
6958
  // src/services/export.ts
@@ -6675,7 +7092,7 @@ async function searchMessages(query) {
6675
7092
  "claude",
6676
7093
  "codex",
6677
7094
  "opencode"
6678
- ].filter((provider) => readConfig().enabledProviders[provider]).map(async (provider) => {
7095
+ ].filter((provider) => readConfig()[provider].enabled).map(async (provider) => {
6679
7096
  try {
6680
7097
  return {
6681
7098
  provider,
@@ -8064,6 +8481,15 @@ const lastUsedSchema = z.discriminatedUnion("provider", [z.strictObject({
8064
8481
  accountId: z.null(),
8065
8482
  model: z.string()
8066
8483
  })]);
8484
+ const lastEffortSchema = z.strictObject({
8485
+ provider: z.enum([
8486
+ "claude",
8487
+ "codex",
8488
+ "opencode"
8489
+ ]),
8490
+ model: z.string(),
8491
+ effort: z.string().nullable()
8492
+ });
8067
8493
  function readLastUsed() {
8068
8494
  return db.select().from(lastUsedModels).all();
8069
8495
  }
@@ -8084,6 +8510,22 @@ function saveLastUsed(value) {
8084
8510
  });
8085
8511
  return null;
8086
8512
  }
8513
+ function readLastEfforts() {
8514
+ return db.select().from(lastEfforts).all();
8515
+ }
8516
+ function saveLastEffort(value) {
8517
+ const result = lastEffortSchema.safeParse(value);
8518
+ if (!result.success) throw new Error(z.prettifyError(result.error));
8519
+ db.insert(lastEfforts).values(result.data).onConflictDoUpdate({
8520
+ target: [lastEfforts.provider, lastEfforts.model],
8521
+ set: { effort: result.data.effort }
8522
+ }).run();
8523
+ broadcast({
8524
+ type: "lastEfforts",
8525
+ lastEfforts: readLastEfforts()
8526
+ });
8527
+ return null;
8528
+ }
8087
8529
  // ../core/src/app-icons.ts
8088
8530
  const appIcons = [
8089
8531
  {
@@ -8184,6 +8626,7 @@ const handlers = {
8184
8626
  return {
8185
8627
  ...configInfo(),
8186
8628
  lastUsed: readLastUsed(),
8629
+ lastEfforts: readLastEfforts(),
8187
8630
  hostname: os.hostname(),
8188
8631
  projects: projects(),
8189
8632
  sessions: sessions(),
@@ -8195,6 +8638,7 @@ const handlers = {
8195
8638
  },
8196
8639
  "config.get": () => configInfo(),
8197
8640
  "lastUsed.save": (params) => saveLastUsed(params),
8641
+ "lastEffort.save": (params) => saveLastEffort(params),
8198
8642
  "appIcon.save": (params) => saveAppIcon(params),
8199
8643
  "config.save": async (params) => {
8200
8644
  const before = readConfig();
@@ -8203,8 +8647,8 @@ const handlers = {
8203
8647
  "claude",
8204
8648
  "codex",
8205
8649
  "opencode"
8206
- ]) if (before.enabledProviders[provider] && !saved.enabledProviders[provider]) await stopProvider(provider);
8207
- if (JSON.stringify(saved.claudeBaseDir) === JSON.stringify(before.claudeBaseDir) && JSON.stringify(saved.enabledProviders) === JSON.stringify(before.enabledProviders)) return saved;
8650
+ ]) if (before[provider].enabled && !saved[provider].enabled) await stopProvider(provider);
8651
+ if (JSON.stringify(saved.claude.baseDir) === JSON.stringify(before.claude.baseDir) && saved.claude.enabled === before.claude.enabled && saved.codex.enabled === before.codex.enabled && saved.opencode.enabled === before.opencode.enabled) return saved;
8208
8652
  watchSessions();
8209
8653
  refreshAll();
8210
8654
  publishAccounts();
@@ -8256,13 +8700,7 @@ const handlers = {
8256
8700
  "sessions.earlier": (params) => earlier(params.sessionId, params.before),
8257
8701
  "sessions.export": (params) => exportSession(params.sessionId, params.projectId),
8258
8702
  "sessions.image": (params) => image(params.sessionId, params.itemId, params.index),
8259
- "sessions.agent": (params) => {
8260
- switch (providerOf(params.sessionId)) {
8261
- case "claude": return agentMessages(params.sessionId, projectPath(params.projectId), params.agentId, params.parent);
8262
- case "codex": return codexAgent(params.agentId, params.parent);
8263
- case "opencode": return openCodeAgent(params.agentId, params.parent);
8264
- }
8265
- },
8703
+ "sessions.agent": (params) => agentHistory(params.sessionId, projectPath(params.projectId), params.agentId, params.parent),
8266
8704
  "sessions.send": async (params, client) => {
8267
8705
  await send$1(client, params);
8268
8706
  return null;
@@ -8279,6 +8717,10 @@ const handlers = {
8279
8717
  await runs.get(params.sessionId)?.interrupt();
8280
8718
  return null;
8281
8719
  },
8720
+ "sessions.restart": async (params) => {
8721
+ await restart(params.sessionId);
8722
+ return null;
8723
+ },
8282
8724
  "sessions.stopTask": async (params) => {
8283
8725
  await runs.get(params.sessionId)?.stopTask(params.taskId);
8284
8726
  return null;
@@ -8291,7 +8733,7 @@ const handlers = {
8291
8733
  const run = runs.get(params.sessionId);
8292
8734
  if (run === void 0) return null;
8293
8735
  await run.respond(params.requestId, params.decision, params.answers);
8294
- recordInteraction(params.sessionId, run.provider);
8736
+ recordInteraction(params.sessionId, run.provider, null);
8295
8737
  return null;
8296
8738
  },
8297
8739
  "sessions.rewind": (params) => rewind(params),
@@ -8301,21 +8743,11 @@ const handlers = {
8301
8743
  return result;
8302
8744
  },
8303
8745
  "accounts.list": () => accounts(),
8304
- "accounts.add": (params) => {
8305
- addAccount(params.provider, params.name, params.configDir);
8306
- publishAccounts();
8307
- return null;
8308
- },
8309
- "accounts.update": async (params) => {
8310
- await updateAccount(params);
8311
- publishAccounts();
8312
- return null;
8313
- },
8314
- "accounts.remove": (params) => {
8315
- removeAccount(params.id);
8316
- publishAccounts();
8317
- return null;
8318
- },
8746
+ "accounts.add": (params) => addAccount(params.provider, params.name, params.configDir),
8747
+ "accounts.update": (params) => updateAccount(params),
8748
+ "accounts.remove": (params) => removeAccount(params.id),
8749
+ "accounts.link": (params) => linkAccount(params.id),
8750
+ "accounts.linkPlan": (params) => accountLinkPlan(params.id),
8319
8751
  "accounts.usage": (params) => readUsage(params.id, params.force),
8320
8752
  capabilities: (params) => capabilities(params.provider, params.accountId, params.projectId === null || params.projectId === "no-project" ? os.homedir() : projectPath(params.projectId)),
8321
8753
  "files.list": (params) => params.projectId === "no-project" ? [] : listFiles(projectPath(params.projectId)),
@@ -9700,7 +10132,7 @@ async function backfillSessionProviders() {
9700
10132
  const rows = db.select().from(sessions$1).where(isNull(sessions$1.provider)).all();
9701
10133
  if (rows.length === 0) return;
9702
10134
  const paths = new Map(projects().map((project) => [project.path, project.id]));
9703
- const enabled = allProviders.filter((provider) => readConfig().enabledProviders[provider]);
10135
+ const enabled = allProviders.filter((provider) => readConfig()[provider].enabled);
9704
10136
  for (const row of rows) {
9705
10137
  const answers = await Promise.all(enabled.map(async (provider) => {
9706
10138
  try {