Skip to content

Commit 20b4b21

Browse files
sanchitrkclaude
andauthored
feat(desktop): installable canary builds, composer notices, Servers settings (#89)
* feat(desktop): composer notice when models can't be listed The composer now says why models can't be listed and offers the one fix, and the model and reasoning pickers read "Unavailable", disabled, until the list loads again. A down server is named from its own status (not running, not answering, incompatible version, not installed); a broken settings file shows its error with Reveal; the catalog offers Try Again. The list is asked for again every few seconds while it fails, so the notice clears itself once the cause is fixed. - ModelsError carries the source and path of the broken file. - desktopBridge.reveal shows a file in Finder. - The sidebar's offline message and the composer share problem.ts. - Server status loads at launch and stays held, so Settings › Servers opens without a skeleton and rechecks behind it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(web): centered, faint empty state for a session with no messages Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(web): drop the Default badge from the reasoning menu Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(desktop): Make Default button, server status lines, command executables - Settings › Servers: "Make Default" sits in the server's header beside Remove; the list's default switch is gone. Each list row reads the server's state under its name (Running · host:port, Stopped, Not answering, Incompatible version, Not installed, Won't start). - A server's executable may be a command whose first word is a full path, such as node with --conditions=development and a source entry, so a server can run from source. A plain path, spaces included, stays the binary as before; ~ expands in every word. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(desktop): package the desktop app and publish canary builds to nightly Every canary push packages the desktop app as "CodeWork Canary" for macOS arm64 (dmg, zip) and Linux x64/arm64 (AppImage, deb, pacman), and replaces the `nightly` prerelease with it, titled desktop@v<version>, after npm. - The app server bundles into one dist/index.mjs with every dependency inlined, so a packaged app needs no node_modules. - Packaged, the app loads the server and the web build from Resources. - One identity per channel (CodeWork, CodeWork Dev, CodeWork Canary), so channels install side by side; a local run is sh.codework.desktop.local. - The desktop app has its own version, from 0.3.0, bumped with `vp run bump desktop`; `vp run package <channel>` builds and packages it. - macOS builds are ad-hoc signed until they are signed and notarized. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(desktop): move the app server's RPC contract into @codeworksh/contract The web app imported the contract from @codeworksh/server while the app server imported the widget backends from @codeworksh/web, a package cycle that stopped `vp run build` and `vp run typecheck` from planning. The contract now lives in apps/contract, depending only on effect and @codeworksh/widget; the web app and the app server both import it, and the web app no longer depends on the app server. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 102e692 commit 20b4b21

44 files changed

Lines changed: 2438 additions & 525 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/publish.yml‎

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ name: Publish
55
# - latest: run by hand on `main`; publishes each package whose version is not on npm yet
66
# dev and latest also make GitHub Releases (scripts/github.mjs): `devel`, one prerelease with the
77
# standalone binary replaced on every dev push, or `<pkg>@v<version>` per package released on
8-
# latest, the CLI's with the binary and marked latest. canary stays npm-only.
8+
# latest, the CLI's with the binary and marked latest. canary makes `nightly`, one prerelease
9+
# with the desktop app replaced on every canary push.
910
on:
1011
push:
1112
branches: [canary, dev]
@@ -195,3 +196,76 @@ jobs:
195196
# codework.sh/install verifies each download against SHA256SUMS.
196197
- run: (cd release && sha256sum *.tar.gz > SHA256SUMS)
197198
- run: node scripts/github.mjs "$CHANNEL" release
199+
200+
desktop:
201+
needs: guard
202+
if: github.ref_name == 'canary'
203+
name: desktop (${{ matrix.target }})
204+
runs-on: ${{ matrix.runner }}
205+
strategy:
206+
fail-fast: true
207+
matrix:
208+
# Each on its own platform: electron-builder packages for the host, and macOS signs.
209+
include:
210+
- target: mac-arm64
211+
runner: macos-14
212+
args: --mac --arm64
213+
- target: linux-arm64
214+
runner: ubuntu-24.04-arm
215+
args: --linux --arm64
216+
- target: linux-x64
217+
runner: ubuntu-24.04
218+
args: --linux --x64
219+
env:
220+
CODEWORK_RELEASE_ID: ${{ needs.guard.outputs.id }}
221+
steps:
222+
- uses: actions/checkout@v6
223+
# The pacman target packs with bsdtar.
224+
- if: runner.os == 'Linux'
225+
run: sudo apt-get update && sudo apt-get install -y libarchive-tools
226+
- uses: voidzero-dev/setup-vp@v1.21.1
227+
with:
228+
node-version: 24
229+
run-install: false
230+
cache: false
231+
# The app packages the web app and the app server it builds, so those install too.
232+
- run: vp install --frozen-lockfile --filter '@codeworksh/desktop...' --filter '@codeworksh/server...'
233+
- working-directory: apps/desktop
234+
run: vp run package "$CHANNEL" ${{ matrix.args }}
235+
# Installers only; electron-builder's update blockmaps and unpacked app stay behind.
236+
- uses: actions/upload-artifact@v7
237+
with:
238+
name: desktop-${{ matrix.target }}
239+
path: |
240+
apps/desktop/dist/codework-desktop-*.dmg
241+
apps/desktop/dist/codework-desktop-*.zip
242+
apps/desktop/dist/codework-desktop-*.AppImage
243+
apps/desktop/dist/codework-desktop-*.deb
244+
apps/desktop/dist/codework-desktop-*.pacman
245+
if-no-files-found: error
246+
retention-days: 1
247+
248+
# After npm, so `nightly` never names a commit whose packages did not ship.
249+
nightly:
250+
needs: [guard, publish, desktop]
251+
if: github.ref_name == 'canary'
252+
runs-on: ubuntu-latest
253+
permissions:
254+
contents: write
255+
env:
256+
CODEWORK_RELEASE_ID: ${{ needs.guard.outputs.id }}
257+
GH_TOKEN: ${{ github.token }}
258+
steps:
259+
- uses: actions/checkout@v6
260+
- uses: voidzero-dev/setup-vp@v1.21.1
261+
with:
262+
node-version: 24
263+
run-install: false
264+
cache: false
265+
- uses: actions/download-artifact@v8
266+
with:
267+
pattern: desktop-*
268+
path: release
269+
merge-multiple: true
270+
- run: (cd release && sha256sum codework-desktop-* > SHA256SUMS)
271+
- run: node scripts/github.mjs canary release

‎CLAUDE.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,13 +80,13 @@ Conventional Commit style with optional package scopes, e.g `feat(aikit): ...`,
8080

8181
Every package is published by CI from `.github/workflows/publish.yml`, which runs `vp run release <channel>` (`scripts/release.mjs`). It builds the TUI binaries, then publishes the packages in dependency order, derived from their `package.json` workspace dependencies (`scripts/workspace.mjs`), pinning each package to the exact versions uploaded in the same run. `scripts/publish.mjs` builds a single package, rewrites its manifest and publishes it from a temp dir.
8282

83-
- **canary**: every push to `canary`, without `vp check` or typecheck, for quick testing. Versions are `<version>-canary.<run>.<attempt>.g<sha>`.
83+
- **canary**: every push to `canary`, without `vp check` or typecheck, for quick testing. Versions are `<version>-canary.<run>.<attempt>.g<sha>`. It also packages the desktop app (`apps/desktop`, `vp run package <channel>`) as "CodeWork Canary" for macOS arm64 and Linux x64/arm64, and replaces the `nightly` prerelease with it.
8484
- **dev**: every push to `dev`. Versions are `<version>-dev.<YYYYMMDDHHMMSS>` (UTC).
8585
- **latest**: run the workflow by hand on `main`. It publishes each package whose manifest version is not on npm yet and skips the rest.
8686

87-
GitHub Releases (`scripts/github.mjs`): dev replaces the `devel` prerelease with the CLI binaries; latest makes a `<pkg>@v<version>` release per package not released yet, the CLI's carrying the binaries. canary has none.
87+
GitHub Releases (`scripts/github.mjs`): dev replaces the `devel` prerelease with the CLI binaries; latest makes a `<pkg>@v<version>` release per package not released yet, the CLI's carrying the binaries. canary replaces the `nightly` prerelease with the desktop app.
8888

89-
Versions are bumped locally with `vp run bump <aikit|plugin|harness|tui|cli> [version]`, which runs `bumpp` with the package's `bump.config.ts` and commits and tags `<pkg>@v<version>` without pushing. `vp run bump tui` moves the launcher and its three platform packages together.
89+
Versions are bumped locally with `vp run bump <aikit|plugin|harness|tui|cli|desktop> [version]`, which runs `bumpp` with the package's `bump.config.ts` and commits and tags `<pkg>@v<version>` without pushing. `vp run bump tui` moves the launcher and its three platform packages together.
9090

9191
Flow:
9292

‎apps/contract/package.json‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "@codeworksh/contract",
3+
"version": "0.0.0",
4+
"private": true,
5+
"description": "The desktop app server's RPC contract, shared by the app server and the web app.",
6+
"type": "module",
7+
"exports": {
8+
".": "./src/contract.ts"
9+
},
10+
"scripts": {
11+
"typecheck": "tsc --noEmit"
12+
},
13+
"dependencies": {
14+
"@codeworksh/widget": "workspace:*",
15+
"effect": "4.0.5"
16+
},
17+
"devDependencies": {
18+
"typescript": "catalog:"
19+
}
20+
}
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,10 @@ export class CodeworkUnavailable extends Schema.TaggedError<CodeworkUnavailable>
2727
message: Schema.String,
2828
}) {}
2929

30-
/** The models could not be listed: a settings file or the model catalog is broken. */
30+
/** The models could not be listed: a settings file or the model catalog, at `path`, is broken. */
3131
export class ModelsError extends Schema.TaggedError<ModelsError>()("Server.ModelsError", {
32+
source: Schema.Literals(["settings", "catalog"]),
33+
path: Schema.String,
3234
message: Schema.String,
3335
}) {}
3436

‎apps/contract/tsconfig.json‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"extends": "../../tsconfig.json",
3+
"compilerOptions": {
4+
"tsBuildInfoFile": "../../.cache/typescript/contract.tsbuildinfo"
5+
},
6+
"include": ["src"]
7+
}

‎apps/desktop/bump.config.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { defineConfig } from "bumpp";
2+
3+
// Release config for the desktop app, loaded automatically by `bumpp`. Its version is its own;
4+
// canary and dev builds add their run id to it as the npm packages do.
5+
export default defineConfig({
6+
tag: "desktop@v%s",
7+
commit: "release: desktop@v%s",
8+
push: false,
9+
});
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import type { Configuration } from "electron-builder";
2+
3+
// One identity per channel, so channels install side by side with their own data. The version
4+
// is the run's, passed by scripts/package.mjs.
5+
const CHANNELS = {
6+
latest: { productName: "CodeWork", appId: "sh.codework.desktop", packageName: "codework-desktop" },
7+
dev: { productName: "CodeWork Dev", appId: "sh.codework.desktop.dev", packageName: "codework-desktop-dev" },
8+
canary: {
9+
productName: "CodeWork Canary",
10+
appId: "sh.codework.desktop.canary",
11+
packageName: "codework-desktop-canary",
12+
},
13+
} as const;
14+
15+
const channel = process.env.CODEWORK_CHANNEL;
16+
if (channel !== "latest" && channel !== "dev" && channel !== "canary") {
17+
throw new Error(`CODEWORK_CHANNEL must be latest, dev or canary, not ${channel}`);
18+
}
19+
const { productName, appId, packageName } = CHANNELS[channel];
20+
21+
export default {
22+
appId,
23+
productName,
24+
// The same name on every channel; the release a file sits in says which channel it is.
25+
artifactName: "codework-desktop-${os}-${arch}.${ext}",
26+
// Nothing native to rebuild: the app server uses node:sqlite.
27+
npmRebuild: false,
28+
copyright: "Copyright © CodeWork",
29+
directories: { output: "dist", buildResources: "resources" },
30+
extraMetadata: { name: packageName, productName, homepage: "https://www.codework.sh" },
31+
// The app is its two bundles and its icon; Electron is the only dependency, and it is the runtime.
32+
files: ["out/*.cjs", "resources/icon.png", "package.json"],
33+
// The app server runs as its own process from Resources, so it sits outside the asar archive.
34+
extraResources: [
35+
{ from: "../server/dist/index.mjs", to: "server/index.mjs" },
36+
{ from: "../web/dist", to: "web" },
37+
],
38+
mac: {
39+
target: [
40+
{ target: "dmg", arch: "arm64" },
41+
{ target: "zip", arch: "arm64" },
42+
],
43+
category: "public.app-category.developer-tools",
44+
icon: "resources/icon.png",
45+
// Ad-hoc: Apple silicon runs nothing unsigned. A downloaded build still needs its quarantine
46+
// flag removed until builds are signed and notarized.
47+
identity: "-",
48+
hardenedRuntime: false,
49+
},
50+
linux: {
51+
// Each format names x64 and arm64 its own way (x86_64, amd64, aarch64); one name for all,
52+
// from the arch scripts/package.mjs builds for.
53+
artifactName: "codework-desktop-linux-${env.CODEWORK_ARCH}.${ext}",
54+
target: ["AppImage", "deb", "pacman"],
55+
category: "Development",
56+
icon: "resources/icon.png",
57+
maintainer: "CodeWork",
58+
executableName: packageName,
59+
},
60+
deb: { packageName },
61+
pacman: { packageName },
62+
// The static AppImage runtime, so it runs without libfuse2.
63+
toolsets: { appimage: "1.0.3" },
64+
publish: null,
65+
} satisfies Configuration;

‎apps/desktop/package.json‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
11
{
22
"name": "@codeworksh/desktop",
3-
"version": "0.0.0",
3+
"version": "0.3.0",
44
"private": true,
5+
"description": "The CodeWork desktop app.",
56
"type": "module",
67
"main": "out/main.cjs",
78
"scripts": {
89
"build": "vp pack",
910
"dev": "node scripts/dev.mjs",
1011
"start": "node scripts/start-electron.mjs",
11-
"typecheck": "tsc --noEmit"
12-
},
13-
"dependencies": {
14-
"electron": "44.4.2"
12+
"typecheck": "tsc --noEmit",
13+
"package": "node scripts/package.mjs"
1514
},
1615
"devDependencies": {
1716
"@types/node": "catalog:",
17+
"bumpp": "^12.3.0",
18+
"electron": "44.4.2",
19+
"electron-builder": "^26.15.3",
1820
"typescript": "catalog:",
1921
"vite-plus": "catalog:"
2022
}

‎apps/desktop/scripts/package.mjs‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// `vp run package <canary|dev|latest> [electron-builder args]`: builds the web app, the app server
2+
// and this app, then packages them for the channel under the run's version, e.g. `--mac --arm64 --dir`.
3+
// In CI the version carries the run id; locally a dry-run id stands in.
4+
import { spawnSync } from "node:child_process";
5+
import { readFileSync } from "node:fs";
6+
import path from "node:path";
7+
import { fileURLToPath } from "node:url";
8+
9+
import { releaseId, releaseVersion } from "../../../scripts/version.mjs";
10+
11+
const desktopDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
12+
13+
const [channel, ...builderArgs] = process.argv.slice(2);
14+
if (!["canary", "dev", "latest"].includes(channel)) {
15+
console.error("Usage: vp run package <canary|dev|latest> [electron-builder args]");
16+
process.exit(1);
17+
}
18+
19+
const run = (command, args, cwd, env = process.env) => {
20+
const result = spawnSync(command, args, { cwd, env, stdio: "inherit" });
21+
if (result.status !== 0) process.exit(result.status ?? 1);
22+
};
23+
24+
// One arch per run: the one asked for, else this machine's (Node names them as the file names do).
25+
const arch = builderArgs.includes("--arm64") ? "arm64" : builderArgs.includes("--x64") ? "x64" : process.arch;
26+
27+
const { version: base } = JSON.parse(readFileSync(path.join(desktopDir, "package.json"), "utf8"));
28+
const version = releaseVersion(base, channel, releaseId(channel, { dryRun: !process.env.CI }));
29+
30+
run("vp", ["build"], path.resolve(desktopDir, "../web"));
31+
run("vp", ["pack"], path.resolve(desktopDir, "../server"));
32+
run("vp", ["pack"], desktopDir);
33+
run(
34+
"pnpm",
35+
[
36+
"exec",
37+
"electron-builder",
38+
"--config",
39+
"electron-builder.config.ts",
40+
`-c.extraMetadata.version=${version}`,
41+
"--publish",
42+
"never",
43+
...builderArgs,
44+
],
45+
desktopDir,
46+
{ ...process.env, CODEWORK_CHANNEL: channel, CODEWORK_ARCH: arch },
47+
);

‎apps/desktop/scripts/start-electron.mjs‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { fileURLToPath } from "node:url";
77
const desktopDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
88
const electronBinary = createRequire(import.meta.url)("electron");
99
const APP_NAME = "CodeWork";
10+
// Its own identity, so a local run never shares data or Dock state with an installed channel.
11+
const APP_ID = "sh.codework.desktop.local";
1012

1113
// macOS names the Dock icon after the running bundle's Info.plist, so on macOS
1214
// Electron runs from a renamed copy of Electron.app, rebuilt when Electron changes.
@@ -15,7 +17,8 @@ function brandedElectron() {
1517
const target = path.join(desktopDir, ".electron-runtime", `${APP_NAME}.app`);
1618
const marker = path.join(desktopDir, ".electron-runtime", "source");
1719
const binary = path.join(target, "Contents", "MacOS", "Electron");
18-
if (existsSync(marker) && readFileSync(marker, "utf8") === source) return binary;
20+
const stamp = `${source}\n${APP_ID}`;
21+
if (existsSync(marker) && readFileSync(marker, "utf8") === stamp) return binary;
1922

2023
rmSync(target, { recursive: true, force: true });
2124
// Keeps the frameworks' relative symlinks; absolute ones crash the helper processes.
@@ -24,11 +27,11 @@ function brandedElectron() {
2427
for (const [key, value] of [
2528
["CFBundleName", APP_NAME],
2629
["CFBundleDisplayName", APP_NAME],
27-
["CFBundleIdentifier", "sh.codework.desktop.dev"],
30+
["CFBundleIdentifier", APP_ID],
2831
])
2932
execFileSync("plutil", ["-replace", key, "-string", value, plist]);
3033
execFileSync("codesign", ["--force", "--deep", "--sign", "-", target]);
31-
writeFileSync(marker, source);
34+
writeFileSync(marker, stamp);
3235
return binary;
3336
}
3437

0 commit comments

Comments
 (0)