offsite 0.0.70 → 0.0.72
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/client/dist/assets/{XIcon-BTZO46wb.js → XIcon-DL0NnGXV.js} +1 -1
- package/apps/client/dist/assets/{context-menu-D16WxIXt.js → context-menu-BfvCZuoZ.js} +26 -26
- package/apps/client/dist/assets/{diff-panel-CGW6idmO.js → diff-panel-CLdhUPhz.js} +1 -1
- package/apps/client/dist/assets/editor-dialog-B0L0Qv3X.js +52 -0
- package/apps/client/dist/assets/{image-viewer-BDChyfSI.js → image-viewer-da331qFf.js} +1 -1
- package/apps/client/dist/assets/index-CyH7c3Ln.js +73 -0
- package/apps/client/dist/assets/{preview-dialog-BWCK-x3l.js → preview-dialog-f6zcwrYq.js} +1 -1
- package/apps/client/dist/assets/{video-viewer-BXp_ueAz.js → video-viewer-BYejPn-Z.js} +1 -1
- package/apps/client/dist/index.html +4 -4
- package/apps/client/dist/sw.js +1 -1
- package/apps/server/dist/main.mjs +108 -26
- package/apps/server/dist/main.mjs.map +1 -1
- package/apps/server/drizzle/20261103024553_project_scratch/migration.sql +1 -0
- package/apps/server/drizzle/20261103024553_project_scratch/snapshot.json +388 -0
- package/package.json +1 -1
- package/apps/client/dist/assets/editor-dialog-BdpvXPtR.js +0 -52
- package/apps/client/dist/assets/index-CVRb_eva.js +0 -73
|
@@ -8,10 +8,10 @@ import { WebSocketServer } from "ws";
|
|
|
8
8
|
import { z } from "zod";
|
|
9
9
|
import os, { tmpdir } from "node:os";
|
|
10
10
|
import { deleteSession, forkSession, getSessionInfo, getSessionMessages, getSubagentMessages, listSessions, query, renameSession } from "@anthropic-ai/claude-agent-sdk";
|
|
11
|
-
import { execFile, spawn } from "node:child_process";
|
|
11
|
+
import { execFile, spawn, spawnSync } from "node:child_process";
|
|
12
12
|
import readline from "node:readline";
|
|
13
13
|
import { Command } from "@commander-js/extra-typings";
|
|
14
|
-
import { lstat, mkdir, mkdtemp, open, readFile, readdir, readlink, rm, stat, writeFile } from "node:fs/promises";
|
|
14
|
+
import { lstat, mkdir, mkdtemp, open, readFile, readdir, readlink, rm, rmdir, stat, writeFile } from "node:fs/promises";
|
|
15
15
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
16
16
|
import { OpenCode, isSessionNotFoundError } from "@opencode/client";
|
|
17
17
|
import { ensure, headers } from "@opencode/client/service";
|
|
@@ -127,6 +127,7 @@ const home = args.dir;
|
|
|
127
127
|
const lockFile = path.join(home, "server.pid");
|
|
128
128
|
const savedServer = z.object({ port: z.number() }).nullable();
|
|
129
129
|
mkdirSync(home, { recursive: true });
|
|
130
|
+
const scratchAvailable = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], { cwd: home }).status !== 0;
|
|
130
131
|
function listenPort() {
|
|
131
132
|
if (args.command !== "pair" || args.portGiven) return args.port;
|
|
132
133
|
const saved = readSettings("server.json", savedServer, null);
|
|
@@ -1050,7 +1051,8 @@ function configInfo() {
|
|
|
1050
1051
|
platform: platform(),
|
|
1051
1052
|
defaultShell: defaultShell(),
|
|
1052
1053
|
defaultClaudeBaseDir: path.join(os.homedir(), ".claude"),
|
|
1053
|
-
homeDir: os.homedir()
|
|
1054
|
+
homeDir: os.homedir(),
|
|
1055
|
+
scratchAvailable
|
|
1054
1056
|
};
|
|
1055
1057
|
}
|
|
1056
1058
|
function readConfig() {
|
|
@@ -2795,7 +2797,8 @@ const projects$1 = snakeCase.table("projects", {
|
|
|
2795
2797
|
name: text().notNull(),
|
|
2796
2798
|
logo: text(),
|
|
2797
2799
|
position: integer().notNull().default(0),
|
|
2798
|
-
hidden: integer({ mode: "boolean" }).notNull().default(false)
|
|
2800
|
+
hidden: integer({ mode: "boolean" }).notNull().default(false),
|
|
2801
|
+
scratch: integer({ mode: "boolean" }).notNull().default(false)
|
|
2799
2802
|
});
|
|
2800
2803
|
const sessions$1 = snakeCase.table("sessions", {
|
|
2801
2804
|
sessionId: text().primaryKey(),
|
|
@@ -3640,13 +3643,15 @@ function logoRank(file) {
|
|
|
3640
3643
|
return -1;
|
|
3641
3644
|
}
|
|
3642
3645
|
// src/services/projects.ts
|
|
3646
|
+
const scratchRoot = path.join(home, "scratch");
|
|
3643
3647
|
function projects() {
|
|
3644
3648
|
return db.select({
|
|
3645
3649
|
id: projects$1.id,
|
|
3646
3650
|
path: projects$1.path,
|
|
3647
3651
|
name: projects$1.name,
|
|
3648
3652
|
logo: projects$1.logo,
|
|
3649
|
-
hidden: projects$1.hidden
|
|
3653
|
+
hidden: projects$1.hidden,
|
|
3654
|
+
scratch: projects$1.scratch
|
|
3650
3655
|
}).from(projects$1).orderBy(projects$1.position).all();
|
|
3651
3656
|
}
|
|
3652
3657
|
function projectPath(id) {
|
|
@@ -3657,19 +3662,58 @@ function projectPath(id) {
|
|
|
3657
3662
|
async function addProject(input) {
|
|
3658
3663
|
const folder = path.resolve(expandHome(input.trim()));
|
|
3659
3664
|
if (!(await stat(folder)).isDirectory()) throw new Error(`${folder} is not a folder`);
|
|
3660
|
-
const last = db.select({ position: max(projects$1.position) }).from(projects$1).get();
|
|
3661
3665
|
db.insert(projects$1).values({
|
|
3662
3666
|
id: randomUUID(),
|
|
3663
3667
|
path: folder,
|
|
3664
3668
|
name: path.basename(folder) || folder,
|
|
3665
3669
|
logo: await findLogo(folder),
|
|
3666
|
-
position:
|
|
3670
|
+
position: nextPosition()
|
|
3667
3671
|
}).onConflictDoNothing({ target: projects$1.path }).run();
|
|
3668
3672
|
publish$1();
|
|
3669
3673
|
const project = projects().find((item) => item.path === folder);
|
|
3670
3674
|
if (project === void 0) throw new Error(`Could not add ${folder}`);
|
|
3671
3675
|
return project;
|
|
3672
3676
|
}
|
|
3677
|
+
async function addScratchProject(text) {
|
|
3678
|
+
if (!scratchAvailable) throw new Error(`Chats without a project are unavailable because ${home} is inside a Git repository`);
|
|
3679
|
+
await mkdir(scratchRoot, { recursive: true });
|
|
3680
|
+
const words = text.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean).slice(0, 5).join("-").slice(0, 48).replace(/-+$/, "");
|
|
3681
|
+
const date = (/* @__PURE__ */ new Date()).toLocaleDateString("en-CA");
|
|
3682
|
+
while (true) {
|
|
3683
|
+
const folder = path.join(scratchRoot, [
|
|
3684
|
+
date,
|
|
3685
|
+
words,
|
|
3686
|
+
randomBytes(4).toString("hex")
|
|
3687
|
+
].filter(Boolean).join("-"));
|
|
3688
|
+
try {
|
|
3689
|
+
await mkdir(folder);
|
|
3690
|
+
} catch (error) {
|
|
3691
|
+
if (error.code === "EEXIST") continue;
|
|
3692
|
+
throw error;
|
|
3693
|
+
}
|
|
3694
|
+
const id = randomUUID();
|
|
3695
|
+
db.insert(projects$1).values({
|
|
3696
|
+
id,
|
|
3697
|
+
path: folder,
|
|
3698
|
+
name: "No Project",
|
|
3699
|
+
scratch: true
|
|
3700
|
+
}).run();
|
|
3701
|
+
publish$1();
|
|
3702
|
+
return {
|
|
3703
|
+
id,
|
|
3704
|
+
path: folder
|
|
3705
|
+
};
|
|
3706
|
+
}
|
|
3707
|
+
}
|
|
3708
|
+
async function removeScratchProject(id, folder) {
|
|
3709
|
+
db.delete(projects$1).where(eq(projects$1.id, id)).run();
|
|
3710
|
+
publish$1();
|
|
3711
|
+
try {
|
|
3712
|
+
await rmdir(folder);
|
|
3713
|
+
} catch (error) {
|
|
3714
|
+
if (error.code !== "ENOTEMPTY") throw error;
|
|
3715
|
+
}
|
|
3716
|
+
}
|
|
3673
3717
|
async function updateProject(id, patch) {
|
|
3674
3718
|
if (patch.name !== void 0) {
|
|
3675
3719
|
const name = patch.name.trim();
|
|
@@ -3677,6 +3721,10 @@ async function updateProject(id, patch) {
|
|
|
3677
3721
|
db.update(projects$1).set({ name }).where(eq(projects$1.id, id)).run();
|
|
3678
3722
|
}
|
|
3679
3723
|
if (patch.logo !== void 0) db.update(projects$1).set({ logo: patch.logo === null ? null : await prepareLogo(patch.logo) }).where(eq(projects$1.id, id)).run();
|
|
3724
|
+
if (patch.scratch === false) db.update(projects$1).set({
|
|
3725
|
+
scratch: false,
|
|
3726
|
+
position: nextPosition()
|
|
3727
|
+
}).where(eq(projects$1.id, id)).run();
|
|
3680
3728
|
if (patch.hidden !== void 0) db.update(projects$1).set({ hidden: patch.hidden }).where(eq(projects$1.id, id)).run();
|
|
3681
3729
|
publish$1();
|
|
3682
3730
|
return null;
|
|
@@ -3686,7 +3734,7 @@ function removeProject(id) {
|
|
|
3686
3734
|
publish$1();
|
|
3687
3735
|
}
|
|
3688
3736
|
function reorderProjects(ids) {
|
|
3689
|
-
const current = projects().map((project) => project.id);
|
|
3737
|
+
const current = projects().filter((project) => !project.scratch).map((project) => project.id);
|
|
3690
3738
|
if (ids.length !== current.length || !current.every((id) => ids.includes(id))) throw new Error("The projects changed, try again");
|
|
3691
3739
|
db.transaction((transaction) => {
|
|
3692
3740
|
ids.forEach((id, position) => {
|
|
@@ -3709,6 +3757,10 @@ async function directories(input) {
|
|
|
3709
3757
|
})).sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()))
|
|
3710
3758
|
};
|
|
3711
3759
|
}
|
|
3760
|
+
function nextPosition() {
|
|
3761
|
+
const last = db.select({ position: max(projects$1.position) }).from(projects$1).get();
|
|
3762
|
+
return last === void 0 || last.position === null ? 0 : last.position + 1;
|
|
3763
|
+
}
|
|
3712
3764
|
function publish$1() {
|
|
3713
3765
|
broadcast({
|
|
3714
3766
|
type: "projects",
|
|
@@ -3848,7 +3900,7 @@ function sessions() {
|
|
|
3848
3900
|
return withStatus(allProviders.flatMap((provider) => listed[provider]).filter((item) => known.has(item.projectId))).filter((session) => session.status !== "inactive");
|
|
3849
3901
|
}
|
|
3850
3902
|
async function inactiveSessions(projectId) {
|
|
3851
|
-
return (await everything(
|
|
3903
|
+
return (await everything(projects().filter((project) => projectId === null || project.id === projectId || projectId === "no-project" && project.scratch).map((project) => [project.path, project.id]))).filter((session) => session.status === "inactive").sort((a, b) => b.updatedAt - a.updatedAt);
|
|
3852
3904
|
}
|
|
3853
3905
|
async function searchableSessions() {
|
|
3854
3906
|
const paths = projectPaths();
|
|
@@ -4036,11 +4088,17 @@ async function tracked(provider, paths) {
|
|
|
4036
4088
|
if (provider === "codex" && !codexInstalled() || provider === "opencode" && !openCodeInstalled()) return null;
|
|
4037
4089
|
return (await Promise.all(db.select().from(sessions$1).all().map((record) => {
|
|
4038
4090
|
const owner = ownerOf(record.sessionId);
|
|
4039
|
-
if (owner === provider) return
|
|
4040
|
-
if (owner === void 0) return
|
|
4091
|
+
if (owner === provider) return lookup(provider, record.sessionId, paths);
|
|
4092
|
+
if (owner === void 0) return lookup(provider, record.sessionId, paths).catch(() => null);
|
|
4041
4093
|
return null;
|
|
4042
4094
|
}))).filter((item) => item !== null);
|
|
4043
4095
|
}
|
|
4096
|
+
async function lookup(provider, sessionId, paths) {
|
|
4097
|
+
const known = listed[provider].find((item) => item.id === sessionId);
|
|
4098
|
+
const knownPath = known === void 0 ? void 0 : [...paths].find((entry) => entry[1] === known.projectId);
|
|
4099
|
+
if (knownPath === void 0) return info(provider, sessionId, paths);
|
|
4100
|
+
return await info(provider, sessionId, new Map([knownPath])) ?? info(provider, sessionId, paths);
|
|
4101
|
+
}
|
|
4044
4102
|
function watchSessions() {
|
|
4045
4103
|
for (const watcher of watchers) watcher.close();
|
|
4046
4104
|
watchers = [["claude", path.join(claudeBaseDir(), "projects")], ["codex", path.join(codexBaseDir, "sessions")]].flatMap(([provider, dir]) => {
|
|
@@ -4095,7 +4153,7 @@ async function updateSession(id, patch) {
|
|
|
4095
4153
|
}
|
|
4096
4154
|
if (patch.title !== void 0) refreshSessions(providerOf(id));
|
|
4097
4155
|
}
|
|
4098
|
-
async function removeSession(id) {
|
|
4156
|
+
async function removeSession(id, projectId) {
|
|
4099
4157
|
const provider = providerOf(id);
|
|
4100
4158
|
const run = runs.get(id);
|
|
4101
4159
|
if (run !== void 0) {
|
|
@@ -4114,6 +4172,13 @@ async function removeSession(id) {
|
|
|
4114
4172
|
db.delete(sessions$1).where(eq(sessions$1.sessionId, id)).run();
|
|
4115
4173
|
db.delete(sessionOpens).where(eq(sessionOpens.sessionId, id)).run();
|
|
4116
4174
|
listed[provider] = listed[provider].filter((item) => item.id !== id);
|
|
4175
|
+
const project = projects().find((item) => item.id === projectId);
|
|
4176
|
+
if (project === void 0 || !project.scratch) return;
|
|
4177
|
+
try {
|
|
4178
|
+
if ((await everything([[project.path, project.id]])).filter((item) => item.id !== id).length === 0) removeProject(project.id);
|
|
4179
|
+
} catch (error) {
|
|
4180
|
+
console.warn(`could not check other chats in ${project.path}: ${error}`);
|
|
4181
|
+
}
|
|
4117
4182
|
}
|
|
4118
4183
|
async function fork(sessionId, projectId, messageId) {
|
|
4119
4184
|
const provider = providerOf(sessionId);
|
|
@@ -5930,25 +5995,37 @@ async function open$1(client, sessionId, projectId) {
|
|
|
5930
5995
|
}
|
|
5931
5996
|
async function send$1(client, request) {
|
|
5932
5997
|
assertEnabled(request.settings.provider, request.settings.accountId);
|
|
5998
|
+
if (request.projectId === "no-project") {
|
|
5999
|
+
if (request.sessionId !== null) throw new Error("A started chat always has a project");
|
|
6000
|
+
const project = await addScratchProject(request.prompt.text);
|
|
6001
|
+
await submitFirst(client, await startRun(project.path, request.settings).catch(async (error) => {
|
|
6002
|
+
await removeScratchProject(project.id, project.path);
|
|
6003
|
+
throw error;
|
|
6004
|
+
}), request, project.id);
|
|
6005
|
+
return;
|
|
6006
|
+
}
|
|
5933
6007
|
const cwd = projectPath(request.projectId);
|
|
5934
6008
|
if (!statSync(cwd, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`${cwd} is not a folder`);
|
|
5935
6009
|
if (request.sessionId === null) {
|
|
5936
|
-
|
|
5937
|
-
const sessionId = run.sessionId;
|
|
5938
|
-
client.opening = sessionId;
|
|
5939
|
-
client.sessionId = sessionId;
|
|
5940
|
-
client.send({
|
|
5941
|
-
type: "started",
|
|
5942
|
-
sessionId,
|
|
5943
|
-
projectId: request.projectId
|
|
5944
|
-
});
|
|
5945
|
-
await run.submit(request.prompt, request.delivery);
|
|
5946
|
-
recordInteraction(sessionId);
|
|
6010
|
+
await submitFirst(client, await startRun(cwd, request.settings), request, request.projectId);
|
|
5947
6011
|
return;
|
|
5948
6012
|
}
|
|
5949
6013
|
const sessionId = request.sessionId;
|
|
5950
6014
|
await locked(sessionId, () => sendExisting(client, sessionId, cwd, request));
|
|
5951
6015
|
}
|
|
6016
|
+
async function submitFirst(client, run, request, projectId) {
|
|
6017
|
+
const sessionId = run.sessionId;
|
|
6018
|
+
client.opening = sessionId;
|
|
6019
|
+
client.sessionId = sessionId;
|
|
6020
|
+
client.send({
|
|
6021
|
+
type: "started",
|
|
6022
|
+
sessionId,
|
|
6023
|
+
projectId,
|
|
6024
|
+
from: request.projectId
|
|
6025
|
+
});
|
|
6026
|
+
await run.submit(request.prompt, request.delivery);
|
|
6027
|
+
recordInteraction(sessionId);
|
|
6028
|
+
}
|
|
5952
6029
|
async function sendExisting(client, sessionId, cwd, request) {
|
|
5953
6030
|
const provider = providerOf(sessionId);
|
|
5954
6031
|
if (request.settings.provider !== provider) throw new Error(`This session belongs to ${providerName(provider)}`);
|
|
@@ -7643,7 +7720,7 @@ const handlers = {
|
|
|
7643
7720
|
},
|
|
7644
7721
|
"sessions.remove": async (params) => {
|
|
7645
7722
|
try {
|
|
7646
|
-
await removeSession(params.id);
|
|
7723
|
+
await removeSession(params.id, params.projectId);
|
|
7647
7724
|
} finally {
|
|
7648
7725
|
publishSessions();
|
|
7649
7726
|
}
|
|
@@ -7721,8 +7798,8 @@ const handlers = {
|
|
|
7721
7798
|
publishAccounts();
|
|
7722
7799
|
return null;
|
|
7723
7800
|
},
|
|
7724
|
-
capabilities: (params) => capabilities(params.provider, params.accountId, params.projectId === null ? os.homedir() : projectPath(params.projectId)),
|
|
7725
|
-
"files.list": (params) => listFiles(projectPath(params.projectId)),
|
|
7801
|
+
capabilities: (params) => capabilities(params.provider, params.accountId, params.projectId === null || params.projectId === "no-project" ? os.homedir() : projectPath(params.projectId)),
|
|
7802
|
+
"files.list": (params) => params.projectId === "no-project" ? [] : listFiles(projectPath(params.projectId)),
|
|
7726
7803
|
"files.entries": (params) => listEntries(projectPath(params.projectId)),
|
|
7727
7804
|
"files.read": (params) => readProjectFile(projectPath(params.projectId), params.path),
|
|
7728
7805
|
"files.write": (params) => writeProjectFile(projectPath(params.projectId), params.path, params.content, params.base),
|
|
@@ -9375,6 +9452,11 @@ function serveFile(pathname, request, response) {
|
|
|
9375
9452
|
}
|
|
9376
9453
|
const requested = path.join(publicDir, path.normalize(pathname));
|
|
9377
9454
|
const file = requested.startsWith(publicDir + path.sep) && statSync(requested, { throwIfNoEntry: false })?.isFile() ? requested : path.join(publicDir, "index.html");
|
|
9455
|
+
if (file !== requested && pathname.startsWith("/assets/")) {
|
|
9456
|
+
response.statusCode = 404;
|
|
9457
|
+
response.end("not found");
|
|
9458
|
+
return;
|
|
9459
|
+
}
|
|
9378
9460
|
if (!existsSync(file)) {
|
|
9379
9461
|
response.statusCode = 404;
|
|
9380
9462
|
response.end("web app not built; run pnpm build");
|