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