feat(tui): improve session fork handling
This commit is contained in:
parent
e312f0d775
commit
96717c1a8c
24 changed files with 335 additions and 387 deletions
1
packages/core/src/database/migration.gen.ts
generated
1
packages/core/src/database/migration.gen.ts
generated
|
|
@ -46,5 +46,6 @@ export const migrations = (
|
|||
import("./migration/20260703190000_reset_v2_shell_event_payloads"),
|
||||
import("./migration/20260703200000_reset_v2_session_events"),
|
||||
import("./migration/20260705180000_rename_instructions"),
|
||||
import("./migration/20260706223930_add-session-fork"),
|
||||
])
|
||||
).map((module) => module.default) satisfies DatabaseMigration.Migration[]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260706223930_add-session-fork",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`ALTER TABLE \`session\` ADD \`fork_session_id\` text;`)
|
||||
yield* tx.run(`ALTER TABLE \`session\` ADD \`fork_message_id\` text;`)
|
||||
yield* tx.run(`
|
||||
UPDATE \`session\`
|
||||
SET
|
||||
\`parent_id\` = NULL,
|
||||
\`fork_session_id\` = (
|
||||
SELECT json_extract(\`event\`.\`data\`, '$.parentID')
|
||||
FROM \`event\`
|
||||
WHERE \`event\`.\`aggregate_id\` = \`session\`.\`id\`
|
||||
AND \`event\`.\`type\` = 'session.forked'
|
||||
ORDER BY \`event\`.\`seq\`
|
||||
LIMIT 1
|
||||
),
|
||||
\`fork_message_id\` = (
|
||||
SELECT json_extract(\`event\`.\`data\`, '$.from')
|
||||
FROM \`event\`
|
||||
WHERE \`event\`.\`aggregate_id\` = \`session\`.\`id\`
|
||||
AND \`event\`.\`type\` = 'session.forked'
|
||||
ORDER BY \`event\`.\`seq\`
|
||||
LIMIT 1
|
||||
)
|
||||
WHERE EXISTS (
|
||||
SELECT 1
|
||||
FROM \`event\`
|
||||
WHERE \`event\`.\`aggregate_id\` = \`session\`.\`id\`
|
||||
AND \`event\`.\`type\` = 'session.forked'
|
||||
);
|
||||
`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
|
@ -196,6 +196,8 @@ export default {
|
|||
\`project_id\` text NOT NULL,
|
||||
\`workspace_id\` text,
|
||||
\`parent_id\` text,
|
||||
\`fork_session_id\` text,
|
||||
\`fork_message_id\` text,
|
||||
\`slug\` text NOT NULL,
|
||||
\`directory\` text NOT NULL,
|
||||
\`path\` text,
|
||||
|
|
|
|||
|
|
@ -746,6 +746,7 @@ const materializeAttachment = Effect.fn("V2Session.materializeAttachment")(funct
|
|||
start: undefined,
|
||||
end: undefined,
|
||||
name: undefined,
|
||||
mime: undefined,
|
||||
}
|
||||
: yield* readFileAttachment(fs, input.uri)
|
||||
if (resolved.bytes.byteLength > MAX_ATTACHMENT_BYTES)
|
||||
|
|
@ -754,7 +755,7 @@ const materializeAttachment = Effect.fn("V2Session.materializeAttachment")(funct
|
|||
message: `Attachment exceeds the ${MAX_ATTACHMENT_BYTES} byte limit: ${input.uri}`,
|
||||
})
|
||||
|
||||
const mime = Mime.detect(resolved.bytes)
|
||||
const mime = resolved.mime ?? Mime.detect(resolved.bytes)
|
||||
const content =
|
||||
mime === "text/plain" && resolved.start !== undefined
|
||||
? Buffer.from(
|
||||
|
|
@ -791,6 +792,25 @@ const readFileAttachment = Effect.fn("V2Session.readFileAttachment")(function* (
|
|||
const info = yield* fs.stat(target).pipe(
|
||||
Effect.mapError(() => new AttachmentError({ uri, message: `Unable to read attachment: ${uri}` })),
|
||||
)
|
||||
if (info.type === "Directory") {
|
||||
const entries = yield* fs.readDirectoryEntries(target).pipe(
|
||||
Effect.mapError(() => new AttachmentError({ uri, message: `Unable to read attachment: ${uri}` })),
|
||||
)
|
||||
return {
|
||||
bytes: Buffer.from(
|
||||
entries
|
||||
.filter((entry) => entry.type === "file" || entry.type === "directory")
|
||||
.sort((a, b) => (a.type === b.type ? a.name.localeCompare(b.name) : a.type === "directory" ? -1 : 1))
|
||||
.map((entry) => entry.name + (entry.type === "directory" ? path.sep : ""))
|
||||
.join("\n"),
|
||||
),
|
||||
source: { type: "uri" as const, uri },
|
||||
start: undefined,
|
||||
end: undefined,
|
||||
name: path.basename(target),
|
||||
mime: "application/x-directory",
|
||||
}
|
||||
}
|
||||
if (info.type !== "File") return yield* new AttachmentError({ uri, message: `Attachment is not a file: ${uri}` })
|
||||
if (Number(info.size) > MAX_ATTACHMENT_BYTES)
|
||||
return yield* new AttachmentError({
|
||||
|
|
@ -800,7 +820,7 @@ const readFileAttachment = Effect.fn("V2Session.readFileAttachment")(function* (
|
|||
const bytes = yield* fs.readFile(target).pipe(
|
||||
Effect.mapError(() => new AttachmentError({ uri, message: `Unable to read attachment: ${uri}` })),
|
||||
)
|
||||
return { bytes, source: { type: "uri" as const, uri }, start, end, name: path.basename(target) }
|
||||
return { bytes, source: { type: "uri" as const, uri }, start, end, name: path.basename(target), mime: undefined }
|
||||
})
|
||||
|
||||
function decodeDataURL(uri: string) {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,12 @@ export function fromRow(row: typeof SessionTable.$inferSelect): SessionSchema.In
|
|||
projectID: ProjectV2.ID.make(row.project_id),
|
||||
title: row.title,
|
||||
parentID: row.parent_id ? SessionSchema.ID.make(row.parent_id) : undefined,
|
||||
fork: row.fork_session_id
|
||||
? {
|
||||
sessionID: SessionSchema.ID.make(row.fork_session_id),
|
||||
messageID: row.fork_message_id ? SessionMessage.ID.make(row.fork_message_id) : undefined,
|
||||
}
|
||||
: undefined,
|
||||
agent: row.agent ? AgentV2.ID.make(row.agent) : undefined,
|
||||
model: row.model
|
||||
? {
|
||||
|
|
|
|||
|
|
@ -190,7 +190,9 @@ const projectFork = Effect.fn("SessionProjector.projectFork")(function* (
|
|||
.insert(SessionTable)
|
||||
.values({
|
||||
id: event.data.sessionID,
|
||||
parent_id: event.data.parentID,
|
||||
parent_id: null,
|
||||
fork_session_id: event.data.parentID,
|
||||
fork_message_id: event.data.from,
|
||||
project_id: parent.project_id,
|
||||
workspace_id: parent.workspace_id,
|
||||
slug: Slug.create(),
|
||||
|
|
|
|||
|
|
@ -41,6 +41,26 @@ const textAttachment = (file: FileAttachment) =>
|
|||
},
|
||||
})
|
||||
|
||||
const directoryAttachment = (file: FileAttachment) =>
|
||||
Message.make({
|
||||
role: "user",
|
||||
content: [
|
||||
`Attached directory: ${file.name ?? (file.source.type === "uri" ? file.source.uri : "directory")}`,
|
||||
file.description === undefined ? undefined : `Description: ${file.description}`,
|
||||
file.data.length === 0 ? undefined : "",
|
||||
file.data.length === 0 ? undefined : Buffer.from(file.data, "base64").toString("utf8"),
|
||||
]
|
||||
.filter((line): line is string => line !== undefined)
|
||||
.join("\n"),
|
||||
metadata: {
|
||||
attachment: {
|
||||
source: file.source,
|
||||
name: file.name,
|
||||
description: file.description,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const decodeToolInput = Schema.decodeUnknownOption(Schema.UnknownFromJsonString)
|
||||
|
||||
const providerMetadata = (
|
||||
|
|
@ -157,6 +177,7 @@ function toLLMMessage(message: SessionMessage.Message, model: ModelV2.Ref): Mess
|
|||
const files = message.files ?? []
|
||||
return [
|
||||
...files.filter((file) => file.mime === "text/plain").map(textAttachment),
|
||||
...files.filter((file) => file.mime === "application/x-directory").map(directoryAttachment),
|
||||
Message.make({
|
||||
id: message.id,
|
||||
role: "user",
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ export const SessionTable = sqliteTable(
|
|||
.references(() => ProjectTable.id, { onDelete: "cascade" }),
|
||||
workspace_id: text().$type<WorkspaceV2.ID>(),
|
||||
parent_id: text().$type<SessionSchema.ID>(),
|
||||
fork_session_id: text().$type<SessionSchema.ID>(),
|
||||
fork_message_id: text().$type<SessionMessage.ID>(),
|
||||
slug: text().notNull(),
|
||||
directory: directoryColumn().notNull(),
|
||||
path: pathColumn(),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue