Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
ec92fdf
feat(harness): carry an offset on live text and thinking deltas
sanchitrk Nov 5, 2026
23f3ba7
feat(harness): durable run lifecycle, stored run state and startup sweep
sanchitrk Nov 5, 2026
10bd785
feat(harness): store-wide revision and tombstones
sanchitrk Nov 5, 2026
ef46b1b
Merge branch 'feat/harness-revision' into feat/harness-sync
sanchitrk Nov 5, 2026
2b3bbad
Merge branch 'feat/harness-run-lifecycle' into feat/harness-sync
sanchitrk Nov 5, 2026
6e67b8e
Merge branch 'feat/harness-delta-offset' into feat/harness-sync
sanchitrk Nov 5, 2026
0e0654b
fix(harness): stamp rev on run-state projections
sanchitrk Nov 5, 2026
b0e4ae7
fix(harness): end an abandoned run with its failure only
sanchitrk Nov 5, 2026
cb6788d
feat(harness): read the index by revision, with fork cuts and full se…
sanchitrk Nov 5, 2026
c7249f0
feat(codework): store identity on server.info and the index.sync stream
sanchitrk Nov 5, 2026
e6dec02
feat(harness): read a thread's line by page and its detail by revision
sanchitrk Nov 5, 2026
e191ab5
feat(codework): the thread.sync stream
sanchitrk Nov 5, 2026
8bf2b38
feat(codework): thread.page reads older pages of a thread's line
sanchitrk Nov 5, 2026
610baeb
feat(server): create the sync tables and keep servers in the machine …
sanchitrk Nov 5, 2026
8311181
feat(widget): mirror the CodeWork sync streams and store identity
sanchitrk Nov 5, 2026
afc0e3c
feat(server): sync every machine's CodeWork into app.db
sanchitrk Nov 5, 2026
3c89e83
feat(server): track threads with user-set status, priority, groups, o…
sanchitrk Nov 5, 2026
92316c1
feat(codework): carry a queued prompt's admittedSeq on the wire
sanchitrk Nov 5, 2026
1e75577
fix(server): store pending inbox prompts and undelete rows that come …
sanchitrk Nov 5, 2026
b9ee11a
Merge branch 'fix/sync-inbox' into feat/server-tracking
sanchitrk Nov 5, 2026
862f1d9
test(server): sync across OrbStack Linux machines
sanchitrk Nov 5, 2026
54baf80
test(server): time the stop/start shuffle step by its resume only
sanchitrk Nov 5, 2026
a094d15
feat(web): read the sidebar and session threads from the synced tables
sanchitrk Nov 5, 2026
3aa665c
fix(web): spin a fork's tab for its own run, start parts from the fir…
sanchitrk Nov 5, 2026
70379e5
feat(server): add the dashboard read over the desktop columns
sanchitrk Nov 5, 2026
5d8764b
feat(web): add the dashboard widget and pending and stopping run states
sanchitrk Nov 5, 2026
d19608e
fix(web): fail a follow at once for a thread its machine does not have
sanchitrk Nov 5, 2026
7f1b8fe
fix(server): order the sidebar by thread activity and leave subagents…
sanchitrk Nov 5, 2026
cb1c9cd
fix(web): show a tab's run state as stored and clear a stop once ever…
sanchitrk Nov 5, 2026
1943208
fix(web): track the start page's first prompt as pending
sanchitrk Nov 5, 2026
e4f1aaa
fix(web): drop a thread on a dashboard list's header at the top of th…
sanchitrk Nov 5, 2026
ea6793d
feat(web): fork a session at a message
sanchitrk Nov 5, 2026
e595d3e
feat(web): replace the Dashboard with a Linear-like All Threads list
sanchitrk Nov 5, 2026
697c61e
feat(web): open All Threads from the sidebar and lighten its titles
sanchitrk Nov 5, 2026
20b3873
feat(web): open All Threads from the sidebar as a page of its own
sanchitrk Nov 5, 2026
a8562fa
feat(web): drop the title filter from All Threads
sanchitrk Nov 5, 2026
2c631e7
feat(web): pick a thread's status, priority, group and color as Linea…
sanchitrk Nov 5, 2026
38cb998
feat(web): give All Threads' menus shadcn's spacing at the app's type…
sanchitrk Nov 5, 2026
1ced7f0
feat(web): set All Threads' menus in smaller type
sanchitrk Nov 5, 2026
22ae01a
fix(web): steady All Threads' pickers and the sidebar's current row
sanchitrk Nov 5, 2026
ce7c152
feat(web): make All Threads' display options dense
sanchitrk Nov 5, 2026
0c387c9
perf(web): render All Threads rows only when their own thread changes
sanchitrk Nov 5, 2026
db78b1f
fix(web): open a session at its end without moving
sanchitrk Nov 5, 2026
3ebb478
fix(web): keep the theme chosen in the palette and group it by Naviga…
sanchitrk Nov 5, 2026
7d8e168
feat(web): show Projects as a tree of sessions and their forks
sanchitrk Nov 5, 2026
384a90c
fix(web): draw the All Threads page as a lone frame so its title stay…
sanchitrk Nov 5, 2026
5b57638
Revert "fix(web): draw the All Threads page as a lone frame so its ti…
sanchitrk Nov 5, 2026
0d12dd6
feat(web): make Projects a page under Navigation instead of a widget
sanchitrk Nov 5, 2026
9a3e2bb
feat(web): lay out the Projects page as All Threads is
sanchitrk Nov 5, 2026
31b816d
perf(web): compile the web app with the React Compiler
sanchitrk Nov 5, 2026
0eb963d
perf(web): keep All Threads' run display under the React Compiler
sanchitrk Nov 6, 2026
117bf3c
fix(web): let a group be renamed from its menu
sanchitrk Nov 6, 2026
ac23980
feat(web): show a session frame's run in its header
sanchitrk Nov 6, 2026
c200bf5
perf(web): render only what a streaming run changes
sanchitrk Nov 6, 2026
80fca51
fix(web): keep the Projects page's sticky band opaque and show its fo…
sanchitrk Nov 6, 2026
318f14d
fix(web): set Projects bands in regular weight
sanchitrk Nov 6, 2026
4b9f14f
feat(web): nest forks under the thread they were cut from in the Proj…
sanchitrk Nov 6, 2026
a0e032f
perf(web): share clock ticks, pause off-screen loaders, drop compiler…
sanchitrk Nov 6, 2026
cbc139e
Merge branch canary into claude/all-threads-abb106
sanchitrk Nov 6, 2026
d64f518
chore: check dead files and dependencies in every workspace with knip
sanchitrk Nov 6, 2026
2816299
chore: leave the site out of knip, CI does not install it
sanchitrk Nov 6, 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
43 changes: 23 additions & 20 deletions apps/contract/src/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,17 +346,20 @@ export const SyncTracking = Schema.Union([
]);
export type SyncTracking = typeof SyncTracking.Type;

/** What the dashboard splits an organization's threads by. */
export const DashboardBy = Schema.Literals(["status", "priority", "grouping"]);
export type DashboardBy = typeof DashboardBy.Type;
/** What All Threads splits an organization's threads by. */
export const ThreadsBy = Schema.Literals(["status", "priority", "grouping"]);
export type ThreadsBy = typeof ThreadsBy.Type;

/** A thread as the dashboard lists it, with its desktop columns. */
export const DashboardThread = Schema.Struct({
/** A thread as All Threads lists it, with its machine and desktop columns. */
export const ListedThread = Schema.Struct({
id: Schema.String,
sessionId: Schema.String,
originKind: Schema.Literals(["main", "fork"]),
/** The thread a fork was cut from (a fork's, for a fork of a fork); null for a main thread. */
originThreadId: Schema.NullOr(Schema.String),
/** Its own title, else its nearest titled ancestor's, else what an untitled one is called. */
title: Schema.String,
machine: Schema.Struct({ id: Schema.String, name: Schema.String }),
project: Schema.Struct({ id: Schema.String, name: Schema.String }),
runState: RunState,
status: ThreadStatus,
Expand All @@ -368,29 +371,29 @@ export const DashboardThread = Schema.Struct({
/** Last activity, epoch milliseconds. */
updatedAt: Schema.Number,
});
export type DashboardThread = typeof DashboardThread.Type;
export type ListedThread = typeof ListedThread.Type;

/** One list of the dashboard: the threads with one status, one priority, or in one group (`null` is ungrouped), in `ordering`. */
export const DashboardList = Schema.Union([
Schema.TaggedStruct("Status", { status: ThreadStatus, threads: Schema.Array(DashboardThread) }),
Schema.TaggedStruct("Priority", { priority: Priority, threads: Schema.Array(DashboardThread) }),
/** One list of All Threads: the threads with one status, one priority, or in one group (`null` is ungrouped), in `ordering`. */
export const ThreadList = Schema.Union([
Schema.TaggedStruct("Status", { status: ThreadStatus, threads: Schema.Array(ListedThread) }),
Schema.TaggedStruct("Priority", { priority: Priority, threads: Schema.Array(ListedThread) }),
Schema.TaggedStruct("Grouping", {
groupingId: Schema.NullOr(Schema.String),
threads: Schema.Array(DashboardThread),
threads: Schema.Array(ListedThread),
}),
]);
export type DashboardList = typeof DashboardList.Type;
export type ThreadList = typeof ThreadList.Type;

/**
* An organization's threads split into lists, each present even when empty: by status, by priority
* (urgent first, none last), or by group (groups in their `ordering`, ungrouped last). The
* organization's groups come with it, in their `ordering`, whatever the split.
*/
export const Dashboard = Schema.Struct({
lists: Schema.Array(DashboardList),
export const AllThreads = Schema.Struct({
lists: Schema.Array(ThreadList),
groups: Schema.Array(Grouping),
});
export type Dashboard = typeof Dashboard.Type;
export type AllThreads = typeof AllThreads.Type;

/**
* The topics a client follows to keep each read current; it reads again when one fires.
Expand All @@ -399,12 +402,12 @@ export type Dashboard = typeof Dashboard.Type;
* - `threads.line`: `sync/changed` whose `threadId` is the thread (its entries and inbox) or that
* names a thread (the thread itself, or the ancestor an untitled fork takes its title from);
* `sync/delta/<thread>` streams its draft between checkpoints.
* - `dashboard.list`: `sync/changed` naming a thread or a removed session; every `sync/status` and
* - `threads.list`: `sync/changed` naming a thread or a removed session; every `sync/status` and
* `sync/tracking`.
*/
export const readTopics = {
sidebar: [syncTopics.changed, syncTopics.tracking],
dashboard: [syncTopics.changed, syncTopics.status, syncTopics.tracking],
threads: [syncTopics.changed, syncTopics.status, syncTopics.tracking],
line: (threadId: string) => [syncTopics.changed, syncTopics.delta(threadId)],
};

Expand Down Expand Up @@ -432,9 +435,9 @@ export const Api = RpcGroup.make(
error: SyncError,
}),
/** An organization's threads that are not deleted, subagents left out, split by `by` into lists in `ordering`. Read from app.db alone. */
Rpc.make("dashboard.list", {
payload: { organizationId: Schema.String, by: DashboardBy },
success: Dashboard,
Rpc.make("threads.list", {
payload: { organizationId: Schema.String, by: ThreadsBy },
success: AllThreads,
}),
/** The models a new session can use; `hostDir` is its folder, whose project settings apply. */
Rpc.make("models.list", {
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export const layer = Contract.Api.toLayer(
...tracking,

"sidebar.list": (input) => Reads.sidebar(input).pipe(Effect.provideService(SqlClient.SqlClient, sql)),
"dashboard.list": (input) => Reads.dashboard(input).pipe(Effect.provideService(SqlClient.SqlClient, sql)),
"threads.list": (input) => Reads.threads(input).pipe(Effect.provideService(SqlClient.SqlClient, sql)),
"threads.line": (input) => Reads.line(input).pipe(Effect.provideService(SqlClient.SqlClient, sql)),

"host.info": Effect.fn("host.info")(function* () {
Expand Down
39 changes: 22 additions & 17 deletions apps/server/src/reads.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Effect, Schema } from "effect";
import { SqlClient } from "effect/sql";
import {
type Dashboard,
type DashboardBy,
type DashboardList,
type DashboardThread,
type AllThreads,
type ListedThread,
type ThreadList,
type ThreadsBy,
type Grouping,
type LineRequest,
LineThread,
Expand Down Expand Up @@ -120,7 +120,7 @@ export const sidebar = Effect.fn("Reads.sidebar")(function* ({
return listed.toSorted((a, b) => activity(b) - activity(a));
}, Effect.orDie);

/** The status lists in the order the dashboard shows them: what needs the user first. */
/** The status lists in the order All Threads shows them: what needs the user first. */
const STATUSES: ReadonlyArray<ThreadStatus> = ["working", "attention", "settled", "cancelled"];
/** Urgent first and none last, as Linear. */
const PRIORITIES: ReadonlyArray<Priority> = [1, 2, 3, 4, 0];
Expand All @@ -130,25 +130,27 @@ const SPLIT = {
status: "thread.status, thread.ordering",
priority: "thread.priority, thread.ordering",
grouping: "thread.grouping_id, thread.ordering",
} satisfies Record<DashboardBy, string>;
} satisfies Record<ThreadsBy, string>;

export const dashboard = Effect.fn("Reads.dashboard")(function* ({
export const threads = Effect.fn("Reads.threads")(function* ({
organizationId,
by,
}: {
readonly organizationId: string;
readonly by: DashboardBy;
readonly by: ThreadsBy;
}) {
const sql = yield* SqlClient.SqlClient;
const rows = yield* sql<{
id: string;
sessionId: string;
originKind: DashboardThread["originKind"];
originKind: ListedThread["originKind"];
originThreadId: string | null;
title: string | null;
machineId: string;
machineName: string;
projectId: string;
projectName: string;
runState: DashboardThread["runState"];
runState: ListedThread["runState"];
status: ThreadStatus;
priority: Priority;
groupingId: string | null;
Expand All @@ -157,12 +159,13 @@ export const dashboard = Effect.fn("Reads.dashboard")(function* ({
updatedAt: number;
}>`
SELECT thread.id, thread.session_id, thread.origin_kind, thread.origin_thread_id, thread.title,
project.id AS project_id, project.name AS project_name, thread.run_state, thread.status, thread.priority,
machine.id AS machine_id, machine.name AS machine_name, project.id AS project_id, project.name AS project_name, thread.run_state, thread.status, thread.priority,
thread.grouping_id, thread.ordering, COALESCE(thread.color, session.color) AS color, thread.updated_at
FROM thread
JOIN session ON session.id = thread.session_id
JOIN space ON space.machine_id = session.machine_id AND space.id = session.space_id
JOIN project ON project.machine_id = space.machine_id AND project.id = space.project_id
JOIN machine ON machine.id = session.machine_id
WHERE thread.organization_id = ${organizationId} AND thread.deleted_at IS NULL AND thread.origin_kind <> 'subagent'
ORDER BY ${sql.literal(SPLIT[by])}
`;
Expand All @@ -181,11 +184,13 @@ export const dashboard = Effect.fn("Reads.dashboard")(function* ({
}
return UNTITLED;
};
const threads = rows.map((row): DashboardThread => ({
const listed = rows.map((row): ListedThread => ({
id: row.id,
sessionId: row.sessionId,
originKind: row.originKind,
originThreadId: row.originThreadId,
title: titleOf(row),
machine: { id: row.machineId, name: row.machineName },
project: { id: row.projectId, name: row.projectName },
runState: row.runState,
status: row.status,
Expand All @@ -195,25 +200,25 @@ export const dashboard = Effect.fn("Reads.dashboard")(function* ({
color: row.color,
updatedAt: row.updatedAt,
}));
const lists: Array<DashboardList> =
const lists: Array<ThreadList> =
by === "status"
? STATUSES.map((status) => ({
_tag: "Status",
status,
threads: threads.filter((thread) => thread.status === status),
threads: listed.filter((thread) => thread.status === status),
}))
: by === "priority"
? PRIORITIES.map((priority) => ({
_tag: "Priority",
priority,
threads: threads.filter((thread) => thread.priority === priority),
threads: listed.filter((thread) => thread.priority === priority),
}))
: [...groups.map((group) => group.id), null].map((groupingId) => ({
_tag: "Grouping",
groupingId,
threads: threads.filter((thread) => thread.groupingId === groupingId),
threads: listed.filter((thread) => thread.groupingId === groupingId),
}));
return { lists, groups } satisfies Dashboard;
return { lists, groups } satisfies AllThreads;
}, Effect.orDie);

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@
"id": "<thread B>",
"sessionId": "<session B>",
"originKind": "main",
"originThreadId": null,
"title": "Thread B",
"machine": {
"id": "<machine>",
"name": "Scripted"
},
"project": {
"id": "<id 1>",
"name": "Board"
Expand All @@ -31,7 +36,12 @@
"id": "<thread C>",
"sessionId": "<session C>",
"originKind": "main",
"originThreadId": null,
"title": "Thread C",
"machine": {
"id": "<machine>",
"name": "Scripted"
},
"project": {
"id": "<id 1>",
"name": "Board"
Expand All @@ -48,7 +58,12 @@
"id": "<thread A>",
"sessionId": "<session A>",
"originKind": "main",
"originThreadId": null,
"title": "Thread A",
"machine": {
"id": "<machine>",
"name": "Scripted"
},
"project": {
"id": "<id 1>",
"name": "Board"
Expand All @@ -71,7 +86,12 @@
"id": "<thread A fork>",
"sessionId": "<session A>",
"originKind": "fork",
"originThreadId": "<thread A>",
"title": "Thread A",
"machine": {
"id": "<machine>",
"name": "Scripted"
},
"project": {
"id": "<id 1>",
"name": "Board"
Expand All @@ -88,7 +108,12 @@
"id": "<thread D>",
"sessionId": "<session D>",
"originKind": "main",
"originThreadId": null,
"title": "Thread D",
"machine": {
"id": "<machine>",
"name": "Scripted"
},
"project": {
"id": "<id 1>",
"name": "Board"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@
"id": "<thread C>",
"sessionId": "<session C>",
"originKind": "main",
"originThreadId": null,
"title": "Thread C",
"machine": {
"id": "<machine>",
"name": "Scripted"
},
"project": {
"id": "<id 1>",
"name": "Board"
Expand All @@ -25,7 +30,12 @@
"id": "<thread B>",
"sessionId": "<session B>",
"originKind": "main",
"originThreadId": null,
"title": "Thread B",
"machine": {
"id": "<machine>",
"name": "Scripted"
},
"project": {
"id": "<id 1>",
"name": "Board"
Expand Down Expand Up @@ -53,7 +63,12 @@
"id": "<thread D>",
"sessionId": "<session D>",
"originKind": "main",
"originThreadId": null,
"title": "Thread D",
"machine": {
"id": "<machine>",
"name": "Scripted"
},
"project": {
"id": "<id 1>",
"name": "Board"
Expand All @@ -76,7 +91,12 @@
"id": "<thread A>",
"sessionId": "<session A>",
"originKind": "main",
"originThreadId": null,
"title": "Thread A",
"machine": {
"id": "<machine>",
"name": "Scripted"
},
"project": {
"id": "<id 1>",
"name": "Board"
Expand All @@ -99,7 +119,12 @@
"id": "<thread A fork>",
"sessionId": "<session A>",
"originKind": "fork",
"originThreadId": "<thread A>",
"title": "Thread A",
"machine": {
"id": "<machine>",
"name": "Scripted"
},
"project": {
"id": "<id 1>",
"name": "Board"
Expand Down
Loading
Loading