offsite 0.0.45 → 0.0.47

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 (126) hide show
  1. package/README.md +1 -10
  2. package/apps/server/dist/main.mjs +665 -238
  3. package/apps/server/dist/main.mjs.map +1 -1
  4. package/apps/web/dist/assets/abap-08VXUWAP.js +1 -0
  5. package/apps/web/dist/assets/accounts-CccrDGY4.js +512 -0
  6. package/apps/web/dist/assets/apex-BWPQTe0t.js +1 -0
  7. package/apps/web/dist/assets/audio-player-BE-oXEaT.js +1 -0
  8. package/apps/web/dist/assets/azcli-Bc_sGQ0U.js +1 -0
  9. package/apps/web/dist/assets/bat-i0X4ZdIN.js +1 -0
  10. package/apps/web/dist/assets/bicep-B5-_aFwp.js +2 -0
  11. package/apps/web/dist/assets/button-Dhs1rqob.js +1 -0
  12. package/apps/web/dist/assets/cameligo-DMUM7wLl.js +1 -0
  13. package/apps/web/dist/assets/clojure-Cm7r79vr.js +1 -0
  14. package/apps/web/dist/assets/codicon-DF1abBS2.ttf +0 -0
  15. package/apps/web/dist/assets/coffee-Ba7i2nA0.js +1 -0
  16. package/apps/web/dist/assets/cpp-C7h46wYY.js +1 -0
  17. package/apps/web/dist/assets/csharp-BKxtCVv1.js +1 -0
  18. package/apps/web/dist/assets/csp-bTuwJoIa.js +1 -0
  19. package/apps/web/dist/assets/css-DIMkf-bt.js +3 -0
  20. package/apps/web/dist/assets/cypher-CVaqCwHa.js +1 -0
  21. package/apps/web/dist/assets/dart-onAF5SnQ.js +1 -0
  22. package/apps/web/dist/assets/{preview-dialog-jk_R2034.js → dialog-Bnoy9Qj1.js} +12 -12
  23. package/apps/web/dist/assets/dist-BrDshb6Z.js +153 -0
  24. package/apps/web/dist/assets/dist-CFVYlZbS.js +1 -0
  25. package/apps/web/dist/assets/dockerfile-DZFCIeNp.js +1 -0
  26. package/apps/web/dist/assets/ecl-D05T4iGw.js +1 -0
  27. package/apps/web/dist/assets/editor-DjpsE8to.css +1 -0
  28. package/apps/web/dist/assets/editor-dialog-BNFI__d8.css +1 -0
  29. package/apps/web/dist/assets/editor-dialog-OElejzVH.js +52 -0
  30. package/apps/web/dist/assets/editor.api-4LH2ijIL.js +908 -0
  31. package/apps/web/dist/assets/editor.worker-BmdWdrHZ.js +33 -0
  32. package/apps/web/dist/assets/editor.worker-D2ouzCu8.js +1 -0
  33. package/apps/web/dist/assets/elixir-6RTg0lbw.js +1 -0
  34. package/apps/web/dist/assets/flow9-C5_-GSwl.js +1 -0
  35. package/apps/web/dist/assets/freemarker2-6NYS9HqW.js +3 -0
  36. package/apps/web/dist/assets/fsharp-C8Ef5oNN.js +1 -0
  37. package/apps/web/dist/assets/go-C-y9NEjX.js +1 -0
  38. package/apps/web/dist/assets/graphql-fmXr3nnJ.js +1 -0
  39. package/apps/web/dist/assets/handlebars-DOeNnhTE.js +1 -0
  40. package/apps/web/dist/assets/hcl-CpzslTdj.js +1 -0
  41. package/apps/web/dist/assets/html-BwGTVsqZ.js +1 -0
  42. package/apps/web/dist/assets/image-viewer-CwKW6KbM.js +1 -0
  43. package/apps/web/dist/assets/index-B57iKlGT.js +9 -0
  44. package/apps/web/dist/assets/index-pX3wrk_t.css +2 -0
  45. package/apps/web/dist/assets/ini-sBoK_t0W.js +1 -0
  46. package/apps/web/dist/assets/java-BEtHBSE6.js +1 -0
  47. package/apps/web/dist/assets/javascript-Dj-Vzbp-.js +1 -0
  48. package/apps/web/dist/assets/julia-Bri6UV-V.js +1 -0
  49. package/apps/web/dist/assets/kotlin-BOotOW0E.js +1 -0
  50. package/apps/web/dist/assets/less-B9JPFI3C.js +2 -0
  51. package/apps/web/dist/assets/lexon-CfSJPG6W.js +1 -0
  52. package/apps/web/dist/assets/liquid-DDSU3ltH.js +1 -0
  53. package/apps/web/dist/assets/lua-CsQS60Ue.js +1 -0
  54. package/apps/web/dist/assets/m3-D-oSqn_W.js +1 -0
  55. package/apps/web/dist/assets/markdown-Cimd5fb3.js +1 -0
  56. package/apps/web/dist/assets/mdx-YllfWysm.js +1 -0
  57. package/apps/web/dist/assets/mips-CIPQ_RoX.js +1 -0
  58. package/apps/web/dist/assets/msdax-DauUninz.js +1 -0
  59. package/apps/web/dist/assets/mysql-SOo6toE5.js +1 -0
  60. package/apps/web/dist/assets/objective-c-FvmIjYaQ.js +1 -0
  61. package/apps/web/dist/assets/pascal-DrH0SRf2.js +1 -0
  62. package/apps/web/dist/assets/pascaligo-D-ptJ9y-.js +1 -0
  63. package/apps/web/dist/assets/perl-oz_6vUea.js +1 -0
  64. package/apps/web/dist/assets/pgsql-DTj74zXo.js +1 -0
  65. package/apps/web/dist/assets/php-nr791fC2.js +1 -0
  66. package/apps/web/dist/assets/pla-CopQ2nXW.js +1 -0
  67. package/apps/web/dist/assets/postiats-43DmfD33.js +1 -0
  68. package/apps/web/dist/assets/powerquery-D3hlyOfw.js +1 -0
  69. package/apps/web/dist/assets/powershell-DmHpPYUd.js +1 -0
  70. package/apps/web/dist/assets/preview-dialog-dl8oUCEx.js +1 -0
  71. package/apps/web/dist/assets/protobuf-C531GsRP.js +2 -0
  72. package/apps/web/dist/assets/pug-Z5eAx3Zn.js +1 -0
  73. package/apps/web/dist/assets/python-Bsfjs4Fs.js +1 -0
  74. package/apps/web/dist/assets/qsharp-DkqhCAOL.js +1 -0
  75. package/apps/web/dist/assets/r-BwWrilGY.js +1 -0
  76. package/apps/web/dist/assets/razor-3MUz77_n.js +1 -0
  77. package/apps/web/dist/assets/{react-99wk3OSj.js → react-DHkIXOR_.js} +1 -1
  78. package/apps/web/dist/assets/redis-ClamHrr6.js +1 -0
  79. package/apps/web/dist/assets/redshift-DT7zqm-g.js +1 -0
  80. package/apps/web/dist/assets/restructuredtext-BYgofb2h.js +1 -0
  81. package/apps/web/dist/assets/ruby-DezsRK8O.js +1 -0
  82. package/apps/web/dist/assets/rust-DdL9SqIa.js +1 -0
  83. package/apps/web/dist/assets/sb-CcwsVR0C.js +1 -0
  84. package/apps/web/dist/assets/scala-DHpiXF5c.js +1 -0
  85. package/apps/web/dist/assets/scheme-BeGwcela.js +1 -0
  86. package/apps/web/dist/assets/scss-gp-XZpBa.js +3 -0
  87. package/apps/web/dist/assets/shell-CC2rA5mh.js +1 -0
  88. package/apps/web/dist/assets/solidity-BEEn4gHE.js +1 -0
  89. package/apps/web/dist/assets/sophia-CRfGWb83.js +1 -0
  90. package/apps/web/dist/assets/sparql-D_Lu-MrJ.js +1 -0
  91. package/apps/web/dist/assets/sql-NEE52Syq.js +1 -0
  92. package/apps/web/dist/assets/st-DbInun42.js +1 -0
  93. package/apps/web/dist/assets/swift-Bxkupp3x.js +1 -0
  94. package/apps/web/dist/assets/systemverilog-Bz4Y3fRF.js +1 -0
  95. package/apps/web/dist/assets/tcl-DISqw1ZD.js +1 -0
  96. package/apps/web/dist/assets/tooltip-CGSEdyeY.js +1 -0
  97. package/apps/web/dist/assets/twig-De2hgUGE.js +1 -0
  98. package/apps/web/dist/assets/typescript-CfCJo9p3.js +1 -0
  99. package/apps/web/dist/assets/typescript-fNZ3iur6.js +1 -0
  100. package/apps/web/dist/assets/typespec-B8J7ngcE.js +1 -0
  101. package/apps/web/dist/assets/utils-KVq36YOZ.js +1 -0
  102. package/apps/web/dist/assets/vb-DV3o63ZY.js +1 -0
  103. package/apps/web/dist/assets/video-viewer-DIbmaY7P.js +1 -0
  104. package/apps/web/dist/assets/wgsl-DpFanUEy.js +298 -0
  105. package/apps/web/dist/assets/xml-S-oLW5B5.js +1 -0
  106. package/apps/web/dist/assets/yaml-DlmqY7sN.js +1 -0
  107. package/apps/web/dist/assets/zoomable-image-Dpslbx28.js +1 -0
  108. package/apps/web/dist/assets/zoomable-image-oNBiTN5V.js +41 -0
  109. package/apps/web/dist/index.html +8 -8
  110. package/apps/web/dist/sw.js +1 -1
  111. package/package.json +2 -1
  112. package/apps/web/dist/assets/audio-player-BBuXiV2_.js +0 -1
  113. package/apps/web/dist/assets/button-BF3wjXXJ.js +0 -1
  114. package/apps/web/dist/assets/dist-3jCLwLR5.js +0 -1
  115. package/apps/web/dist/assets/dist-CkDQKgh1.js +0 -153
  116. package/apps/web/dist/assets/image-viewer-4LhMaTO_.js +0 -1
  117. package/apps/web/dist/assets/index-CDkqSE9K.js +0 -782
  118. package/apps/web/dist/assets/index-D3NpzVu2.css +0 -2
  119. package/apps/web/dist/assets/jsx-runtime-DKj5X9O4.js +0 -1
  120. package/apps/web/dist/assets/tooltip-CiWlkHa6.js +0 -1
  121. package/apps/web/dist/assets/utils-PtDt7m8t.js +0 -1
  122. package/apps/web/dist/assets/video-player-Bcow2OC5.js +0 -1
  123. package/apps/web/dist/assets/video-player-hDoAwJRk.js +0 -1
  124. package/apps/web/dist/assets/video-viewer-CJ129jI3.js +0 -1
  125. package/apps/web/dist/assets/zoomable-image-CZlWBm_z.js +0 -41
  126. package/apps/web/dist/assets/zoomable-image-DyuZJmov.js +0 -1
@@ -1,5 +1,5 @@
1
1
  import { createHash, randomBytes, randomUUID } from "node:crypto";
2
- import { chmodSync, closeSync, createReadStream, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, watch, writeFileSync } from "node:fs";
2
+ import { appendFileSync, chmodSync, closeSync, createReadStream, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, watch, writeFileSync } from "node:fs";
3
3
  import http, { createServer } from "node:http";
4
4
  import path from "node:path";
5
5
  import { consola } from "consola";
@@ -21,6 +21,7 @@ import { DatabaseSync } from "node:sqlite";
21
21
  import { drizzle } from "drizzle-orm/node-sqlite";
22
22
  import { migrate } from "drizzle-orm/node-sqlite/migrator";
23
23
  import { integer, snakeCase, text } from "drizzle-orm/sqlite-core";
24
+ import sharp from "sharp";
24
25
  import { promisify } from "node:util";
25
26
  import webPush from "web-push";
26
27
  import { spawn as spawn$1 } from "@lydell/node-pty";
@@ -47,6 +48,24 @@ const mediaTypes = /* @__PURE__ */ new Map([
47
48
  [".flac", "audio/flac"]
48
49
  ]);
49
50
  //#endregion
51
+ //#region ../core/src/terminal.ts
52
+ const GLOBAL_TERMINALS = "global";
53
+ function encodeFrame(kind, id, data) {
54
+ const frame = new Uint8Array(5 + data.length);
55
+ frame[0] = kind;
56
+ new DataView(frame.buffer).setUint32(1, id);
57
+ frame.set(data, 5);
58
+ return frame;
59
+ }
60
+ function decodeFrame(frame) {
61
+ if (frame.length < 5) throw new Error("terminal frame is too short");
62
+ return {
63
+ kind: frame[0],
64
+ id: new DataView(frame.buffer, frame.byteOffset, frame.byteLength).getUint32(1),
65
+ data: frame.subarray(5)
66
+ };
67
+ }
68
+ //#endregion
50
69
  //#region src/constants.ts
51
70
  const deprecatedModels = {
52
71
  claude: [
@@ -171,6 +190,43 @@ function readJson(name) {
171
190
  throw error;
172
191
  }
173
192
  }
193
+ function readSettings(name, schema, defaults) {
194
+ const file = path.join(home, name);
195
+ let value;
196
+ try {
197
+ value = readJson(name);
198
+ } catch (error) {
199
+ console.warn(`invalid ${file}, using the defaults: ${error}`);
200
+ return structuredClone(defaults);
201
+ }
202
+ if (value === null) return structuredClone(defaults);
203
+ let result = schema.safeParse(value);
204
+ if (result.success) return result.data;
205
+ while (!result.success) {
206
+ const issue = result.error.issues[0];
207
+ const targets = issue.code === "unrecognized_keys" ? issue.keys.map((key) => [...issue.path, key]) : [issue.path];
208
+ for (const target of targets) {
209
+ const discarded = discard(value, target);
210
+ if (discarded === null) {
211
+ console.warn(`invalid ${file}, using the defaults:\n${z.prettifyError(result.error)}`);
212
+ return structuredClone(defaults);
213
+ }
214
+ console.warn(`${file}: discarded ${discarded.map(String).join(".")} (${issue.message} at ${target.map(String).join(".")})`);
215
+ }
216
+ result = schema.safeParse(value);
217
+ }
218
+ writeJson(name, result.data);
219
+ return result.data;
220
+ }
221
+ function discard(value, target) {
222
+ const key = target.at(0);
223
+ if (key === void 0 || typeof value !== "object" || value === null || !Object.hasOwn(value, key)) return null;
224
+ const inner = target.length > 1 ? discard(Reflect.get(value, key), target.slice(1)) : null;
225
+ if (inner !== null) return [key, ...inner];
226
+ if (Array.isArray(value)) value.splice(Number(key), 1);
227
+ else Reflect.deleteProperty(value, key);
228
+ return [key];
229
+ }
174
230
  function writeJson(name, value) {
175
231
  let file = path.join(home, name);
176
232
  mkdirSync(home, { recursive: true });
@@ -880,7 +936,6 @@ function record$1(value) {
880
936
  }
881
937
  //#endregion
882
938
  //#region src/storage/config.ts
883
- const configFile = path.join(home, "config.json");
884
939
  const modelSetting = z.strictObject({
885
940
  hidden: z.boolean(),
886
941
  effort: z.string().nullable()
@@ -901,8 +956,7 @@ function accountSchema(provider) {
901
956
  name: z.string(),
902
957
  color: z.string().regex(/^#[0-9a-f]{6}$/i, "must be a hex color like #3b82f6").nullable().default(null),
903
958
  configDir: platformDirs,
904
- enabled: z.boolean().default(true),
905
- hiddenModels: z.record(z.string(), z.boolean()).default({})
959
+ enabled: z.boolean().default(true)
906
960
  });
907
961
  }
908
962
  const configSchema = z.strictObject({
@@ -919,6 +973,19 @@ const configSchema = z.strictObject({
919
973
  codex: z.boolean().default(true),
920
974
  opencode: z.boolean().default(true)
921
975
  }).prefault({}),
976
+ newThreads: z.strictObject({
977
+ provider: z.enum([
978
+ "claude",
979
+ "codex",
980
+ "opencode"
981
+ ]).default("claude"),
982
+ accountId: z.string().nullable().default(null),
983
+ models: z.strictObject({
984
+ claude: z.string().nullable().default(null),
985
+ codex: z.string().nullable().default(null),
986
+ opencode: z.string().nullable().default(null)
987
+ }).prefault({})
988
+ }).prefault({}),
922
989
  commitMessage: z.strictObject({
923
990
  provider: z.enum([
924
991
  "claude",
@@ -981,20 +1048,7 @@ function configInfo() {
981
1048
  };
982
1049
  }
983
1050
  function readConfig() {
984
- let user;
985
- try {
986
- user = readJson("config.json");
987
- } catch (error) {
988
- console.warn(`invalid ${configFile}, using the default config: ${error}`);
989
- return structuredClone(defaults$1);
990
- }
991
- if (user === null) return structuredClone(defaults$1);
992
- const result = configSchema.safeParse(user);
993
- if (!result.success) {
994
- console.warn(`invalid ${configFile}, using the default config:\n${z.prettifyError(result.error)}`);
995
- return structuredClone(defaults$1);
996
- }
997
- return result.data;
1051
+ return readSettings("config.json", configSchema, defaults$1);
998
1052
  }
999
1053
  function saveConfig(value) {
1000
1054
  const result = configSchema.safeParse(value);
@@ -2564,6 +2618,7 @@ const sessions$1 = snakeCase.table("sessions", {
2564
2618
  status: text({ enum: [
2565
2619
  "active",
2566
2620
  "pinned",
2621
+ "snoozed",
2567
2622
  "minimized"
2568
2623
  ] }).notNull()
2569
2624
  });
@@ -2589,13 +2644,201 @@ const pushSubscriptions = snakeCase.table("push_subscriptions", {
2589
2644
  server: text().notNull()
2590
2645
  });
2591
2646
  //#endregion
2647
+ //#region src/services/files.ts
2648
+ const FILE_LIMIT = 2e4;
2649
+ const OUTPUT_LIMIT$1 = 262144;
2650
+ const READ_LIMIT = 1048576;
2651
+ const MEDIA_LINK_LIFETIME = 36e5;
2652
+ const browseToken = randomBytes(32).toString("base64url");
2653
+ const mediaLinks = /* @__PURE__ */ new Map();
2654
+ const skipped = /* @__PURE__ */ new Set([
2655
+ "node_modules",
2656
+ ".git",
2657
+ ".DS_Store",
2658
+ "dist",
2659
+ "target",
2660
+ ".next",
2661
+ "__pycache__",
2662
+ ".venv"
2663
+ ]);
2664
+ async function listFiles(cwd) {
2665
+ return (await listEntries(cwd)).files;
2666
+ }
2667
+ async function listEntries(cwd) {
2668
+ const files = [];
2669
+ const folders = [];
2670
+ const pending = [""];
2671
+ while (pending.length > 0 && files.length < FILE_LIMIT) {
2672
+ const dir = pending.pop() ?? "";
2673
+ let entries;
2674
+ try {
2675
+ entries = await readdir(path.join(cwd, dir), { withFileTypes: true });
2676
+ } catch {
2677
+ continue;
2678
+ }
2679
+ for (const entry of entries) {
2680
+ if (skipped.has(entry.name)) continue;
2681
+ const relative = dir === "" ? entry.name : `${dir}/${entry.name}`;
2682
+ if (entry.isDirectory()) {
2683
+ pending.push(relative);
2684
+ folders.push(relative);
2685
+ }
2686
+ if (entry.isFile()) files.push(relative);
2687
+ if (entry.isSymbolicLink()) {
2688
+ if ((await stat(path.join(cwd, relative)).catch(() => null))?.isFile() === true) files.push(relative);
2689
+ }
2690
+ }
2691
+ }
2692
+ return {
2693
+ files: files.slice(0, FILE_LIMIT).sort(),
2694
+ folders: folders.sort()
2695
+ };
2696
+ }
2697
+ function readProjectFile(cwd, file) {
2698
+ const full = resolveProjectFile(cwd, file);
2699
+ const stats = statSync(full);
2700
+ if (!stats.isFile()) throw new Error(`${file} is not a file`);
2701
+ if (stats.size > READ_LIMIT) return {
2702
+ kind: "large",
2703
+ size: stats.size,
2704
+ limit: READ_LIMIT
2705
+ };
2706
+ const data = readFileSync(full);
2707
+ const mediaType = mediaTypes.get(path.extname(full).toLowerCase());
2708
+ if (mediaType?.startsWith("image/") === true) return {
2709
+ kind: "image",
2710
+ size: stats.size,
2711
+ mediaType,
2712
+ data: data.toString("base64")
2713
+ };
2714
+ if (data.includes(0)) return {
2715
+ kind: "binary",
2716
+ size: stats.size
2717
+ };
2718
+ return {
2719
+ kind: "text",
2720
+ size: stats.size,
2721
+ content: data.toString("utf8")
2722
+ };
2723
+ }
2724
+ function writeProjectFile(cwd, file, content) {
2725
+ const full = resolveProjectFile(cwd, file);
2726
+ if (!statSync(full).isFile()) throw new Error(`${file} is not a file`);
2727
+ writeFileSync(full, content);
2728
+ }
2729
+ function createProjectEntry(cwd, file, folder) {
2730
+ const full = resolveNewProjectPath(cwd, file);
2731
+ if (folder) {
2732
+ mkdirSync(full, { recursive: true });
2733
+ return;
2734
+ }
2735
+ mkdirSync(path.dirname(full), { recursive: true });
2736
+ writeFileSync(full, "", { flag: "wx" });
2737
+ }
2738
+ function renameProjectEntry(cwd, file, to) {
2739
+ const source = resolveProjectEntry(cwd, file);
2740
+ const target = resolveNewProjectPath(cwd, to);
2741
+ mkdirSync(path.dirname(target), { recursive: true });
2742
+ renameSync(source, target);
2743
+ }
2744
+ function deleteProjectEntry(cwd, file) {
2745
+ rmSync(resolveProjectEntry(cwd, file), { recursive: true });
2746
+ }
2747
+ function linkProjectMedia(cwd, file) {
2748
+ const resolved = path.resolve(realpathSync(cwd), file);
2749
+ if (!existsSync(resolved)) throw new Error(`${file} does not exist`);
2750
+ const full = realpathSync(resolved);
2751
+ const mediaType = mediaTypes.get(path.extname(full).toLowerCase());
2752
+ if (mediaType === void 0) throw new Error(`${file} is not an image, video or audio file`);
2753
+ return addLink(full, mediaType);
2754
+ }
2755
+ function addLink(file, mediaType) {
2756
+ for (const [token, link] of mediaLinks) if (link.expiresAt <= Date.now()) mediaLinks.delete(token);
2757
+ const token = randomBytes(32).toString("base64url");
2758
+ mediaLinks.set(token, {
2759
+ file,
2760
+ mediaType,
2761
+ expiresAt: Date.now() + MEDIA_LINK_LIFETIME
2762
+ });
2763
+ return token;
2764
+ }
2765
+ function findMediaLink(token) {
2766
+ if (token === null) return null;
2767
+ const link = mediaLinks.get(token);
2768
+ if (link === void 0 || link.expiresAt <= Date.now()) return null;
2769
+ return link;
2770
+ }
2771
+ function saveUpload(name, data) {
2772
+ const dir = path.join(home, "uploads", randomUUID());
2773
+ mkdirSync(dir, { recursive: true });
2774
+ const file = path.join(dir, path.basename(name));
2775
+ writeFileSync(file, data);
2776
+ return file;
2777
+ }
2778
+ async function taskOutput(sessionId, file) {
2779
+ const tasks = path.dirname(file);
2780
+ if (!/^[\w-]+$/.test(sessionId) || !path.isAbsolute(file) || path.normalize(file) !== file || !/^\w+\.output$/.test(path.basename(file)) || path.basename(tasks) !== "tasks" || path.basename(path.dirname(tasks)) !== sessionId) throw new Error("Not a background task output file of this thread");
2781
+ let stats;
2782
+ try {
2783
+ stats = await lstat(file);
2784
+ } catch (error) {
2785
+ if (hasCode(error, "ENOENT")) return null;
2786
+ throw error;
2787
+ }
2788
+ if (!stats.isFile()) throw new Error("Background task output is not a regular file");
2789
+ const start = Math.max(0, stats.size - OUTPUT_LIMIT$1);
2790
+ const handle = await open(file, "r");
2791
+ try {
2792
+ const buffer = Buffer.alloc(stats.size - start);
2793
+ const read = await handle.read(buffer, 0, buffer.length, start);
2794
+ return {
2795
+ text: buffer.subarray(0, read.bytesRead).toString("utf8"),
2796
+ truncated: start > 0
2797
+ };
2798
+ } finally {
2799
+ await handle.close();
2800
+ }
2801
+ }
2802
+ function resolveProjectFile(cwd, file) {
2803
+ const root = realpathSync(cwd);
2804
+ const resolved = path.resolve(root, file);
2805
+ if (!existsSync(resolved)) throw new Error(`${file} does not exist`);
2806
+ const full = realpathSync(resolved);
2807
+ checkInProject(root, full, file);
2808
+ return full;
2809
+ }
2810
+ function resolveProjectEntry(cwd, file) {
2811
+ const root = realpathSync(cwd);
2812
+ const resolved = path.resolve(root, file);
2813
+ if (!existsSync(path.dirname(resolved))) throw new Error(`${file} does not exist`);
2814
+ const full = path.join(realpathSync(path.dirname(resolved)), path.basename(resolved));
2815
+ if (full === root) throw new Error("The project folder itself cannot be changed");
2816
+ checkInProject(root, full, file);
2817
+ lstatSync(full);
2818
+ return full;
2819
+ }
2820
+ function resolveNewProjectPath(cwd, file) {
2821
+ const root = realpathSync(cwd);
2822
+ const resolved = path.resolve(root, file);
2823
+ if (existsSync(resolved)) throw new Error(`${file} already exists`);
2824
+ let existing = path.dirname(resolved);
2825
+ while (!existsSync(existing)) existing = path.dirname(existing);
2826
+ const full = path.join(realpathSync(existing), path.relative(existing, resolved));
2827
+ checkInProject(root, full, file);
2828
+ return full;
2829
+ }
2830
+ function checkInProject(root, full, file) {
2831
+ const relative = path.relative(root, full);
2832
+ if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) throw new Error(`${file} is outside the project`);
2833
+ }
2834
+ //#endregion
2592
2835
  //#region src/services/git.ts
2593
2836
  const DIFF_LIMIT = 8e4;
2594
2837
  const UNTRACKED_LIMIT = 100;
2595
2838
  const NEW_FILE_LIMIT = 262144;
2596
2839
  const REPO_DEPTH = 3;
2597
2840
  const FETCH_TIMEOUT = 6e4;
2598
- const run$1 = promisify(execFile);
2841
+ const run$2 = promisify(execFile);
2599
2842
  const PULL_REQUEST_INSTRUCTIONS = `You write GitHub pull requests as Markdown.
2600
2843
 
2601
2844
  ## TITLE RULES
@@ -2631,7 +2874,7 @@ async function gitRepos(cwd) {
2631
2874
  })));
2632
2875
  }
2633
2876
  async function commit(cwd, message, andPush) {
2634
- const text = message.trim() === "" ? await commitMessage(cwd) : message;
2877
+ const text = message.trim() === "" ? await commitMessage(cwd, false) : message;
2635
2878
  await git(cwd, ["add", "--all"]);
2636
2879
  await git(cwd, [
2637
2880
  "commit",
@@ -2682,7 +2925,7 @@ async function sync(cwd) {
2682
2925
  async function fetchProject(cwd) {
2683
2926
  await Promise.all((await projectRepos(cwd)).map(async (repo) => {
2684
2927
  try {
2685
- await run$1("git", ["fetch"], {
2928
+ await run$2("git", ["fetch"], {
2686
2929
  cwd: path.join(cwd, repo),
2687
2930
  env: {
2688
2931
  ...process.env,
@@ -2713,7 +2956,7 @@ async function switchBranch(cwd, branch, create) {
2713
2956
  async function pullRequestBranches(cwd) {
2714
2957
  const head = (await gitStatus(cwd)).branch;
2715
2958
  if (head === null) throw new Error("Not on a branch");
2716
- const base = (await run$1("gh", [
2959
+ const base = (await run$2("gh", [
2717
2960
  "repo",
2718
2961
  "view",
2719
2962
  "--json",
@@ -2738,7 +2981,7 @@ async function preparePullRequest(cwd, base, commitChanges) {
2738
2981
  if (commitChanges) await commit(cwd, "", true);
2739
2982
  const status = await gitStatus(cwd);
2740
2983
  if (status.upstream === null || status.ahead > 0) await push$1(cwd);
2741
- const open = (await run$1("gh", [
2984
+ const open = (await run$2("gh", [
2742
2985
  "pr",
2743
2986
  "list",
2744
2987
  "--head",
@@ -2785,7 +3028,7 @@ async function preparePullRequest(cwd, base, commitChanges) {
2785
3028
  };
2786
3029
  }
2787
3030
  async function createPullRequest(cwd, request) {
2788
- const url = (await run$1("gh", [
3031
+ const url = (await run$2("gh", [
2789
3032
  "pr",
2790
3033
  "create",
2791
3034
  "--title",
@@ -2802,8 +3045,13 @@ async function createPullRequest(cwd, request) {
2802
3045
  if (url === void 0) throw new Error("gh did not return a pull request URL");
2803
3046
  return { url };
2804
3047
  }
2805
- async function commitMessage(cwd) {
2806
- const diff = (await repoDiff(cwd, "")).slice(0, DIFF_LIMIT);
3048
+ async function commitMessage(cwd, staged) {
3049
+ const diff = (staged ? (await git(cwd, [
3050
+ "diff",
3051
+ "--cached",
3052
+ "--no-color",
3053
+ "--no-ext-diff"
3054
+ ])).stdout : await repoDiff(cwd, "")).slice(0, DIFF_LIMIT);
2807
3055
  if (diff === "") throw new Error("There is nothing to commit");
2808
3056
  return generateText(cwd, "You write concise git commit messages.", `Write a git commit message for this diff. Reply with only the message: a short summary line, optionally followed by a blank line and a brief body.\n\n${diff}`);
2809
3057
  }
@@ -2841,7 +3089,7 @@ async function projectDiff(cwd) {
2841
3089
  return (await Promise.all(repos.map((repo) => repoDiff(path.join(cwd, repo), repo === "" ? "" : `${repo}/`)))).join("");
2842
3090
  }
2843
3091
  function git(cwd, args) {
2844
- return run$1("git", args, {
3092
+ return run$2("git", args, {
2845
3093
  cwd,
2846
3094
  maxBuffer: 67108864
2847
3095
  });
@@ -2966,13 +3214,27 @@ function newFilePatch(cwd, file, prefix) {
2966
3214
  if (lines.at(-1) === "") lines.pop();
2967
3215
  return `${header}@@ -0,0 +1,${lines.length} @@\n${lines.map((line) => `+${line}\n`).join("")}`;
2968
3216
  }
3217
+ //#endregion
3218
+ //#region src/services/logos.ts
3219
+ const LOGO_LIMIT = 524288;
3220
+ const LOGO_SIZE = 256;
3221
+ const RESIZED_TYPES = [
3222
+ "image/png",
3223
+ "image/jpeg",
3224
+ "image/webp"
3225
+ ];
3226
+ const KEPT_TYPES = [
3227
+ "image/svg+xml",
3228
+ "image/x-icon",
3229
+ "image/vnd.microsoft.icon"
3230
+ ];
3231
+ const PICKABLE_FILE = /\.(png|jpe?g|webp|svg)$/i;
2969
3232
  const LOGO_FILES = /* @__PURE__ */ new Map([
2970
3233
  [".svg", "image/svg+xml"],
2971
3234
  [".png", "image/png"],
2972
3235
  [".webp", "image/webp"],
2973
3236
  [".jpg", "image/jpeg"],
2974
3237
  [".jpeg", "image/jpeg"],
2975
- [".gif", "image/gif"],
2976
3238
  [".ico", "image/x-icon"]
2977
3239
  ]);
2978
3240
  const PREFERRED = [
@@ -2995,11 +3257,42 @@ async function findLogo(folder) {
2995
3257
  for (const { file } of candidates) {
2996
3258
  const full = path.join(folder, file);
2997
3259
  if (!existsSync(full)) continue;
2998
- const logo = `data:${LOGO_FILES.get(path.extname(file).toLowerCase())};base64,${(await readFile(full)).toString("base64")}`;
2999
- if (logo.length <= 524288) return logo;
3260
+ const type = LOGO_FILES.get(path.extname(file).toLowerCase());
3261
+ const data = await readFile(full);
3262
+ try {
3263
+ return await prepareLogo(`data:${type};base64,${data.toString("base64")}`);
3264
+ } catch {
3265
+ continue;
3266
+ }
3000
3267
  }
3001
3268
  return null;
3002
3269
  }
3270
+ async function logoFiles(folder) {
3271
+ const repos = await projectRepos(folder);
3272
+ if (repos.length === 0) return (await listFiles(folder)).filter((file) => PICKABLE_FILE.test(file));
3273
+ const listed = await Promise.all(repos.map(async (repo) => {
3274
+ return (await git(path.join(folder, repo), [
3275
+ "ls-files",
3276
+ "-z",
3277
+ "--cached",
3278
+ "--others",
3279
+ "--exclude-standard"
3280
+ ])).stdout.split("\0").map((file) => repo === "" ? file : `${repo}/${file}`);
3281
+ }));
3282
+ return [...new Set(listed.flat())].filter((file) => PICKABLE_FILE.test(file) && existsSync(path.join(folder, file)));
3283
+ }
3284
+ async function prepareLogo(logo) {
3285
+ const header = /^data:([^;,]+);base64,/.exec(logo);
3286
+ if (header !== null && RESIZED_TYPES.includes(header[1])) return `data:image/webp;base64,${(await sharp(Buffer.from(logo.slice(header[0].length), "base64")).resize(LOGO_SIZE, LOGO_SIZE, {
3287
+ fit: "inside",
3288
+ withoutEnlargement: true
3289
+ }).webp({ quality: 90 }).toBuffer()).toString("base64")}`;
3290
+ if (header !== null && KEPT_TYPES.includes(header[1])) {
3291
+ if (logo.length > LOGO_LIMIT) throw new Error("Use an SVG or ICO image smaller than 384 KB");
3292
+ return logo;
3293
+ }
3294
+ throw new Error("Use a PNG, JPEG, WebP, SVG or ICO image");
3295
+ }
3003
3296
  function logoRank(file) {
3004
3297
  const preferred = PREFERRED.indexOf(path.basename(file).toLowerCase());
3005
3298
  if (preferred !== -1) return preferred;
@@ -3011,15 +3304,6 @@ function logoRank(file) {
3011
3304
  }
3012
3305
  //#endregion
3013
3306
  //#region src/services/projects.ts
3014
- const LOGO_TYPES = [
3015
- "image/png",
3016
- "image/jpeg",
3017
- "image/gif",
3018
- "image/webp",
3019
- "image/svg+xml",
3020
- "image/x-icon",
3021
- "image/vnd.microsoft.icon"
3022
- ];
3023
3307
  function projects$1() {
3024
3308
  return db.select({
3025
3309
  id: projects$2.id,
@@ -3049,16 +3333,13 @@ async function addProject(input) {
3049
3333
  if (project === void 0) throw new Error(`Could not add ${folder}`);
3050
3334
  return project;
3051
3335
  }
3052
- function updateProject(id, patch) {
3336
+ async function updateProject(id, patch) {
3053
3337
  if (patch.name !== void 0) {
3054
3338
  const name = patch.name.trim();
3055
3339
  if (name === "") throw new Error("Project name cannot be empty");
3056
3340
  db.update(projects$2).set({ name }).where(eq(projects$2.id, id)).run();
3057
3341
  }
3058
- if (patch.logo !== void 0) {
3059
- if (patch.logo !== null) checkLogo(patch.logo);
3060
- db.update(projects$2).set({ logo: patch.logo }).where(eq(projects$2.id, id)).run();
3061
- }
3342
+ if (patch.logo !== void 0) db.update(projects$2).set({ logo: patch.logo === null ? null : await prepareLogo(patch.logo) }).where(eq(projects$2.id, id)).run();
3062
3343
  publish$1();
3063
3344
  return null;
3064
3345
  }
@@ -3096,11 +3377,6 @@ function publish$1() {
3096
3377
  projects: projects$1()
3097
3378
  });
3098
3379
  }
3099
- function checkLogo(logo) {
3100
- const type = /^data:([^;,]+);base64,/.exec(logo)?.[1];
3101
- if (type === void 0 || !LOGO_TYPES.includes(type)) throw new Error("Use a PNG, JPEG, GIF, WebP, SVG or ICO image");
3102
- if (logo.length > 524288) throw new Error("Use an image smaller than 384 KB");
3103
- }
3104
3380
  function expandHome(input) {
3105
3381
  if (input === "~") return os.homedir();
3106
3382
  if (input.startsWith("~/")) return path.join(os.homedir(), input.slice(2));
@@ -3189,21 +3465,6 @@ async function find(provider, paths) {
3189
3465
  });
3190
3466
  }
3191
3467
  }
3192
- function encodeFrame(kind, id, data) {
3193
- const frame = new Uint8Array(5 + data.length);
3194
- frame[0] = kind;
3195
- new DataView(frame.buffer).setUint32(1, id);
3196
- frame.set(data, 5);
3197
- return frame;
3198
- }
3199
- function decodeFrame(frame) {
3200
- if (frame.length < 5) throw new Error("terminal frame is too short");
3201
- return {
3202
- kind: frame[0],
3203
- id: new DataView(frame.buffer, frame.byteOffset, frame.byteLength).getUint32(1),
3204
- data: frame.subarray(5)
3205
- };
3206
- }
3207
3468
  //#endregion
3208
3469
  //#region src/services/push.ts
3209
3470
  const TITLE_LIMIT = 100;
@@ -3669,7 +3930,7 @@ function writeScript(target, source) {
3669
3930
  }
3670
3931
  //#endregion
3671
3932
  //#region src/services/terminal/process.ts
3672
- const run = promisify(execFile);
3933
+ const run$1 = promisify(execFile);
3673
3934
  const iterm2Version = "3.6.8";
3674
3935
  const ghosttyVersion = "1.3.1";
3675
3936
  const shellNames = process.platform === "win32" ? [
@@ -3730,7 +3991,7 @@ function startShell(cwd) {
3730
3991
  async function currentDirectory(pid, reported) {
3731
3992
  if (process.platform === "linux") return readlink(`/proc/${pid}/cwd`);
3732
3993
  if (process.platform === "darwin") {
3733
- const { stdout } = await run("lsof", [
3994
+ const { stdout } = await run$1("lsof", [
3734
3995
  "-a",
3735
3996
  "-d",
3736
3997
  "cwd",
@@ -3750,7 +4011,7 @@ async function currentDirectory(pid, reported) {
3750
4011
  }
3751
4012
  async function busyProgram(pid) {
3752
4013
  if (process.platform === "win32") {
3753
- const { stdout } = await run("powershell.exe", [
4014
+ const { stdout } = await run$1("powershell.exe", [
3754
4015
  "-NoProfile",
3755
4016
  "-Command",
3756
4017
  `Get-CimInstance Win32_Process -Filter "ParentProcessId=${pid}" | Select-Object -ExpandProperty Name`
@@ -3773,7 +4034,7 @@ async function foregroundGroup(pid) {
3773
4034
  const text = await readFile(`/proc/${pid}/stat`, "utf8");
3774
4035
  return Number(text.slice(text.lastIndexOf(")") + 2).split(" ")[5]);
3775
4036
  }
3776
- const { stdout } = await run("ps", [
4037
+ const { stdout } = await run$1("ps", [
3777
4038
  "-o",
3778
4039
  "tpgid=",
3779
4040
  "-p",
@@ -3783,7 +4044,7 @@ async function foregroundGroup(pid) {
3783
4044
  }
3784
4045
  async function programName(pid) {
3785
4046
  if (process.platform === "linux") return (await readFile(`/proc/${pid}/comm`, "utf8")).trimEnd();
3786
- const { stdout } = await run("ps", [
4047
+ const { stdout } = await run$1("ps", [
3787
4048
  "-o",
3788
4049
  "comm=",
3789
4050
  "-p",
@@ -3852,7 +4113,7 @@ function executable$1(name) {
3852
4113
  //#endregion
3853
4114
  //#region src/services/terminal/terminal.ts
3854
4115
  const HISTORY_LIMIT = 2097152;
3855
- const OUTPUT_LIMIT$1 = 1048576;
4116
+ const OUTPUT_LIMIT = 1048576;
3856
4117
  const STALL_LIMIT = 1e4;
3857
4118
  const SILENCE_LIMIT$1 = 9e4;
3858
4119
  const KILL_DELAY = 3e3;
@@ -4081,7 +4342,7 @@ function output(id, terminal, data) {
4081
4342
  }
4082
4343
  for (const connection of connections.values()) forward(connection, id, false, data);
4083
4344
  scan(id, terminal, data);
4084
- if ([...connections.values()].some((connection) => connection.client.socket.bufferedAmount > OUTPUT_LIMIT$1)) {
4345
+ if ([...connections.values()].some((connection) => connection.client.socket.bufferedAmount > OUTPUT_LIMIT)) {
4085
4346
  terminal.pty.pause();
4086
4347
  paused.add(terminal.pty);
4087
4348
  draining ??= drain();
@@ -4093,7 +4354,7 @@ function drain() {
4093
4354
  let full = false;
4094
4355
  for (const connection of connections.values()) {
4095
4356
  const queued = connection.client.socket.bufferedAmount;
4096
- if (queued <= OUTPUT_LIMIT$1) continue;
4357
+ if (queued <= OUTPUT_LIMIT) continue;
4097
4358
  const now = Date.now();
4098
4359
  const last = progress.get(connection) ?? {
4099
4360
  queued: Infinity,
@@ -4169,6 +4430,10 @@ function publish() {
4169
4430
  layout: layout()
4170
4431
  };
4171
4432
  for (const connection of connections.values()) send$1(connection, event);
4433
+ broadcast({
4434
+ type: "terminals",
4435
+ count: terminalCount(GLOBAL_TERMINALS)
4436
+ });
4172
4437
  }
4173
4438
  function forward(connection, id, replay, data) {
4174
4439
  connection.client.socket.send(encodeFrame(replay ? 3 : 2, id, data));
@@ -4260,7 +4525,7 @@ function withStatus(items) {
4260
4525
  return {
4261
4526
  ...item,
4262
4527
  updatedAt,
4263
- status: record === void 0 || record.status === "active" && Date.now() - updatedAt > day ? "inactive" : record.status
4528
+ status: record === void 0 || (record.status === "active" || record.status === "snoozed") && Date.now() - updatedAt > day ? "inactive" : record.status
4264
4529
  };
4265
4530
  });
4266
4531
  }
@@ -4290,7 +4555,7 @@ function recordInteraction(id) {
4290
4555
  target: sessions$1.sessionId,
4291
4556
  set: {
4292
4557
  interactedAt: Date.now(),
4293
- status: sql`CASE ${sessions$1.status} WHEN 'pinned' THEN 'pinned' WHEN 'minimized' THEN 'minimized' ELSE 'active' END`
4558
+ status: sql`CASE ${sessions$1.status} WHEN 'pinned' THEN 'pinned' WHEN 'snoozed' THEN 'snoozed' WHEN 'minimized' THEN 'minimized' ELSE 'active' END`
4294
4559
  }
4295
4560
  }).run();
4296
4561
  }
@@ -4419,7 +4684,7 @@ async function updateSession(id, patch) {
4419
4684
  const session = all.find((item) => item.id === id);
4420
4685
  if (session !== void 0 && all.every((item) => item.projectId !== session.projectId || item.status === "inactive") && [...runs.values()].every((other) => other.sessionId === id || other.cwd !== projectPath(session.projectId) || all.some((item) => item.id === other.sessionId))) closeProjectTabs(session.projectId);
4421
4686
  } else if (patch.status !== void 0) {
4422
- const set = patch.status === "active" ? {
4687
+ const set = patch.status === "active" || patch.status === "snoozed" ? {
4423
4688
  status: patch.status,
4424
4689
  interactedAt: Date.now()
4425
4690
  } : { status: patch.status };
@@ -4675,7 +4940,6 @@ async function capabilities(provider, accountId, cwd) {
4675
4940
  assertEnabled(provider, accountId);
4676
4941
  const result = await providerCapabilities(provider, accountId, cwd);
4677
4942
  const settings = readConfig().models[provider];
4678
- const overrides = provider === "opencode" ? {} : storedAccount(provider, accountId).hiddenModels;
4679
4943
  return {
4680
4944
  ...result,
4681
4945
  defaultModel: defaultModels[provider],
@@ -4690,7 +4954,7 @@ async function capabilities(provider, accountId, cwd) {
4690
4954
  return {
4691
4955
  ...model,
4692
4956
  deprecated,
4693
- hidden: overrides[model.value] ?? setting.hidden,
4957
+ hidden: setting.hidden,
4694
4958
  effort: setting.effort ?? effort
4695
4959
  };
4696
4960
  })
@@ -4717,8 +4981,7 @@ function addAccount(provider, name, configDir) {
4717
4981
  name: account.name,
4718
4982
  color: null,
4719
4983
  configDir: { [platform()]: account.configDir },
4720
- enabled: true,
4721
- hiddenModels: {}
4984
+ enabled: true
4722
4985
  }]
4723
4986
  });
4724
4987
  }
@@ -4738,8 +5001,7 @@ async function updateAccount(update) {
4738
5001
  ...item.configDir,
4739
5002
  [platform()]: account.configDir
4740
5003
  },
4741
- enabled: update.enabled,
4742
- hiddenModels: update.hiddenModels
5004
+ enabled: update.enabled
4743
5005
  } : item)
4744
5006
  });
4745
5007
  forgetClaude(update.id);
@@ -5551,7 +5813,7 @@ async function image(sessionId, uuid, index) {
5551
5813
  data: block.data
5552
5814
  };
5553
5815
  }
5554
- async function history(sessionId, cwd) {
5816
+ async function history$1(sessionId, cwd) {
5555
5817
  const found = runs.get(sessionId);
5556
5818
  const run = found?.provider === "claude" ? found : void 0;
5557
5819
  const [disk, session] = await Promise.all([getSessionMessages(sessionId, { dir: cwd }), loadSession(sessionId)]);
@@ -5982,142 +6244,6 @@ var Run = class {
5982
6244
  }
5983
6245
  };
5984
6246
  //#endregion
5985
- //#region src/services/files.ts
5986
- const FILE_LIMIT = 2e4;
5987
- const OUTPUT_LIMIT = 262144;
5988
- const READ_LIMIT = 1048576;
5989
- const MEDIA_LINK_LIFETIME = 36e5;
5990
- const browseToken = randomBytes(32).toString("base64url");
5991
- const mediaLinks = /* @__PURE__ */ new Map();
5992
- const skipped = /* @__PURE__ */ new Set([
5993
- "node_modules",
5994
- ".git",
5995
- ".DS_Store",
5996
- "dist",
5997
- "target",
5998
- ".next",
5999
- "__pycache__",
6000
- ".venv"
6001
- ]);
6002
- async function listFiles(cwd) {
6003
- const files = [];
6004
- const pending = [""];
6005
- while (pending.length > 0 && files.length < FILE_LIMIT) {
6006
- const dir = pending.pop() ?? "";
6007
- let entries;
6008
- try {
6009
- entries = await readdir(path.join(cwd, dir), { withFileTypes: true });
6010
- } catch {
6011
- continue;
6012
- }
6013
- for (const entry of entries) {
6014
- if (skipped.has(entry.name)) continue;
6015
- const relative = dir === "" ? entry.name : `${dir}/${entry.name}`;
6016
- if (entry.isDirectory()) pending.push(relative);
6017
- if (entry.isFile()) files.push(relative);
6018
- if (entry.isSymbolicLink()) {
6019
- if ((await stat(path.join(cwd, relative)).catch(() => null))?.isFile() === true) files.push(relative);
6020
- }
6021
- }
6022
- }
6023
- return files.slice(0, FILE_LIMIT).sort();
6024
- }
6025
- function readProjectFile(cwd, file) {
6026
- const full = resolveProjectFile(cwd, file);
6027
- const stats = statSync(full);
6028
- if (!stats.isFile()) throw new Error(`${file} is not a file`);
6029
- if (stats.size > READ_LIMIT) return {
6030
- kind: "large",
6031
- size: stats.size,
6032
- limit: READ_LIMIT
6033
- };
6034
- const data = readFileSync(full);
6035
- const mediaType = mediaTypes.get(path.extname(full).toLowerCase());
6036
- if (mediaType?.startsWith("image/") === true) return {
6037
- kind: "image",
6038
- size: stats.size,
6039
- mediaType,
6040
- data: data.toString("base64")
6041
- };
6042
- if (data.includes(0)) return {
6043
- kind: "binary",
6044
- size: stats.size
6045
- };
6046
- return {
6047
- kind: "text",
6048
- size: stats.size,
6049
- content: data.toString("utf8")
6050
- };
6051
- }
6052
- function linkProjectMedia(cwd, file) {
6053
- const resolved = path.resolve(realpathSync(cwd), file);
6054
- if (!existsSync(resolved)) throw new Error(`${file} does not exist`);
6055
- const full = realpathSync(resolved);
6056
- const mediaType = mediaTypes.get(path.extname(full).toLowerCase());
6057
- if (mediaType === void 0) throw new Error(`${file} is not an image, video or audio file`);
6058
- return addLink(full, mediaType, null);
6059
- }
6060
- function linkProjectDownload(cwd, file) {
6061
- return addLink(resolveProjectFile(cwd, file), "application/octet-stream", path.basename(file));
6062
- }
6063
- function addLink(file, mediaType, download) {
6064
- for (const [token, link] of mediaLinks) if (link.expiresAt <= Date.now()) mediaLinks.delete(token);
6065
- const token = randomBytes(32).toString("base64url");
6066
- mediaLinks.set(token, {
6067
- file,
6068
- mediaType,
6069
- download,
6070
- expiresAt: Date.now() + MEDIA_LINK_LIFETIME
6071
- });
6072
- return token;
6073
- }
6074
- function findMediaLink(token) {
6075
- if (token === null) return null;
6076
- const link = mediaLinks.get(token);
6077
- if (link === void 0 || link.expiresAt <= Date.now()) return null;
6078
- return link;
6079
- }
6080
- function saveUpload(name, data) {
6081
- const dir = path.join(home, "uploads", randomUUID());
6082
- mkdirSync(dir, { recursive: true });
6083
- const file = path.join(dir, path.basename(name));
6084
- writeFileSync(file, data);
6085
- return file;
6086
- }
6087
- async function taskOutput(sessionId, file) {
6088
- const tasks = path.dirname(file);
6089
- if (!/^[\w-]+$/.test(sessionId) || !path.isAbsolute(file) || path.normalize(file) !== file || !/^\w+\.output$/.test(path.basename(file)) || path.basename(tasks) !== "tasks" || path.basename(path.dirname(tasks)) !== sessionId) throw new Error("Not a background task output file of this thread");
6090
- let stats;
6091
- try {
6092
- stats = await lstat(file);
6093
- } catch (error) {
6094
- if (hasCode(error, "ENOENT")) return null;
6095
- throw error;
6096
- }
6097
- if (!stats.isFile()) throw new Error("Background task output is not a regular file");
6098
- const start = Math.max(0, stats.size - OUTPUT_LIMIT);
6099
- const handle = await open(file, "r");
6100
- try {
6101
- const buffer = Buffer.alloc(stats.size - start);
6102
- const read = await handle.read(buffer, 0, buffer.length, start);
6103
- return {
6104
- text: buffer.subarray(0, read.bytesRead).toString("utf8"),
6105
- truncated: start > 0
6106
- };
6107
- } finally {
6108
- await handle.close();
6109
- }
6110
- }
6111
- function resolveProjectFile(cwd, file) {
6112
- const root = realpathSync(cwd);
6113
- const resolved = path.resolve(root, file);
6114
- if (!existsSync(resolved)) throw new Error(`${file} does not exist`);
6115
- const full = realpathSync(resolved);
6116
- const relative = path.relative(root, full);
6117
- if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) throw new Error(`${file} is outside the project`);
6118
- return full;
6119
- }
6120
- //#endregion
6121
6247
  //#region src/services/proxy.ts
6122
6248
  const proxies = /* @__PURE__ */ new Map();
6123
6249
  const pageScriptPath = "/__offsite/browser.js";
@@ -6261,7 +6387,7 @@ function stopAll() {
6261
6387
  }
6262
6388
  function sessionHistory(sessionId, cwd) {
6263
6389
  switch (providerOf(sessionId)) {
6264
- case "claude": return history(sessionId, cwd);
6390
+ case "claude": return history$1(sessionId, cwd);
6265
6391
  case "codex": return codexHistory(sessionId);
6266
6392
  case "opencode": return openCodeHistory(sessionId, cwd);
6267
6393
  }
@@ -6313,7 +6439,7 @@ async function rewindNow(request) {
6313
6439
  resumeAt: disk[index - 1].uuid,
6314
6440
  settings: request.settings
6315
6441
  });
6316
- publish$2(sessionId, await history(sessionId, cwd));
6442
+ publish$2(sessionId, await history$1(sessionId, cwd));
6317
6443
  return result;
6318
6444
  }
6319
6445
  async function open$1(client, sessionId, projectId) {
@@ -6406,6 +6532,294 @@ async function changedElsewhere(run, cwd) {
6406
6532
  return last !== void 0 && !run.messages.some((item) => item.uuid === last);
6407
6533
  }
6408
6534
  //#endregion
6535
+ //#region src/services/source-control.ts
6536
+ const SHOW_LIMIT = 1048576;
6537
+ const NETWORK_TIMEOUT = 12e4;
6538
+ const run = promisify(execFile);
6539
+ async function sourceControl(cwd) {
6540
+ return Promise.all((await projectRepos(cwd)).map(async (repo) => ({
6541
+ ...await repoChanges(path.join(cwd, repo)),
6542
+ path: repo
6543
+ })));
6544
+ }
6545
+ async function showFile(cwd, ref, file) {
6546
+ const output = await git(cwd, [
6547
+ "cat-file",
6548
+ "blob",
6549
+ `${ref}:${file}`
6550
+ ]);
6551
+ if (output.stdout.length > SHOW_LIMIT || output.stdout.includes("\0")) return null;
6552
+ return output.stdout;
6553
+ }
6554
+ async function history(cwd, limit) {
6555
+ const upstream = (await git(cwd, ["branch", "--format=%(if)%(HEAD)%(then)%(if:notequals=[gone])%(upstream:track)%(then)%(upstream)%(end)%(end)"])).stdout.trim();
6556
+ return (await git(cwd, [
6557
+ "log",
6558
+ "--topo-order",
6559
+ `--max-count=${limit}`,
6560
+ "--format=%H%x1f%P%x1f%an%x1f%at%x1f%D%x1f%s%x1e",
6561
+ "HEAD",
6562
+ ...upstream === "" ? [] : [upstream],
6563
+ "--"
6564
+ ])).stdout.split("").map((record) => record.trim()).filter(Boolean).map((record) => {
6565
+ const fields = record.split("");
6566
+ return {
6567
+ hash: fields[0],
6568
+ parents: fields[1].split(" ").filter(Boolean),
6569
+ author: fields[2],
6570
+ date: Number(fields[3]) * 1e3,
6571
+ refs: fields[4].split(", ").filter(Boolean),
6572
+ subject: fields[5]
6573
+ };
6574
+ });
6575
+ }
6576
+ async function commitFiles(cwd, hash, parent) {
6577
+ const tokens = (await git(cwd, [
6578
+ "diff",
6579
+ "--name-status",
6580
+ "-M",
6581
+ "-z",
6582
+ parent ?? (await git(cwd, [
6583
+ "hash-object",
6584
+ "-t",
6585
+ "tree",
6586
+ "/dev/null"
6587
+ ])).stdout.trim(),
6588
+ hash
6589
+ ])).stdout.split("\0");
6590
+ const files = [];
6591
+ for (let index = 0; index < tokens.length - 1; index++) {
6592
+ const status = letter(tokens[index][0]);
6593
+ if (status === "R" || status === "C") {
6594
+ files.push({
6595
+ path: tokens[index + 2],
6596
+ from: tokens[index + 1],
6597
+ status
6598
+ });
6599
+ index += 2;
6600
+ continue;
6601
+ }
6602
+ files.push({
6603
+ path: tokens[index + 1],
6604
+ from: null,
6605
+ status
6606
+ });
6607
+ index += 1;
6608
+ }
6609
+ return files;
6610
+ }
6611
+ async function undoCommit(cwd) {
6612
+ const message = (await git(cwd, [
6613
+ "log",
6614
+ "-1",
6615
+ "--format=%B"
6616
+ ])).stdout.trim();
6617
+ if ((await git(cwd, [
6618
+ "rev-list",
6619
+ "--parents",
6620
+ "--max-count=1",
6621
+ "HEAD"
6622
+ ])).stdout.trim().split(" ").length === 1) await git(cwd, [
6623
+ "update-ref",
6624
+ "-d",
6625
+ "HEAD"
6626
+ ]);
6627
+ else await git(cwd, [
6628
+ "reset",
6629
+ "--soft",
6630
+ "HEAD~1"
6631
+ ]);
6632
+ return message;
6633
+ }
6634
+ async function runAction(cwd, action) {
6635
+ if (action.kind === "stage") {
6636
+ await git(cwd, [
6637
+ "--literal-pathspecs",
6638
+ "add",
6639
+ "-A",
6640
+ "--",
6641
+ ...action.paths
6642
+ ]);
6643
+ return;
6644
+ }
6645
+ if (action.kind === "stageContent") {
6646
+ const entry = await git(cwd, [
6647
+ "--literal-pathspecs",
6648
+ "ls-files",
6649
+ "--stage",
6650
+ "--",
6651
+ action.path
6652
+ ]);
6653
+ if (entry.stdout === "") throw new Error(`${action.path} is not in the index`);
6654
+ const hashing = run("git", [
6655
+ "hash-object",
6656
+ "-w",
6657
+ "--stdin",
6658
+ `--path=${action.path}`
6659
+ ], { cwd });
6660
+ hashing.child.stdin?.end(action.content);
6661
+ const hash = (await hashing).stdout.trim();
6662
+ await git(cwd, [
6663
+ "update-index",
6664
+ "--cacheinfo",
6665
+ `${entry.stdout.split(" ")[0]},${hash},${action.path}`
6666
+ ]);
6667
+ return;
6668
+ }
6669
+ if (action.kind === "unstage") {
6670
+ await git(cwd, [
6671
+ "--literal-pathspecs",
6672
+ "reset",
6673
+ "-q",
6674
+ await diffBase(cwd),
6675
+ "--",
6676
+ ...action.paths
6677
+ ]);
6678
+ return;
6679
+ }
6680
+ if (action.kind === "discard") {
6681
+ if (action.tracked.length > 0) await git(cwd, [
6682
+ "--literal-pathspecs",
6683
+ "checkout",
6684
+ "-q",
6685
+ "--",
6686
+ ...action.tracked
6687
+ ]);
6688
+ if (action.untracked.length > 0) await git(cwd, [
6689
+ "--literal-pathspecs",
6690
+ "clean",
6691
+ "-f",
6692
+ "-q",
6693
+ "--",
6694
+ ...action.untracked
6695
+ ]);
6696
+ return;
6697
+ }
6698
+ if (action.kind === "ignore") {
6699
+ const file = path.join(cwd, ".gitignore");
6700
+ const current = existsSync(file) ? readFileSync(file, "utf8") : "";
6701
+ appendFileSync(file, `${current === "" || current.endsWith("\n") ? "" : "\n"}${action.paths.join("\n")}\n`);
6702
+ return;
6703
+ }
6704
+ if (action.kind === "commit") {
6705
+ if (action.all) await git(cwd, ["add", "--all"]);
6706
+ await git(cwd, [
6707
+ "commit",
6708
+ ...action.amend ? ["--amend"] : [],
6709
+ ...action.message === "" ? ["--no-edit"] : ["--message", action.message]
6710
+ ]);
6711
+ return;
6712
+ }
6713
+ if (action.kind === "pull") {
6714
+ await network(cwd, action.rebase ? ["pull", "--rebase"] : ["pull"]);
6715
+ return;
6716
+ }
6717
+ if (action.kind === "fetch") {
6718
+ await network(cwd, [
6719
+ "fetch",
6720
+ ...action.prune ? ["--prune"] : [],
6721
+ ...action.all ? ["--all"] : []
6722
+ ]);
6723
+ return;
6724
+ }
6725
+ if (action.kind === "stash") {
6726
+ if (action.mode === "tracked") await git(cwd, ["stash", "push"]);
6727
+ if (action.mode === "untracked") await git(cwd, [
6728
+ "stash",
6729
+ "push",
6730
+ "--include-untracked"
6731
+ ]);
6732
+ if (action.mode === "staged") await git(cwd, [
6733
+ "stash",
6734
+ "push",
6735
+ "--staged"
6736
+ ]);
6737
+ return;
6738
+ }
6739
+ if (action.kind === "unstash") {
6740
+ await git(cwd, ["stash", action.pop ? "pop" : "apply"]);
6741
+ return;
6742
+ }
6743
+ throw new Error("Unknown git action");
6744
+ }
6745
+ async function network(cwd, args) {
6746
+ try {
6747
+ await run("git", args, {
6748
+ cwd,
6749
+ env: {
6750
+ ...process.env,
6751
+ GIT_TERMINAL_PROMPT: "0"
6752
+ },
6753
+ timeout: NETWORK_TIMEOUT
6754
+ });
6755
+ } catch (error) {
6756
+ throw gitError(error);
6757
+ }
6758
+ }
6759
+ async function repoChanges(cwd) {
6760
+ const output = await git(cwd, [
6761
+ "--no-optional-locks",
6762
+ "status",
6763
+ "--porcelain=v2",
6764
+ "--branch",
6765
+ "--untracked-files=all",
6766
+ "-z"
6767
+ ]);
6768
+ const repo = {
6769
+ head: null,
6770
+ branch: null,
6771
+ upstream: null,
6772
+ remote: (await git(cwd, ["remote"])).stdout.trim() !== "",
6773
+ ahead: 0,
6774
+ behind: 0,
6775
+ merge: [],
6776
+ staged: [],
6777
+ changes: []
6778
+ };
6779
+ const tokens = output.stdout.split("\0");
6780
+ for (let index = 0; index < tokens.length; index++) {
6781
+ const fields = tokens[index].split(" ");
6782
+ if (fields[0] === "#" && fields[1] === "branch.oid") repo.head = fields[2] === "(initial)" ? null : fields[2];
6783
+ if (fields[0] === "#" && fields[1] === "branch.head") repo.branch = fields[2] === "(detached)" ? null : fields[2];
6784
+ if (fields[0] === "#" && fields[1] === "branch.upstream") repo.upstream = fields[2];
6785
+ if (fields[0] === "#" && fields[1] === "branch.ab") {
6786
+ repo.ahead = Number(fields[2]);
6787
+ repo.behind = -Number(fields[3]);
6788
+ }
6789
+ if (fields[0] === "?") repo.changes.push({
6790
+ path: tokens[index].slice(2),
6791
+ from: null,
6792
+ status: "U"
6793
+ });
6794
+ if (fields[0] === "u") repo.merge.push({
6795
+ path: fields.slice(10).join(" "),
6796
+ from: null,
6797
+ status: "!"
6798
+ });
6799
+ if (fields[0] === "1" || fields[0] === "2") {
6800
+ const renamed = fields[0] === "2";
6801
+ const file = fields.slice(renamed ? 9 : 8).join(" ");
6802
+ const from = renamed ? tokens[index + 1] : null;
6803
+ if (fields[1][0] !== ".") repo.staged.push({
6804
+ path: file,
6805
+ from,
6806
+ status: letter(fields[1][0])
6807
+ });
6808
+ if (fields[1][1] !== ".") repo.changes.push({
6809
+ path: file,
6810
+ from: null,
6811
+ status: letter(fields[1][1])
6812
+ });
6813
+ if (renamed) index++;
6814
+ }
6815
+ }
6816
+ return repo;
6817
+ }
6818
+ function letter(code) {
6819
+ if (code === "M" || code === "T" || code === "A" || code === "D" || code === "R" || code === "C") return code;
6820
+ throw new Error(`Unknown git status ${code}`);
6821
+ }
6822
+ //#endregion
6409
6823
  //#region src/services/terminal/paste.ts
6410
6824
  const pasteTypes = {
6411
6825
  "image/png": "png",
@@ -6424,7 +6838,6 @@ function savePastedImage(type, data) {
6424
6838
  }
6425
6839
  //#endregion
6426
6840
  //#region src/storage/state.ts
6427
- const stateFile = path.join(home, "state.json");
6428
6841
  const stateSchema = z.strictObject({ composer: z.strictObject({
6429
6842
  provider: z.enum([
6430
6843
  "claude",
@@ -6472,20 +6885,7 @@ const stateSchema = z.strictObject({ composer: z.strictObject({
6472
6885
  }).prefault({}) });
6473
6886
  const defaults = stateSchema.parse({});
6474
6887
  function readComposer() {
6475
- let user;
6476
- try {
6477
- user = readJson("state.json");
6478
- } catch (error) {
6479
- console.warn(`invalid ${stateFile}, using the default state: ${error}`);
6480
- return structuredClone(defaults.composer);
6481
- }
6482
- if (user === null) return structuredClone(defaults.composer);
6483
- const result = stateSchema.safeParse(user);
6484
- if (!result.success) {
6485
- console.warn(`invalid ${stateFile}, using the default state:\n${z.prettifyError(result.error)}`);
6486
- return structuredClone(defaults.composer);
6487
- }
6488
- return result.data.composer;
6888
+ return readSettings("state.json", stateSchema, defaults).composer;
6489
6889
  }
6490
6890
  function saveComposer(value) {
6491
6891
  const result = stateSchema.safeParse({ composer: value });
@@ -6510,7 +6910,8 @@ const handlers = {
6510
6910
  projects: projects$1(),
6511
6911
  sessions: sessions(),
6512
6912
  providers: providerStatuses(),
6513
- activity: activity()
6913
+ activity: activity(),
6914
+ terminals: terminalCount(GLOBAL_TERMINALS)
6514
6915
  };
6515
6916
  },
6516
6917
  "config.get": () => configInfo(),
@@ -6542,6 +6943,7 @@ const handlers = {
6542
6943
  return null;
6543
6944
  },
6544
6945
  "projects.reorder": (params) => reorderProjects(params.ids),
6946
+ "projects.logoFiles": (params) => logoFiles(projectPath(params.id)),
6545
6947
  "directories.list": (params) => directories(params.path),
6546
6948
  "sessions.update": async (params) => {
6547
6949
  try {
@@ -6630,9 +7032,25 @@ const handlers = {
6630
7032
  },
6631
7033
  capabilities: (params) => capabilities(params.provider, params.accountId, params.projectId === null ? os.homedir() : projectPath(params.projectId)),
6632
7034
  "files.list": (params) => listFiles(projectPath(params.projectId)),
7035
+ "files.entries": (params) => listEntries(projectPath(params.projectId)),
6633
7036
  "files.read": (params) => readProjectFile(projectPath(params.projectId), params.path),
7037
+ "files.write": (params) => {
7038
+ writeProjectFile(projectPath(params.projectId), params.path, params.content);
7039
+ return null;
7040
+ },
7041
+ "files.create": (params) => {
7042
+ createProjectEntry(projectPath(params.projectId), params.path, params.folder);
7043
+ return null;
7044
+ },
7045
+ "files.rename": (params) => {
7046
+ renameProjectEntry(projectPath(params.projectId), params.path, params.to);
7047
+ return null;
7048
+ },
7049
+ "files.delete": (params) => {
7050
+ deleteProjectEntry(projectPath(params.projectId), params.path);
7051
+ return null;
7052
+ },
6634
7053
  "files.media": (params) => linkProjectMedia(projectPath(params.projectId), params.path),
6635
- "files.download": (params) => linkProjectDownload(projectPath(params.projectId), params.path),
6636
7054
  "files.upload": (params) => saveUpload(params.name, Buffer.from(params.data, "base64")),
6637
7055
  "files.browse": () => browseToken,
6638
7056
  "browser.proxy": (params) => proxyPort(params.origin),
@@ -6640,6 +7058,16 @@ const handlers = {
6640
7058
  "git.status": (params) => gitRepos(projectPath(params.projectId)),
6641
7059
  "git.diff": (params) => projectDiff(projectPath(params.projectId)),
6642
7060
  "git.commit": (params) => commit(path.join(projectPath(params.projectId), params.repo), params.message, params.push),
7061
+ "git.changes": (params) => sourceControl(projectPath(params.projectId)),
7062
+ "git.show": (params) => showFile(path.join(projectPath(params.projectId), params.repo), params.ref, params.path),
7063
+ "git.history": (params) => history(path.join(projectPath(params.projectId), params.repo), params.limit),
7064
+ "git.commitFiles": (params) => commitFiles(path.join(projectPath(params.projectId), params.repo), params.hash, params.parent),
7065
+ "git.message": (params) => commitMessage(path.join(projectPath(params.projectId), params.repo), params.staged),
7066
+ "git.undoCommit": (params) => undoCommit(path.join(projectPath(params.projectId), params.repo)),
7067
+ "git.run": async (params) => {
7068
+ await runAction(path.join(projectPath(params.projectId), params.repo), params.action);
7069
+ return null;
7070
+ },
6643
7071
  "git.push": async (params) => {
6644
7072
  await push$1(path.join(projectPath(params.projectId), params.repo));
6645
7073
  return null;
@@ -6967,7 +7395,6 @@ function serveMedia(token, request, response) {
6967
7395
  }
6968
7396
  response.setHeader("access-control-allow-origin", "*");
6969
7397
  response.setHeader("content-type", link.mediaType);
6970
- if (link.download !== null) response.setHeader("content-disposition", `attachment; filename*=UTF-8''${encodeURIComponent(link.download)}`);
6971
7398
  response.setHeader("cache-control", "private, max-age=3600");
6972
7399
  response.setHeader("content-security-policy", "sandbox");
6973
7400
  response.setHeader("x-content-type-options", "nosniff");