mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-10 10:48:28 +00:00
feat(tui): wire up undo/redo and revert for V2 sessions (#34263)
This commit is contained in:
parent
cd942d0669
commit
d1d7ebc2c6
20 changed files with 478 additions and 87 deletions
|
|
@ -89,15 +89,25 @@ const Endpoint0_5 = (raw: RawClient["server.session"]) => (input: Endpoint0_5Inp
|
|||
Effect.mapError(mapClientError),
|
||||
)
|
||||
|
||||
type Endpoint0_6Request = Parameters<RawClient["server.session"]["session.prompt"]>[0]
|
||||
type Endpoint0_6Request = Parameters<RawClient["server.session"]["session.rename"]>[0]
|
||||
type Endpoint0_6Input = {
|
||||
readonly sessionID: Endpoint0_6Request["params"]["sessionID"]
|
||||
readonly id?: Endpoint0_6Request["payload"]["id"]
|
||||
readonly prompt: Endpoint0_6Request["payload"]["prompt"]
|
||||
readonly delivery?: Endpoint0_6Request["payload"]["delivery"]
|
||||
readonly resume?: Endpoint0_6Request["payload"]["resume"]
|
||||
readonly title: Endpoint0_6Request["payload"]["title"]
|
||||
}
|
||||
const Endpoint0_6 = (raw: RawClient["server.session"]) => (input: Endpoint0_6Input) =>
|
||||
raw["session.rename"]({ params: { sessionID: input.sessionID }, payload: { title: input.title } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
)
|
||||
|
||||
type Endpoint0_7Request = Parameters<RawClient["server.session"]["session.prompt"]>[0]
|
||||
type Endpoint0_7Input = {
|
||||
readonly sessionID: Endpoint0_7Request["params"]["sessionID"]
|
||||
readonly id?: Endpoint0_7Request["payload"]["id"]
|
||||
readonly prompt: Endpoint0_7Request["payload"]["prompt"]
|
||||
readonly delivery?: Endpoint0_7Request["payload"]["delivery"]
|
||||
readonly resume?: Endpoint0_7Request["payload"]["resume"]
|
||||
}
|
||||
const Endpoint0_7 = (raw: RawClient["server.session"]) => (input: Endpoint0_7Input) =>
|
||||
raw["session.prompt"]({
|
||||
params: { sessionID: input.sessionID },
|
||||
payload: { id: input.id, prompt: input.prompt, delivery: input.delivery, resume: input.resume },
|
||||
|
|
@ -106,23 +116,23 @@ const Endpoint0_6 = (raw: RawClient["server.session"]) => (input: Endpoint0_6Inp
|
|||
Effect.map((value) => value.data),
|
||||
)
|
||||
|
||||
type Endpoint0_7Request = Parameters<RawClient["server.session"]["session.compact"]>[0]
|
||||
type Endpoint0_7Input = { readonly sessionID: Endpoint0_7Request["params"]["sessionID"] }
|
||||
const Endpoint0_7 = (raw: RawClient["server.session"]) => (input: Endpoint0_7Input) =>
|
||||
raw["session.compact"]({ params: { sessionID: input.sessionID } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint0_8Request = Parameters<RawClient["server.session"]["session.wait"]>[0]
|
||||
type Endpoint0_8Request = Parameters<RawClient["server.session"]["session.compact"]>[0]
|
||||
type Endpoint0_8Input = { readonly sessionID: Endpoint0_8Request["params"]["sessionID"] }
|
||||
const Endpoint0_8 = (raw: RawClient["server.session"]) => (input: Endpoint0_8Input) =>
|
||||
raw["session.compact"]({ params: { sessionID: input.sessionID } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint0_9Request = Parameters<RawClient["server.session"]["session.wait"]>[0]
|
||||
type Endpoint0_9Input = { readonly sessionID: Endpoint0_9Request["params"]["sessionID"] }
|
||||
const Endpoint0_9 = (raw: RawClient["server.session"]) => (input: Endpoint0_9Input) =>
|
||||
raw["session.wait"]({ params: { sessionID: input.sessionID } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint0_9Request = Parameters<RawClient["server.session"]["session.revert.stage"]>[0]
|
||||
type Endpoint0_9Input = {
|
||||
readonly sessionID: Endpoint0_9Request["params"]["sessionID"]
|
||||
readonly messageID: Endpoint0_9Request["payload"]["messageID"]
|
||||
readonly files?: Endpoint0_9Request["payload"]["files"]
|
||||
type Endpoint0_10Request = Parameters<RawClient["server.session"]["session.revert.stage"]>[0]
|
||||
type Endpoint0_10Input = {
|
||||
readonly sessionID: Endpoint0_10Request["params"]["sessionID"]
|
||||
readonly messageID: Endpoint0_10Request["payload"]["messageID"]
|
||||
readonly files?: Endpoint0_10Request["payload"]["files"]
|
||||
}
|
||||
const Endpoint0_9 = (raw: RawClient["server.session"]) => (input: Endpoint0_9Input) =>
|
||||
const Endpoint0_10 = (raw: RawClient["server.session"]) => (input: Endpoint0_10Input) =>
|
||||
raw["session.revert.stage"]({
|
||||
params: { sessionID: input.sessionID },
|
||||
payload: { messageID: input.messageID, files: input.files },
|
||||
|
|
@ -131,30 +141,30 @@ const Endpoint0_9 = (raw: RawClient["server.session"]) => (input: Endpoint0_9Inp
|
|||
Effect.map((value) => value.data),
|
||||
)
|
||||
|
||||
type Endpoint0_10Request = Parameters<RawClient["server.session"]["session.revert.clear"]>[0]
|
||||
type Endpoint0_10Input = { readonly sessionID: Endpoint0_10Request["params"]["sessionID"] }
|
||||
const Endpoint0_10 = (raw: RawClient["server.session"]) => (input: Endpoint0_10Input) =>
|
||||
raw["session.revert.clear"]({ params: { sessionID: input.sessionID } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint0_11Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0]
|
||||
type Endpoint0_11Request = Parameters<RawClient["server.session"]["session.revert.clear"]>[0]
|
||||
type Endpoint0_11Input = { readonly sessionID: Endpoint0_11Request["params"]["sessionID"] }
|
||||
const Endpoint0_11 = (raw: RawClient["server.session"]) => (input: Endpoint0_11Input) =>
|
||||
raw["session.revert.commit"]({ params: { sessionID: input.sessionID } }).pipe(Effect.mapError(mapClientError))
|
||||
raw["session.revert.clear"]({ params: { sessionID: input.sessionID } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint0_12Request = Parameters<RawClient["server.session"]["session.context"]>[0]
|
||||
type Endpoint0_12Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0]
|
||||
type Endpoint0_12Input = { readonly sessionID: Endpoint0_12Request["params"]["sessionID"] }
|
||||
const Endpoint0_12 = (raw: RawClient["server.session"]) => (input: Endpoint0_12Input) =>
|
||||
raw["session.revert.commit"]({ params: { sessionID: input.sessionID } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint0_13Request = Parameters<RawClient["server.session"]["session.context"]>[0]
|
||||
type Endpoint0_13Input = { readonly sessionID: Endpoint0_13Request["params"]["sessionID"] }
|
||||
const Endpoint0_13 = (raw: RawClient["server.session"]) => (input: Endpoint0_13Input) =>
|
||||
raw["session.context"]({ params: { sessionID: input.sessionID } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
Effect.map((value) => value.data),
|
||||
)
|
||||
|
||||
type Endpoint0_13Request = Parameters<RawClient["server.session"]["session.events"]>[0]
|
||||
type Endpoint0_13Input = {
|
||||
readonly sessionID: Endpoint0_13Request["params"]["sessionID"]
|
||||
readonly after?: Endpoint0_13Request["query"]["after"]
|
||||
type Endpoint0_14Request = Parameters<RawClient["server.session"]["session.events"]>[0]
|
||||
type Endpoint0_14Input = {
|
||||
readonly sessionID: Endpoint0_14Request["params"]["sessionID"]
|
||||
readonly after?: Endpoint0_14Request["query"]["after"]
|
||||
}
|
||||
const Endpoint0_13 = (raw: RawClient["server.session"]) => (input: Endpoint0_13Input) =>
|
||||
const Endpoint0_14 = (raw: RawClient["server.session"]) => (input: Endpoint0_14Input) =>
|
||||
Stream.unwrap(
|
||||
raw["session.events"]({ params: { sessionID: input.sessionID }, query: { after: input.after } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
|
|
@ -162,17 +172,17 @@ const Endpoint0_13 = (raw: RawClient["server.session"]) => (input: Endpoint0_13I
|
|||
),
|
||||
)
|
||||
|
||||
type Endpoint0_14Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
|
||||
type Endpoint0_14Input = { readonly sessionID: Endpoint0_14Request["params"]["sessionID"] }
|
||||
const Endpoint0_14 = (raw: RawClient["server.session"]) => (input: Endpoint0_14Input) =>
|
||||
type Endpoint0_15Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
|
||||
type Endpoint0_15Input = { readonly sessionID: Endpoint0_15Request["params"]["sessionID"] }
|
||||
const Endpoint0_15 = (raw: RawClient["server.session"]) => (input: Endpoint0_15Input) =>
|
||||
raw["session.interrupt"]({ params: { sessionID: input.sessionID } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint0_15Request = Parameters<RawClient["server.session"]["session.message"]>[0]
|
||||
type Endpoint0_15Input = {
|
||||
readonly sessionID: Endpoint0_15Request["params"]["sessionID"]
|
||||
readonly messageID: Endpoint0_15Request["params"]["messageID"]
|
||||
type Endpoint0_16Request = Parameters<RawClient["server.session"]["session.message"]>[0]
|
||||
type Endpoint0_16Input = {
|
||||
readonly sessionID: Endpoint0_16Request["params"]["sessionID"]
|
||||
readonly messageID: Endpoint0_16Request["params"]["messageID"]
|
||||
}
|
||||
const Endpoint0_15 = (raw: RawClient["server.session"]) => (input: Endpoint0_15Input) =>
|
||||
const Endpoint0_16 = (raw: RawClient["server.session"]) => (input: Endpoint0_16Input) =>
|
||||
raw["session.message"]({ params: { sessionID: input.sessionID, messageID: input.messageID } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
Effect.map((value) => value.data),
|
||||
|
|
@ -185,16 +195,17 @@ const adaptGroup0 = (raw: RawClient["server.session"]) => ({
|
|||
get: Endpoint0_3(raw),
|
||||
switchAgent: Endpoint0_4(raw),
|
||||
switchModel: Endpoint0_5(raw),
|
||||
prompt: Endpoint0_6(raw),
|
||||
compact: Endpoint0_7(raw),
|
||||
wait: Endpoint0_8(raw),
|
||||
stage: Endpoint0_9(raw),
|
||||
clear: Endpoint0_10(raw),
|
||||
commit: Endpoint0_11(raw),
|
||||
context: Endpoint0_12(raw),
|
||||
events: Endpoint0_13(raw),
|
||||
interrupt: Endpoint0_14(raw),
|
||||
message: Endpoint0_15(raw),
|
||||
rename: Endpoint0_6(raw),
|
||||
prompt: Endpoint0_7(raw),
|
||||
compact: Endpoint0_8(raw),
|
||||
wait: Endpoint0_9(raw),
|
||||
stage: Endpoint0_10(raw),
|
||||
clear: Endpoint0_11(raw),
|
||||
commit: Endpoint0_12(raw),
|
||||
context: Endpoint0_13(raw),
|
||||
events: Endpoint0_14(raw),
|
||||
interrupt: Endpoint0_15(raw),
|
||||
message: Endpoint0_16(raw),
|
||||
})
|
||||
|
||||
const adaptClient = (raw: RawClient) => ({ sessions: adaptGroup0(raw["server.session"]) })
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import type {
|
|||
SessionsSwitchAgentOutput,
|
||||
SessionsSwitchModelInput,
|
||||
SessionsSwitchModelOutput,
|
||||
SessionsRenameInput,
|
||||
SessionsRenameOutput,
|
||||
SessionsPromptInput,
|
||||
SessionsPromptOutput,
|
||||
SessionsCompactInput,
|
||||
|
|
@ -245,6 +247,18 @@ export function make(options: ClientOptions) {
|
|||
},
|
||||
requestOptions,
|
||||
),
|
||||
rename: (input: SessionsRenameInput, requestOptions?: RequestOptions) =>
|
||||
request<SessionsRenameOutput>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/rename`,
|
||||
body: { title: input.title },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [404, 400, 401],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
prompt: (input: SessionsPromptInput, requestOptions?: RequestOptions) =>
|
||||
request<{ readonly data: SessionsPromptOutput }>(
|
||||
{
|
||||
|
|
@ -286,7 +300,7 @@ export function make(options: ClientOptions) {
|
|||
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/stage`,
|
||||
body: { messageID: input.messageID, files: input.files },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [404, 500, 400, 401],
|
||||
declaredStatuses: [404, 409, 500, 400, 401],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
|
|
@ -297,7 +311,7 @@ export function make(options: ClientOptions) {
|
|||
method: "POST",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/clear`,
|
||||
successStatus: 204,
|
||||
declaredStatuses: [404, 500, 400, 401],
|
||||
declaredStatuses: [404, 409, 500, 400, 401],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
|
|
@ -308,7 +322,7 @@ export function make(options: ClientOptions) {
|
|||
method: "POST",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/commit`,
|
||||
successStatus: 204,
|
||||
declaredStatuses: [404, 400, 401],
|
||||
declaredStatuses: [404, 409, 400, 401],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
|
|
|
|||
|
|
@ -56,6 +56,14 @@ export type MessageNotFoundError = {
|
|||
export const isMessageNotFoundError = (value: unknown): value is MessageNotFoundError =>
|
||||
typeof value === "object" && value !== null && "_tag" in value && value._tag === "MessageNotFoundError"
|
||||
|
||||
export type SessionBusyError = {
|
||||
readonly _tag: "SessionBusyError"
|
||||
readonly sessionID: string
|
||||
readonly message: string
|
||||
}
|
||||
export const isSessionBusyError = (value: unknown): value is SessionBusyError =>
|
||||
typeof value === "object" && value !== null && "_tag" in value && value._tag === "SessionBusyError"
|
||||
|
||||
export type UnknownError = {
|
||||
readonly _tag: "UnknownError"
|
||||
readonly message: string
|
||||
|
|
@ -297,6 +305,13 @@ export type SessionsSwitchModelInput = {
|
|||
|
||||
export type SessionsSwitchModelOutput = void
|
||||
|
||||
export type SessionsRenameInput = {
|
||||
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
||||
readonly title: { readonly title: string }["title"]
|
||||
}
|
||||
|
||||
export type SessionsRenameOutput = void
|
||||
|
||||
export type SessionsPromptInput = {
|
||||
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
||||
readonly id?: {
|
||||
|
|
@ -637,6 +652,14 @@ export type SessionsEventsOutput =
|
|||
readonly subdirectory?: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.renamed"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: { readonly timestamp: number; readonly sessionID: string; readonly title: string }
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
|
|
|
|||
|
|
@ -102,6 +102,9 @@ export class PromptConflictError extends Schema.TaggedErrorClass<PromptConflictE
|
|||
sessionID: SessionSchema.ID,
|
||||
messageID: SessionMessage.ID,
|
||||
}) {}
|
||||
export class BusyError extends Schema.TaggedErrorClass<BusyError>()("Session.BusyError", {
|
||||
sessionID: SessionSchema.ID,
|
||||
}) {}
|
||||
export const MessageNotFoundError = SessionRevert.MessageNotFoundError
|
||||
export type MessageNotFoundError = SessionRevert.MessageNotFoundError
|
||||
|
||||
|
|
@ -136,6 +139,10 @@ export interface Interface {
|
|||
sessionID: SessionSchema.ID
|
||||
model: ModelV2.Ref
|
||||
}) => Effect.Effect<void, NotFoundError>
|
||||
readonly rename: (input: {
|
||||
sessionID: SessionSchema.ID
|
||||
title: string
|
||||
}) => Effect.Effect<void, NotFoundError>
|
||||
readonly prompt: (input: {
|
||||
id?: SessionMessage.ID
|
||||
sessionID: SessionSchema.ID
|
||||
|
|
@ -165,9 +172,9 @@ export interface Interface {
|
|||
sessionID: SessionSchema.ID
|
||||
messageID: SessionMessage.ID
|
||||
files?: boolean
|
||||
}) => Effect.Effect<Revert.State, NotFoundError | MessageNotFoundError | Snapshot.Error>
|
||||
readonly clear: (sessionID: SessionSchema.ID) => Effect.Effect<void, NotFoundError | Snapshot.Error>
|
||||
readonly commit: (sessionID: SessionSchema.ID) => Effect.Effect<void, NotFoundError>
|
||||
}) => Effect.Effect<Revert.State, NotFoundError | MessageNotFoundError | BusyError | Snapshot.Error>
|
||||
readonly clear: (sessionID: SessionSchema.ID) => Effect.Effect<void, NotFoundError | BusyError | Snapshot.Error>
|
||||
readonly commit: (sessionID: SessionSchema.ID) => Effect.Effect<void, NotFoundError | BusyError>
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -404,6 +411,14 @@ export const layer = Layer.unwrap(
|
|||
model: input.model,
|
||||
})
|
||||
}),
|
||||
rename: Effect.fn("V2Session.rename")(function* (input) {
|
||||
yield* result.get(input.sessionID)
|
||||
yield* events.publish(SessionEvent.Renamed, {
|
||||
sessionID: input.sessionID,
|
||||
timestamp: yield* DateTime.now,
|
||||
title: input.title,
|
||||
})
|
||||
}),
|
||||
compact: Effect.fn("V2Session.compact")(function* (input) {
|
||||
yield* result.get(input.sessionID)
|
||||
return yield* new OperationUnavailableError({ operation: "compact" })
|
||||
|
|
@ -423,6 +438,8 @@ export const layer = Layer.unwrap(
|
|||
revert: {
|
||||
stage: Effect.fn("V2Session.revert.stage")(function* (input) {
|
||||
const session = yield* result.get(input.sessionID)
|
||||
if ((yield* execution.active).has(input.sessionID))
|
||||
return yield* new BusyError({ sessionID: input.sessionID })
|
||||
return yield* SessionRevert.stage({ session, messageID: input.messageID, files: input.files }).pipe(
|
||||
Effect.provideService(Database.Service, database),
|
||||
Effect.provideService(EventV2.Service, events),
|
||||
|
|
@ -431,6 +448,8 @@ export const layer = Layer.unwrap(
|
|||
}),
|
||||
clear: Effect.fn("V2Session.revert.clear")(function* (sessionID) {
|
||||
const session = yield* result.get(sessionID)
|
||||
if ((yield* execution.active).has(sessionID))
|
||||
return yield* new BusyError({ sessionID })
|
||||
yield* SessionRevert.clear(session).pipe(
|
||||
Effect.provideService(EventV2.Service, events),
|
||||
Effect.provide(locations.get(session.location)),
|
||||
|
|
@ -438,6 +457,8 @@ export const layer = Layer.unwrap(
|
|||
}),
|
||||
commit: Effect.fn("V2Session.revert.commit")(function* (sessionID) {
|
||||
const session = yield* result.get(sessionID)
|
||||
if ((yield* execution.active).has(sessionID))
|
||||
return yield* new BusyError({ sessionID })
|
||||
yield* SessionRevert.commit(session).pipe(Effect.provideService(EventV2.Service, events))
|
||||
}),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -123,6 +123,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
|
|||
)
|
||||
},
|
||||
"session.next.moved": () => Effect.void,
|
||||
"session.next.renamed": () => Effect.void,
|
||||
"session.next.prompted": (event) => {
|
||||
return adapter.appendMessage(
|
||||
SessionMessage.User.make({
|
||||
|
|
|
|||
|
|
@ -347,6 +347,14 @@ export const layer = Layer.effectDiscard(
|
|||
yield* run(db, event)
|
||||
}),
|
||||
)
|
||||
yield* events.project(SessionEvent.Renamed, (event) =>
|
||||
db
|
||||
.update(SessionTable)
|
||||
.set({ title: event.data.title, time_updated: DateTime.toEpochMillis(event.data.timestamp) })
|
||||
.where(eq(SessionTable.id, event.data.sessionID))
|
||||
.run()
|
||||
.pipe(Effect.orDie),
|
||||
)
|
||||
yield* events.project(SessionEvent.Prompted, (event) =>
|
||||
Effect.gen(function* () {
|
||||
if (event.durable === undefined) return yield* Effect.die("Durable Session event is missing aggregate sequence")
|
||||
|
|
|
|||
|
|
@ -251,6 +251,9 @@ describe("PublicApi OpenAPI v2 errors", () => {
|
|||
["post", "/session/{sessionID}/revert"],
|
||||
["post", "/session/{sessionID}/unrevert"],
|
||||
["delete", "/session/{sessionID}/message/{messageID}"],
|
||||
["post", "/api/session/{sessionID}/revert/stage"],
|
||||
["post", "/api/session/{sessionID}/revert/clear"],
|
||||
["post", "/api/session/{sessionID}/revert/commit"],
|
||||
] as const) {
|
||||
expect(componentName(responseRef(spec.paths[route[1]]?.[route[0]]?.responses?.["409"]) ?? "")).toBe(
|
||||
"SessionBusyError",
|
||||
|
|
|
|||
|
|
@ -25,6 +25,15 @@ export class ConflictError extends Schema.TaggedErrorClass<ConflictError>()(
|
|||
{ httpApiStatus: 409 },
|
||||
) {}
|
||||
|
||||
export class SessionBusyError extends Schema.TaggedErrorClass<SessionBusyError>()(
|
||||
"SessionBusyError",
|
||||
{
|
||||
sessionID: Schema.String,
|
||||
message: Schema.String,
|
||||
},
|
||||
{ httpApiStatus: 409 },
|
||||
) {}
|
||||
|
||||
export class ServiceUnavailableError extends Schema.TaggedErrorClass<ServiceUnavailableError>()(
|
||||
"ServiceUnavailableError",
|
||||
{
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
InvalidRequestError,
|
||||
MessageNotFoundError,
|
||||
ServiceUnavailableError,
|
||||
SessionBusyError,
|
||||
SessionNotFoundError,
|
||||
UnknownError,
|
||||
} from "../errors"
|
||||
|
|
@ -187,6 +188,22 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
|
|||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("session.rename", "/api/session/:sessionID/rename", {
|
||||
params: { sessionID: Session.ID },
|
||||
payload: Schema.Struct({ title: Schema.String }),
|
||||
success: HttpApiSchema.NoContent,
|
||||
error: SessionNotFoundError,
|
||||
})
|
||||
.middleware(sessionLocationMiddleware)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.rename",
|
||||
summary: "Rename session",
|
||||
description: "Update the session title.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("session.prompt", "/api/session/:sessionID/prompt", {
|
||||
params: { sessionID: Session.ID },
|
||||
|
|
@ -243,7 +260,7 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
|
|||
params: { sessionID: Session.ID },
|
||||
payload: Schema.Struct({ messageID: SessionMessage.ID, files: Schema.Boolean.pipe(Schema.optional) }),
|
||||
success: Schema.Struct({ data: Revert.State }),
|
||||
error: [MessageNotFoundError, SessionNotFoundError, UnknownError],
|
||||
error: [MessageNotFoundError, SessionNotFoundError, SessionBusyError, UnknownError],
|
||||
})
|
||||
.middleware(sessionLocationMiddleware)
|
||||
.annotateMerge(
|
||||
|
|
@ -258,7 +275,7 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
|
|||
HttpApiEndpoint.post("session.revert.clear", "/api/session/:sessionID/revert/clear", {
|
||||
params: { sessionID: Session.ID },
|
||||
success: HttpApiSchema.NoContent,
|
||||
error: [SessionNotFoundError, UnknownError],
|
||||
error: [SessionNotFoundError, SessionBusyError, UnknownError],
|
||||
})
|
||||
.middleware(sessionLocationMiddleware)
|
||||
.annotateMerge(OpenApi.annotations({ identifier: "v2.session.revert.clear", summary: "Clear staged revert" })),
|
||||
|
|
@ -267,7 +284,7 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
|
|||
HttpApiEndpoint.post("session.revert.commit", "/api/session/:sessionID/revert/commit", {
|
||||
params: { sessionID: Session.ID },
|
||||
success: HttpApiSchema.NoContent,
|
||||
error: SessionNotFoundError,
|
||||
error: [SessionNotFoundError, SessionBusyError],
|
||||
})
|
||||
.middleware(sessionLocationMiddleware)
|
||||
.annotateMerge(
|
||||
|
|
|
|||
|
|
@ -84,6 +84,16 @@ export const Moved = Event.define({
|
|||
})
|
||||
export type Moved = typeof Moved.Type
|
||||
|
||||
export const Renamed = Event.define({
|
||||
type: "session.next.renamed",
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
title: Schema.String,
|
||||
},
|
||||
})
|
||||
export type Renamed = typeof Renamed.Type
|
||||
|
||||
export const Prompted = Event.define({
|
||||
type: "session.next.prompted",
|
||||
...options,
|
||||
|
|
@ -449,6 +459,7 @@ export const DurableDefinitions = Event.inventory(
|
|||
AgentSwitched,
|
||||
ModelSwitched,
|
||||
Moved,
|
||||
Renamed,
|
||||
Prompted,
|
||||
PromptAdmitted,
|
||||
ContextUpdated,
|
||||
|
|
@ -480,6 +491,7 @@ export const Definitions = Event.inventory(
|
|||
AgentSwitched,
|
||||
ModelSwitched,
|
||||
Moved,
|
||||
Renamed,
|
||||
Prompted,
|
||||
PromptAdmitted,
|
||||
ContextUpdated,
|
||||
|
|
|
|||
|
|
@ -369,6 +369,8 @@ import type {
|
|||
V2SessionQuestionRejectResponses,
|
||||
V2SessionQuestionReplyErrors,
|
||||
V2SessionQuestionReplyResponses,
|
||||
V2SessionRenameErrors,
|
||||
V2SessionRenameResponses,
|
||||
V2SessionRevertClearErrors,
|
||||
V2SessionRevertClearResponses,
|
||||
V2SessionRevertCommitErrors,
|
||||
|
|
@ -5612,6 +5614,41 @@ export class Session3 extends HeyApiClient {
|
|||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename session
|
||||
*
|
||||
* Update the session title.
|
||||
*/
|
||||
public rename<ThrowOnError extends boolean = false>(
|
||||
parameters: {
|
||||
sessionID: string
|
||||
title?: string
|
||||
},
|
||||
options?: Options<never, ThrowOnError>,
|
||||
) {
|
||||
const params = buildClientParams(
|
||||
[parameters],
|
||||
[
|
||||
{
|
||||
args: [
|
||||
{ in: "path", key: "sessionID" },
|
||||
{ in: "body", key: "title" },
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
return (options?.client ?? this.client).post<V2SessionRenameResponses, V2SessionRenameErrors, ThrowOnError>({
|
||||
url: "/api/session/{sessionID}/rename",
|
||||
...options,
|
||||
...params,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...options?.headers,
|
||||
...params.headers,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Send message
|
||||
*
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export type Event =
|
|||
| EventSessionNextAgentSwitched
|
||||
| EventSessionNextModelSwitched
|
||||
| EventSessionNextMoved
|
||||
| EventSessionNextRenamed
|
||||
| EventSessionNextPrompted
|
||||
| EventSessionNextPromptAdmitted
|
||||
| EventSessionNextContextUpdated
|
||||
|
|
@ -848,6 +849,15 @@ export type GlobalEvent = {
|
|||
subdirectory?: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
id: string
|
||||
type: "session.next.renamed"
|
||||
properties: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
title: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
id: string
|
||||
type: "session.next.prompted"
|
||||
|
|
@ -1611,6 +1621,7 @@ export type GlobalEvent = {
|
|||
| SyncEventSessionNextAgentSwitched
|
||||
| SyncEventSessionNextModelSwitched
|
||||
| SyncEventSessionNextMoved
|
||||
| SyncEventSessionNextRenamed
|
||||
| SyncEventSessionNextPrompted
|
||||
| SyncEventSessionNextPromptAdmitted
|
||||
| SyncEventSessionNextContextUpdated
|
||||
|
|
@ -2760,6 +2771,7 @@ export type V2Event =
|
|||
| V2EventSessionNextAgentSwitched
|
||||
| V2EventSessionNextModelSwitched
|
||||
| V2EventSessionNextMoved
|
||||
| V2EventSessionNextRenamed
|
||||
| V2EventSessionNextPrompted
|
||||
| V2EventSessionNextPromptAdmitted
|
||||
| V2EventSessionNextContextUpdated
|
||||
|
|
@ -3240,6 +3252,22 @@ export type SyncEventSessionNextMoved = {
|
|||
}
|
||||
}
|
||||
|
||||
export type SyncEventSessionNextRenamed = {
|
||||
type: "sync"
|
||||
id: string
|
||||
syncEvent: {
|
||||
type: "session.next.renamed.1"
|
||||
id: string
|
||||
seq: number
|
||||
aggregateID: string
|
||||
data: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
title: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type SyncEventSessionNextPrompted = {
|
||||
type: "sync"
|
||||
id: string
|
||||
|
|
@ -4113,6 +4141,25 @@ export type SessionNextMoved = {
|
|||
}
|
||||
}
|
||||
|
||||
export type SessionNextRenamed = {
|
||||
id: string
|
||||
metadata?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
type: "session.next.renamed"
|
||||
durable?: {
|
||||
aggregateID: string
|
||||
seq: number | "NaN" | "Infinity" | "-Infinity"
|
||||
version: number | "NaN" | "Infinity" | "-Infinity"
|
||||
}
|
||||
location?: LocationRef
|
||||
data: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
title: string
|
||||
}
|
||||
}
|
||||
|
||||
export type SessionNextPrompted = {
|
||||
id: string
|
||||
metadata?: {
|
||||
|
|
@ -5168,6 +5215,25 @@ export type V2EventSessionNextMoved = {
|
|||
}
|
||||
}
|
||||
|
||||
export type V2EventSessionNextRenamed = {
|
||||
id: string
|
||||
metadata?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
durable?: {
|
||||
aggregateID: string
|
||||
seq: number
|
||||
version: number
|
||||
}
|
||||
location?: LocationRef
|
||||
type: "session.next.renamed"
|
||||
data: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
title: string
|
||||
}
|
||||
}
|
||||
|
||||
export type V2EventSessionNextPrompted = {
|
||||
id: string
|
||||
metadata?: {
|
||||
|
|
@ -6836,6 +6902,16 @@ export type EventSessionNextMoved = {
|
|||
}
|
||||
}
|
||||
|
||||
export type EventSessionNextRenamed = {
|
||||
id: string
|
||||
type: "session.next.renamed"
|
||||
properties: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
title: string
|
||||
}
|
||||
}
|
||||
|
||||
export type EventSessionNextPrompted = {
|
||||
id: string
|
||||
type: "session.next.prompted"
|
||||
|
|
@ -12105,6 +12181,43 @@ export type V2SessionSwitchModelResponses = {
|
|||
|
||||
export type V2SessionSwitchModelResponse = V2SessionSwitchModelResponses[keyof V2SessionSwitchModelResponses]
|
||||
|
||||
export type V2SessionRenameData = {
|
||||
body: {
|
||||
title: string
|
||||
}
|
||||
path: {
|
||||
sessionID: string
|
||||
}
|
||||
query?: never
|
||||
url: "/api/session/{sessionID}/rename"
|
||||
}
|
||||
|
||||
export type V2SessionRenameErrors = {
|
||||
/**
|
||||
* InvalidRequestError
|
||||
*/
|
||||
400: InvalidRequestError
|
||||
/**
|
||||
* UnauthorizedError
|
||||
*/
|
||||
401: UnauthorizedError
|
||||
/**
|
||||
* SessionNotFoundError
|
||||
*/
|
||||
404: SessionNotFoundError
|
||||
}
|
||||
|
||||
export type V2SessionRenameError = V2SessionRenameErrors[keyof V2SessionRenameErrors]
|
||||
|
||||
export type V2SessionRenameResponses = {
|
||||
/**
|
||||
* <No Content>
|
||||
*/
|
||||
204: void
|
||||
}
|
||||
|
||||
export type V2SessionRenameResponse = V2SessionRenameResponses[keyof V2SessionRenameResponses]
|
||||
|
||||
export type V2SessionPromptData = {
|
||||
body: {
|
||||
id?: string
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
InvalidCursorError,
|
||||
MessageNotFoundError,
|
||||
ServiceUnavailableError,
|
||||
SessionBusyError,
|
||||
SessionNotFoundError,
|
||||
UnknownError,
|
||||
} from "@opencode-ai/protocol/errors"
|
||||
|
|
@ -135,6 +136,22 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
|
|||
return HttpApiSchema.NoContent.make()
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"session.rename",
|
||||
Effect.fn(function* (ctx) {
|
||||
yield* session.rename({ sessionID: ctx.params.sessionID, title: ctx.payload.title }).pipe(
|
||||
Effect.catchTag("Session.NotFoundError", (error) =>
|
||||
Effect.fail(
|
||||
new SessionNotFoundError({
|
||||
sessionID: error.sessionID,
|
||||
message: `Session not found: ${error.sessionID}`,
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
return HttpApiSchema.NoContent.make()
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"session.prompt",
|
||||
Effect.fn(function* (ctx) {
|
||||
|
|
@ -238,6 +255,14 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
|
|||
message: `Message not found: ${error.messageID}`,
|
||||
}),
|
||||
),
|
||||
Effect.catchTag(
|
||||
"Session.BusyError",
|
||||
(error) =>
|
||||
new SessionBusyError({
|
||||
sessionID: error.sessionID,
|
||||
message: `Session is busy: ${error.sessionID}`,
|
||||
}),
|
||||
),
|
||||
Effect.catchTag("Snapshot.Error", (error) => {
|
||||
const ref = `err_${crypto.randomUUID().slice(0, 8)}`
|
||||
return Effect.logError("failed to stage session revert", { cause: error }).pipe(
|
||||
|
|
@ -267,6 +292,14 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
|
|||
message: `Session not found: ${error.sessionID}`,
|
||||
}),
|
||||
),
|
||||
Effect.catchTag(
|
||||
"Session.BusyError",
|
||||
(error) =>
|
||||
new SessionBusyError({
|
||||
sessionID: error.sessionID,
|
||||
message: `Session is busy: ${error.sessionID}`,
|
||||
}),
|
||||
),
|
||||
Effect.catchTag("Snapshot.Error", (error) => {
|
||||
const ref = `err_${crypto.randomUUID().slice(0, 8)}`
|
||||
return Effect.logError("failed to clear session revert", { cause: error }).pipe(
|
||||
|
|
@ -296,6 +329,14 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
|
|||
message: `Session not found: ${error.sessionID}`,
|
||||
}),
|
||||
),
|
||||
Effect.catchTag(
|
||||
"Session.BusyError",
|
||||
(error) =>
|
||||
new SessionBusyError({
|
||||
sessionID: error.sessionID,
|
||||
message: `Session is busy: ${error.sessionID}`,
|
||||
}),
|
||||
),
|
||||
)
|
||||
return HttpApiSchema.NoContent.make()
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { useLocal } from "../context/local"
|
|||
import { createDebouncedSignal } from "../util/signal"
|
||||
import { useToast } from "../ui/toast"
|
||||
import { useCommandShortcut } from "../keymap"
|
||||
import { DialogSessionRename } from "./dialog-session-rename"
|
||||
import { Spinner } from "./spinner"
|
||||
|
||||
export function DialogSessionList() {
|
||||
|
|
@ -121,7 +122,8 @@ export function DialogSessionList() {
|
|||
{
|
||||
command: "session.rename",
|
||||
title: "rename",
|
||||
onTrigger: () => unavailable("Renaming"),
|
||||
onTrigger: (option: { value: string }) =>
|
||||
DialogSessionRename.show(dialog, option.value, data.session.get(option.value)?.title),
|
||||
},
|
||||
]}
|
||||
footerHints={quickSwitchFooterHints()}
|
||||
|
|
|
|||
|
|
@ -1,31 +1,33 @@
|
|||
import { DialogPrompt } from "../ui/dialog-prompt"
|
||||
import { useDialog } from "../ui/dialog"
|
||||
import { useSync } from "../context/sync"
|
||||
import { createMemo } from "solid-js"
|
||||
import { type DialogContext, useDialog } from "../ui/dialog"
|
||||
import { useSDK } from "../context/sdk"
|
||||
import { useToast } from "../ui/toast"
|
||||
import { errorMessage } from "../util/error"
|
||||
|
||||
interface DialogSessionRenameProps {
|
||||
session: string
|
||||
}
|
||||
|
||||
export function DialogSessionRename(props: DialogSessionRenameProps) {
|
||||
export function DialogSessionRename(props: { sessionID: string; currentTitle?: string }) {
|
||||
const dialog = useDialog()
|
||||
const sync = useSync()
|
||||
const sdk = useSDK()
|
||||
const session = createMemo(() => sync.session.get(props.session))
|
||||
const toast = useToast()
|
||||
|
||||
return (
|
||||
<DialogPrompt
|
||||
title="Rename Session"
|
||||
value={session()?.title}
|
||||
title="Rename session"
|
||||
placeholder="Session title"
|
||||
value={props.currentTitle}
|
||||
onConfirm={(value) => {
|
||||
void sdk.client.session.update({
|
||||
sessionID: props.session,
|
||||
title: value,
|
||||
})
|
||||
dialog.clear()
|
||||
const title = value.trim()
|
||||
if (!title) return
|
||||
void sdk.client.v2.session
|
||||
.rename({ sessionID: props.sessionID, title }, { throwOnError: true })
|
||||
.then(() => dialog.clear())
|
||||
.catch((error) =>
|
||||
toast.show({ message: `Failed to rename session: ${errorMessage(error)}`, variant: "error", duration: 5000 }),
|
||||
)
|
||||
}}
|
||||
onCancel={() => dialog.clear()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
DialogSessionRename.show = (dialog: DialogContext, sessionID: string, currentTitle?: string) =>
|
||||
dialog.replace(() => <DialogSessionRename sessionID={sessionID} currentTitle={currentTitle} />)
|
||||
|
|
|
|||
|
|
@ -1104,6 +1104,13 @@ export function Prompt(props: PromptProps) {
|
|||
{ throwOnError: true },
|
||||
)
|
||||
}
|
||||
if (session?.revert) {
|
||||
const revertResult = await sdk.client.v2.session.revert.commit({ sessionID })
|
||||
if (revertResult.error) {
|
||||
toast.show({ title: "Failed to commit revert", message: errorMessage(revertResult.error), variant: "error" })
|
||||
return false
|
||||
}
|
||||
}
|
||||
const result = await sdk.client.v2.session.prompt({
|
||||
sessionID,
|
||||
prompt: {
|
||||
|
|
|
|||
|
|
@ -169,6 +169,10 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
})
|
||||
})
|
||||
break
|
||||
case "session.next.renamed":
|
||||
if (store.session.info[event.data.sessionID])
|
||||
setStore("session", "info", event.data.sessionID, "title", event.data.title)
|
||||
break
|
||||
case "session.next.prompted": {
|
||||
setStore("session", "status", event.data.sessionID, "running")
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
|
|
@ -405,6 +409,15 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
case "session.next.compaction.started":
|
||||
setStore("session", "status", event.data.sessionID, "running")
|
||||
break
|
||||
case "session.next.revert.staged":
|
||||
if (store.session.info[event.data.sessionID])
|
||||
setStore("session", "info", event.data.sessionID, "revert", event.data.revert)
|
||||
break
|
||||
case "session.next.revert.cleared":
|
||||
case "session.next.revert.committed":
|
||||
if (store.session.info[event.data.sessionID])
|
||||
setStore("session", "info", event.data.sessionID, "revert", undefined)
|
||||
break
|
||||
case "session.next.compaction.delta":
|
||||
break
|
||||
case "session.next.compaction.ended":
|
||||
|
|
|
|||
|
|
@ -3,11 +3,14 @@ import { useData } from "../../context/data"
|
|||
import { DialogSelect } from "../../ui/dialog-select"
|
||||
import { useClipboard } from "../../context/clipboard"
|
||||
import { useToast } from "../../ui/toast"
|
||||
import { useSDK } from "../../context/sdk"
|
||||
import { errorMessage } from "../../util/error"
|
||||
|
||||
export function DialogMessage(props: { messageID: string; sessionID: string; setPrompt?: unknown }) {
|
||||
const data = useData()
|
||||
const clipboard = useClipboard()
|
||||
const toast = useToast()
|
||||
const sdk = useSDK()
|
||||
const message = createMemo(() =>
|
||||
data.session.message.get(props.sessionID, props.messageID),
|
||||
)
|
||||
|
|
@ -20,8 +23,12 @@ export function DialogMessage(props: { messageID: string; sessionID: string; set
|
|||
title: "Revert",
|
||||
value: "session.revert",
|
||||
description: "undo messages and file changes",
|
||||
onSelect: (dialog) => {
|
||||
toast.show({ message: "Reverting is not implemented for V2 sessions yet", variant: "error", duration: 5000 })
|
||||
onSelect: async (dialog) => {
|
||||
const result = await sdk.client.v2.session.revert.stage({
|
||||
sessionID: props.sessionID,
|
||||
messageID: props.messageID,
|
||||
})
|
||||
if (result.error) toast.show({ message: errorMessage(result.error), variant: "error", duration: 5000 })
|
||||
dialog.clear()
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ import { useSDK } from "../../context/sdk"
|
|||
import { useEditorContext } from "../../context/editor"
|
||||
import { openEditor } from "../../editor"
|
||||
import { useDialog } from "../../ui/dialog"
|
||||
import { DialogSessionRename } from "../../component/dialog-session-rename"
|
||||
import { TodoItem } from "../../component/todo-item"
|
||||
import { DialogMessage } from "./dialog-message"
|
||||
import { Sidebar } from "./sidebar"
|
||||
|
|
@ -335,7 +336,7 @@ export function Session() {
|
|||
value: "session.rename",
|
||||
category: "Session",
|
||||
slash: { name: "rename" },
|
||||
run: () => unavailable("Renaming"),
|
||||
run: () => DialogSessionRename.show(dialog, route.sessionID, session()?.title),
|
||||
},
|
||||
{
|
||||
title: "Jump to message",
|
||||
|
|
@ -377,7 +378,28 @@ export function Session() {
|
|||
value: "session.undo",
|
||||
category: "Session",
|
||||
slash: { name: "undo" },
|
||||
run: () => unavailable("Undo"),
|
||||
run: () => {
|
||||
void (async () => {
|
||||
const boundary = session()?.revert?.messageID
|
||||
const list = messages()
|
||||
let target: string | undefined
|
||||
for (let i = list.length - 1; i >= 0; i--) {
|
||||
const message = list[i]
|
||||
if (message.type !== "user" || !message.text.trim()) continue
|
||||
if (boundary && message.id >= boundary) continue
|
||||
target = message.id
|
||||
break
|
||||
}
|
||||
if (!target) {
|
||||
toast.show({ message: "Nothing to undo", variant: "error", duration: 3000 })
|
||||
dialog.clear()
|
||||
return
|
||||
}
|
||||
const result = await sdk.client.v2.session.revert.stage({ sessionID: route.sessionID, messageID: target })
|
||||
if (result.error) toast.show({ message: errorMessage(result.error), variant: "error", duration: 5000 })
|
||||
dialog.clear()
|
||||
})()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Redo",
|
||||
|
|
@ -385,7 +407,13 @@ export function Session() {
|
|||
category: "Session",
|
||||
enabled: !!session()?.revert?.messageID,
|
||||
slash: { name: "redo" },
|
||||
run: () => unavailable("Redo"),
|
||||
run: () => {
|
||||
void (async () => {
|
||||
const result = await sdk.client.v2.session.revert.clear({ sessionID: route.sessionID })
|
||||
if (result.error) toast.show({ message: errorMessage(result.error), variant: "error", duration: 5000 })
|
||||
dialog.clear()
|
||||
})()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: sidebarVisible() ? "Hide sidebar" : "Show sidebar",
|
||||
|
|
@ -848,6 +876,11 @@ export function Session() {
|
|||
/>
|
||||
)}
|
||||
</For>
|
||||
<Show when={session()?.revert?.messageID}>
|
||||
<RevertMessage
|
||||
count={messages().filter((message) => message.id > session()!.revert!.messageID).length}
|
||||
/>
|
||||
</Show>
|
||||
</scrollbox>
|
||||
<box flexShrink={0}>
|
||||
<Show when={permissions().length > 0}>
|
||||
|
|
@ -1136,9 +1169,10 @@ function CompactionMessage() {
|
|||
|
||||
function RevertMessage(props: { count: number }) {
|
||||
const { theme } = useTheme()
|
||||
const dialog = useDialog()
|
||||
const renderer = useRenderer()
|
||||
const route = useRouteData("session")
|
||||
const sdk = useSDK()
|
||||
const toast = useToast()
|
||||
const renderer = useRenderer()
|
||||
const [hover, setHover] = createSignal(false)
|
||||
return (
|
||||
<box
|
||||
|
|
@ -1146,8 +1180,10 @@ function RevertMessage(props: { count: number }) {
|
|||
onMouseOut={() => setHover(false)}
|
||||
onMouseUp={() => {
|
||||
if (renderer.getSelection()?.getSelectedText()) return
|
||||
toast.show({ message: "Redo is not implemented for V2 sessions yet", variant: "error", duration: 5000 })
|
||||
dialog.clear()
|
||||
void (async () => {
|
||||
const result = await sdk.client.v2.session.revert.clear({ sessionID: route.sessionID })
|
||||
if (result.error) toast.show({ message: errorMessage(result.error), variant: "error", duration: 5000 })
|
||||
})()
|
||||
}}
|
||||
flexShrink={0}
|
||||
border={["left"]}
|
||||
|
|
@ -1156,7 +1192,7 @@ function RevertMessage(props: { count: number }) {
|
|||
>
|
||||
<box paddingTop={1} paddingBottom={1} paddingLeft={2} backgroundColor={hover() ? theme.backgroundElement : theme.backgroundPanel}>
|
||||
<text fg={theme.textMuted}>{props.count} message{props.count === 1 ? "" : "s"} reverted</text>
|
||||
<text fg={theme.textMuted}>Redo is not implemented for V2 sessions yet</text>
|
||||
<text fg={theme.textMuted}>Click to redo</text>
|
||||
</box>
|
||||
</box>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -23,6 +23,13 @@ export type SessionRow =
|
|||
export function createSessionRows(sessionID: Accessor<string>) {
|
||||
const data = useData()
|
||||
const [rows, setRows] = createStore<SessionRow[]>([])
|
||||
const revertBoundary = () => data.session.get(sessionID())?.revert?.messageID
|
||||
|
||||
function reduce() {
|
||||
const messages = data.session.message.list(sessionID())
|
||||
const boundary = revertBoundary()
|
||||
return reduceSessionRows(boundary ? messages.filter((message) => message.id <= boundary) : messages)
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
const pending = new Set(
|
||||
|
|
@ -44,17 +51,24 @@ export function createSessionRows(sessionID: Accessor<string>) {
|
|||
|
||||
createEffect(
|
||||
on(sessionID, (id) => {
|
||||
setRows(reconcile(reduceSessionRows(data.session.message.list(id))))
|
||||
setRows(reconcile(reduce()))
|
||||
void data.session.message.refresh(id).then(
|
||||
() => {
|
||||
if (sessionID() !== id) return
|
||||
setRows(reconcile(reduceSessionRows(data.session.message.list(id))))
|
||||
setRows(reconcile(reduce()))
|
||||
},
|
||||
() => undefined,
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
// Re-reduce when the revert boundary changes (stage/clear/commit).
|
||||
createEffect(
|
||||
on(revertBoundary, () => {
|
||||
setRows(reconcile(reduce()))
|
||||
}),
|
||||
)
|
||||
|
||||
const appendMessage = (messageID: string) =>
|
||||
setRows(
|
||||
produce((draft) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue