mirror of
https://github.com/different-ai/openwork
synced 2026-05-08 16:22:04 +02:00
* feat(server): add workspace session read APIs Expose workspace-scoped session list, detail, message, and snapshot reads so the client can fetch session data without depending on activation choreography. * feat(app): route mounted session reads through OpenWork APIs Use the new workspace-scoped session read endpoints for mounted OpenWork clients so the current frontend stops depending on direct session proxy reads for list, detail, message, and todo loading. * feat(app): add React read-only session transcript Introduce a feature-gated React island for the session transcript so we can replace the session surface incrementally while keeping the Solid shell intact. * feat(app): add React session composer surface Extend the feature-gated React session island to own its draft, prompt send, stop flow, and snapshot polling so the session body can evolve independently from the Solid composer. * feat(app): add React session transition model Keep the React session surface stable during session switches by tracking rendered vs intended session state and exposing a developer debug panel for render-source and transition inspection. * docs(prd): add React migration plan to repo Copy the incremental React adoption PRD into the OpenWork repo so the migration plan lives next to the implementation and PR branch. * docs(prd): sync full React migration plan Replace the shortened repo copy with the full incremental React adoption PRD so the implementation branch and product plan stay in sync. * feat(desktop): add React session launch modes Add dedicated Tauri dev and debug-build entrypoints for the React session path and honor a build-time React session flag before local storage so the alternate shell is easy to launch and reproduce. * fix(app): fall back to legacy mounted session reads Keep the new app working against older OpenWork servers by falling back to the original mounted OpenCode session reads when the workspace-scoped session read APIs are unavailable.
142 lines
4.0 KiB
TypeScript
142 lines
4.0 KiB
TypeScript
import { z } from "zod";
|
|
|
|
import { ApiError } from "./errors.js";
|
|
|
|
const sessionTimeSchema = z
|
|
.object({
|
|
created: z.number().optional(),
|
|
updated: z.number().optional(),
|
|
completed: z.number().optional(),
|
|
archived: z.number().optional(),
|
|
})
|
|
.passthrough();
|
|
|
|
const sessionSummarySchema = z
|
|
.object({
|
|
additions: z.number().optional(),
|
|
deletions: z.number().optional(),
|
|
files: z.number().optional(),
|
|
})
|
|
.passthrough();
|
|
|
|
export const sessionStatusSchema = z.discriminatedUnion("type", [
|
|
z.object({ type: z.literal("idle") }),
|
|
z.object({ type: z.literal("busy") }),
|
|
z.object({ type: z.literal("retry"), attempt: z.number(), message: z.string(), next: z.number() }),
|
|
]);
|
|
|
|
export const sessionTodoSchema = z
|
|
.object({
|
|
content: z.string(),
|
|
status: z.string(),
|
|
priority: z.string(),
|
|
})
|
|
.passthrough();
|
|
|
|
export const sessionInfoSchema = z
|
|
.object({
|
|
id: z.string(),
|
|
title: z.string().nullish(),
|
|
slug: z.string().nullish(),
|
|
parentID: z.string().nullish(),
|
|
directory: z.string().nullish(),
|
|
time: sessionTimeSchema.optional(),
|
|
summary: sessionSummarySchema.optional(),
|
|
})
|
|
.passthrough();
|
|
|
|
const sessionMessageInfoSchema = z
|
|
.object({
|
|
id: z.string(),
|
|
sessionID: z.string(),
|
|
role: z.string(),
|
|
parentID: z.string().nullish(),
|
|
time: sessionTimeSchema.optional(),
|
|
})
|
|
.passthrough();
|
|
|
|
const sessionPartSchema = z
|
|
.object({
|
|
id: z.string(),
|
|
messageID: z.string(),
|
|
sessionID: z.string(),
|
|
})
|
|
.passthrough();
|
|
|
|
export const sessionMessageSchema = z
|
|
.object({
|
|
info: sessionMessageInfoSchema,
|
|
parts: z.array(sessionPartSchema),
|
|
})
|
|
.passthrough();
|
|
|
|
const sessionListSchema = z.array(sessionInfoSchema);
|
|
const sessionMessagesSchema = z.array(sessionMessageSchema);
|
|
const sessionTodosSchema = z.array(sessionTodoSchema);
|
|
const sessionStatusesSchema = z.record(z.string(), sessionStatusSchema);
|
|
|
|
const sessionSnapshotSchema = z.object({
|
|
session: sessionInfoSchema,
|
|
messages: sessionMessagesSchema,
|
|
todos: sessionTodosSchema,
|
|
status: sessionStatusSchema,
|
|
});
|
|
|
|
export type SessionInfoReadModel = z.infer<typeof sessionInfoSchema>;
|
|
export type SessionMessageReadModel = z.infer<typeof sessionMessageSchema>;
|
|
export type SessionTodoReadModel = z.infer<typeof sessionTodoSchema>;
|
|
export type SessionStatusReadModel = z.infer<typeof sessionStatusSchema>;
|
|
export type SessionSnapshotReadModel = z.infer<typeof sessionSnapshotSchema>;
|
|
|
|
const IDLE_STATUS: SessionStatusReadModel = { type: "idle" };
|
|
|
|
function parseOrThrow<T>(schema: z.ZodType<T>, value: unknown, label: string): T {
|
|
const result = schema.safeParse(value);
|
|
if (result.success) return result.data;
|
|
throw new ApiError(502, "opencode_invalid_response", `OpenCode returned invalid ${label}`, {
|
|
issues: result.error.issues,
|
|
});
|
|
}
|
|
|
|
export function buildSessionList(value: unknown): SessionInfoReadModel[] {
|
|
return parseOrThrow(sessionListSchema, value, "session list");
|
|
}
|
|
|
|
export function buildSession(value: unknown): SessionInfoReadModel {
|
|
return parseOrThrow(sessionInfoSchema, value, "session");
|
|
}
|
|
|
|
export function buildSessionMessages(value: unknown): SessionMessageReadModel[] {
|
|
return parseOrThrow(sessionMessagesSchema, value, "session messages");
|
|
}
|
|
|
|
export function buildSessionTodos(value: unknown): SessionTodoReadModel[] {
|
|
return parseOrThrow(sessionTodosSchema, value, "session todos");
|
|
}
|
|
|
|
export function buildSessionStatuses(value: unknown): Record<string, SessionStatusReadModel> {
|
|
return parseOrThrow(sessionStatusesSchema, value, "session statuses");
|
|
}
|
|
|
|
export function buildSessionSnapshot(input: {
|
|
session: unknown;
|
|
messages: unknown;
|
|
todos: unknown;
|
|
statuses: unknown;
|
|
}): SessionSnapshotReadModel {
|
|
const session = buildSession(input.session);
|
|
const messages = buildSessionMessages(input.messages);
|
|
const todos = buildSessionTodos(input.todos);
|
|
const statuses = buildSessionStatuses(input.statuses);
|
|
return parseOrThrow(
|
|
sessionSnapshotSchema,
|
|
{
|
|
session,
|
|
messages,
|
|
todos,
|
|
status: statuses[session.id] ?? IDLE_STATUS,
|
|
},
|
|
"session snapshot",
|
|
);
|
|
}
|