From d7402ba923553e20b5d5127a2fc44e7b0f6dc5ae Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 31 Mar 2026 21:48:28 +0000 Subject: [PATCH 1/2] Initial plan From 4526e01c166a5ce0a2872b909ab9678476e19084 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 31 Mar 2026 22:07:00 +0000 Subject: [PATCH 2/2] feat: add thread archiving support - Add archivedAt field to OrchestrationThread schema in contracts - Add ThreadArchiveCommand and ThreadUnarchiveCommand to contracts - Add ThreadArchivedPayload and ThreadUnarchivedPayload to contracts - Add thread.archived and thread.unarchived to OrchestrationEventType - Update DispatchableClientOrchestrationCommand and ClientOrchestrationCommand - Add archive/unarchive handlers in the decider - Add archive/unarchive event handlers in the projection pipeline - Add archive/unarchive to the projector - Add migration 018 for archived_at column - Add archivedAt to ProjectionThread schema and SQL queries - Add archivedAt to web Thread type - Add showArchivedThreads setting to appSettings - Update Sidebar: archive/unarchive context menu, show toggle button - Filter archived threads in sidebar based on showArchivedThreads setting - Update all tests to include archivedAt field Agent-Logs-Url: https://github.com/OpenKnots/okcode/sessions/b99a8fc7-a48f-4212-9ce8-eb793dcf4110 Co-authored-by: BunsDev <68980965+BunsDev@users.noreply.github.com> --- .../Layers/CheckpointDiffQuery.test.ts | 1 + .../Layers/ProjectionPipeline.ts | 31 +++++ .../Layers/ProjectionSnapshotQuery.test.ts | 1 + apps/server/src/orchestration/Schemas.ts | 4 + .../orchestration/commandInvariants.test.ts | 2 + apps/server/src/orchestration/decider.ts | 44 +++++++ .../src/orchestration/projector.test.ts | 1 + apps/server/src/orchestration/projector.ts | 25 ++++ .../persistence/Layers/ProjectionThreads.ts | 15 ++- apps/server/src/persistence/Migrations.ts | 2 + .../018_ProjectionThreadsArchivedAt.ts | 11 ++ .../persistence/Services/ProjectionThreads.ts | 1 + apps/web/src/appSettings.ts | 1 + apps/web/src/components/ChatView.browser.tsx | 2 + .../components/KeybindingsToast.browser.tsx | 1 + apps/web/src/components/Sidebar.tsx | 112 +++++++++++++++++- apps/web/src/store.test.ts | 2 + apps/web/src/store.ts | 1 + apps/web/src/types.ts | 1 + packages/contracts/src/orchestration.ts | 39 ++++++ 20 files changed, 287 insertions(+), 10 deletions(-) create mode 100644 apps/server/src/persistence/Migrations/018_ProjectionThreadsArchivedAt.ts diff --git a/apps/server/src/checkpointing/Layers/CheckpointDiffQuery.test.ts b/apps/server/src/checkpointing/Layers/CheckpointDiffQuery.test.ts index 76a5a2c1..03875e87 100644 --- a/apps/server/src/checkpointing/Layers/CheckpointDiffQuery.test.ts +++ b/apps/server/src/checkpointing/Layers/CheckpointDiffQuery.test.ts @@ -59,6 +59,7 @@ function makeSnapshot(input: { createdAt: "2026-01-01T00:00:00.000Z", updatedAt: "2026-01-01T00:00:00.000Z", deletedAt: null, + archivedAt: null, messages: [], activities: [], proposedPlans: [], diff --git a/apps/server/src/orchestration/Layers/ProjectionPipeline.ts b/apps/server/src/orchestration/Layers/ProjectionPipeline.ts index 36313b8c..dd22265f 100644 --- a/apps/server/src/orchestration/Layers/ProjectionPipeline.ts +++ b/apps/server/src/orchestration/Layers/ProjectionPipeline.ts @@ -425,6 +425,7 @@ const makeOrchestrationProjectionPipeline = Effect.gen(function* () { createdAt: event.payload.createdAt, updatedAt: event.payload.updatedAt, deletedAt: null, + archivedAt: null, }); return; @@ -494,6 +495,36 @@ const makeOrchestrationProjectionPipeline = Effect.gen(function* () { return; } + case "thread.archived": { + const existingRow = yield* projectionThreadRepository.getById({ + threadId: event.payload.threadId, + }); + if (Option.isNone(existingRow)) { + return; + } + yield* projectionThreadRepository.upsert({ + ...existingRow.value, + archivedAt: event.payload.archivedAt, + updatedAt: event.payload.archivedAt, + }); + return; + } + + case "thread.unarchived": { + const existingRow = yield* projectionThreadRepository.getById({ + threadId: event.payload.threadId, + }); + if (Option.isNone(existingRow)) { + return; + } + yield* projectionThreadRepository.upsert({ + ...existingRow.value, + archivedAt: null, + updatedAt: event.payload.unarchivedAt, + }); + return; + } + case "thread.message-sent": case "thread.proposed-plan-upserted": case "thread.activity-appended": { diff --git a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts index 5d649186..15a0cdd5 100644 --- a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts +++ b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts @@ -274,6 +274,7 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => { createdAt: "2026-02-24T00:00:02.000Z", updatedAt: "2026-02-24T00:00:03.000Z", deletedAt: null, + archivedAt: null, messages: [ { id: asMessageId("message-1"), diff --git a/apps/server/src/orchestration/Schemas.ts b/apps/server/src/orchestration/Schemas.ts index 35eff8f4..1f662238 100644 --- a/apps/server/src/orchestration/Schemas.ts +++ b/apps/server/src/orchestration/Schemas.ts @@ -7,6 +7,8 @@ import { ThreadRuntimeModeSetPayload as ContractsThreadRuntimeModeSetPayloadSchema, ThreadInteractionModeSetPayload as ContractsThreadInteractionModeSetPayloadSchema, ThreadDeletedPayload as ContractsThreadDeletedPayloadSchema, + ThreadArchivedPayload as ContractsThreadArchivedPayloadSchema, + ThreadUnarchivedPayload as ContractsThreadUnarchivedPayloadSchema, ThreadMessageSentPayload as ContractsThreadMessageSentPayloadSchema, ThreadProposedPlanUpsertedPayload as ContractsThreadProposedPlanUpsertedPayloadSchema, ThreadSessionSetPayload as ContractsThreadSessionSetPayloadSchema, @@ -30,6 +32,8 @@ export const ThreadMetaUpdatedPayload = ContractsThreadMetaUpdatedPayloadSchema; export const ThreadRuntimeModeSetPayload = ContractsThreadRuntimeModeSetPayloadSchema; export const ThreadInteractionModeSetPayload = ContractsThreadInteractionModeSetPayloadSchema; export const ThreadDeletedPayload = ContractsThreadDeletedPayloadSchema; +export const ThreadArchivedPayload = ContractsThreadArchivedPayloadSchema; +export const ThreadUnarchivedPayload = ContractsThreadUnarchivedPayloadSchema; export const MessageSentPayloadSchema = ContractsThreadMessageSentPayloadSchema; export const ThreadProposedPlanUpsertedPayload = ContractsThreadProposedPlanUpsertedPayloadSchema; diff --git a/apps/server/src/orchestration/commandInvariants.test.ts b/apps/server/src/orchestration/commandInvariants.test.ts index b53f636b..cef16eac 100644 --- a/apps/server/src/orchestration/commandInvariants.test.ts +++ b/apps/server/src/orchestration/commandInvariants.test.ts @@ -64,6 +64,7 @@ const readModel: OrchestrationReadModel = { proposedPlans: [], checkpoints: [], deletedAt: null, + archivedAt: null, }, { id: ThreadId.makeUnsafe("thread-2"), @@ -83,6 +84,7 @@ const readModel: OrchestrationReadModel = { proposedPlans: [], checkpoints: [], deletedAt: null, + archivedAt: null, }, ], }; diff --git a/apps/server/src/orchestration/decider.ts b/apps/server/src/orchestration/decider.ts index cdd01a61..005db9b2 100644 --- a/apps/server/src/orchestration/decider.ts +++ b/apps/server/src/orchestration/decider.ts @@ -243,6 +243,50 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" }; } + case "thread.archive": { + yield* requireThread({ + readModel, + command, + threadId: command.threadId, + }); + const occurredAt = nowIso(); + return { + ...withEventBase({ + aggregateKind: "thread", + aggregateId: command.threadId, + occurredAt, + commandId: command.commandId, + }), + type: "thread.archived", + payload: { + threadId: command.threadId, + archivedAt: occurredAt, + }, + }; + } + + case "thread.unarchive": { + yield* requireThread({ + readModel, + command, + threadId: command.threadId, + }); + const occurredAt = nowIso(); + return { + ...withEventBase({ + aggregateKind: "thread", + aggregateId: command.threadId, + occurredAt, + commandId: command.commandId, + }), + type: "thread.unarchived", + payload: { + threadId: command.threadId, + unarchivedAt: occurredAt, + }, + }; + } + case "thread.meta.update": { yield* requireThread({ readModel, diff --git a/apps/server/src/orchestration/projector.test.ts b/apps/server/src/orchestration/projector.test.ts index 4c6402b9..9195d563 100644 --- a/apps/server/src/orchestration/projector.test.ts +++ b/apps/server/src/orchestration/projector.test.ts @@ -82,6 +82,7 @@ describe("orchestration projector", () => { createdAt: now, updatedAt: now, deletedAt: null, + archivedAt: null, messages: [], proposedPlans: [], activities: [], diff --git a/apps/server/src/orchestration/projector.ts b/apps/server/src/orchestration/projector.ts index feab1053..6e212470 100644 --- a/apps/server/src/orchestration/projector.ts +++ b/apps/server/src/orchestration/projector.ts @@ -16,6 +16,8 @@ import { ThreadActivityAppendedPayload, ThreadCreatedPayload, ThreadDeletedPayload, + ThreadArchivedPayload, + ThreadUnarchivedPayload, ThreadInteractionModeSetPayload, ThreadMetaUpdatedPayload, ThreadProposedPlanUpsertedPayload, @@ -261,6 +263,7 @@ export function projectEvent( createdAt: payload.createdAt, updatedAt: payload.updatedAt, deletedAt: null, + archivedAt: null, messages: [], activities: [], checkpoints: [], @@ -289,6 +292,28 @@ export function projectEvent( })), ); + case "thread.archived": + return decodeForEvent(ThreadArchivedPayload, event.payload, event.type, "payload").pipe( + Effect.map((payload) => ({ + ...nextBase, + threads: updateThread(nextBase.threads, payload.threadId, { + archivedAt: payload.archivedAt, + updatedAt: payload.archivedAt, + }), + })), + ); + + case "thread.unarchived": + return decodeForEvent(ThreadUnarchivedPayload, event.payload, event.type, "payload").pipe( + Effect.map((payload) => ({ + ...nextBase, + threads: updateThread(nextBase.threads, payload.threadId, { + archivedAt: null, + updatedAt: payload.unarchivedAt, + }), + })), + ); + case "thread.meta-updated": return decodeForEvent(ThreadMetaUpdatedPayload, event.payload, event.type, "payload").pipe( Effect.map((payload) => ({ diff --git a/apps/server/src/persistence/Layers/ProjectionThreads.ts b/apps/server/src/persistence/Layers/ProjectionThreads.ts index 10192697..3ad3c673 100644 --- a/apps/server/src/persistence/Layers/ProjectionThreads.ts +++ b/apps/server/src/persistence/Layers/ProjectionThreads.ts @@ -31,7 +31,8 @@ const makeProjectionThreadRepository = Effect.gen(function* () { latest_turn_id, created_at, updated_at, - deleted_at + deleted_at, + archived_at ) VALUES ( ${row.threadId}, @@ -45,7 +46,8 @@ const makeProjectionThreadRepository = Effect.gen(function* () { ${row.latestTurnId}, ${row.createdAt}, ${row.updatedAt}, - ${row.deletedAt} + ${row.deletedAt}, + ${row.archivedAt} ) ON CONFLICT (thread_id) DO UPDATE SET @@ -59,7 +61,8 @@ const makeProjectionThreadRepository = Effect.gen(function* () { latest_turn_id = excluded.latest_turn_id, created_at = excluded.created_at, updated_at = excluded.updated_at, - deleted_at = excluded.deleted_at + deleted_at = excluded.deleted_at, + archived_at = excluded.archived_at `, }); @@ -80,7 +83,8 @@ const makeProjectionThreadRepository = Effect.gen(function* () { latest_turn_id AS "latestTurnId", created_at AS "createdAt", updated_at AS "updatedAt", - deleted_at AS "deletedAt" + deleted_at AS "deletedAt", + archived_at AS "archivedAt" FROM projection_threads WHERE thread_id = ${threadId} `, @@ -103,7 +107,8 @@ const makeProjectionThreadRepository = Effect.gen(function* () { latest_turn_id AS "latestTurnId", created_at AS "createdAt", updated_at AS "updatedAt", - deleted_at AS "deletedAt" + deleted_at AS "deletedAt", + archived_at AS "archivedAt" FROM projection_threads WHERE project_id = ${projectId} ORDER BY created_at ASC, thread_id ASC diff --git a/apps/server/src/persistence/Migrations.ts b/apps/server/src/persistence/Migrations.ts index 4e1c6d40..4e850cc6 100644 --- a/apps/server/src/persistence/Migrations.ts +++ b/apps/server/src/persistence/Migrations.ts @@ -29,6 +29,7 @@ import Migration0014 from "./Migrations/014_ProjectionThreadProposedPlanImplemen import Migration0015 from "./Migrations/015_ProjectionTurnsSourceProposedPlan.ts"; import Migration0016 from "./Migrations/016_ProjectionThreadsInteractionModeChatCodePlan.ts"; import Migration0017 from "./Migrations/017_EnvironmentVariables.ts"; +import Migration0018 from "./Migrations/018_ProjectionThreadsArchivedAt.ts"; import { Effect } from "effect"; /** @@ -59,6 +60,7 @@ const loader = Migrator.fromRecord({ "15_ProjectionTurnsSourceProposedPlan": Migration0015, "16_ProjectionThreadsInteractionModeChatCodePlan": Migration0016, "17_EnvironmentVariables": Migration0017, + "18_ProjectionThreadsArchivedAt": Migration0018, }); /** diff --git a/apps/server/src/persistence/Migrations/018_ProjectionThreadsArchivedAt.ts b/apps/server/src/persistence/Migrations/018_ProjectionThreadsArchivedAt.ts new file mode 100644 index 00000000..8115a5d4 --- /dev/null +++ b/apps/server/src/persistence/Migrations/018_ProjectionThreadsArchivedAt.ts @@ -0,0 +1,11 @@ +import * as SqlClient from "effect/unstable/sql/SqlClient"; +import * as Effect from "effect/Effect"; + +export default Effect.gen(function* () { + const sql = yield* SqlClient.SqlClient; + + yield* sql` + ALTER TABLE projection_threads + ADD COLUMN archived_at TEXT + `; +}); diff --git a/apps/server/src/persistence/Services/ProjectionThreads.ts b/apps/server/src/persistence/Services/ProjectionThreads.ts index a9df9b2b..053fe11d 100644 --- a/apps/server/src/persistence/Services/ProjectionThreads.ts +++ b/apps/server/src/persistence/Services/ProjectionThreads.ts @@ -32,6 +32,7 @@ export const ProjectionThread = Schema.Struct({ createdAt: IsoDateTime, updatedAt: IsoDateTime, deletedAt: Schema.NullOr(IsoDateTime), + archivedAt: Schema.NullOr(IsoDateTime), }); export type ProjectionThread = typeof ProjectionThread.Type; diff --git a/apps/web/src/appSettings.ts b/apps/web/src/appSettings.ts index 0164ad84..a6794cc1 100644 --- a/apps/web/src/appSettings.ts +++ b/apps/web/src/appSettings.ts @@ -62,6 +62,7 @@ export const AppSettingsSchema = Schema.Struct({ codexHomePath: Schema.String.check(Schema.isMaxLength(4096)).pipe(withDefaults(() => "")), defaultThreadEnvMode: EnvMode.pipe(withDefaults(() => "worktree" as const satisfies EnvMode)), confirmThreadDelete: Schema.Boolean.pipe(withDefaults(() => true)), + showArchivedThreads: Schema.Boolean.pipe(withDefaults(() => false)), diffWordWrap: Schema.Boolean.pipe(withDefaults(() => false)), enableAssistantStreaming: Schema.Boolean.pipe(withDefaults(() => false)), openLinksExternally: Schema.Boolean.pipe(withDefaults(() => false)), diff --git a/apps/web/src/components/ChatView.browser.tsx b/apps/web/src/components/ChatView.browser.tsx index a25018ca..9ce7fbaa 100644 --- a/apps/web/src/components/ChatView.browser.tsx +++ b/apps/web/src/components/ChatView.browser.tsx @@ -242,6 +242,7 @@ function createSnapshotForTargetUser(options: { createdAt: NOW_ISO, updatedAt: NOW_ISO, deletedAt: null, + archivedAt: null, messages, activities: [], proposedPlans: [], @@ -296,6 +297,7 @@ function addThreadToSnapshot( createdAt: NOW_ISO, updatedAt: NOW_ISO, deletedAt: null, + archivedAt: null, messages: [], activities: [], proposedPlans: [], diff --git a/apps/web/src/components/KeybindingsToast.browser.tsx b/apps/web/src/components/KeybindingsToast.browser.tsx index dedd4eed..5a3ac777 100644 --- a/apps/web/src/components/KeybindingsToast.browser.tsx +++ b/apps/web/src/components/KeybindingsToast.browser.tsx @@ -85,6 +85,7 @@ function createMinimalSnapshot(): OrchestrationReadModel { createdAt: NOW_ISO, updatedAt: NOW_ISO, deletedAt: null, + archivedAt: null, messages: [ { id: "msg-1" as MessageId, diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 2aa2b91a..033beb31 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -1,4 +1,5 @@ import { + ArchiveIcon, ArrowLeftIcon, ArrowUpDownIcon, ChevronRightIcon, @@ -812,6 +813,41 @@ export default function Sidebar() { ], ); + const archiveThread = useCallback( + async (threadId: ThreadId): Promise => { + const api = readNativeApi(); + if (!api) return; + await api.orchestration.dispatchCommand({ + type: "thread.archive", + commandId: newCommandId(), + threadId, + }); + if (routeThreadId === threadId) { + const fallbackThreadId = + threads.find((entry) => entry.id !== threadId && entry.archivedAt === null)?.id ?? null; + if (fallbackThreadId) { + void navigate({ to: "/$threadId", params: { threadId: fallbackThreadId }, replace: true }); + } else { + void navigate({ to: "/", replace: true }); + } + } + }, + [navigate, routeThreadId, threads], + ); + + const unarchiveThread = useCallback( + async (threadId: ThreadId): Promise => { + const api = readNativeApi(); + if (!api) return; + await api.orchestration.dispatchCommand({ + type: "thread.unarchive", + commandId: newCommandId(), + threadId, + }); + }, + [], + ); + const { copyToClipboard: copyThreadIdToClipboard } = useCopyToClipboard<{ threadId: ThreadId }>({ onCopy: (ctx) => { toastManager.add({ @@ -852,12 +888,16 @@ export default function Sidebar() { if (!thread) return; const threadWorkspacePath = thread.worktreePath ?? projectCwdById.get(thread.projectId) ?? null; + const isArchived = thread.archivedAt !== null; const clicked = await api.contextMenu.show( [ { id: "rename", label: "Rename thread" }, { id: "mark-unread", label: "Mark unread" }, { id: "copy-path", label: "Copy Path" }, { id: "copy-thread-id", label: "Copy Thread ID" }, + isArchived + ? { id: "unarchive", label: "Unarchive" } + : { id: "archive", label: "Archive" }, { id: "delete", label: "Delete", destructive: true }, ], position, @@ -890,6 +930,14 @@ export default function Sidebar() { copyThreadIdToClipboard(threadId, { threadId }); return; } + if (clicked === "archive") { + await archiveThread(threadId); + return; + } + if (clicked === "unarchive") { + await unarchiveThread(threadId); + return; + } if (clicked !== "delete") return; if (appSettings.confirmThreadDelete) { const confirmed = await api.dialogs.confirm( @@ -906,6 +954,8 @@ export default function Sidebar() { }, [ appSettings.confirmThreadDelete, + archiveThread, + unarchiveThread, copyPathToClipboard, copyThreadIdToClipboard, deleteThread, @@ -1135,6 +1185,7 @@ export default function Sidebar() { ); const attentionThreads = useMemo(() => { return threads + .filter((thread) => thread.archivedAt === null) .map((thread) => { const status = resolveThreadStatusPill({ thread, @@ -1179,7 +1230,11 @@ export default function Sidebar() { dragHandleProps: SortableProjectHandleProps | null, ) { const projectThreads = sortThreadsForSidebar( - threads.filter((thread) => thread.projectId === project.id), + threads.filter( + (thread) => + thread.projectId === project.id && + (appSettings.showArchivedThreads || thread.archivedAt === null), + ), appSettings.sidebarThreadSortOrder, ); const projectStatus = resolveProjectStatusIndicator( @@ -1532,7 +1587,11 @@ export default function Sidebar() { // When expanding a project with exactly one thread, navigate directly to it. const project = projects.find((p) => p.id === projectId); if (project && !project.expanded) { - const projectThreads = threads.filter((t) => t.projectId === projectId); + const projectThreads = threads.filter( + (t) => + t.projectId === projectId && + (appSettings.showArchivedThreads || t.archivedAt === null), + ); if (projectThreads.length === 1) { toggleProject(projectId); void navigate({ @@ -1544,7 +1603,15 @@ export default function Sidebar() { } toggleProject(projectId); }, - [clearSelection, navigate, projects, selectedThreadIds.size, threads, toggleProject], + [ + appSettings.showArchivedThreads, + clearSelection, + navigate, + projects, + selectedThreadIds.size, + threads, + toggleProject, + ], ); const handleProjectTitleKeyDown = useCallback( @@ -1557,7 +1624,11 @@ export default function Sidebar() { // When expanding a project with exactly one thread, navigate directly to it. const project = projects.find((p) => p.id === projectId); if (project && !project.expanded) { - const projectThreads = threads.filter((t) => t.projectId === projectId); + const projectThreads = threads.filter( + (t) => + t.projectId === projectId && + (appSettings.showArchivedThreads || t.archivedAt === null), + ); if (projectThreads.length === 1) { toggleProject(projectId); void navigate({ @@ -1569,7 +1640,7 @@ export default function Sidebar() { } toggleProject(projectId); }, - [navigate, projects, threads, toggleProject], + [appSettings.showArchivedThreads, navigate, projects, threads, toggleProject], ); useEffect(() => { @@ -1927,6 +1998,37 @@ export default function Sidebar() { {appSettings.sidebarHideFiles ? "Show files" : "Hide files"} + + + updateSettings({ showArchivedThreads: !appSettings.showArchivedThreads }) + } + /> + } + > + + + + {appSettings.showArchivedThreads + ? "Hide archived threads" + : "Show archived threads"} + + = {}): Thread { latestTurn: null, branch: null, worktreePath: null, + archivedAt: null, ...overrides, }; } @@ -64,6 +65,7 @@ function makeReadModelThread(overrides: Partial ({ ...file })), })), activities: thread.activities.map((activity) => ({ ...activity })), + archivedAt: thread.archivedAt, }; }); return { diff --git a/apps/web/src/types.ts b/apps/web/src/types.ts index fb546fc2..ef2185d6 100644 --- a/apps/web/src/types.ts +++ b/apps/web/src/types.ts @@ -110,6 +110,7 @@ export interface Thread { worktreeBaseBranch?: string | null; turnDiffSummaries: TurnDiffSummary[]; activities: OrchestrationThreadActivity[]; + archivedAt: string | null; } export interface ThreadSession { diff --git a/packages/contracts/src/orchestration.ts b/packages/contracts/src/orchestration.ts index b3787be9..e5ece950 100644 --- a/packages/contracts/src/orchestration.ts +++ b/packages/contracts/src/orchestration.ts @@ -308,6 +308,7 @@ export const OrchestrationThread = Schema.Struct({ createdAt: IsoDateTime, updatedAt: IsoDateTime, deletedAt: Schema.NullOr(IsoDateTime), + archivedAt: Schema.NullOr(IsoDateTime).pipe(Schema.withDecodingDefault(() => null)), messages: Schema.Array(OrchestrationMessage), proposedPlans: Schema.Array(OrchestrationProposedPlan).pipe(Schema.withDecodingDefault(() => [])), activities: Schema.Array(OrchestrationThreadActivity), @@ -373,6 +374,18 @@ const ThreadDeleteCommand = Schema.Struct({ threadId: ThreadId, }); +const ThreadArchiveCommand = Schema.Struct({ + type: Schema.Literal("thread.archive"), + commandId: CommandId, + threadId: ThreadId, +}); + +const ThreadUnarchiveCommand = Schema.Struct({ + type: Schema.Literal("thread.unarchive"), + commandId: CommandId, + threadId: ThreadId, +}); + const ThreadMetaUpdateCommand = Schema.Struct({ type: Schema.Literal("thread.meta.update"), commandId: CommandId, @@ -490,6 +503,8 @@ const DispatchableClientOrchestrationCommand = Schema.Union([ ProjectDeleteCommand, ThreadCreateCommand, ThreadDeleteCommand, + ThreadArchiveCommand, + ThreadUnarchiveCommand, ThreadMetaUpdateCommand, ThreadRuntimeModeSetCommand, ThreadInteractionModeSetCommand, @@ -509,6 +524,8 @@ export const ClientOrchestrationCommand = Schema.Union([ ProjectDeleteCommand, ThreadCreateCommand, ThreadDeleteCommand, + ThreadArchiveCommand, + ThreadUnarchiveCommand, ThreadMetaUpdateCommand, ThreadRuntimeModeSetCommand, ThreadInteractionModeSetCommand, @@ -609,6 +626,8 @@ export const OrchestrationEventType = Schema.Literals([ "project.deleted", "thread.created", "thread.deleted", + "thread.archived", + "thread.unarchived", "thread.meta-updated", "thread.runtime-mode-set", "thread.interaction-mode-set", @@ -675,6 +694,16 @@ export const ThreadDeletedPayload = Schema.Struct({ deletedAt: IsoDateTime, }); +export const ThreadArchivedPayload = Schema.Struct({ + threadId: ThreadId, + archivedAt: IsoDateTime, +}); + +export const ThreadUnarchivedPayload = Schema.Struct({ + threadId: ThreadId, + unarchivedAt: IsoDateTime, +}); + export const ThreadMetaUpdatedPayload = Schema.Struct({ threadId: ThreadId, title: Schema.optional(TrimmedNonEmptyString), @@ -835,6 +864,16 @@ export const OrchestrationEvent = Schema.Union([ type: Schema.Literal("thread.deleted"), payload: ThreadDeletedPayload, }), + Schema.Struct({ + ...EventBaseFields, + type: Schema.Literal("thread.archived"), + payload: ThreadArchivedPayload, + }), + Schema.Struct({ + ...EventBaseFields, + type: Schema.Literal("thread.unarchived"), + payload: ThreadUnarchivedPayload, + }), Schema.Struct({ ...EventBaseFields, type: Schema.Literal("thread.meta-updated"),