Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
4711ad2
chore
sanchitrk Oct 23, 2026
ebf110c
tweaks
sanchitrk Oct 23, 2026
c02fd34
fix(web): unify desktop shell surfaces and tab alignment
sanchitrk Oct 23, 2026
6325f21
fix(web): refine themes and split session surfaces
sanchitrk Oct 23, 2026
8a22941
fix(web): soften frame drag indicators with theme shadows
sanchitrk Oct 23, 2026
416654f
feat(web): add themed startup splash
sanchitrk Oct 23, 2026
aa34e00
feat(web): add New session button to sidebar
sanchitrk Oct 23, 2026
13d2709
ui: tweaks
sanchitrk Oct 23, 2026
4c006c6
wip: tweaks
sanchitrk Oct 23, 2026
a87a8ce
wip: tweaks
sanchitrk Oct 23, 2026
5e346e9
feat(web): close workspace tabs and drop workspace icons
sanchitrk Oct 23, 2026
edc3d34
feat(web): integrate settings with desktop shell
sanchitrk Oct 23, 2026
f1e6231
feat(web): scroll the providers list and detail on their own
sanchitrk Oct 23, 2026
22a53ea
test: use gpt-6-luna instead of the deprecated gpt-5.5
sanchitrk Oct 23, 2026
7c1c681
feat(harness): list, cancel and update queued prompts
sanchitrk Oct 23, 2026
a70742c
feat(web): show queued steers and follow-ups above the session composer
sanchitrk Oct 23, 2026
cedf85a
feat(harness): resume unfinished runs after a restart
sanchitrk Oct 24, 2026
fc85fea
test(aikit): type the deferred tools fixture as provider function tools
sanchitrk Oct 24, 2026
1ce8aee
chore: lint the web app's Tailwind classes
sanchitrk Oct 24, 2026
02ca60a
feat(web): use a text scale and theme radii instead of arbitrary values
sanchitrk Oct 24, 2026
9eacc6e
refactor(web): share one prompt composer for new sessions and replies
sanchitrk Oct 24, 2026
82996c0
feat(web): one-line workspace tabs titled by the focused widget
sanchitrk Oct 24, 2026
f2bfaaa
feat(web): keep each workspace tab's title across restarts
sanchitrk Oct 24, 2026
fe8eea5
test(aikit): expect modelgen to leave out an empty Codex provider
sanchitrk Oct 24, 2026
ff2b2bc
docs: note unreleased harness, cli and aikit changes
sanchitrk Oct 24, 2026
a5c33d6
chore: keep reference projects out of code comments
sanchitrk Oct 24, 2026
618f908
chore(tui): restore its comments as they were
sanchitrk Oct 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 47 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,59 @@ This file is the canonical source for unreleased changes and published release n

## Format

- Add in-progress work under `## [Unreleased]
- Add in-progress work under `## [Unreleased]`.

## [Unreleased]

### Added

- The desktop start page creates a session in the folder you pick, or in a memory sandbox, sends your first prompt and opens the session. A memory session's project is named "In Memory".
- The desktop start page's composer has a model picker and a Reasoning picker. The model picker lists providers down a rail with their models beside it and searches across all of them. With no model picked, settings decide; a default whose provider has no credentials shows muted and cannot be picked. Reasoning offers only the levels the model supports and marks the settings level as Default. The picks are remembered across restarts.

### @codeworksh/harness

#### Added

- Prompts queued while the agent works can be read and changed before it takes them in. `Control.pending` lists them, steers and follow-ups, in the order they were sent. `Control.cancel` withdraws one, recorded as a `session.prompt.cancelled` event; it returns `false` when the prompt was already taken in, and a cancelled message id may be sent again. `Control.update` moves one to the other lane or rewrites its text, keeping its place, recorded as a `session.prompt.updated` event; moving it to the steer lane also wakes the session, so a prompt left queued by a stop runs now.
- A run cut off by a crash or a shutdown resumes when the server starts again (`Control.resumeUnfinished`). A crash counts as a restart and a shutdown does not; a committed turn starts the count over. After 5 restarts (`Control.RESTART_LIMIT`) the session is parked instead of resumed: `Session.Info.parked` is `true`, and its next prompt runs it again. A resumed run answers the reply it was cut off in before a queued follow-up goes in; steers join it at once. A run the user stopped is not resumed, and what was queued behind it stays queued.

#### Fixed

- A turn started for a prompt that was cancelled just before it joined the conversation no longer asks the model anything.
- A prompt changed, cancelled or sent again while it was being taken in no longer fails the run; the run takes in the prompt as it now stands.

#### Internal

- Migrations add `session_input.cancelled_seq`, and `session.execution` and `session.restarts`.
- Tests use `gpt-6-luna` in place of the deprecated `gpt-5.5`.

### @codeworksh/cli

#### Added

- Added the `session.queue` RPC (`{ sessionId }`): the prompts sent while the agent works and not yet taken in, steers and follow-ups, in the order they were sent.
- Added the `session.cancel` RPC (`{ sessionId, messageId }`), which withdraws a queued prompt. `cancelled` is `false` when the prompt was already taken in.
- Added the `session.update` RPC (`{ sessionId, messageId, delivery?, text? }`), which moves a queued prompt to the other lane or rewrites its text, keeping its place. `delivery: "steer"` also wakes the session. `updated` is `false` when the prompt was already taken in.
- `codework serve` resumes the sessions a crash or a shutdown cut off when it starts. Session info carries `parked: true` for a session that restarted too often to resume on its own; its next prompt runs it again.

#### Fixed

- Ctrl+C on `codework run` without `--server` stops the run as a user stop, so `codework serve` does not resume it later.

#### Internal

- Tests use `gpt-6-luna` in place of the deprecated `gpt-5.5`.

### @codeworksh/aikit

#### Fixed

- `modelgen` leaves out providers with no models instead of writing them as empty entries.

#### Internal

- `gpt-5.5` is out of the live OpenAI test models; tests use `gpt-6-luna`.

## [@codeworksh/cli@0.4.0] - 2026-10-20

### Added
Expand Down
7 changes: 3 additions & 4 deletions apps/contract/src/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,27 +46,26 @@ export const Workspace = Schema.Struct({
bit: Schema.Number,
pos: Schema.Number,
name: Schema.String,
/** A name from the app's icon set, e.g. "code". */
icon: Schema.String,
layout: Layout,
/** Master column share of the width, 0–1. */
mfact: Schema.Number,
/** Created to show something the user opened (a session); removed once it shows nothing. */
auto: Schema.Boolean,
/** What its tab last read, as its focused widget set it; absent until a widget has. */
tab: Schema.optionalKey(Schema.String),
});
export type Workspace = typeof Workspace.Type;

export const WorkspaceCreate = Schema.Struct({
name: Schema.String,
icon: Schema.String,
auto: Schema.optionalKey(Schema.Boolean),
});
export const WorkspaceUpdate = Schema.Struct({
id: Schema.String,
name: Schema.optional(Schema.String),
icon: Schema.optional(Schema.String),
layout: Schema.optional(Layout),
mfact: Schema.optional(Schema.Number),
tab: Schema.optional(Schema.String),
});
export const WorkspaceOrder = Schema.Struct({ ids: Schema.Array(Schema.String) });
export const WorkspaceId = Schema.Struct({ id: Schema.String });
Expand Down
5 changes: 5 additions & 0 deletions apps/desktop/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,16 @@ function createMainWindow(): BrowserWindow {
}),
// Windows and Linux take the window's icon; macOS ignores it for the Dock's.
icon: ICON,
// Hidden until the page's first paint, the splash in the saved theme, so it never opens blank.
show: false,
webPreferences: {
preload: path.join(app.getAppPath(), "out/preload.cjs"),
sandbox: true,
},
});
window.once("ready-to-show", () => window.show());
// Linux can miss ready-to-show; the load finishing reveals the window there.
if (process.platform === "linux") window.webContents.once("did-finish-load", () => window.show());
void window.loadURL(APP_URL);
return window;
}
Expand Down
10 changes: 10 additions & 0 deletions apps/server/src/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,16 @@ const migrations = {
})}`;
yield* sql`DELETE FROM kv WHERE package = 'codework' AND key LIKE 'servers.local.%'`;
}),
// Workspace tabs all show the window icon now.
"202610230001_drop_workspace_icon": Effect.gen(function* () {
const sql = yield* SqlClient.SqlClient;
yield* sql`ALTER TABLE workspaces DROP COLUMN icon`;
}),
// What the workspace's tab last read, so a workspace not yet opened since a restart names what it shows.
"202610240001_workspace_tab": Effect.gen(function* () {
const sql = yield* SqlClient.SqlClient;
yield* sql`ALTER TABLE workspaces ADD COLUMN tab TEXT`;
}),
};

const setup = Effect.gen(function* () {
Expand Down
16 changes: 10 additions & 6 deletions apps/server/src/workspaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,17 @@ const fail = (message: string) => new Contract.WorkspaceError({ message });
export const handlers = Effect.gen(function* () {
const sql = yield* SqlClient.SqlClient;

// SQLite has no booleans; `auto` is stored as 0 or 1.
const list = sql<Omit<Workspace, "auto"> & { auto: number }>`
SELECT id, bit, pos, name, icon, layout, mfact, auto FROM workspaces ORDER BY pos
// SQLite has no booleans; `auto` is stored as 0 or 1. A tab never set is NULL.
const list = sql<Omit<Workspace, "auto" | "tab"> & { auto: number; tab: string | null }>`
SELECT id, bit, pos, name, layout, mfact, auto, tab FROM workspaces ORDER BY pos
`.pipe(
Effect.map((rows) => rows.map((row): Workspace => ({ ...row, auto: row.auto === 1 }))),
Effect.map((rows) =>
rows.map(({ auto, tab, ...row }): Workspace => ({
...row,
auto: auto === 1,
...(tab === null ? {} : { tab }),
})),
),
Effect.orDie,
);

Expand All @@ -49,7 +55,6 @@ export const handlers = Effect.gen(function* () {

"workspaces.create": Effect.fn("workspaces.create")(function* ({
name,
icon,
auto = false,
}: typeof WorkspaceCreate.Type) {
return yield* sql
Expand All @@ -64,7 +69,6 @@ export const handlers = Effect.gen(function* () {
bit,
pos: current.length,
name,
icon,
layout: "tile",
mfact: 0.6,
auto,
Expand Down
4 changes: 2 additions & 2 deletions apps/server/test/__artifacts__/desk.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
"bit": 3,
"pos": 3,
"name": "Scratch",
"icon": "box",
"layout": "tile",
"mfact": 0.6,
"auto": false
Expand Down Expand Up @@ -147,7 +146,8 @@
{
"name": "Kept",
"auto": true,
"pos": 3
"pos": 3,
"tab": "Kept / A session"
}
],
"desk": {
Expand Down
17 changes: 13 additions & 4 deletions apps/server/test/desk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ it("desk round-trips and follows workspace deletion", async () => {
const rpc = yield* RpcClient.make(Contract.Api);
const empty = yield* rpc["desk.get"]();

const workspace = yield* rpc["workspaces.create"]({ name: "Scratch", icon: "box" });
const workspace = yield* rpc["workspaces.create"]({ name: "Scratch" });
const bit = 1 << workspace.bit;
const saved: Contract.Desk = {
instances: [
Expand Down Expand Up @@ -93,8 +93,10 @@ it("desk round-trips and follows workspace deletion", async () => {
}).pipe(Effect.flip);

// Two auto workspaces: one keeps a frame, the other is left empty without being closed.
const kept = yield* rpc["workspaces.create"]({ name: "Kept", icon: "box", auto: true });
yield* rpc["workspaces.create"]({ name: "Stray", icon: "box", auto: true });
const kept = yield* rpc["workspaces.create"]({ name: "Kept", auto: true });
// What its tab last read, as its frame set it.
yield* rpc["workspaces.update"]({ id: kept.id, tab: "Kept / A session" });
yield* rpc["workspaces.create"]({ name: "Stray", auto: true });
yield* rpc["desk.save"]({
instances: [{ id: "notes-2", kind: "notes", tags: 1 << kept.bit, props: {}, pinned: false }],
tileOrder: ["notes-2"],
Expand All @@ -121,9 +123,16 @@ it("desk round-trips and follows workspace deletion", async () => {
const rpc = yield* RpcClient.make(Contract.Api);
const workspaces = yield* rpc["workspaces.list"]();
const desk = yield* rpc["desk.get"]();
return { workspaces: workspaces.map(({ name, auto, pos }) => ({ name, auto, pos })), desk };
return { workspaces: workspaces.map(({ name, auto, pos, tab }) => ({ name, auto, pos, tab })), desk };
}).pipe(Effect.scoped, Effect.provide(protocol()), Effect.runPromise);
expect(afterRestart.workspaces.map((workspace) => workspace.name)).toEqual(["Main", "Code", "Notes", "Kept"]);
// A workspace's last tab survives the restart, so it names what it shows before its frame loads.
expect(afterRestart.workspaces.map((workspace) => workspace.tab)).toEqual([
undefined,
undefined,
undefined,
"Kept / A session",
]);

await expect(`${JSON.stringify({ ...steps, afterRestart }, null, "\t")}\n`).toMatchFileSnapshot(
"./__artifacts__/desk.json",
Expand Down
56 changes: 55 additions & 1 deletion apps/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,63 @@
document.documentElement.dataset.theme = localStorage.getItem("codework-theme") ?? "tokyo-night";
} catch {}
</script>
<!-- Linked here rather than imported by style.css, so the splash paints in the theme before the app's JS runs. -->
<link rel="stylesheet" href="/src/themes.css" />
<style>
html,
body,
#root {
height: 100%;
margin: 0;
}

/* The app's desktop tint (--cw-desktop in style.css), so the swap to the app does not shift colour. */
body {
background: color-mix(in oklab, var(--t-bg) 96%, var(--t-text));
color: var(--t-text);
font:
13px/1.4 system-ui,
-apple-system,
"Segoe UI",
sans-serif;
}

#boot-shell {
display: flex;
height: 100%;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
-webkit-app-region: drag;
user-select: none;
}

#boot-shell img {
width: 64px;
height: 64px;
border-radius: 14px;
}

#boot-shell button {
padding: 6px 14px;
border: 1px solid currentColor;
border-radius: 6px;
background: transparent;
color: inherit;
font: inherit;
cursor: pointer;
-webkit-app-region: no-drag;
}
</style>
</head>
<body>
<div id="root"></div>
<!-- The splash: shown until the app mounts and replaces it. -->
<div id="root">
<div id="boot-shell">
<img src="/src/assets/codework.png" alt="CodeWork" />
</div>
</div>
<script type="module" src="/src/bootstrap.ts"></script>
</body>
</html>
18 changes: 15 additions & 3 deletions apps/web/src/bootstrap.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
// Separate entry so this module runs before the (heavier) React tree mounts.
void import("./main").catch((error: unknown) => {
console.error(error);
});
void import("./main")
.then(({ startup }) => startup)
.catch((error: unknown) => {
console.error(error);
// The app never mounted, so the splash is still up: it turns into the failure and a way out.
const shell = document.getElementById("boot-shell");
if (shell === null) return;
const message = document.createElement("p");
message.textContent = "CodeWork could not load.";
const reload = document.createElement("button");
reload.type = "button";
reload.textContent = "Reload";
reload.addEventListener("click", () => window.location.reload());
shell.replaceChildren(message, reload);
});
2 changes: 1 addition & 1 deletion apps/web/src/components/banner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export function ComposerNotice({
return (
<div
role="alert"
className={`mx-5.5 -mb-4 flex items-center gap-2.5 rounded-t-[16px] border border-b-0 ${surface} px-3 pt-2.5 pb-6 text-[12px] leading-snug`}
className={`mx-5.5 -mb-4 flex items-center gap-2.5 rounded-t-2xl border border-b-0 ${surface} px-3 pt-2.5 pb-6 text-xs leading-snug`}
>
<Icon className={`size-4 shrink-0 ${text}`} strokeWidth={2} aria-hidden />
<div className="min-w-0 flex-1">
Expand Down
Loading
Loading