diff --git a/apps/contract/src/contract.ts b/apps/contract/src/contract.ts index c9fa6edf..d399b791 100644 --- a/apps/contract/src/contract.ts +++ b/apps/contract/src/contract.ts @@ -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, @@ -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. @@ -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/` 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)], }; @@ -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", { diff --git a/apps/server/src/handlers.ts b/apps/server/src/handlers.ts index 886425bd..70108f90 100644 --- a/apps/server/src/handlers.ts +++ b/apps/server/src/handlers.ts @@ -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* () { diff --git a/apps/server/src/reads.ts b/apps/server/src/reads.ts index 104f5875..977ee4ad 100644 --- a/apps/server/src/reads.ts +++ b/apps/server/src/reads.ts @@ -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, @@ -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 = ["working", "attention", "settled", "cancelled"]; /** Urgent first and none last, as Linear. */ const PRIORITIES: ReadonlyArray = [1, 2, 3, 4, 0]; @@ -130,25 +130,27 @@ const SPLIT = { status: "thread.status, thread.ordering", priority: "thread.priority, thread.ordering", grouping: "thread.grouping_id, thread.ordering", -} satisfies Record; +} satisfies Record; -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; @@ -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])} `; @@ -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, @@ -195,25 +200,25 @@ export const dashboard = Effect.fn("Reads.dashboard")(function* ({ color: row.color, updatedAt: row.updatedAt, })); - const lists: Array = + const lists: Array = 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); /** diff --git a/apps/server/test/__artifacts__/dashboard.grouping.json b/apps/server/test/__artifacts__/threads.grouping.json similarity index 81% rename from apps/server/test/__artifacts__/dashboard.grouping.json rename to apps/server/test/__artifacts__/threads.grouping.json index 10ce4aef..095ef4f5 100644 --- a/apps/server/test/__artifacts__/dashboard.grouping.json +++ b/apps/server/test/__artifacts__/threads.grouping.json @@ -8,7 +8,12 @@ "id": "", "sessionId": "", "originKind": "main", + "originThreadId": null, "title": "Thread B", + "machine": { + "id": "", + "name": "Scripted" + }, "project": { "id": "", "name": "Board" @@ -31,7 +36,12 @@ "id": "", "sessionId": "", "originKind": "main", + "originThreadId": null, "title": "Thread C", + "machine": { + "id": "", + "name": "Scripted" + }, "project": { "id": "", "name": "Board" @@ -48,7 +58,12 @@ "id": "", "sessionId": "", "originKind": "main", + "originThreadId": null, "title": "Thread A", + "machine": { + "id": "", + "name": "Scripted" + }, "project": { "id": "", "name": "Board" @@ -71,7 +86,12 @@ "id": "", "sessionId": "", "originKind": "fork", + "originThreadId": "", "title": "Thread A", + "machine": { + "id": "", + "name": "Scripted" + }, "project": { "id": "", "name": "Board" @@ -88,7 +108,12 @@ "id": "", "sessionId": "", "originKind": "main", + "originThreadId": null, "title": "Thread D", + "machine": { + "id": "", + "name": "Scripted" + }, "project": { "id": "", "name": "Board" diff --git a/apps/server/test/__artifacts__/dashboard.priority.json b/apps/server/test/__artifacts__/threads.priority.json similarity index 80% rename from apps/server/test/__artifacts__/dashboard.priority.json rename to apps/server/test/__artifacts__/threads.priority.json index 74824b59..57bb27de 100644 --- a/apps/server/test/__artifacts__/dashboard.priority.json +++ b/apps/server/test/__artifacts__/threads.priority.json @@ -8,7 +8,12 @@ "id": "", "sessionId": "", "originKind": "main", + "originThreadId": null, "title": "Thread C", + "machine": { + "id": "", + "name": "Scripted" + }, "project": { "id": "", "name": "Board" @@ -25,7 +30,12 @@ "id": "", "sessionId": "", "originKind": "main", + "originThreadId": null, "title": "Thread B", + "machine": { + "id": "", + "name": "Scripted" + }, "project": { "id": "", "name": "Board" @@ -53,7 +63,12 @@ "id": "", "sessionId": "", "originKind": "main", + "originThreadId": null, "title": "Thread D", + "machine": { + "id": "", + "name": "Scripted" + }, "project": { "id": "", "name": "Board" @@ -76,7 +91,12 @@ "id": "", "sessionId": "", "originKind": "main", + "originThreadId": null, "title": "Thread A", + "machine": { + "id": "", + "name": "Scripted" + }, "project": { "id": "", "name": "Board" @@ -99,7 +119,12 @@ "id": "", "sessionId": "", "originKind": "fork", + "originThreadId": "", "title": "Thread A", + "machine": { + "id": "", + "name": "Scripted" + }, "project": { "id": "", "name": "Board" diff --git a/apps/server/test/__artifacts__/dashboard.status.json b/apps/server/test/__artifacts__/threads.status.json similarity index 80% rename from apps/server/test/__artifacts__/dashboard.status.json rename to apps/server/test/__artifacts__/threads.status.json index 4e8fbb0f..a93b7966 100644 --- a/apps/server/test/__artifacts__/dashboard.status.json +++ b/apps/server/test/__artifacts__/threads.status.json @@ -8,7 +8,12 @@ "id": "", "sessionId": "", "originKind": "fork", + "originThreadId": "", "title": "Thread A", + "machine": { + "id": "", + "name": "Scripted" + }, "project": { "id": "", "name": "Board" @@ -31,7 +36,12 @@ "id": "", "sessionId": "", "originKind": "main", + "originThreadId": null, "title": "Thread A", + "machine": { + "id": "", + "name": "Scripted" + }, "project": { "id": "", "name": "Board" @@ -48,7 +58,12 @@ "id": "", "sessionId": "", "originKind": "main", + "originThreadId": null, "title": "Thread D", + "machine": { + "id": "", + "name": "Scripted" + }, "project": { "id": "", "name": "Board" @@ -71,7 +86,12 @@ "id": "", "sessionId": "", "originKind": "main", + "originThreadId": null, "title": "Thread B", + "machine": { + "id": "", + "name": "Scripted" + }, "project": { "id": "", "name": "Board" @@ -94,7 +114,12 @@ "id": "", "sessionId": "", "originKind": "main", + "originThreadId": null, "title": "Thread C", + "machine": { + "id": "", + "name": "Scripted" + }, "project": { "id": "", "name": "Board" diff --git a/apps/server/test/__artifacts__/dashboard.subagent.json b/apps/server/test/__artifacts__/threads.subagent.json similarity index 79% rename from apps/server/test/__artifacts__/dashboard.subagent.json rename to apps/server/test/__artifacts__/threads.subagent.json index c5dd71a5..4ece71ed 100644 --- a/apps/server/test/__artifacts__/dashboard.subagent.json +++ b/apps/server/test/__artifacts__/threads.subagent.json @@ -18,7 +18,12 @@ "id": "", "sessionId": "", "originKind": "fork", + "originThreadId": "", "title": "Thread A", + "machine": { + "id": "", + "name": "Scripted" + }, "project": { "id": "", "name": "Board" @@ -35,7 +40,12 @@ "id": "", "sessionId": "", "originKind": "main", + "originThreadId": null, "title": "Thread D", + "machine": { + "id": "", + "name": "Scripted" + }, "project": { "id": "", "name": "Board" @@ -52,7 +62,12 @@ "id": "", "sessionId": "", "originKind": "main", + "originThreadId": null, "title": "Thread C", + "machine": { + "id": "", + "name": "Scripted" + }, "project": { "id": "", "name": "Board" @@ -69,7 +84,12 @@ "id": "", "sessionId": "", "originKind": "main", + "originThreadId": null, "title": "Thread B", + "machine": { + "id": "", + "name": "Scripted" + }, "project": { "id": "", "name": "Board" @@ -86,7 +106,12 @@ "id": "", "sessionId": "", "originKind": "main", + "originThreadId": null, "title": "Thread A", + "machine": { + "id": "", + "name": "Scripted" + }, "project": { "id": "", "name": "Board" diff --git a/apps/server/test/__artifacts__/dashboard.topics.json b/apps/server/test/__artifacts__/threads.topics.json similarity index 100% rename from apps/server/test/__artifacts__/dashboard.topics.json rename to apps/server/test/__artifacts__/threads.topics.json diff --git a/apps/server/test/dashboard.test.ts b/apps/server/test/threads.test.ts similarity index 83% rename from apps/server/test/dashboard.test.ts rename to apps/server/test/threads.test.ts index 0248c83b..9fc2054a 100644 --- a/apps/server/test/dashboard.test.ts +++ b/apps/server/test/threads.test.ts @@ -1,6 +1,6 @@ import { Effect, Stream } from "effect"; import { afterEach, expect, it } from "vite-plus/test"; -import { Contract, type Dashboard, type DashboardBy } from "@codeworksh/contract"; +import { type AllThreads, Contract, type ThreadsBy } from "@codeworksh/contract"; import { addMachine, app, @@ -20,22 +20,22 @@ import { afterEach(cleanup); -const snapshot = (name: string, artifact: unknown) => match(`dashboard.${name}`, artifact); +const snapshot = (name: string, artifact: unknown) => match(`threads.${name}`, artifact); -const read = (rpc: AppRpc, by: DashboardBy) => - rpc["dashboard.list"]({ organizationId: Contract.PERSONAL_ORGANIZATION, by }); +const read = (rpc: AppRpc, by: ThreadsBy) => + rpc["threads.list"]({ organizationId: Contract.PERSONAL_ORGANIZATION, by }); -/** Each list as its key and its threads' names, in the order read: what the dashboard draws. */ -const names = (dashboard: Dashboard, label: (id: string) => string) => - dashboard.lists.map((list) => ({ +/** Each list as its key and its threads' names, in the order read: what All Threads draws. */ +const names = (all: AllThreads, label: (id: string) => string) => + all.lists.map((list) => ({ list: list._tag === "Status" ? list.status : list._tag === "Priority" ? list.priority : list.groupingId, names: list.threads.map((thread) => label(thread.id)), })); /** Times as `