mirror of
https://github.com/anomalyco/opencode.git
synced 2026-03-11 09:04:26 +00:00
Compare commits
14 Commits
dev
...
feat/brand
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ca9fb58ec | ||
|
|
96ee60c9ab | ||
|
|
77feb08c2e | ||
|
|
022665c587 | ||
|
|
00197a8566 | ||
|
|
24720d1f29 | ||
|
|
c5bdbe18b4 | ||
|
|
cfa809bf33 | ||
|
|
3dbffd548b | ||
|
|
901135543f | ||
|
|
c3c8aa78b9 | ||
|
|
deecd7ea89 | ||
|
|
ca116111f4 | ||
|
|
e9f973ebc2 |
4
.github/workflows/publish.yml
vendored
4
.github/workflows/publish.yml
vendored
@@ -149,10 +149,6 @@ jobs:
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24"
|
||||
|
||||
- name: Cache apt packages
|
||||
if: contains(matrix.settings.host, 'ubuntu')
|
||||
uses: actions/cache@v4
|
||||
|
||||
@@ -137,4 +137,4 @@ OpenCode 内置两种 Agent,可用 `Tab` 键快速切换:
|
||||
|
||||
---
|
||||
|
||||
**加入我们的社区** [飞书](https://applink.feishu.cn/client/chat/chatter/add_by_link?link_token=de8k6664-1b5e-43f2-8efd-21d6772647b5&qr_code=true) | [X.com](https://x.com/opencode)
|
||||
**加入我们的社区** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode)
|
||||
|
||||
@@ -137,4 +137,4 @@ OpenCode 內建了兩種 Agent,您可以使用 `Tab` 鍵快速切換。
|
||||
|
||||
---
|
||||
|
||||
**加入我們的社群** [飞书](https://applink.feishu.cn/client/chat/chatter/add_by_link?link_token=de8k6664-1b5e-43f2-8efd-21d6772647b5&qr_code=true) | [X.com](https://x.com/opencode)
|
||||
**加入我們的社群** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode)
|
||||
|
||||
@@ -8,12 +8,6 @@ import { useI18n } from "~/context/i18n"
|
||||
export function Footer() {
|
||||
const language = useLanguage()
|
||||
const i18n = useI18n()
|
||||
const community = createMemo(() => {
|
||||
const locale = language.locale()
|
||||
return locale === "zh" || locale === "zht"
|
||||
? ({ key: "footer.feishu", link: language.route("/feishu") } as const)
|
||||
: ({ key: "footer.discord", link: language.route("/discord") } as const)
|
||||
})
|
||||
const githubData = createAsync(() => github())
|
||||
const starCount = createMemo(() =>
|
||||
githubData()?.stars
|
||||
@@ -38,7 +32,7 @@ export function Footer() {
|
||||
<a href={language.route("/changelog")}>{i18n.t("footer.changelog")}</a>
|
||||
</div>
|
||||
<div data-slot="cell">
|
||||
<a href={community().link}>{i18n.t(community().key)}</a>
|
||||
<a href={language.route("/discord")}>{i18n.t("footer.discord")}</a>
|
||||
</div>
|
||||
<div data-slot="cell">
|
||||
<a href={config.social.twitter}>{i18n.t("footer.x")}</a>
|
||||
|
||||
@@ -21,7 +21,6 @@ export const dict = {
|
||||
"footer.github": "GitHub",
|
||||
"footer.docs": "Docs",
|
||||
"footer.changelog": "Changelog",
|
||||
"footer.feishu": "Feishu",
|
||||
"footer.discord": "Discord",
|
||||
"footer.x": "X",
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ export const dict = {
|
||||
"footer.github": "GitHub",
|
||||
"footer.docs": "文档",
|
||||
"footer.changelog": "更新日志",
|
||||
"footer.feishu": "飞书",
|
||||
"footer.discord": "Discord",
|
||||
"footer.x": "X",
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ export const dict = {
|
||||
"footer.github": "GitHub",
|
||||
"footer.docs": "文件",
|
||||
"footer.changelog": "更新日誌",
|
||||
"footer.feishu": "飞书",
|
||||
"footer.discord": "Discord",
|
||||
"footer.x": "X",
|
||||
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { redirect } from "@solidjs/router"
|
||||
|
||||
export async function GET() {
|
||||
return redirect(
|
||||
"https://applink.feishu.cn/client/chat/chatter/add_by_link?link_token=de8k6664-1b5e-43f2-8efd-21d6772647b5&qr_code=true",
|
||||
)
|
||||
}
|
||||
@@ -107,7 +107,7 @@ export function syncCli() {
|
||||
|
||||
let version = ""
|
||||
try {
|
||||
version = execFileSync(installPath, ["--version"], { windowsHide: true }).toString().trim()
|
||||
version = execFileSync(installPath, ["--version"]).toString().trim()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
@@ -147,7 +147,7 @@ export function spawnCommand(args: string, extraEnv: Record<string, string>) {
|
||||
console.log(`[cli] Executing: ${cmd} ${cmdArgs.join(" ")}`)
|
||||
const child = spawn(cmd, cmdArgs, {
|
||||
env: envs,
|
||||
detached: process.platform !== "win32",
|
||||
detached: true,
|
||||
windowsHide: true,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
})
|
||||
|
||||
@@ -12,6 +12,7 @@ const seed = async () => {
|
||||
const { InstanceBootstrap } = await import("../src/project/bootstrap")
|
||||
const { Session } = await import("../src/session")
|
||||
const { Identifier } = await import("../src/id/id")
|
||||
const { MessageID } = await import("../src/session/schema")
|
||||
const { Project } = await import("../src/project/project")
|
||||
|
||||
await Instance.provide({
|
||||
@@ -19,7 +20,7 @@ const seed = async () => {
|
||||
init: InstanceBootstrap,
|
||||
fn: async () => {
|
||||
const session = await Session.create({ title })
|
||||
const messageID = Identifier.descending("message")
|
||||
const messageID = MessageID.ascending()
|
||||
const partID = Identifier.descending("part")
|
||||
const message = {
|
||||
id: messageID,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Provider } from "../../../provider/provider"
|
||||
import { Session } from "../../../session"
|
||||
import type { MessageV2 } from "../../../session/message-v2"
|
||||
import { Identifier } from "../../../id/id"
|
||||
import { MessageID } from "../../../session/schema"
|
||||
import { ToolRegistry } from "../../../tool/registry"
|
||||
import { Instance } from "../../../project/instance"
|
||||
import { PermissionNext } from "../../../permission/next"
|
||||
@@ -113,7 +114,7 @@ function parseToolParams(input?: string) {
|
||||
|
||||
async function createToolContext(agent: Agent.Info) {
|
||||
const session = await Session.create({ title: `Debug tool run (${agent.name})` })
|
||||
const messageID = Identifier.ascending("message")
|
||||
const messageID = MessageID.ascending()
|
||||
const model = agent.model ?? (await Provider.defaultModel())
|
||||
const now = Date.now()
|
||||
const message: MessageV2.Assistant = {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Argv } from "yargs"
|
||||
import { Session } from "../../session"
|
||||
import { SessionID } from "../../session/schema"
|
||||
import { cmd } from "./cmd"
|
||||
import { bootstrap } from "../bootstrap"
|
||||
import { UI } from "../ui"
|
||||
@@ -17,7 +18,7 @@ export const ExportCommand = cmd({
|
||||
},
|
||||
handler: async (args) => {
|
||||
await bootstrap(process.cwd(), async () => {
|
||||
let sessionID = args.sessionID
|
||||
let sessionID = args.sessionID ? SessionID.make(args.sessionID) : undefined
|
||||
process.stderr.write(`Exporting session: ${sessionID ?? "latest"}\n`)
|
||||
|
||||
if (!sessionID) {
|
||||
@@ -58,7 +59,7 @@ export const ExportCommand = cmd({
|
||||
throw new UI.CancelledError()
|
||||
}
|
||||
|
||||
sessionID = selectedSession as string
|
||||
sessionID = selectedSession
|
||||
|
||||
prompts.outro("Exporting session...", {
|
||||
output: process.stderr,
|
||||
@@ -67,7 +68,7 @@ export const ExportCommand = cmd({
|
||||
|
||||
try {
|
||||
const sessionInfo = await Session.get(sessionID!)
|
||||
const messages = await Session.messages({ sessionID: sessionID! })
|
||||
const messages = await Session.messages({ sessionID: sessionInfo.id })
|
||||
|
||||
const exportData = {
|
||||
info: sessionInfo,
|
||||
|
||||
@@ -22,7 +22,9 @@ import { ModelsDev } from "../../provider/models"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { bootstrap } from "../bootstrap"
|
||||
import { Session } from "../../session"
|
||||
import type { SessionID } from "../../session/schema"
|
||||
import { Identifier } from "../../id/id"
|
||||
import { MessageID } from "../../session/schema"
|
||||
import { Provider } from "../../provider/provider"
|
||||
import { Bus } from "../../bus"
|
||||
import { MessageV2 } from "../../session/message-v2"
|
||||
@@ -481,7 +483,7 @@ export const GithubRunCommand = cmd({
|
||||
let octoRest: Octokit
|
||||
let octoGraph: typeof graphql
|
||||
let gitConfig: string
|
||||
let session: { id: string; title: string; version: string }
|
||||
let session: { id: SessionID; title: string; version: string }
|
||||
let shareId: string | undefined
|
||||
let exitCode = 0
|
||||
type PromptFiles = Awaited<ReturnType<typeof getUserPrompt>>["promptFiles"]
|
||||
@@ -934,7 +936,7 @@ export const GithubRunCommand = cmd({
|
||||
|
||||
const result = await SessionPrompt.prompt({
|
||||
sessionID: session.id,
|
||||
messageID: Identifier.ascending("message"),
|
||||
messageID: MessageID.ascending(),
|
||||
variant,
|
||||
model: {
|
||||
providerID,
|
||||
@@ -988,7 +990,7 @@ export const GithubRunCommand = cmd({
|
||||
console.log("Requesting summary from agent...")
|
||||
const summary = await SessionPrompt.prompt({
|
||||
sessionID: session.id,
|
||||
messageID: Identifier.ascending("message"),
|
||||
messageID: MessageID.ascending(),
|
||||
variant,
|
||||
model: {
|
||||
providerID,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Argv } from "yargs"
|
||||
import type { Session as SDKSession, Message, Part } from "@opencode-ai/sdk/v2"
|
||||
import { Session } from "../../session"
|
||||
import { SessionID, MessageID } from "../../session/schema"
|
||||
import { cmd } from "./cmd"
|
||||
import { bootstrap } from "../bootstrap"
|
||||
import { Database } from "../../storage/db"
|
||||
@@ -86,7 +87,7 @@ export const ImportCommand = cmd({
|
||||
await bootstrap(process.cwd(), async () => {
|
||||
let exportData:
|
||||
| {
|
||||
info: Session.Info
|
||||
info: SDKSession
|
||||
messages: Array<{
|
||||
info: Message
|
||||
parts: Part[]
|
||||
@@ -152,7 +153,15 @@ export const ImportCommand = cmd({
|
||||
return
|
||||
}
|
||||
|
||||
const row = { ...Session.toRow(exportData.info), project_id: Instance.project.id }
|
||||
const row = Session.toRow({
|
||||
...exportData.info,
|
||||
id: SessionID.make(exportData.info.id),
|
||||
parentID: exportData.info.parentID ? SessionID.make(exportData.info.parentID) : undefined,
|
||||
projectID: Instance.project.id,
|
||||
revert: exportData.info.revert
|
||||
? { ...exportData.info.revert, messageID: MessageID.make(exportData.info.revert.messageID) }
|
||||
: undefined,
|
||||
})
|
||||
Database.use((db) =>
|
||||
db
|
||||
.insert(SessionTable)
|
||||
@@ -162,28 +171,30 @@ export const ImportCommand = cmd({
|
||||
)
|
||||
|
||||
for (const msg of exportData.messages) {
|
||||
const { id: _mid, sessionID: _msid, ...msgData } = msg.info
|
||||
Database.use((db) =>
|
||||
db
|
||||
.insert(MessageTable)
|
||||
.values({
|
||||
id: msg.info.id,
|
||||
session_id: exportData.info.id,
|
||||
id: MessageID.make(msg.info.id),
|
||||
session_id: row.id,
|
||||
time_created: msg.info.time?.created ?? Date.now(),
|
||||
data: msg.info,
|
||||
data: msgData,
|
||||
})
|
||||
.onConflictDoNothing()
|
||||
.run(),
|
||||
)
|
||||
|
||||
for (const part of msg.parts) {
|
||||
const { id: _pid, sessionID: _psid, messageID: _pmid, ...partData } = part
|
||||
Database.use((db) =>
|
||||
db
|
||||
.insert(PartTable)
|
||||
.values({
|
||||
id: part.id,
|
||||
message_id: msg.info.id,
|
||||
session_id: exportData.info.id,
|
||||
data: part,
|
||||
message_id: MessageID.make(msg.info.id),
|
||||
session_id: row.id,
|
||||
data: partData,
|
||||
})
|
||||
.onConflictDoNothing()
|
||||
.run(),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Argv } from "yargs"
|
||||
import { cmd } from "./cmd"
|
||||
import { Session } from "../../session"
|
||||
import { SessionID } from "../../session/schema"
|
||||
import { bootstrap } from "../bootstrap"
|
||||
import { UI } from "../ui"
|
||||
import { Locale } from "../../util/locale"
|
||||
@@ -57,13 +58,14 @@ export const SessionDeleteCommand = cmd({
|
||||
},
|
||||
handler: async (args) => {
|
||||
await bootstrap(process.cwd(), async () => {
|
||||
const sessionID = SessionID.make(args.sessionID)
|
||||
try {
|
||||
await Session.get(args.sessionID)
|
||||
await Session.get(sessionID)
|
||||
} catch {
|
||||
UI.error(`Session not found: ${args.sessionID}`)
|
||||
process.exit(1)
|
||||
}
|
||||
await Session.remove(args.sessionID)
|
||||
await Session.remove(sessionID)
|
||||
UI.println(UI.Style.TEXT_SUCCESS_BOLD + `Session ${args.sessionID} deleted` + UI.Style.TEXT_NORMAL)
|
||||
})
|
||||
},
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useSDK } from "@tui/context/sdk"
|
||||
import { useRoute } from "@tui/context/route"
|
||||
import { useSync } from "@tui/context/sync"
|
||||
import { Identifier } from "@/id/id"
|
||||
import { MessageID } from "@/session/schema"
|
||||
import { createStore, produce } from "solid-js/store"
|
||||
import { useKeybind } from "@tui/context/keybind"
|
||||
import { usePromptHistory, type PromptInfo } from "./history"
|
||||
@@ -561,7 +562,7 @@ export function Prompt(props: PromptProps) {
|
||||
sessionID = res.data.id
|
||||
}
|
||||
|
||||
const messageID = Identifier.ascending("message")
|
||||
const messageID = MessageID.ascending()
|
||||
let inputText = store.prompt.input
|
||||
|
||||
// Expand pasted text inline before submitting
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import { Bus } from "@/bus"
|
||||
import { SessionID } from "@/session/schema"
|
||||
import z from "zod"
|
||||
|
||||
export const TuiEvent = {
|
||||
@@ -42,7 +43,7 @@ export const TuiEvent = {
|
||||
SessionSelect: BusEvent.define(
|
||||
"tui.session.select",
|
||||
z.object({
|
||||
sessionID: z.string().regex(/^ses/).describe("Session ID to navigate to"),
|
||||
sessionID: SessionID.zod.describe("Session ID to navigate to"),
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import { SessionID, MessageID } from "@/session/schema"
|
||||
import z from "zod"
|
||||
import { Config } from "../config/config"
|
||||
import { Instance } from "../project/instance"
|
||||
@@ -14,9 +15,9 @@ export namespace Command {
|
||||
"command.executed",
|
||||
z.object({
|
||||
name: z.string(),
|
||||
sessionID: Identifier.schema("session"),
|
||||
sessionID: SessionID.zod,
|
||||
arguments: z.string(),
|
||||
messageID: Identifier.schema("message"),
|
||||
messageID: MessageID.zod,
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import z from "zod"
|
||||
import { Identifier } from "@/id/id"
|
||||
import { ProjectID } from "@/project/schema"
|
||||
|
||||
export const WorkspaceInfo = z.object({
|
||||
id: Identifier.schema("workspace"),
|
||||
@@ -8,7 +9,7 @@ export const WorkspaceInfo = z.object({
|
||||
name: z.string().nullable(),
|
||||
directory: z.string().nullable(),
|
||||
extra: z.unknown().nullable(),
|
||||
projectID: z.string(),
|
||||
projectID: ProjectID.zod,
|
||||
})
|
||||
export type WorkspaceInfo = z.infer<typeof WorkspaceInfo>
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { sqliteTable, text } from "drizzle-orm/sqlite-core"
|
||||
import { ProjectTable } from "../project/project.sql"
|
||||
import type { ProjectID } from "../project/schema"
|
||||
|
||||
export const WorkspaceTable = sqliteTable("workspace", {
|
||||
id: text().primaryKey(),
|
||||
@@ -9,6 +10,7 @@ export const WorkspaceTable = sqliteTable("workspace", {
|
||||
directory: text(),
|
||||
extra: text({ mode: "json" }),
|
||||
project_id: text()
|
||||
.$type<ProjectID>()
|
||||
.notNull()
|
||||
.references(() => ProjectTable.id, { onDelete: "cascade" }),
|
||||
})
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Project } from "@/project/project"
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import { GlobalBus } from "@/bus/global"
|
||||
import { Log } from "@/util/log"
|
||||
import { ProjectID } from "@/project/schema"
|
||||
import { WorkspaceTable } from "./workspace.sql"
|
||||
import { getAdaptor } from "./adaptors"
|
||||
import { WorkspaceInfo } from "./types"
|
||||
@@ -48,7 +49,7 @@ export namespace Workspace {
|
||||
id: Identifier.schema("workspace").optional(),
|
||||
type: Info.shape.type,
|
||||
branch: Info.shape.branch,
|
||||
projectID: Info.shape.projectID,
|
||||
projectID: ProjectID.zod,
|
||||
extra: Info.shape.extra,
|
||||
})
|
||||
|
||||
|
||||
@@ -114,7 +114,6 @@ export namespace LSP {
|
||||
return {
|
||||
process: spawn(item.command[0], item.command.slice(1), {
|
||||
cwd: root,
|
||||
windowsHide: true,
|
||||
env: {
|
||||
...process.env,
|
||||
...item.env,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { spawn as launch, type ChildProcessWithoutNullStreams } from "child_process"
|
||||
import { spawn, type ChildProcessWithoutNullStreams } from "child_process"
|
||||
import path from "path"
|
||||
import os from "os"
|
||||
import { Global } from "../global"
|
||||
@@ -14,11 +14,6 @@ import { Process } from "../util/process"
|
||||
import { which } from "../util/which"
|
||||
import { Module } from "@opencode-ai/util/module"
|
||||
|
||||
const spawn = ((cmd, args, opts) => {
|
||||
if (Array.isArray(args)) return launch(cmd, [...args], { ...(opts ?? {}), windowsHide: true })
|
||||
return launch(cmd, { ...(args ?? {}), windowsHide: true })
|
||||
}) as typeof launch
|
||||
|
||||
export namespace LSPServer {
|
||||
const log = Log.create({ service: "lsp.server" })
|
||||
const pathExists = async (p: string) =>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import { Bus } from "@/bus"
|
||||
import { SessionID, MessageID } from "@/session/schema"
|
||||
import z from "zod"
|
||||
import { Log } from "../util/log"
|
||||
import { Identifier } from "../id/id"
|
||||
@@ -24,8 +25,8 @@ export namespace Permission {
|
||||
id: z.string(),
|
||||
type: z.string(),
|
||||
pattern: z.union([z.string(), z.array(z.string())]).optional(),
|
||||
sessionID: z.string(),
|
||||
messageID: z.string(),
|
||||
sessionID: SessionID.zod,
|
||||
messageID: MessageID.zod,
|
||||
callID: z.string().optional(),
|
||||
message: z.string(),
|
||||
metadata: z.record(z.string(), z.any()),
|
||||
@@ -43,7 +44,7 @@ export namespace Permission {
|
||||
Replied: BusEvent.define(
|
||||
"permission.replied",
|
||||
z.object({
|
||||
sessionID: z.string(),
|
||||
sessionID: SessionID.zod,
|
||||
permissionID: z.string(),
|
||||
response: z.string(),
|
||||
}),
|
||||
|
||||
@@ -2,11 +2,13 @@ import { Bus } from "@/bus"
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import { Config } from "@/config/config"
|
||||
import { Identifier } from "@/id/id"
|
||||
import { SessionID, MessageID } from "@/session/schema"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { Database, eq } from "@/storage/db"
|
||||
import { PermissionTable } from "@/session/session.sql"
|
||||
import { fn } from "@/util/fn"
|
||||
import { Log } from "@/util/log"
|
||||
import { ProjectID } from "@/project/schema"
|
||||
import { Wildcard } from "@/util/wildcard"
|
||||
import os from "os"
|
||||
import z from "zod"
|
||||
@@ -68,14 +70,14 @@ export namespace PermissionNext {
|
||||
export const Request = z
|
||||
.object({
|
||||
id: Identifier.schema("permission"),
|
||||
sessionID: Identifier.schema("session"),
|
||||
sessionID: SessionID.zod,
|
||||
permission: z.string(),
|
||||
patterns: z.string().array(),
|
||||
metadata: z.record(z.string(), z.any()),
|
||||
always: z.string().array(),
|
||||
tool: z
|
||||
.object({
|
||||
messageID: z.string(),
|
||||
messageID: MessageID.zod,
|
||||
callID: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
@@ -90,7 +92,7 @@ export namespace PermissionNext {
|
||||
export type Reply = z.infer<typeof Reply>
|
||||
|
||||
export const Approval = z.object({
|
||||
projectID: z.string(),
|
||||
projectID: ProjectID.zod,
|
||||
patterns: z.string().array(),
|
||||
})
|
||||
|
||||
@@ -99,7 +101,7 @@ export namespace PermissionNext {
|
||||
Replied: BusEvent.define(
|
||||
"permission.replied",
|
||||
z.object({
|
||||
sessionID: z.string(),
|
||||
sessionID: SessionID.zod,
|
||||
requestID: z.string(),
|
||||
reply: Reply,
|
||||
}),
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"
|
||||
import { Timestamps } from "../storage/schema.sql"
|
||||
import type { ProjectID } from "./schema"
|
||||
|
||||
export const ProjectTable = sqliteTable("project", {
|
||||
id: text().primaryKey(),
|
||||
id: text().$type<ProjectID>().primaryKey(),
|
||||
worktree: text().notNull(),
|
||||
vcs: text(),
|
||||
name: text(),
|
||||
|
||||
@@ -15,6 +15,7 @@ import { existsSync } from "fs"
|
||||
import { git } from "../util/git"
|
||||
import { Glob } from "../util/glob"
|
||||
import { which } from "../util/which"
|
||||
import { ProjectID } from "./schema"
|
||||
|
||||
export namespace Project {
|
||||
const log = Log.create({ service: "project" })
|
||||
@@ -33,7 +34,7 @@ export namespace Project {
|
||||
|
||||
export const Info = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
id: ProjectID.zod,
|
||||
worktree: z.string(),
|
||||
vcs: z.literal("git").optional(),
|
||||
name: z.string().optional(),
|
||||
@@ -73,7 +74,7 @@ export namespace Project {
|
||||
? { url: row.icon_url ?? undefined, color: row.icon_color ?? undefined }
|
||||
: undefined
|
||||
return {
|
||||
id: row.id,
|
||||
id: ProjectID.make(row.id),
|
||||
worktree: row.worktree,
|
||||
vcs: row.vcs ? Info.shape.vcs.parse(row.vcs) : undefined,
|
||||
name: row.name ?? undefined,
|
||||
@@ -91,6 +92,7 @@ export namespace Project {
|
||||
function readCachedId(dir: string) {
|
||||
return Filesystem.readText(path.join(dir, "opencode"))
|
||||
.then((x) => x.trim())
|
||||
.then(ProjectID.make)
|
||||
.catch(() => undefined)
|
||||
}
|
||||
|
||||
@@ -111,7 +113,7 @@ export namespace Project {
|
||||
|
||||
if (!gitBinary) {
|
||||
return {
|
||||
id: id ?? "global",
|
||||
id: id ?? ProjectID.global,
|
||||
worktree: sandbox,
|
||||
sandbox,
|
||||
vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
|
||||
@@ -130,7 +132,7 @@ export namespace Project {
|
||||
|
||||
if (!worktree) {
|
||||
return {
|
||||
id: id ?? "global",
|
||||
id: id ?? ProjectID.global,
|
||||
worktree: sandbox,
|
||||
sandbox,
|
||||
vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
|
||||
@@ -160,14 +162,14 @@ export namespace Project {
|
||||
|
||||
if (!roots) {
|
||||
return {
|
||||
id: "global",
|
||||
id: ProjectID.global,
|
||||
worktree: sandbox,
|
||||
sandbox,
|
||||
vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
|
||||
}
|
||||
}
|
||||
|
||||
id = roots[0]
|
||||
id = roots[0] ? ProjectID.make(roots[0]) : undefined
|
||||
if (id) {
|
||||
await Filesystem.write(path.join(dotgit, "opencode"), id).catch(() => undefined)
|
||||
}
|
||||
@@ -175,7 +177,7 @@ export namespace Project {
|
||||
|
||||
if (!id) {
|
||||
return {
|
||||
id: "global",
|
||||
id: ProjectID.global,
|
||||
worktree: sandbox,
|
||||
sandbox,
|
||||
vcs: "git",
|
||||
@@ -208,7 +210,7 @@ export namespace Project {
|
||||
}
|
||||
|
||||
return {
|
||||
id: "global",
|
||||
id: ProjectID.global,
|
||||
worktree: "/",
|
||||
sandbox: "/",
|
||||
vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
|
||||
@@ -228,7 +230,7 @@ export namespace Project {
|
||||
updated: Date.now(),
|
||||
},
|
||||
}
|
||||
if (data.id !== "global") {
|
||||
if (data.id !== ProjectID.global) {
|
||||
await migrateFromGlobal(data.id, data.worktree)
|
||||
}
|
||||
return fresh
|
||||
@@ -308,12 +310,12 @@ export namespace Project {
|
||||
return
|
||||
}
|
||||
|
||||
async function migrateFromGlobal(id: string, worktree: string) {
|
||||
const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, "global")).get())
|
||||
async function migrateFromGlobal(id: ProjectID, worktree: string) {
|
||||
const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, ProjectID.global)).get())
|
||||
if (!row) return
|
||||
|
||||
const sessions = Database.use((db) =>
|
||||
db.select().from(SessionTable).where(eq(SessionTable.project_id, "global")).all(),
|
||||
db.select().from(SessionTable).where(eq(SessionTable.project_id, ProjectID.global)).all(),
|
||||
)
|
||||
if (sessions.length === 0) return
|
||||
|
||||
@@ -323,14 +325,14 @@ export namespace Project {
|
||||
// Skip sessions that belong to a different directory
|
||||
if (row.directory && row.directory !== worktree) return
|
||||
|
||||
log.info("migrating session", { sessionID: row.id, from: "global", to: id })
|
||||
log.info("migrating session", { sessionID: row.id, from: ProjectID.global, to: id })
|
||||
Database.use((db) => db.update(SessionTable).set({ project_id: id }).where(eq(SessionTable.id, row.id)).run())
|
||||
}).catch((error) => {
|
||||
log.error("failed to migrate sessions from global to project", { error, projectId: id })
|
||||
})
|
||||
}
|
||||
|
||||
export function setInitialized(id: string) {
|
||||
export function setInitialized(id: ProjectID) {
|
||||
Database.use((db) =>
|
||||
db
|
||||
.update(ProjectTable)
|
||||
@@ -352,7 +354,7 @@ export namespace Project {
|
||||
)
|
||||
}
|
||||
|
||||
export function get(id: string): Info | undefined {
|
||||
export function get(id: ProjectID): Info | undefined {
|
||||
const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get())
|
||||
if (!row) return undefined
|
||||
return fromRow(row)
|
||||
@@ -375,12 +377,13 @@ export namespace Project {
|
||||
|
||||
export const update = fn(
|
||||
z.object({
|
||||
projectID: z.string(),
|
||||
projectID: ProjectID.zod,
|
||||
name: z.string().optional(),
|
||||
icon: Info.shape.icon.optional(),
|
||||
commands: Info.shape.commands.optional(),
|
||||
}),
|
||||
async (input) => {
|
||||
const id = ProjectID.make(input.projectID)
|
||||
const result = Database.use((db) =>
|
||||
db
|
||||
.update(ProjectTable)
|
||||
@@ -391,7 +394,7 @@ export namespace Project {
|
||||
commands: input.commands,
|
||||
time_updated: Date.now(),
|
||||
})
|
||||
.where(eq(ProjectTable.id, input.projectID))
|
||||
.where(eq(ProjectTable.id, id))
|
||||
.returning()
|
||||
.get(),
|
||||
)
|
||||
@@ -407,7 +410,7 @@ export namespace Project {
|
||||
},
|
||||
)
|
||||
|
||||
export async function sandboxes(id: string) {
|
||||
export async function sandboxes(id: ProjectID) {
|
||||
const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get())
|
||||
if (!row) return []
|
||||
const data = fromRow(row)
|
||||
@@ -419,7 +422,7 @@ export namespace Project {
|
||||
return valid
|
||||
}
|
||||
|
||||
export async function addSandbox(id: string, directory: string) {
|
||||
export async function addSandbox(id: ProjectID, directory: string) {
|
||||
const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get())
|
||||
if (!row) throw new Error(`Project not found: ${id}`)
|
||||
const sandboxes = [...row.sandboxes]
|
||||
@@ -443,7 +446,7 @@ export namespace Project {
|
||||
return data
|
||||
}
|
||||
|
||||
export async function removeSandbox(id: string, directory: string) {
|
||||
export async function removeSandbox(id: ProjectID, directory: string) {
|
||||
const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get())
|
||||
if (!row) throw new Error(`Project not found: ${id}`)
|
||||
const sandboxes = row.sandboxes.filter((s) => s !== directory)
|
||||
|
||||
16
packages/opencode/src/project/schema.ts
Normal file
16
packages/opencode/src/project/schema.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Schema } from "effect"
|
||||
import z from "zod"
|
||||
|
||||
import { withStatics } from "@/util/schema"
|
||||
|
||||
const projectIdSchema = Schema.String.pipe(Schema.brand("ProjectId"))
|
||||
|
||||
export type ProjectID = typeof projectIdSchema.Type
|
||||
|
||||
export const ProjectID = projectIdSchema.pipe(
|
||||
withStatics((schema: typeof projectIdSchema) => ({
|
||||
global: schema.makeUnsafe("global"),
|
||||
make: (id: string) => schema.makeUnsafe(id),
|
||||
zod: z.string().pipe(z.custom<ProjectID>()),
|
||||
})),
|
||||
)
|
||||
@@ -67,11 +67,7 @@ export namespace Provider {
|
||||
const project =
|
||||
options["project"] ?? Env.get("GOOGLE_CLOUD_PROJECT") ?? Env.get("GCP_PROJECT") ?? Env.get("GCLOUD_PROJECT")
|
||||
const location =
|
||||
options["location"] ??
|
||||
Env.get("GOOGLE_VERTEX_LOCATION") ??
|
||||
Env.get("GOOGLE_CLOUD_LOCATION") ??
|
||||
Env.get("VERTEX_LOCATION") ??
|
||||
"us-central1"
|
||||
options["location"] ?? Env.get("GOOGLE_CLOUD_LOCATION") ?? Env.get("VERTEX_LOCATION") ?? "us-central1"
|
||||
const endpoint = location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com`
|
||||
|
||||
return {
|
||||
@@ -441,11 +437,7 @@ export namespace Provider {
|
||||
Env.get("GCLOUD_PROJECT")
|
||||
|
||||
const location =
|
||||
provider.options?.location ??
|
||||
Env.get("GOOGLE_VERTEX_LOCATION") ??
|
||||
Env.get("GOOGLE_CLOUD_LOCATION") ??
|
||||
Env.get("VERTEX_LOCATION") ??
|
||||
"us-central1"
|
||||
provider.options?.location ?? Env.get("GOOGLE_CLOUD_LOCATION") ?? Env.get("VERTEX_LOCATION") ?? "us-central1"
|
||||
|
||||
const autoload = Boolean(project)
|
||||
if (!autoload) return { autoload: false }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Bus } from "@/bus"
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import { Identifier } from "@/id/id"
|
||||
import { SessionID, MessageID } from "@/session/schema"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { Log } from "@/util/log"
|
||||
import z from "zod"
|
||||
@@ -34,11 +35,11 @@ export namespace Question {
|
||||
export const Request = z
|
||||
.object({
|
||||
id: Identifier.schema("question"),
|
||||
sessionID: Identifier.schema("session"),
|
||||
sessionID: SessionID.zod,
|
||||
questions: z.array(Info).describe("Questions to ask"),
|
||||
tool: z
|
||||
.object({
|
||||
messageID: z.string(),
|
||||
messageID: MessageID.zod,
|
||||
callID: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
@@ -65,7 +66,7 @@ export namespace Question {
|
||||
Replied: BusEvent.define(
|
||||
"question.replied",
|
||||
z.object({
|
||||
sessionID: z.string(),
|
||||
sessionID: SessionID.zod,
|
||||
requestID: z.string(),
|
||||
answers: z.array(Answer),
|
||||
}),
|
||||
@@ -73,7 +74,7 @@ export namespace Question {
|
||||
Rejected: BusEvent.define(
|
||||
"question.rejected",
|
||||
z.object({
|
||||
sessionID: z.string(),
|
||||
sessionID: SessionID.zod,
|
||||
requestID: z.string(),
|
||||
}),
|
||||
),
|
||||
@@ -95,9 +96,9 @@ export namespace Question {
|
||||
})
|
||||
|
||||
export async function ask(input: {
|
||||
sessionID: string
|
||||
sessionID: SessionID
|
||||
questions: Info[]
|
||||
tool?: { messageID: string; callID: string }
|
||||
tool?: { messageID: MessageID; callID: string }
|
||||
}): Promise<Answer[]> {
|
||||
const s = await state()
|
||||
const id = Identifier.ascending("question")
|
||||
|
||||
@@ -4,6 +4,7 @@ import { resolver } from "hono-openapi"
|
||||
import { Instance } from "../../project/instance"
|
||||
import { Project } from "../../project/project"
|
||||
import z from "zod"
|
||||
import { ProjectID } from "../../project/schema"
|
||||
import { errors } from "../error"
|
||||
import { lazy } from "../../util/lazy"
|
||||
import { InstanceBootstrap } from "../../project/bootstrap"
|
||||
@@ -105,7 +106,7 @@ export const ProjectRoutes = lazy(() =>
|
||||
...errors(400, 404),
|
||||
},
|
||||
}),
|
||||
validator("param", z.object({ projectID: z.string() })),
|
||||
validator("param", z.object({ projectID: ProjectID.zod })),
|
||||
validator("json", Project.update.schema.omit({ projectID: true })),
|
||||
async (c) => {
|
||||
const projectID = c.req.valid("param").projectID
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Hono } from "hono"
|
||||
import { stream } from "hono/streaming"
|
||||
import { describeRoute, validator, resolver } from "hono-openapi"
|
||||
import { SessionID, MessageID } from "@/session/schema"
|
||||
import z from "zod"
|
||||
import { Session } from "../../session"
|
||||
import { MessageV2 } from "../../session/message-v2"
|
||||
@@ -173,7 +174,7 @@ export const SessionRoutes = lazy(() =>
|
||||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
sessionID: z.string().meta({ description: "Session ID" }),
|
||||
sessionID: SessionID.zod,
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
@@ -258,7 +259,7 @@ export const SessionRoutes = lazy(() =>
|
||||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
sessionID: z.string(),
|
||||
sessionID: SessionID.zod,
|
||||
}),
|
||||
),
|
||||
validator(
|
||||
@@ -309,7 +310,7 @@ export const SessionRoutes = lazy(() =>
|
||||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
sessionID: z.string().meta({ description: "Session ID" }),
|
||||
sessionID: SessionID.zod,
|
||||
}),
|
||||
),
|
||||
validator("json", Session.initialize.schema.omit({ sessionID: true })),
|
||||
@@ -372,7 +373,7 @@ export const SessionRoutes = lazy(() =>
|
||||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
sessionID: z.string(),
|
||||
sessionID: SessionID.zod,
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
@@ -401,7 +402,7 @@ export const SessionRoutes = lazy(() =>
|
||||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
sessionID: z.string(),
|
||||
sessionID: SessionID.zod,
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
@@ -502,7 +503,7 @@ export const SessionRoutes = lazy(() =>
|
||||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
sessionID: z.string().meta({ description: "Session ID" }),
|
||||
sessionID: SessionID.zod,
|
||||
}),
|
||||
),
|
||||
validator(
|
||||
@@ -561,7 +562,7 @@ export const SessionRoutes = lazy(() =>
|
||||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
sessionID: z.string().meta({ description: "Session ID" }),
|
||||
sessionID: SessionID.zod,
|
||||
}),
|
||||
),
|
||||
validator(
|
||||
@@ -605,8 +606,8 @@ export const SessionRoutes = lazy(() =>
|
||||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
sessionID: z.string().meta({ description: "Session ID" }),
|
||||
messageID: z.string().meta({ description: "Message ID" }),
|
||||
sessionID: SessionID.zod,
|
||||
messageID: MessageID.zod,
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
@@ -640,8 +641,8 @@ export const SessionRoutes = lazy(() =>
|
||||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
sessionID: z.string().meta({ description: "Session ID" }),
|
||||
messageID: z.string().meta({ description: "Message ID" }),
|
||||
sessionID: SessionID.zod,
|
||||
messageID: MessageID.zod,
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
@@ -674,9 +675,9 @@ export const SessionRoutes = lazy(() =>
|
||||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
sessionID: z.string().meta({ description: "Session ID" }),
|
||||
messageID: z.string().meta({ description: "Message ID" }),
|
||||
partID: z.string().meta({ description: "Part ID" }),
|
||||
sessionID: SessionID.zod,
|
||||
messageID: MessageID.zod,
|
||||
partID: z.string(),
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
@@ -709,9 +710,9 @@ export const SessionRoutes = lazy(() =>
|
||||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
sessionID: z.string().meta({ description: "Session ID" }),
|
||||
messageID: z.string().meta({ description: "Message ID" }),
|
||||
partID: z.string().meta({ description: "Part ID" }),
|
||||
sessionID: SessionID.zod,
|
||||
messageID: MessageID.zod,
|
||||
partID: z.string(),
|
||||
}),
|
||||
),
|
||||
validator("json", MessageV2.Part),
|
||||
@@ -753,7 +754,7 @@ export const SessionRoutes = lazy(() =>
|
||||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
sessionID: z.string().meta({ description: "Session ID" }),
|
||||
sessionID: SessionID.zod,
|
||||
}),
|
||||
),
|
||||
validator("json", SessionPrompt.PromptInput.omit({ sessionID: true })),
|
||||
@@ -785,7 +786,7 @@ export const SessionRoutes = lazy(() =>
|
||||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
sessionID: z.string().meta({ description: "Session ID" }),
|
||||
sessionID: SessionID.zod,
|
||||
}),
|
||||
),
|
||||
validator("json", SessionPrompt.PromptInput.omit({ sessionID: true })),
|
||||
@@ -825,7 +826,7 @@ export const SessionRoutes = lazy(() =>
|
||||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
sessionID: z.string().meta({ description: "Session ID" }),
|
||||
sessionID: SessionID.zod,
|
||||
}),
|
||||
),
|
||||
validator("json", SessionPrompt.CommandInput.omit({ sessionID: true })),
|
||||
@@ -857,7 +858,7 @@ export const SessionRoutes = lazy(() =>
|
||||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
sessionID: z.string().meta({ description: "Session ID" }),
|
||||
sessionID: SessionID.zod,
|
||||
}),
|
||||
),
|
||||
validator("json", SessionPrompt.ShellInput.omit({ sessionID: true })),
|
||||
@@ -889,7 +890,7 @@ export const SessionRoutes = lazy(() =>
|
||||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
sessionID: z.string(),
|
||||
sessionID: SessionID.zod,
|
||||
}),
|
||||
),
|
||||
validator("json", SessionRevert.RevertInput.omit({ sessionID: true })),
|
||||
@@ -924,7 +925,7 @@ export const SessionRoutes = lazy(() =>
|
||||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
sessionID: z.string(),
|
||||
sessionID: SessionID.zod,
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
@@ -955,7 +956,7 @@ export const SessionRoutes = lazy(() =>
|
||||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
sessionID: z.string(),
|
||||
sessionID: SessionID.zod,
|
||||
permissionID: z.string(),
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -2,6 +2,7 @@ import { BusEvent } from "@/bus/bus-event"
|
||||
import { Bus } from "@/bus"
|
||||
import { Session } from "."
|
||||
import { Identifier } from "../id/id"
|
||||
import { SessionID, MessageID } from "./schema"
|
||||
import { Instance } from "../project/instance"
|
||||
import { Provider } from "../provider/provider"
|
||||
import { MessageV2 } from "./message-v2"
|
||||
@@ -22,7 +23,7 @@ export namespace SessionCompaction {
|
||||
Compacted: BusEvent.define(
|
||||
"session.compacted",
|
||||
z.object({
|
||||
sessionID: z.string(),
|
||||
sessionID: SessionID.zod,
|
||||
}),
|
||||
),
|
||||
}
|
||||
@@ -55,7 +56,7 @@ export namespace SessionCompaction {
|
||||
// goes backwards through parts until there are 40_000 tokens worth of tool
|
||||
// calls. then erases output of previous tool calls. idea is to throw away old
|
||||
// tool calls that are no longer relevant.
|
||||
export async function prune(input: { sessionID: string }) {
|
||||
export async function prune(input: { sessionID: SessionID }) {
|
||||
const config = await Config.get()
|
||||
if (config.compaction?.prune === false) return
|
||||
log.info("pruning")
|
||||
@@ -99,9 +100,9 @@ export namespace SessionCompaction {
|
||||
}
|
||||
|
||||
export async function process(input: {
|
||||
parentID: string
|
||||
parentID: MessageID
|
||||
messages: MessageV2.WithParts[]
|
||||
sessionID: string
|
||||
sessionID: SessionID
|
||||
abort: AbortSignal
|
||||
auto: boolean
|
||||
overflow?: boolean
|
||||
@@ -133,7 +134,7 @@ export namespace SessionCompaction {
|
||||
? await Provider.getModel(agent.model.providerID, agent.model.modelID)
|
||||
: await Provider.getModel(userMessage.model.providerID, userMessage.model.modelID)
|
||||
const msg = (await Session.updateMessage({
|
||||
id: Identifier.ascending("message"),
|
||||
id: MessageID.ascending(),
|
||||
role: "assistant",
|
||||
parentID: input.parentID,
|
||||
sessionID: input.sessionID,
|
||||
@@ -236,7 +237,7 @@ When constructing the summary, try to stick to this template:
|
||||
if (replay) {
|
||||
const original = replay.info as MessageV2.User
|
||||
const replayMsg = await Session.updateMessage({
|
||||
id: Identifier.ascending("message"),
|
||||
id: MessageID.ascending(),
|
||||
role: "user",
|
||||
sessionID: input.sessionID,
|
||||
time: { created: Date.now() },
|
||||
@@ -262,7 +263,7 @@ When constructing the summary, try to stick to this template:
|
||||
}
|
||||
} else {
|
||||
const continueMsg = await Session.updateMessage({
|
||||
id: Identifier.ascending("message"),
|
||||
id: MessageID.ascending(),
|
||||
role: "user",
|
||||
sessionID: input.sessionID,
|
||||
time: { created: Date.now() },
|
||||
@@ -295,7 +296,7 @@ When constructing the summary, try to stick to this template:
|
||||
|
||||
export const create = fn(
|
||||
z.object({
|
||||
sessionID: Identifier.schema("session"),
|
||||
sessionID: SessionID.zod,
|
||||
agent: z.string(),
|
||||
model: z.object({
|
||||
providerID: z.string(),
|
||||
@@ -306,7 +307,7 @@ When constructing the summary, try to stick to this template:
|
||||
}),
|
||||
async (input) => {
|
||||
const msg = await Session.updateMessage({
|
||||
id: Identifier.ascending("message"),
|
||||
id: MessageID.ascending(),
|
||||
role: "user",
|
||||
model: input.model,
|
||||
sessionID: input.sessionID,
|
||||
|
||||
@@ -23,6 +23,8 @@ import { fn } from "@/util/fn"
|
||||
import { Command } from "../command"
|
||||
import { Snapshot } from "@/snapshot"
|
||||
import { WorkspaceContext } from "../control-plane/workspace-context"
|
||||
import { ProjectID } from "../project/schema"
|
||||
import { SessionID, MessageID } from "./schema"
|
||||
|
||||
import type { Provider } from "@/provider/provider"
|
||||
import { PermissionNext } from "@/permission/next"
|
||||
@@ -118,12 +120,12 @@ export namespace Session {
|
||||
|
||||
export const Info = z
|
||||
.object({
|
||||
id: Identifier.schema("session"),
|
||||
id: SessionID.zod,
|
||||
slug: z.string(),
|
||||
projectID: z.string(),
|
||||
projectID: ProjectID.zod,
|
||||
workspaceID: z.string().optional(),
|
||||
directory: z.string(),
|
||||
parentID: Identifier.schema("session").optional(),
|
||||
parentID: SessionID.zod.optional(),
|
||||
summary: z
|
||||
.object({
|
||||
additions: z.number(),
|
||||
@@ -148,7 +150,7 @@ export namespace Session {
|
||||
permission: PermissionNext.Ruleset.optional(),
|
||||
revert: z
|
||||
.object({
|
||||
messageID: z.string(),
|
||||
messageID: MessageID.zod,
|
||||
partID: z.string().optional(),
|
||||
snapshot: z.string().optional(),
|
||||
diff: z.string().optional(),
|
||||
@@ -162,7 +164,7 @@ export namespace Session {
|
||||
|
||||
export const ProjectInfo = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
id: ProjectID.zod,
|
||||
name: z.string().optional(),
|
||||
worktree: z.string(),
|
||||
})
|
||||
@@ -200,14 +202,14 @@ export namespace Session {
|
||||
Diff: BusEvent.define(
|
||||
"session.diff",
|
||||
z.object({
|
||||
sessionID: z.string(),
|
||||
sessionID: SessionID.zod,
|
||||
diff: Snapshot.FileDiff.array(),
|
||||
}),
|
||||
),
|
||||
Error: BusEvent.define(
|
||||
"session.error",
|
||||
z.object({
|
||||
sessionID: z.string().optional(),
|
||||
sessionID: SessionID.zod.optional(),
|
||||
error: MessageV2.Assistant.shape.error,
|
||||
}),
|
||||
),
|
||||
@@ -216,7 +218,7 @@ export namespace Session {
|
||||
export const create = fn(
|
||||
z
|
||||
.object({
|
||||
parentID: Identifier.schema("session").optional(),
|
||||
parentID: SessionID.zod.optional(),
|
||||
title: z.string().optional(),
|
||||
permission: Info.shape.permission,
|
||||
workspaceID: Identifier.schema("workspace").optional(),
|
||||
@@ -235,8 +237,8 @@ export namespace Session {
|
||||
|
||||
export const fork = fn(
|
||||
z.object({
|
||||
sessionID: Identifier.schema("session"),
|
||||
messageID: Identifier.schema("message").optional(),
|
||||
sessionID: SessionID.zod,
|
||||
messageID: MessageID.zod.optional(),
|
||||
}),
|
||||
async (input) => {
|
||||
const original = await get(input.sessionID)
|
||||
@@ -248,11 +250,11 @@ export namespace Session {
|
||||
title,
|
||||
})
|
||||
const msgs = await messages({ sessionID: input.sessionID })
|
||||
const idMap = new Map<string, string>()
|
||||
const idMap = new Map<string, MessageID>()
|
||||
|
||||
for (const msg of msgs) {
|
||||
if (input.messageID && msg.info.id >= input.messageID) break
|
||||
const newID = Identifier.ascending("message")
|
||||
const newID = MessageID.ascending()
|
||||
idMap.set(msg.info.id, newID)
|
||||
|
||||
const parentID = msg.info.role === "assistant" && msg.info.parentID ? idMap.get(msg.info.parentID) : undefined
|
||||
@@ -276,7 +278,7 @@ export namespace Session {
|
||||
},
|
||||
)
|
||||
|
||||
export const touch = fn(Identifier.schema("session"), async (sessionID) => {
|
||||
export const touch = fn(SessionID.zod, async (sessionID) => {
|
||||
const now = Date.now()
|
||||
Database.use((db) => {
|
||||
const row = db
|
||||
@@ -292,15 +294,15 @@ export namespace Session {
|
||||
})
|
||||
|
||||
export async function createNext(input: {
|
||||
id?: string
|
||||
id?: SessionID
|
||||
title?: string
|
||||
parentID?: string
|
||||
parentID?: SessionID
|
||||
workspaceID?: string
|
||||
directory: string
|
||||
permission?: PermissionNext.Ruleset
|
||||
}) {
|
||||
const result: Info = {
|
||||
id: Identifier.descending("session", input.id),
|
||||
id: SessionID.descending(input.id),
|
||||
slug: Slug.create(),
|
||||
version: Installation.VERSION,
|
||||
projectID: Instance.project.id,
|
||||
@@ -341,13 +343,13 @@ export namespace Session {
|
||||
return path.join(base, [input.time.created, input.slug].join("-") + ".md")
|
||||
}
|
||||
|
||||
export const get = fn(Identifier.schema("session"), async (id) => {
|
||||
export const get = fn(SessionID.zod, async (id) => {
|
||||
const row = Database.use((db) => db.select().from(SessionTable).where(eq(SessionTable.id, id)).get())
|
||||
if (!row) throw new NotFoundError({ message: `Session not found: ${id}` })
|
||||
return fromRow(row)
|
||||
})
|
||||
|
||||
export const share = fn(Identifier.schema("session"), async (id) => {
|
||||
export const share = fn(SessionID.zod, async (id) => {
|
||||
const cfg = await Config.get()
|
||||
if (cfg.share === "disabled") {
|
||||
throw new Error("Sharing is disabled in configuration")
|
||||
@@ -363,7 +365,7 @@ export namespace Session {
|
||||
return share
|
||||
})
|
||||
|
||||
export const unshare = fn(Identifier.schema("session"), async (id) => {
|
||||
export const unshare = fn(SessionID.zod, async (id) => {
|
||||
// Use ShareNext to remove the share (same as share function uses ShareNext to create)
|
||||
const { ShareNext } = await import("@/share/share-next")
|
||||
await ShareNext.remove(id)
|
||||
@@ -377,7 +379,7 @@ export namespace Session {
|
||||
|
||||
export const setTitle = fn(
|
||||
z.object({
|
||||
sessionID: Identifier.schema("session"),
|
||||
sessionID: SessionID.zod,
|
||||
title: z.string(),
|
||||
}),
|
||||
async (input) => {
|
||||
@@ -398,7 +400,7 @@ export namespace Session {
|
||||
|
||||
export const setArchived = fn(
|
||||
z.object({
|
||||
sessionID: Identifier.schema("session"),
|
||||
sessionID: SessionID.zod,
|
||||
time: z.number().optional(),
|
||||
}),
|
||||
async (input) => {
|
||||
@@ -419,7 +421,7 @@ export namespace Session {
|
||||
|
||||
export const setPermission = fn(
|
||||
z.object({
|
||||
sessionID: Identifier.schema("session"),
|
||||
sessionID: SessionID.zod,
|
||||
permission: PermissionNext.Ruleset,
|
||||
}),
|
||||
async (input) => {
|
||||
@@ -440,7 +442,7 @@ export namespace Session {
|
||||
|
||||
export const setRevert = fn(
|
||||
z.object({
|
||||
sessionID: Identifier.schema("session"),
|
||||
sessionID: SessionID.zod,
|
||||
revert: Info.shape.revert,
|
||||
summary: Info.shape.summary,
|
||||
}),
|
||||
@@ -466,7 +468,7 @@ export namespace Session {
|
||||
},
|
||||
)
|
||||
|
||||
export const clearRevert = fn(Identifier.schema("session"), async (sessionID) => {
|
||||
export const clearRevert = fn(SessionID.zod, async (sessionID) => {
|
||||
return Database.use((db) => {
|
||||
const row = db
|
||||
.update(SessionTable)
|
||||
@@ -486,7 +488,7 @@ export namespace Session {
|
||||
|
||||
export const setSummary = fn(
|
||||
z.object({
|
||||
sessionID: Identifier.schema("session"),
|
||||
sessionID: SessionID.zod,
|
||||
summary: Info.shape.summary,
|
||||
}),
|
||||
async (input) => {
|
||||
@@ -510,7 +512,7 @@ export namespace Session {
|
||||
},
|
||||
)
|
||||
|
||||
export const diff = fn(Identifier.schema("session"), async (sessionID) => {
|
||||
export const diff = fn(SessionID.zod, async (sessionID) => {
|
||||
try {
|
||||
return await Storage.read<Snapshot.FileDiff[]>(["session_diff", sessionID])
|
||||
} catch {
|
||||
@@ -520,7 +522,7 @@ export namespace Session {
|
||||
|
||||
export const messages = fn(
|
||||
z.object({
|
||||
sessionID: Identifier.schema("session"),
|
||||
sessionID: SessionID.zod,
|
||||
limit: z.number().optional(),
|
||||
}),
|
||||
async (input) => {
|
||||
@@ -646,7 +648,7 @@ export namespace Session {
|
||||
}
|
||||
}
|
||||
|
||||
export const children = fn(Identifier.schema("session"), async (parentID) => {
|
||||
export const children = fn(SessionID.zod, async (parentID) => {
|
||||
const project = Instance.project
|
||||
const rows = Database.use((db) =>
|
||||
db
|
||||
@@ -658,7 +660,7 @@ export namespace Session {
|
||||
return rows.map(fromRow)
|
||||
})
|
||||
|
||||
export const remove = fn(Identifier.schema("session"), async (sessionID) => {
|
||||
export const remove = fn(SessionID.zod, async (sessionID) => {
|
||||
const project = Instance.project
|
||||
try {
|
||||
const session = await get(sessionID)
|
||||
@@ -704,8 +706,8 @@ export namespace Session {
|
||||
|
||||
export const removeMessage = fn(
|
||||
z.object({
|
||||
sessionID: Identifier.schema("session"),
|
||||
messageID: Identifier.schema("message"),
|
||||
sessionID: SessionID.zod,
|
||||
messageID: MessageID.zod,
|
||||
}),
|
||||
async (input) => {
|
||||
// CASCADE delete handles parts automatically
|
||||
@@ -726,8 +728,8 @@ export namespace Session {
|
||||
|
||||
export const removePart = fn(
|
||||
z.object({
|
||||
sessionID: Identifier.schema("session"),
|
||||
messageID: Identifier.schema("message"),
|
||||
sessionID: SessionID.zod,
|
||||
messageID: MessageID.zod,
|
||||
partID: Identifier.schema("part"),
|
||||
}),
|
||||
async (input) => {
|
||||
@@ -774,8 +776,8 @@ export namespace Session {
|
||||
|
||||
export const updatePartDelta = fn(
|
||||
z.object({
|
||||
sessionID: z.string(),
|
||||
messageID: z.string(),
|
||||
sessionID: SessionID.zod,
|
||||
messageID: MessageID.zod,
|
||||
partID: z.string(),
|
||||
field: z.string(),
|
||||
delta: z.string(),
|
||||
@@ -872,10 +874,10 @@ export namespace Session {
|
||||
|
||||
export const initialize = fn(
|
||||
z.object({
|
||||
sessionID: Identifier.schema("session"),
|
||||
sessionID: SessionID.zod,
|
||||
modelID: z.string(),
|
||||
providerID: z.string(),
|
||||
messageID: Identifier.schema("message"),
|
||||
messageID: MessageID.zod,
|
||||
}),
|
||||
async (input) => {
|
||||
await SessionPrompt.command({
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import { SessionID, MessageID } from "./schema"
|
||||
import z from "zod"
|
||||
import { NamedError } from "@opencode-ai/util/error"
|
||||
import { APICallError, convertToModelMessages, LoadAPIKeyError, type ModelMessage, type UIMessage } from "ai"
|
||||
import { Identifier } from "../id/id"
|
||||
import { LSP } from "../lsp"
|
||||
import { Snapshot } from "@/snapshot"
|
||||
import { fn } from "@/util/fn"
|
||||
@@ -79,8 +79,8 @@ export namespace MessageV2 {
|
||||
|
||||
const PartBase = z.object({
|
||||
id: z.string(),
|
||||
sessionID: z.string(),
|
||||
messageID: z.string(),
|
||||
sessionID: SessionID.zod,
|
||||
messageID: MessageID.zod,
|
||||
})
|
||||
|
||||
export const SnapshotPart = PartBase.extend({
|
||||
@@ -343,8 +343,8 @@ export namespace MessageV2 {
|
||||
export type ToolPart = z.infer<typeof ToolPart>
|
||||
|
||||
const Base = z.object({
|
||||
id: z.string(),
|
||||
sessionID: z.string(),
|
||||
id: MessageID.zod,
|
||||
sessionID: SessionID.zod,
|
||||
})
|
||||
|
||||
export const User = Base.extend({
|
||||
@@ -410,7 +410,7 @@ export namespace MessageV2 {
|
||||
APIError.Schema,
|
||||
])
|
||||
.optional(),
|
||||
parentID: z.string(),
|
||||
parentID: MessageID.zod,
|
||||
modelID: z.string(),
|
||||
providerID: z.string(),
|
||||
/**
|
||||
@@ -457,8 +457,8 @@ export namespace MessageV2 {
|
||||
Removed: BusEvent.define(
|
||||
"message.removed",
|
||||
z.object({
|
||||
sessionID: z.string(),
|
||||
messageID: z.string(),
|
||||
sessionID: SessionID.zod,
|
||||
messageID: MessageID.zod,
|
||||
}),
|
||||
),
|
||||
PartUpdated: BusEvent.define(
|
||||
@@ -470,8 +470,8 @@ export namespace MessageV2 {
|
||||
PartDelta: BusEvent.define(
|
||||
"message.part.delta",
|
||||
z.object({
|
||||
sessionID: z.string(),
|
||||
messageID: z.string(),
|
||||
sessionID: SessionID.zod,
|
||||
messageID: MessageID.zod,
|
||||
partID: z.string(),
|
||||
field: z.string(),
|
||||
delta: z.string(),
|
||||
@@ -480,8 +480,8 @@ export namespace MessageV2 {
|
||||
PartRemoved: BusEvent.define(
|
||||
"message.part.removed",
|
||||
z.object({
|
||||
sessionID: z.string(),
|
||||
messageID: z.string(),
|
||||
sessionID: SessionID.zod,
|
||||
messageID: MessageID.zod,
|
||||
partID: z.string(),
|
||||
}),
|
||||
),
|
||||
@@ -698,7 +698,7 @@ export namespace MessageV2 {
|
||||
// media (images, PDFs) in tool results
|
||||
if (media.length > 0) {
|
||||
result.push({
|
||||
id: Identifier.ascending("message"),
|
||||
id: MessageID.ascending(),
|
||||
role: "user",
|
||||
parts: [
|
||||
{
|
||||
@@ -728,7 +728,7 @@ export namespace MessageV2 {
|
||||
)
|
||||
}
|
||||
|
||||
export const stream = fn(Identifier.schema("session"), async function* (sessionID) {
|
||||
export const stream = fn(SessionID.zod, async function* (sessionID) {
|
||||
const size = 50
|
||||
let offset = 0
|
||||
while (true) {
|
||||
@@ -781,7 +781,7 @@ export namespace MessageV2 {
|
||||
}
|
||||
})
|
||||
|
||||
export const parts = fn(Identifier.schema("message"), async (message_id) => {
|
||||
export const parts = fn(MessageID.zod, async (message_id) => {
|
||||
const rows = Database.use((db) =>
|
||||
db.select().from(PartTable).where(eq(PartTable.message_id, message_id)).orderBy(PartTable.id).all(),
|
||||
)
|
||||
@@ -792,8 +792,8 @@ export namespace MessageV2 {
|
||||
|
||||
export const get = fn(
|
||||
z.object({
|
||||
sessionID: Identifier.schema("session"),
|
||||
messageID: Identifier.schema("message"),
|
||||
sessionID: SessionID.zod,
|
||||
messageID: MessageID.zod,
|
||||
}),
|
||||
async (input): Promise<WithParts> => {
|
||||
const row = Database.use((db) => db.select().from(MessageTable).where(eq(MessageTable.id, input.messageID)).get())
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import z from "zod"
|
||||
import { SessionID } from "./schema"
|
||||
import { NamedError } from "@opencode-ai/util/error"
|
||||
|
||||
export namespace Message {
|
||||
@@ -142,7 +143,7 @@ export namespace Message {
|
||||
error: z
|
||||
.discriminatedUnion("name", [AuthError.Schema, NamedError.Unknown.Schema, OutputLengthError.Schema])
|
||||
.optional(),
|
||||
sessionID: z.string(),
|
||||
sessionID: SessionID.zod,
|
||||
tool: z.record(
|
||||
z.string(),
|
||||
z
|
||||
|
||||
@@ -15,6 +15,7 @@ import { Config } from "@/config/config"
|
||||
import { SessionCompaction } from "./compaction"
|
||||
import { PermissionNext } from "@/permission/next"
|
||||
import { Question } from "@/question"
|
||||
import type { SessionID, MessageID } from "./schema"
|
||||
|
||||
export namespace SessionProcessor {
|
||||
const DOOM_LOOP_THRESHOLD = 3
|
||||
@@ -25,7 +26,7 @@ export namespace SessionProcessor {
|
||||
|
||||
export function create(input: {
|
||||
assistantMessage: MessageV2.Assistant
|
||||
sessionID: string
|
||||
sessionID: SessionID
|
||||
model: Provider.Model
|
||||
abort: AbortSignal
|
||||
}) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import fs from "fs/promises"
|
||||
import z from "zod"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
import { Identifier } from "../id/id"
|
||||
import { SessionID, MessageID } from "./schema"
|
||||
import { MessageV2 } from "./message-v2"
|
||||
import { Log } from "../util/log"
|
||||
import { SessionRevert } from "./revert"
|
||||
@@ -84,14 +85,14 @@ export namespace SessionPrompt {
|
||||
},
|
||||
)
|
||||
|
||||
export function assertNotBusy(sessionID: string) {
|
||||
export function assertNotBusy(sessionID: SessionID) {
|
||||
const match = state()[sessionID]
|
||||
if (match) throw new Session.BusyError(sessionID)
|
||||
}
|
||||
|
||||
export const PromptInput = z.object({
|
||||
sessionID: Identifier.schema("session"),
|
||||
messageID: Identifier.schema("message").optional(),
|
||||
sessionID: SessionID.zod,
|
||||
messageID: MessageID.zod.optional(),
|
||||
model: z
|
||||
.object({
|
||||
providerID: z.string(),
|
||||
@@ -254,7 +255,7 @@ export namespace SessionPrompt {
|
||||
return s[sessionID].abort.signal
|
||||
}
|
||||
|
||||
export function cancel(sessionID: string) {
|
||||
export function cancel(sessionID: SessionID) {
|
||||
log.info("cancel", { sessionID })
|
||||
const s = state()
|
||||
const match = s[sessionID]
|
||||
@@ -269,7 +270,7 @@ export namespace SessionPrompt {
|
||||
}
|
||||
|
||||
export const LoopInput = z.object({
|
||||
sessionID: Identifier.schema("session"),
|
||||
sessionID: SessionID.zod,
|
||||
resume_existing: z.boolean().optional(),
|
||||
})
|
||||
export const loop = fn(LoopInput, async (input) => {
|
||||
@@ -354,7 +355,7 @@ export namespace SessionPrompt {
|
||||
const taskTool = await TaskTool.init()
|
||||
const taskModel = task.model ? await Provider.getModel(task.model.providerID, task.model.modelID) : model
|
||||
const assistantMessage = (await Session.updateMessage({
|
||||
id: Identifier.ascending("message"),
|
||||
id: MessageID.ascending(),
|
||||
role: "assistant",
|
||||
parentID: lastUser.id,
|
||||
sessionID,
|
||||
@@ -503,7 +504,7 @@ export namespace SessionPrompt {
|
||||
// If we create assistant messages w/ out user ones following mid loop thinking signatures
|
||||
// will be missing and it can cause errors for models like gemini for example
|
||||
const summaryUserMsg: MessageV2.User = {
|
||||
id: Identifier.ascending("message"),
|
||||
id: MessageID.ascending(),
|
||||
sessionID,
|
||||
role: "user",
|
||||
time: {
|
||||
@@ -567,7 +568,7 @@ export namespace SessionPrompt {
|
||||
|
||||
const processor = SessionProcessor.create({
|
||||
assistantMessage: (await Session.updateMessage({
|
||||
id: Identifier.ascending("message"),
|
||||
id: MessageID.ascending(),
|
||||
parentID: lastUser.id,
|
||||
role: "assistant",
|
||||
mode: agent.name,
|
||||
@@ -726,7 +727,7 @@ export namespace SessionPrompt {
|
||||
throw new Error("Impossible")
|
||||
})
|
||||
|
||||
async function lastModel(sessionID: string) {
|
||||
async function lastModel(sessionID: SessionID) {
|
||||
for await (const item of MessageV2.stream(sessionID)) {
|
||||
if (item.info.role === "user" && item.info.model) return item.info.model
|
||||
}
|
||||
@@ -965,7 +966,7 @@ export namespace SessionPrompt {
|
||||
const variant = input.variant ?? (agent.variant && full?.variants?.[agent.variant] ? agent.variant : undefined)
|
||||
|
||||
const info: MessageV2.Info = {
|
||||
id: input.messageID ?? Identifier.ascending("message"),
|
||||
id: input.messageID ?? MessageID.ascending(),
|
||||
role: "user",
|
||||
sessionID: input.sessionID,
|
||||
time: {
|
||||
@@ -1462,7 +1463,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
}
|
||||
|
||||
export const ShellInput = z.object({
|
||||
sessionID: Identifier.schema("session"),
|
||||
sessionID: SessionID.zod,
|
||||
agent: z.string(),
|
||||
model: z
|
||||
.object({
|
||||
@@ -1499,7 +1500,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
const agent = await Agent.get(input.agent)
|
||||
const model = input.model ?? agent.model ?? (await lastModel(input.sessionID))
|
||||
const userMsg: MessageV2.User = {
|
||||
id: Identifier.ascending("message"),
|
||||
id: MessageID.ascending(),
|
||||
sessionID: input.sessionID,
|
||||
time: {
|
||||
created: Date.now(),
|
||||
@@ -1523,7 +1524,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
await Session.updatePart(userPart)
|
||||
|
||||
const msg: MessageV2.Assistant = {
|
||||
id: Identifier.ascending("message"),
|
||||
id: MessageID.ascending(),
|
||||
sessionID: input.sessionID,
|
||||
parentID: userMsg.id,
|
||||
mode: input.agent,
|
||||
@@ -1629,7 +1630,6 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
const proc = spawn(shell, args, {
|
||||
cwd,
|
||||
detached: process.platform !== "win32",
|
||||
windowsHide: process.platform === "win32",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
env: {
|
||||
...process.env,
|
||||
@@ -1713,8 +1713,8 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
}
|
||||
|
||||
export const CommandInput = z.object({
|
||||
messageID: Identifier.schema("message").optional(),
|
||||
sessionID: Identifier.schema("session"),
|
||||
messageID: MessageID.zod.optional(),
|
||||
sessionID: SessionID.zod,
|
||||
agent: z.string().optional(),
|
||||
model: z.string().optional(),
|
||||
arguments: z.string(),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import z from "zod"
|
||||
import { Identifier } from "../id/id"
|
||||
import { SessionID, MessageID } from "./schema"
|
||||
import { Snapshot } from "../snapshot"
|
||||
import { MessageV2 } from "./message-v2"
|
||||
import { Session } from "."
|
||||
@@ -15,8 +16,8 @@ export namespace SessionRevert {
|
||||
const log = Log.create({ service: "session.revert" })
|
||||
|
||||
export const RevertInput = z.object({
|
||||
sessionID: Identifier.schema("session"),
|
||||
messageID: Identifier.schema("message"),
|
||||
sessionID: SessionID.zod,
|
||||
messageID: MessageID.zod,
|
||||
partID: Identifier.schema("part").optional(),
|
||||
})
|
||||
export type RevertInput = z.infer<typeof RevertInput>
|
||||
@@ -79,7 +80,7 @@ export namespace SessionRevert {
|
||||
return session
|
||||
}
|
||||
|
||||
export async function unrevert(input: { sessionID: string }) {
|
||||
export async function unrevert(input: { sessionID: SessionID }) {
|
||||
log.info("unreverting", input)
|
||||
SessionPrompt.assertNotBusy(input.sessionID)
|
||||
const session = await Session.get(input.sessionID)
|
||||
|
||||
29
packages/opencode/src/session/schema.ts
Normal file
29
packages/opencode/src/session/schema.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Schema } from "effect"
|
||||
import z from "zod"
|
||||
|
||||
import { withStatics } from "@/util/schema"
|
||||
import { Identifier } from "@/id/id"
|
||||
|
||||
const sessionIdSchema = Schema.String.pipe(Schema.brand("SessionId"))
|
||||
|
||||
export type SessionID = typeof sessionIdSchema.Type
|
||||
|
||||
export const SessionID = sessionIdSchema.pipe(
|
||||
withStatics((schema: typeof sessionIdSchema) => ({
|
||||
make: (id: string) => schema.makeUnsafe(id),
|
||||
descending: (id?: string) => schema.makeUnsafe(Identifier.descending("session", id)),
|
||||
zod: z.string().startsWith("ses").pipe(z.custom<SessionID>()),
|
||||
})),
|
||||
)
|
||||
|
||||
const messageIdSchema = Schema.String.pipe(Schema.brand("MessageId"))
|
||||
|
||||
export type MessageID = typeof messageIdSchema.Type
|
||||
|
||||
export const MessageID = messageIdSchema.pipe(
|
||||
withStatics((schema: typeof messageIdSchema) => ({
|
||||
make: (id: string) => schema.makeUnsafe(id),
|
||||
ascending: (id?: string) => schema.makeUnsafe(Identifier.ascending("message", id)),
|
||||
zod: z.string().startsWith("msg").pipe(z.custom<MessageID>()),
|
||||
})),
|
||||
)
|
||||
@@ -3,6 +3,8 @@ import { ProjectTable } from "../project/project.sql"
|
||||
import type { MessageV2 } from "./message-v2"
|
||||
import type { Snapshot } from "../snapshot"
|
||||
import type { PermissionNext } from "../permission/next"
|
||||
import type { ProjectID } from "../project/schema"
|
||||
import type { SessionID, MessageID } from "./schema"
|
||||
import { Timestamps } from "../storage/schema.sql"
|
||||
|
||||
type PartData = Omit<MessageV2.Part, "id" | "sessionID" | "messageID">
|
||||
@@ -11,12 +13,13 @@ type InfoData = Omit<MessageV2.Info, "id" | "sessionID">
|
||||
export const SessionTable = sqliteTable(
|
||||
"session",
|
||||
{
|
||||
id: text().primaryKey(),
|
||||
id: text().$type<SessionID>().primaryKey(),
|
||||
project_id: text()
|
||||
.$type<ProjectID>()
|
||||
.notNull()
|
||||
.references(() => ProjectTable.id, { onDelete: "cascade" }),
|
||||
workspace_id: text(),
|
||||
parent_id: text(),
|
||||
parent_id: text().$type<SessionID>(),
|
||||
slug: text().notNull(),
|
||||
directory: text().notNull(),
|
||||
title: text().notNull(),
|
||||
@@ -26,7 +29,7 @@ export const SessionTable = sqliteTable(
|
||||
summary_deletions: integer(),
|
||||
summary_files: integer(),
|
||||
summary_diffs: text({ mode: "json" }).$type<Snapshot.FileDiff[]>(),
|
||||
revert: text({ mode: "json" }).$type<{ messageID: string; partID?: string; snapshot?: string; diff?: string }>(),
|
||||
revert: text({ mode: "json" }).$type<{ messageID: MessageID; partID?: string; snapshot?: string; diff?: string }>(),
|
||||
permission: text({ mode: "json" }).$type<PermissionNext.Ruleset>(),
|
||||
...Timestamps,
|
||||
time_compacting: integer(),
|
||||
@@ -42,8 +45,9 @@ export const SessionTable = sqliteTable(
|
||||
export const MessageTable = sqliteTable(
|
||||
"message",
|
||||
{
|
||||
id: text().primaryKey(),
|
||||
id: text().$type<MessageID>().primaryKey(),
|
||||
session_id: text()
|
||||
.$type<SessionID>()
|
||||
.notNull()
|
||||
.references(() => SessionTable.id, { onDelete: "cascade" }),
|
||||
...Timestamps,
|
||||
@@ -57,9 +61,10 @@ export const PartTable = sqliteTable(
|
||||
{
|
||||
id: text().primaryKey(),
|
||||
message_id: text()
|
||||
.$type<MessageID>()
|
||||
.notNull()
|
||||
.references(() => MessageTable.id, { onDelete: "cascade" }),
|
||||
session_id: text().notNull(),
|
||||
session_id: text().$type<SessionID>().notNull(),
|
||||
...Timestamps,
|
||||
data: text({ mode: "json" }).notNull().$type<PartData>(),
|
||||
},
|
||||
@@ -70,6 +75,7 @@ export const TodoTable = sqliteTable(
|
||||
"todo",
|
||||
{
|
||||
session_id: text()
|
||||
.$type<SessionID>()
|
||||
.notNull()
|
||||
.references(() => SessionTable.id, { onDelete: "cascade" }),
|
||||
content: text().notNull(),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import { Bus } from "@/bus"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { SessionID } from "./schema"
|
||||
import z from "zod"
|
||||
|
||||
export namespace SessionStatus {
|
||||
@@ -28,7 +29,7 @@ export namespace SessionStatus {
|
||||
Status: BusEvent.define(
|
||||
"session.status",
|
||||
z.object({
|
||||
sessionID: z.string(),
|
||||
sessionID: SessionID.zod,
|
||||
status: Info,
|
||||
}),
|
||||
),
|
||||
@@ -36,7 +37,7 @@ export namespace SessionStatus {
|
||||
Idle: BusEvent.define(
|
||||
"session.idle",
|
||||
z.object({
|
||||
sessionID: z.string(),
|
||||
sessionID: SessionID.zod,
|
||||
}),
|
||||
),
|
||||
}
|
||||
@@ -46,7 +47,7 @@ export namespace SessionStatus {
|
||||
return data
|
||||
})
|
||||
|
||||
export function get(sessionID: string) {
|
||||
export function get(sessionID: SessionID) {
|
||||
return (
|
||||
state()[sessionID] ?? {
|
||||
type: "idle",
|
||||
@@ -58,7 +59,7 @@ export namespace SessionStatus {
|
||||
return state()
|
||||
}
|
||||
|
||||
export function set(sessionID: string, status: Info) {
|
||||
export function set(sessionID: SessionID, status: Info) {
|
||||
Bus.publish(Event.Status, {
|
||||
sessionID,
|
||||
status,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Session } from "."
|
||||
|
||||
import { MessageV2 } from "./message-v2"
|
||||
import { Identifier } from "@/id/id"
|
||||
import { SessionID, MessageID } from "./schema"
|
||||
import { Snapshot } from "@/snapshot"
|
||||
|
||||
import { Storage } from "@/storage/storage"
|
||||
@@ -68,8 +69,8 @@ export namespace SessionSummary {
|
||||
|
||||
export const summarize = fn(
|
||||
z.object({
|
||||
sessionID: z.string(),
|
||||
messageID: z.string(),
|
||||
sessionID: SessionID.zod,
|
||||
messageID: MessageID.zod,
|
||||
}),
|
||||
async (input) => {
|
||||
const all = await Session.messages({ sessionID: input.sessionID })
|
||||
@@ -80,7 +81,7 @@ export namespace SessionSummary {
|
||||
},
|
||||
)
|
||||
|
||||
async function summarizeSession(input: { sessionID: string; messages: MessageV2.WithParts[] }) {
|
||||
async function summarizeSession(input: { sessionID: SessionID; messages: MessageV2.WithParts[] }) {
|
||||
const diffs = await computeDiff({ messages: input.messages })
|
||||
await Session.setSummary({
|
||||
sessionID: input.sessionID,
|
||||
@@ -113,8 +114,8 @@ export namespace SessionSummary {
|
||||
|
||||
export const diff = fn(
|
||||
z.object({
|
||||
sessionID: Identifier.schema("session"),
|
||||
messageID: Identifier.schema("message").optional(),
|
||||
sessionID: SessionID.zod,
|
||||
messageID: MessageID.zod.optional(),
|
||||
}),
|
||||
async (input) => {
|
||||
const diffs = await Storage.read<Snapshot.FileDiff[]>(["session_diff", input.sessionID]).catch(() => [])
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import { Bus } from "@/bus"
|
||||
import { SessionID } from "./schema"
|
||||
import z from "zod"
|
||||
import { Database, eq, asc } from "../storage/db"
|
||||
import { TodoTable } from "./session.sql"
|
||||
@@ -18,13 +19,13 @@ export namespace Todo {
|
||||
Updated: BusEvent.define(
|
||||
"todo.updated",
|
||||
z.object({
|
||||
sessionID: z.string(),
|
||||
sessionID: SessionID.zod,
|
||||
todos: z.array(Info),
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
export function update(input: { sessionID: string; todos: Info[] }) {
|
||||
export function update(input: { sessionID: SessionID; todos: Info[] }) {
|
||||
Database.transaction((db) => {
|
||||
db.delete(TodoTable).where(eq(TodoTable.session_id, input.sessionID)).run()
|
||||
if (input.todos.length === 0) return
|
||||
@@ -43,7 +44,7 @@ export namespace Todo {
|
||||
Bus.publish(Event.Updated, input)
|
||||
}
|
||||
|
||||
export function get(sessionID: string) {
|
||||
export function get(sessionID: SessionID) {
|
||||
const rows = Database.use((db) =>
|
||||
db.select().from(TodoTable).where(eq(TodoTable.session_id, sessionID)).orderBy(asc(TodoTable.position)).all(),
|
||||
)
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Account } from "@/account"
|
||||
import { Config } from "@/config/config"
|
||||
import { Provider } from "@/provider/provider"
|
||||
import { Session } from "@/session"
|
||||
import type { SessionID } from "@/session/schema"
|
||||
import { MessageV2 } from "@/session/message-v2"
|
||||
import { Database, eq } from "@/storage/db"
|
||||
import { SessionShareTable } from "./share.sql"
|
||||
@@ -109,7 +110,7 @@ export namespace ShareNext {
|
||||
})
|
||||
}
|
||||
|
||||
export async function create(sessionID: string) {
|
||||
export async function create(sessionID: SessionID) {
|
||||
if (disabled) return { id: "", url: "", secret: "" }
|
||||
log.info("creating share", { sessionID })
|
||||
const req = await request()
|
||||
@@ -140,7 +141,7 @@ export namespace ShareNext {
|
||||
return result
|
||||
}
|
||||
|
||||
function get(sessionID: string) {
|
||||
function get(sessionID: SessionID) {
|
||||
const row = Database.use((db) =>
|
||||
db.select().from(SessionShareTable).where(eq(SessionShareTable.session_id, sessionID)).get(),
|
||||
)
|
||||
@@ -186,7 +187,7 @@ export namespace ShareNext {
|
||||
}
|
||||
|
||||
const queue = new Map<string, { timeout: NodeJS.Timeout; data: Map<string, Data> }>()
|
||||
async function sync(sessionID: string, data: Data[]) {
|
||||
async function sync(sessionID: SessionID, data: Data[]) {
|
||||
if (disabled) return
|
||||
const existing = queue.get(sessionID)
|
||||
if (existing) {
|
||||
@@ -225,7 +226,7 @@ export namespace ShareNext {
|
||||
queue.set(sessionID, { timeout, data: dataMap })
|
||||
}
|
||||
|
||||
export async function remove(sessionID: string) {
|
||||
export async function remove(sessionID: SessionID) {
|
||||
if (disabled) return
|
||||
log.info("removing share", { sessionID })
|
||||
const share = get(sessionID)
|
||||
@@ -248,7 +249,7 @@ export namespace ShareNext {
|
||||
Database.use((db) => db.delete(SessionShareTable).where(eq(SessionShareTable.session_id, sessionID)).run())
|
||||
}
|
||||
|
||||
async function fullSync(sessionID: string) {
|
||||
async function fullSync(sessionID: SessionID) {
|
||||
log.info("full sync", { sessionID })
|
||||
const session = await Session.get(sessionID)
|
||||
const diffs = await Session.diff(sessionID)
|
||||
|
||||
@@ -15,10 +15,7 @@ export namespace Shell {
|
||||
|
||||
if (process.platform === "win32") {
|
||||
await new Promise<void>((resolve) => {
|
||||
const killer = spawn("taskkill", ["/pid", String(pid), "/f", "/t"], {
|
||||
stdio: "ignore",
|
||||
windowsHide: true,
|
||||
})
|
||||
const killer = spawn("taskkill", ["/pid", String(pid), "/f", "/t"], { stdio: "ignore" })
|
||||
killer.once("exit", () => resolve())
|
||||
killer.once("error", () => resolve())
|
||||
})
|
||||
|
||||
@@ -173,7 +173,6 @@ export const BashTool = Tool.define("bash", async () => {
|
||||
},
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
detached: process.platform !== "win32",
|
||||
windowsHide: process.platform === "win32",
|
||||
})
|
||||
|
||||
let output = ""
|
||||
|
||||
@@ -7,9 +7,10 @@ import { MessageV2 } from "../session/message-v2"
|
||||
import { Identifier } from "../id/id"
|
||||
import { Provider } from "../provider/provider"
|
||||
import { Instance } from "../project/instance"
|
||||
import { type SessionID, MessageID } from "../session/schema"
|
||||
import EXIT_DESCRIPTION from "./plan-exit.txt"
|
||||
|
||||
async function getLastModel(sessionID: string) {
|
||||
async function getLastModel(sessionID: SessionID) {
|
||||
for await (const item of MessageV2.stream(sessionID)) {
|
||||
if (item.info.role === "user" && item.info.model) return item.info.model
|
||||
}
|
||||
@@ -44,7 +45,7 @@ export const PlanExitTool = Tool.define("plan_exit", {
|
||||
const model = await getLastModel(ctx.sessionID)
|
||||
|
||||
const userMsg: MessageV2.User = {
|
||||
id: Identifier.ascending("message"),
|
||||
id: MessageID.ascending(),
|
||||
sessionID: ctx.sessionID,
|
||||
role: "user",
|
||||
time: {
|
||||
@@ -102,7 +103,7 @@ export const PlanEnterTool = Tool.define("plan_enter", {
|
||||
const model = await getLastModel(ctx.sessionID)
|
||||
|
||||
const userMsg: MessageV2.User = {
|
||||
id: Identifier.ascending("message"),
|
||||
id: MessageID.ascending(),
|
||||
sessionID: ctx.sessionID,
|
||||
role: "user",
|
||||
time: {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Tool } from "./tool"
|
||||
import DESCRIPTION from "./task.txt"
|
||||
import z from "zod"
|
||||
import { Session } from "../session"
|
||||
import { SessionID, MessageID } from "../session/schema"
|
||||
import { MessageV2 } from "../session/message-v2"
|
||||
import { Identifier } from "../id/id"
|
||||
import { Agent } from "../agent/agent"
|
||||
@@ -65,7 +66,7 @@ export const TaskTool = Tool.define("task", async (ctx) => {
|
||||
|
||||
const session = await iife(async () => {
|
||||
if (params.task_id) {
|
||||
const found = await Session.get(params.task_id).catch(() => {})
|
||||
const found = await Session.get(SessionID.make(params.task_id)).catch(() => {})
|
||||
if (found) return found
|
||||
}
|
||||
|
||||
@@ -116,7 +117,7 @@ export const TaskTool = Tool.define("task", async (ctx) => {
|
||||
},
|
||||
})
|
||||
|
||||
const messageID = Identifier.ascending("message")
|
||||
const messageID = MessageID.ascending()
|
||||
|
||||
function cancel() {
|
||||
SessionPrompt.cancel(session.id)
|
||||
|
||||
@@ -2,6 +2,7 @@ import z from "zod"
|
||||
import type { MessageV2 } from "../session/message-v2"
|
||||
import type { Agent } from "../agent/agent"
|
||||
import type { PermissionNext } from "../permission/next"
|
||||
import type { SessionID, MessageID } from "../session/schema"
|
||||
import { Truncate } from "./truncation"
|
||||
|
||||
export namespace Tool {
|
||||
@@ -14,8 +15,8 @@ export namespace Tool {
|
||||
}
|
||||
|
||||
export type Context<M extends Metadata = Metadata> = {
|
||||
sessionID: string
|
||||
messageID: string
|
||||
sessionID: SessionID
|
||||
messageID: MessageID
|
||||
agent: string
|
||||
abort: AbortSignal
|
||||
callID?: string
|
||||
|
||||
@@ -60,7 +60,6 @@ export namespace Process {
|
||||
cwd: opts.cwd,
|
||||
env: opts.env === null ? {} : opts.env ? { ...process.env, ...opts.env } : undefined,
|
||||
stdio: [opts.stdin ?? "ignore", opts.stdout ?? "ignore", opts.stderr ?? "ignore"],
|
||||
windowsHide: process.platform === "win32",
|
||||
})
|
||||
|
||||
let closed = false
|
||||
|
||||
@@ -8,6 +8,7 @@ import { InstanceBootstrap } from "../project/bootstrap"
|
||||
import { Project } from "../project/project"
|
||||
import { Database, eq } from "../storage/db"
|
||||
import { ProjectTable } from "../project/project.sql"
|
||||
import type { ProjectID } from "../project/schema"
|
||||
import { fn } from "../util/fn"
|
||||
import { Log } from "../util/log"
|
||||
import { Process } from "../util/process"
|
||||
@@ -310,7 +311,7 @@ export namespace Worktree {
|
||||
return false
|
||||
}
|
||||
|
||||
async function runStartScripts(directory: string, input: { projectID: string; extra?: string }) {
|
||||
async function runStartScripts(directory: string, input: { projectID: ProjectID; extra?: string }) {
|
||||
const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, input.projectID)).get())
|
||||
const project = row ? Project.fromRow(row) : undefined
|
||||
const startup = project?.commands?.start?.trim() ?? ""
|
||||
@@ -322,7 +323,7 @@ export namespace Worktree {
|
||||
return true
|
||||
}
|
||||
|
||||
function queueStartScripts(directory: string, input: { projectID: string; extra?: string }) {
|
||||
function queueStartScripts(directory: string, input: { projectID: ProjectID; extra?: string }) {
|
||||
setTimeout(() => {
|
||||
const start = async () => {
|
||||
await runStartScripts(directory, input)
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { test, expect, describe } from "bun:test"
|
||||
import { extractResponseText, formatPromptTooLargeError } from "../../src/cli/cmd/github"
|
||||
import type { MessageV2 } from "../../src/session/message-v2"
|
||||
import { SessionID, MessageID } from "../../src/session/schema"
|
||||
|
||||
// Helper to create minimal valid parts
|
||||
function createTextPart(text: string): MessageV2.Part {
|
||||
return {
|
||||
id: "1",
|
||||
sessionID: "s",
|
||||
messageID: "m",
|
||||
sessionID: SessionID.make("s"),
|
||||
messageID: MessageID.make("m"),
|
||||
type: "text" as const,
|
||||
text,
|
||||
}
|
||||
@@ -16,8 +17,8 @@ function createTextPart(text: string): MessageV2.Part {
|
||||
function createReasoningPart(text: string): MessageV2.Part {
|
||||
return {
|
||||
id: "1",
|
||||
sessionID: "s",
|
||||
messageID: "m",
|
||||
sessionID: SessionID.make("s"),
|
||||
messageID: MessageID.make("m"),
|
||||
type: "reasoning" as const,
|
||||
text,
|
||||
time: { start: 0 },
|
||||
@@ -28,8 +29,8 @@ function createToolPart(tool: string, title: string, status: "completed" | "runn
|
||||
if (status === "completed") {
|
||||
return {
|
||||
id: "1",
|
||||
sessionID: "s",
|
||||
messageID: "m",
|
||||
sessionID: SessionID.make("s"),
|
||||
messageID: MessageID.make("m"),
|
||||
type: "tool" as const,
|
||||
callID: "c1",
|
||||
tool,
|
||||
@@ -45,8 +46,8 @@ function createToolPart(tool: string, title: string, status: "completed" | "runn
|
||||
}
|
||||
return {
|
||||
id: "1",
|
||||
sessionID: "s",
|
||||
messageID: "m",
|
||||
sessionID: SessionID.make("s"),
|
||||
messageID: MessageID.make("m"),
|
||||
type: "tool" as const,
|
||||
callID: "c1",
|
||||
tool,
|
||||
@@ -61,8 +62,8 @@ function createToolPart(tool: string, title: string, status: "completed" | "runn
|
||||
function createStepStartPart(): MessageV2.Part {
|
||||
return {
|
||||
id: "1",
|
||||
sessionID: "s",
|
||||
messageID: "m",
|
||||
sessionID: SessionID.make("s"),
|
||||
messageID: MessageID.make("m"),
|
||||
type: "step-start" as const,
|
||||
}
|
||||
}
|
||||
@@ -70,8 +71,8 @@ function createStepStartPart(): MessageV2.Part {
|
||||
function createStepFinishPart(): MessageV2.Part {
|
||||
return {
|
||||
id: "1",
|
||||
sessionID: "s",
|
||||
messageID: "m",
|
||||
sessionID: SessionID.make("s"),
|
||||
messageID: MessageID.make("m"),
|
||||
type: "step-finish" as const,
|
||||
reason: "done",
|
||||
cost: 0,
|
||||
|
||||
@@ -8,6 +8,7 @@ import path from "path"
|
||||
import fs from "fs/promises"
|
||||
import { pathToFileURL } from "url"
|
||||
import { Global } from "../../src/global"
|
||||
import { ProjectID } from "../../src/project/schema"
|
||||
import { Filesystem } from "../../src/util/filesystem"
|
||||
|
||||
// Get managed config directory from environment (set in preload.ts)
|
||||
@@ -44,7 +45,7 @@ async function check(map: (dir: string) => string) {
|
||||
const cfg = await Config.get()
|
||||
expect(cfg.snapshot).toBe(true)
|
||||
expect(Instance.directory).toBe(Filesystem.resolve(tmp.path))
|
||||
expect(Instance.project.id).not.toBe("global")
|
||||
expect(Instance.project.id).not.toBe(ProjectID.global)
|
||||
},
|
||||
})
|
||||
} finally {
|
||||
|
||||
@@ -2,12 +2,13 @@ import { describe, test, expect } from "bun:test"
|
||||
import path from "path"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { WebFetchTool } from "../../src/tool/webfetch"
|
||||
import { SessionID, MessageID } from "../../src/session/schema"
|
||||
|
||||
const projectRoot = path.join(__dirname, "../..")
|
||||
|
||||
const ctx = {
|
||||
sessionID: "test",
|
||||
messageID: "",
|
||||
sessionID: SessionID.make("ses_test"),
|
||||
messageID: MessageID.make(""),
|
||||
callID: "",
|
||||
agent: "build",
|
||||
abort: new AbortController().signal,
|
||||
|
||||
@@ -3,6 +3,7 @@ import os from "os"
|
||||
import { PermissionNext } from "../../src/permission/next"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import { SessionID } from "../../src/session/schema"
|
||||
|
||||
// fromConfig tests
|
||||
|
||||
@@ -462,7 +463,7 @@ test("ask - resolves immediately when action is allow", async () => {
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const result = await PermissionNext.ask({
|
||||
sessionID: "session_test",
|
||||
sessionID: SessionID.make("session_test"),
|
||||
permission: "bash",
|
||||
patterns: ["ls"],
|
||||
metadata: {},
|
||||
@@ -481,7 +482,7 @@ test("ask - throws RejectedError when action is deny", async () => {
|
||||
fn: async () => {
|
||||
await expect(
|
||||
PermissionNext.ask({
|
||||
sessionID: "session_test",
|
||||
sessionID: SessionID.make("session_test"),
|
||||
permission: "bash",
|
||||
patterns: ["rm -rf /"],
|
||||
metadata: {},
|
||||
@@ -499,7 +500,7 @@ test("ask - returns pending promise when action is ask", async () => {
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const promise = PermissionNext.ask({
|
||||
sessionID: "session_test",
|
||||
sessionID: SessionID.make("session_test"),
|
||||
permission: "bash",
|
||||
patterns: ["ls"],
|
||||
metadata: {},
|
||||
@@ -522,7 +523,7 @@ test("reply - once resolves the pending ask", async () => {
|
||||
fn: async () => {
|
||||
const askPromise = PermissionNext.ask({
|
||||
id: "permission_test1",
|
||||
sessionID: "session_test",
|
||||
sessionID: SessionID.make("session_test"),
|
||||
permission: "bash",
|
||||
patterns: ["ls"],
|
||||
metadata: {},
|
||||
@@ -547,7 +548,7 @@ test("reply - reject throws RejectedError", async () => {
|
||||
fn: async () => {
|
||||
const askPromise = PermissionNext.ask({
|
||||
id: "permission_test2",
|
||||
sessionID: "session_test",
|
||||
sessionID: SessionID.make("session_test"),
|
||||
permission: "bash",
|
||||
patterns: ["ls"],
|
||||
metadata: {},
|
||||
@@ -572,7 +573,7 @@ test("reply - always persists approval and resolves", async () => {
|
||||
fn: async () => {
|
||||
const askPromise = PermissionNext.ask({
|
||||
id: "permission_test3",
|
||||
sessionID: "session_test",
|
||||
sessionID: SessionID.make("session_test"),
|
||||
permission: "bash",
|
||||
patterns: ["ls"],
|
||||
metadata: {},
|
||||
@@ -594,7 +595,7 @@ test("reply - always persists approval and resolves", async () => {
|
||||
fn: async () => {
|
||||
// Stored approval should allow without asking
|
||||
const result = await PermissionNext.ask({
|
||||
sessionID: "session_test2",
|
||||
sessionID: SessionID.make("session_test2"),
|
||||
permission: "bash",
|
||||
patterns: ["ls"],
|
||||
metadata: {},
|
||||
@@ -613,7 +614,7 @@ test("reply - reject cancels all pending for same session", async () => {
|
||||
fn: async () => {
|
||||
const askPromise1 = PermissionNext.ask({
|
||||
id: "permission_test4a",
|
||||
sessionID: "session_same",
|
||||
sessionID: SessionID.make("session_same"),
|
||||
permission: "bash",
|
||||
patterns: ["ls"],
|
||||
metadata: {},
|
||||
@@ -623,7 +624,7 @@ test("reply - reject cancels all pending for same session", async () => {
|
||||
|
||||
const askPromise2 = PermissionNext.ask({
|
||||
id: "permission_test4b",
|
||||
sessionID: "session_same",
|
||||
sessionID: SessionID.make("session_same"),
|
||||
permission: "edit",
|
||||
patterns: ["foo.ts"],
|
||||
metadata: {},
|
||||
@@ -655,7 +656,7 @@ test("ask - checks all patterns and stops on first deny", async () => {
|
||||
fn: async () => {
|
||||
await expect(
|
||||
PermissionNext.ask({
|
||||
sessionID: "session_test",
|
||||
sessionID: SessionID.make("session_test"),
|
||||
permission: "bash",
|
||||
patterns: ["echo hello", "rm -rf /"],
|
||||
metadata: {},
|
||||
@@ -676,7 +677,7 @@ test("ask - allows all patterns when all match allow rules", async () => {
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const result = await PermissionNext.ask({
|
||||
sessionID: "session_test",
|
||||
sessionID: SessionID.make("session_test"),
|
||||
permission: "bash",
|
||||
patterns: ["echo hello", "ls -la", "pwd"],
|
||||
metadata: {},
|
||||
|
||||
@@ -6,6 +6,7 @@ import path from "path"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import { Filesystem } from "../../src/util/filesystem"
|
||||
import { GlobalBus } from "../../src/bus/global"
|
||||
import { ProjectID } from "../../src/project/schema"
|
||||
|
||||
Log.init({ print: false })
|
||||
|
||||
@@ -74,7 +75,7 @@ describe("Project.fromDirectory", () => {
|
||||
const { project } = await p.fromDirectory(tmp.path)
|
||||
|
||||
expect(project).toBeDefined()
|
||||
expect(project.id).toBe("global")
|
||||
expect(project.id).toBe(ProjectID.global)
|
||||
expect(project.vcs).toBe("git")
|
||||
expect(project.worktree).toBe(tmp.path)
|
||||
|
||||
@@ -90,7 +91,7 @@ describe("Project.fromDirectory", () => {
|
||||
const { project } = await p.fromDirectory(tmp.path)
|
||||
|
||||
expect(project).toBeDefined()
|
||||
expect(project.id).not.toBe("global")
|
||||
expect(project.id).not.toBe(ProjectID.global)
|
||||
expect(project.vcs).toBe("git")
|
||||
expect(project.worktree).toBe(tmp.path)
|
||||
|
||||
@@ -107,7 +108,7 @@ describe("Project.fromDirectory", () => {
|
||||
await withMode("rev-list-fail", async () => {
|
||||
const { project } = await p.fromDirectory(tmp.path)
|
||||
expect(project.vcs).toBe("git")
|
||||
expect(project.id).toBe("global")
|
||||
expect(project.id).toBe(ProjectID.global)
|
||||
expect(project.worktree).toBe(tmp.path)
|
||||
})
|
||||
})
|
||||
@@ -301,7 +302,7 @@ describe("Project.update", () => {
|
||||
|
||||
await expect(
|
||||
Project.update({
|
||||
projectID: "nonexistent-project-id",
|
||||
projectID: ProjectID.make("nonexistent-project-id"),
|
||||
name: "Should Fail",
|
||||
}),
|
||||
).rejects.toThrow("Project not found: nonexistent-project-id")
|
||||
|
||||
@@ -2,6 +2,7 @@ import { test, expect } from "bun:test"
|
||||
import { Question } from "../../src/question"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import { SessionID } from "../../src/session/schema"
|
||||
|
||||
test("ask - returns pending promise", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
@@ -9,7 +10,7 @@ test("ask - returns pending promise", async () => {
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const promise = Question.ask({
|
||||
sessionID: "ses_test",
|
||||
sessionID: SessionID.make("ses_test"),
|
||||
questions: [
|
||||
{
|
||||
question: "What would you like to do?",
|
||||
@@ -43,7 +44,7 @@ test("ask - adds to pending list", async () => {
|
||||
]
|
||||
|
||||
Question.ask({
|
||||
sessionID: "ses_test",
|
||||
sessionID: SessionID.make("ses_test"),
|
||||
questions,
|
||||
})
|
||||
|
||||
@@ -73,7 +74,7 @@ test("reply - resolves the pending ask with answers", async () => {
|
||||
]
|
||||
|
||||
const askPromise = Question.ask({
|
||||
sessionID: "ses_test",
|
||||
sessionID: SessionID.make("ses_test"),
|
||||
questions,
|
||||
})
|
||||
|
||||
@@ -97,7 +98,7 @@ test("reply - removes from pending list", async () => {
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
Question.ask({
|
||||
sessionID: "ses_test",
|
||||
sessionID: SessionID.make("ses_test"),
|
||||
questions: [
|
||||
{
|
||||
question: "What would you like to do?",
|
||||
@@ -146,7 +147,7 @@ test("reject - throws RejectedError", async () => {
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const askPromise = Question.ask({
|
||||
sessionID: "ses_test",
|
||||
sessionID: SessionID.make("ses_test"),
|
||||
questions: [
|
||||
{
|
||||
question: "What would you like to do?",
|
||||
@@ -173,7 +174,7 @@ test("reject - removes from pending list", async () => {
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const askPromise = Question.ask({
|
||||
sessionID: "ses_test",
|
||||
sessionID: SessionID.make("ses_test"),
|
||||
questions: [
|
||||
{
|
||||
question: "What would you like to do?",
|
||||
@@ -236,7 +237,7 @@ test("ask - handles multiple questions", async () => {
|
||||
]
|
||||
|
||||
const askPromise = Question.ask({
|
||||
sessionID: "ses_test",
|
||||
sessionID: SessionID.make("ses_test"),
|
||||
questions,
|
||||
})
|
||||
|
||||
@@ -261,7 +262,7 @@ test("list - returns all pending requests", async () => {
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
Question.ask({
|
||||
sessionID: "ses_test1",
|
||||
sessionID: SessionID.make("ses_test1"),
|
||||
questions: [
|
||||
{
|
||||
question: "Question 1?",
|
||||
@@ -272,7 +273,7 @@ test("list - returns all pending requests", async () => {
|
||||
})
|
||||
|
||||
Question.ask({
|
||||
sessionID: "ses_test2",
|
||||
sessionID: SessionID.make("ses_test2"),
|
||||
questions: [
|
||||
{
|
||||
question: "Question 2?",
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Filesystem } from "../../src/util/filesystem"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import type { Agent } from "../../src/agent/agent"
|
||||
import type { MessageV2 } from "../../src/session/message-v2"
|
||||
import { SessionID, MessageID } from "../../src/session/schema"
|
||||
|
||||
describe("session.llm.hasToolCalls", () => {
|
||||
test("returns false for empty messages array", () => {
|
||||
@@ -265,7 +266,7 @@ describe("session.llm.stream", () => {
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const resolved = await Provider.getModel(providerID, model.id)
|
||||
const sessionID = "session-test-1"
|
||||
const sessionID = SessionID.make("session-test-1")
|
||||
const agent = {
|
||||
name: "test",
|
||||
mode: "primary",
|
||||
@@ -276,7 +277,7 @@ describe("session.llm.stream", () => {
|
||||
} satisfies Agent.Info
|
||||
|
||||
const user = {
|
||||
id: "user-1",
|
||||
id: MessageID.make("user-1"),
|
||||
sessionID,
|
||||
role: "user",
|
||||
time: { created: Date.now() },
|
||||
@@ -395,7 +396,7 @@ describe("session.llm.stream", () => {
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const resolved = await Provider.getModel("openai", model.id)
|
||||
const sessionID = "session-test-2"
|
||||
const sessionID = SessionID.make("session-test-2")
|
||||
const agent = {
|
||||
name: "test",
|
||||
mode: "primary",
|
||||
@@ -405,7 +406,7 @@ describe("session.llm.stream", () => {
|
||||
} satisfies Agent.Info
|
||||
|
||||
const user = {
|
||||
id: "user-2",
|
||||
id: MessageID.make("user-2"),
|
||||
sessionID,
|
||||
role: "user",
|
||||
time: { created: Date.now() },
|
||||
@@ -517,7 +518,7 @@ describe("session.llm.stream", () => {
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const resolved = await Provider.getModel(providerID, model.id)
|
||||
const sessionID = "session-test-3"
|
||||
const sessionID = SessionID.make("session-test-3")
|
||||
const agent = {
|
||||
name: "test",
|
||||
mode: "primary",
|
||||
@@ -528,7 +529,7 @@ describe("session.llm.stream", () => {
|
||||
} satisfies Agent.Info
|
||||
|
||||
const user = {
|
||||
id: "user-3",
|
||||
id: MessageID.make("user-3"),
|
||||
sessionID,
|
||||
role: "user",
|
||||
time: { created: Date.now() },
|
||||
@@ -618,7 +619,7 @@ describe("session.llm.stream", () => {
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const resolved = await Provider.getModel(providerID, model.id)
|
||||
const sessionID = "session-test-4"
|
||||
const sessionID = SessionID.make("session-test-4")
|
||||
const agent = {
|
||||
name: "test",
|
||||
mode: "primary",
|
||||
@@ -629,7 +630,7 @@ describe("session.llm.stream", () => {
|
||||
} satisfies Agent.Info
|
||||
|
||||
const user = {
|
||||
id: "user-4",
|
||||
id: MessageID.make("user-4"),
|
||||
sessionID,
|
||||
role: "user",
|
||||
time: { created: Date.now() },
|
||||
|
||||
@@ -2,8 +2,9 @@ import { describe, expect, test } from "bun:test"
|
||||
import { APICallError } from "ai"
|
||||
import { MessageV2 } from "../../src/session/message-v2"
|
||||
import type { Provider } from "../../src/provider/provider"
|
||||
import { SessionID, MessageID } from "../../src/session/schema"
|
||||
|
||||
const sessionID = "session"
|
||||
const sessionID = SessionID.make("session")
|
||||
const model: Provider.Model = {
|
||||
id: "test-model",
|
||||
providerID: "test",
|
||||
@@ -99,7 +100,7 @@ function basePart(messageID: string, id: string) {
|
||||
return {
|
||||
id,
|
||||
sessionID,
|
||||
messageID,
|
||||
messageID: MessageID.make(messageID),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { MessageV2 } from "../../src/session/message-v2"
|
||||
import { Log } from "../../src/util/log"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { Identifier } from "../../src/id/id"
|
||||
import { MessageID } from "../../src/session/schema"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
|
||||
const projectRoot = path.join(__dirname, "../..")
|
||||
@@ -24,7 +25,7 @@ describe("revert + compact workflow", () => {
|
||||
|
||||
// Create a user message
|
||||
const userMsg1 = await Session.updateMessage({
|
||||
id: Identifier.ascending("message"),
|
||||
id: MessageID.ascending(),
|
||||
role: "user",
|
||||
sessionID,
|
||||
agent: "default",
|
||||
@@ -48,7 +49,7 @@ describe("revert + compact workflow", () => {
|
||||
|
||||
// Create an assistant response message
|
||||
const assistantMsg1: MessageV2.Assistant = {
|
||||
id: Identifier.ascending("message"),
|
||||
id: MessageID.ascending(),
|
||||
role: "assistant",
|
||||
sessionID,
|
||||
mode: "default",
|
||||
@@ -85,7 +86,7 @@ describe("revert + compact workflow", () => {
|
||||
|
||||
// Create another user message
|
||||
const userMsg2 = await Session.updateMessage({
|
||||
id: Identifier.ascending("message"),
|
||||
id: MessageID.ascending(),
|
||||
role: "user",
|
||||
sessionID,
|
||||
agent: "default",
|
||||
@@ -108,7 +109,7 @@ describe("revert + compact workflow", () => {
|
||||
|
||||
// Create another assistant response
|
||||
const assistantMsg2: MessageV2.Assistant = {
|
||||
id: Identifier.ascending("message"),
|
||||
id: MessageID.ascending(),
|
||||
role: "assistant",
|
||||
sessionID,
|
||||
mode: "default",
|
||||
@@ -200,7 +201,7 @@ describe("revert + compact workflow", () => {
|
||||
|
||||
// Create initial messages
|
||||
const userMsg = await Session.updateMessage({
|
||||
id: Identifier.ascending("message"),
|
||||
id: MessageID.ascending(),
|
||||
role: "user",
|
||||
sessionID,
|
||||
agent: "default",
|
||||
@@ -222,7 +223,7 @@ describe("revert + compact workflow", () => {
|
||||
})
|
||||
|
||||
const assistantMsg: MessageV2.Assistant = {
|
||||
id: Identifier.ascending("message"),
|
||||
id: MessageID.ascending(),
|
||||
role: "assistant",
|
||||
sessionID,
|
||||
mode: "default",
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Log } from "../../src/util/log"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { MessageV2 } from "../../src/session/message-v2"
|
||||
import { Identifier } from "../../src/id/id"
|
||||
import { MessageID } from "../../src/session/schema"
|
||||
|
||||
const projectRoot = path.join(__dirname, "../..")
|
||||
Log.init({ print: false })
|
||||
@@ -81,7 +82,7 @@ describe("step-finish token propagation via Bus event", () => {
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
|
||||
const messageID = Identifier.ascending("message")
|
||||
const messageID = MessageID.ascending()
|
||||
await Session.updateMessage({
|
||||
id: messageID,
|
||||
sessionID: session.id,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { MessageV2 } from "../../src/session/message-v2"
|
||||
import { SessionPrompt } from "../../src/session/prompt"
|
||||
import { SessionID } from "../../src/session/schema"
|
||||
|
||||
describe("structured-output.OutputFormat", () => {
|
||||
test("parses text format", () => {
|
||||
@@ -96,7 +97,7 @@ describe("structured-output.UserMessage", () => {
|
||||
test("user message accepts outputFormat", () => {
|
||||
const result = MessageV2.User.safeParse({
|
||||
id: "test-id",
|
||||
sessionID: "test-session",
|
||||
sessionID: SessionID.descending(),
|
||||
role: "user",
|
||||
time: { created: Date.now() },
|
||||
agent: "default",
|
||||
@@ -112,7 +113,7 @@ describe("structured-output.UserMessage", () => {
|
||||
test("user message works without outputFormat (optional)", () => {
|
||||
const result = MessageV2.User.safeParse({
|
||||
id: "test-id",
|
||||
sessionID: "test-session",
|
||||
sessionID: SessionID.descending(),
|
||||
role: "user",
|
||||
time: { created: Date.now() },
|
||||
agent: "default",
|
||||
@@ -125,7 +126,7 @@ describe("structured-output.UserMessage", () => {
|
||||
describe("structured-output.AssistantMessage", () => {
|
||||
const baseAssistantMessage = {
|
||||
id: "test-id",
|
||||
sessionID: "test-session",
|
||||
sessionID: SessionID.descending(),
|
||||
role: "assistant" as const,
|
||||
parentID: "parent-id",
|
||||
modelID: "claude-3",
|
||||
|
||||
@@ -8,8 +8,10 @@ import { readFileSync, readdirSync } from "fs"
|
||||
import { JsonMigration } from "../../src/storage/json-migration"
|
||||
import { Global } from "../../src/global"
|
||||
import { ProjectTable } from "../../src/project/project.sql"
|
||||
import { ProjectID } from "../../src/project/schema"
|
||||
import { SessionTable, MessageTable, PartTable, TodoTable, PermissionTable } from "../../src/session/session.sql"
|
||||
import { SessionShareTable } from "../../src/share/share.sql"
|
||||
import { SessionID, MessageID } from "../../src/session/schema"
|
||||
|
||||
// Test fixtures
|
||||
const fixtures = {
|
||||
@@ -123,7 +125,7 @@ describe("JSON to SQLite migration", () => {
|
||||
const db = drizzle({ client: sqlite })
|
||||
const projects = db.select().from(ProjectTable).all()
|
||||
expect(projects.length).toBe(1)
|
||||
expect(projects[0].id).toBe("proj_test123abc")
|
||||
expect(projects[0].id).toBe(ProjectID.make("proj_test123abc"))
|
||||
expect(projects[0].worktree).toBe("/test/path")
|
||||
expect(projects[0].name).toBe("Test Project")
|
||||
expect(projects[0].sandboxes).toEqual(["/test/sandbox"])
|
||||
@@ -148,7 +150,7 @@ describe("JSON to SQLite migration", () => {
|
||||
const db = drizzle({ client: sqlite })
|
||||
const projects = db.select().from(ProjectTable).all()
|
||||
expect(projects.length).toBe(1)
|
||||
expect(projects[0].id).toBe("proj_filename") // Uses filename, not JSON id
|
||||
expect(projects[0].id).toBe(ProjectID.make("proj_filename")) // Uses filename, not JSON id
|
||||
})
|
||||
|
||||
test("migrates project with commands", async () => {
|
||||
@@ -169,7 +171,7 @@ describe("JSON to SQLite migration", () => {
|
||||
const db = drizzle({ client: sqlite })
|
||||
const projects = db.select().from(ProjectTable).all()
|
||||
expect(projects.length).toBe(1)
|
||||
expect(projects[0].id).toBe("proj_with_commands")
|
||||
expect(projects[0].id).toBe(ProjectID.make("proj_with_commands"))
|
||||
expect(projects[0].commands).toEqual({ start: "npm run dev" })
|
||||
})
|
||||
|
||||
@@ -190,7 +192,7 @@ describe("JSON to SQLite migration", () => {
|
||||
const db = drizzle({ client: sqlite })
|
||||
const projects = db.select().from(ProjectTable).all()
|
||||
expect(projects.length).toBe(1)
|
||||
expect(projects[0].id).toBe("proj_no_commands")
|
||||
expect(projects[0].id).toBe(ProjectID.make("proj_no_commands"))
|
||||
expect(projects[0].commands).toBeNull()
|
||||
})
|
||||
|
||||
@@ -219,8 +221,8 @@ describe("JSON to SQLite migration", () => {
|
||||
const db = drizzle({ client: sqlite })
|
||||
const sessions = db.select().from(SessionTable).all()
|
||||
expect(sessions.length).toBe(1)
|
||||
expect(sessions[0].id).toBe("ses_test456def")
|
||||
expect(sessions[0].project_id).toBe("proj_test123abc")
|
||||
expect(sessions[0].id).toBe(SessionID.make("ses_test456def"))
|
||||
expect(sessions[0].project_id).toBe(ProjectID.make("proj_test123abc"))
|
||||
expect(sessions[0].slug).toBe("test-session")
|
||||
expect(sessions[0].title).toBe("Test Session Title")
|
||||
expect(sessions[0].summary_additions).toBe(10)
|
||||
@@ -253,7 +255,7 @@ describe("JSON to SQLite migration", () => {
|
||||
const db = drizzle({ client: sqlite })
|
||||
const messages = db.select().from(MessageTable).all()
|
||||
expect(messages.length).toBe(1)
|
||||
expect(messages[0].id).toBe("msg_test789ghi")
|
||||
expect(messages[0].id).toBe(MessageID.make("msg_test789ghi"))
|
||||
|
||||
const parts = db.select().from(PartTable).all()
|
||||
expect(parts.length).toBe(1)
|
||||
@@ -293,16 +295,16 @@ describe("JSON to SQLite migration", () => {
|
||||
const db = drizzle({ client: sqlite })
|
||||
const messages = db.select().from(MessageTable).all()
|
||||
expect(messages.length).toBe(1)
|
||||
expect(messages[0].id).toBe("msg_test789ghi")
|
||||
expect(messages[0].session_id).toBe("ses_test456def")
|
||||
expect(messages[0].id).toBe(MessageID.make("msg_test789ghi"))
|
||||
expect(messages[0].session_id).toBe(SessionID.make("ses_test456def"))
|
||||
expect(messages[0].data).not.toHaveProperty("id")
|
||||
expect(messages[0].data).not.toHaveProperty("sessionID")
|
||||
|
||||
const parts = db.select().from(PartTable).all()
|
||||
expect(parts.length).toBe(1)
|
||||
expect(parts[0].id).toBe("prt_testabc123")
|
||||
expect(parts[0].message_id).toBe("msg_test789ghi")
|
||||
expect(parts[0].session_id).toBe("ses_test456def")
|
||||
expect(parts[0].message_id).toBe(MessageID.make("msg_test789ghi"))
|
||||
expect(parts[0].session_id).toBe(SessionID.make("ses_test456def"))
|
||||
expect(parts[0].data).not.toHaveProperty("id")
|
||||
expect(parts[0].data).not.toHaveProperty("messageID")
|
||||
expect(parts[0].data).not.toHaveProperty("sessionID")
|
||||
@@ -334,8 +336,8 @@ describe("JSON to SQLite migration", () => {
|
||||
const db = drizzle({ client: sqlite })
|
||||
const messages = db.select().from(MessageTable).all()
|
||||
expect(messages.length).toBe(1)
|
||||
expect(messages[0].id).toBe("msg_from_filename") // Uses filename, not JSON id
|
||||
expect(messages[0].session_id).toBe("ses_test456def")
|
||||
expect(messages[0].id).toBe(MessageID.make("msg_from_filename")) // Uses filename, not JSON id
|
||||
expect(messages[0].session_id).toBe(SessionID.make("ses_test456def"))
|
||||
})
|
||||
|
||||
test("uses paths for part id and messageID when JSON has different values", async () => {
|
||||
@@ -373,7 +375,7 @@ describe("JSON to SQLite migration", () => {
|
||||
const parts = db.select().from(PartTable).all()
|
||||
expect(parts.length).toBe(1)
|
||||
expect(parts[0].id).toBe("prt_from_filename") // Uses filename, not JSON id
|
||||
expect(parts[0].message_id).toBe("msg_realmsgid") // Uses parent dir, not JSON messageID
|
||||
expect(parts[0].message_id).toBe(MessageID.make("msg_realmsgid")) // Uses parent dir, not JSON messageID
|
||||
})
|
||||
|
||||
test("skips orphaned sessions (no parent project)", async () => {
|
||||
@@ -425,8 +427,8 @@ describe("JSON to SQLite migration", () => {
|
||||
const db = drizzle({ client: sqlite })
|
||||
const sessions = db.select().from(SessionTable).all()
|
||||
expect(sessions.length).toBe(1)
|
||||
expect(sessions[0].id).toBe("ses_migrated")
|
||||
expect(sessions[0].project_id).toBe(gitBasedProjectID) // Uses directory, not stale JSON
|
||||
expect(sessions[0].id).toBe(SessionID.make("ses_migrated"))
|
||||
expect(sessions[0].project_id).toBe(ProjectID.make(gitBasedProjectID)) // Uses directory, not stale JSON
|
||||
})
|
||||
|
||||
test("uses filename for session id when JSON has different value", async () => {
|
||||
@@ -457,8 +459,8 @@ describe("JSON to SQLite migration", () => {
|
||||
const db = drizzle({ client: sqlite })
|
||||
const sessions = db.select().from(SessionTable).all()
|
||||
expect(sessions.length).toBe(1)
|
||||
expect(sessions[0].id).toBe("ses_from_filename") // Uses filename, not JSON id
|
||||
expect(sessions[0].project_id).toBe("proj_test123abc")
|
||||
expect(sessions[0].id).toBe(SessionID.make("ses_from_filename")) // Uses filename, not JSON id
|
||||
expect(sessions[0].project_id).toBe(ProjectID.make("proj_test123abc"))
|
||||
})
|
||||
|
||||
test("is idempotent (running twice doesn't duplicate)", async () => {
|
||||
@@ -643,7 +645,7 @@ describe("JSON to SQLite migration", () => {
|
||||
const db = drizzle({ client: sqlite })
|
||||
const projects = db.select().from(ProjectTable).all()
|
||||
expect(projects.length).toBe(1)
|
||||
expect(projects[0].id).toBe("proj_test123abc")
|
||||
expect(projects[0].id).toBe(ProjectID.make("proj_test123abc"))
|
||||
})
|
||||
|
||||
test("skips invalid todo entries while preserving source positions", async () => {
|
||||
|
||||
@@ -4,10 +4,11 @@ import * as fs from "fs/promises"
|
||||
import { ApplyPatchTool } from "../../src/tool/apply_patch"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import { SessionID, MessageID } from "../../src/session/schema"
|
||||
|
||||
const baseCtx = {
|
||||
sessionID: "test",
|
||||
messageID: "",
|
||||
sessionID: SessionID.make("ses_test"),
|
||||
messageID: MessageID.make(""),
|
||||
callID: "",
|
||||
agent: "build",
|
||||
abort: AbortSignal.any([]),
|
||||
|
||||
@@ -7,10 +7,11 @@ import { Filesystem } from "../../src/util/filesystem"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import type { PermissionNext } from "../../src/permission/next"
|
||||
import { Truncate } from "../../src/tool/truncation"
|
||||
import { SessionID, MessageID } from "../../src/session/schema"
|
||||
|
||||
const ctx = {
|
||||
sessionID: "test",
|
||||
messageID: "",
|
||||
sessionID: SessionID.make("ses_test"),
|
||||
messageID: MessageID.make(""),
|
||||
callID: "",
|
||||
agent: "build",
|
||||
abort: AbortSignal.any([]),
|
||||
|
||||
@@ -5,10 +5,11 @@ import { EditTool } from "../../src/tool/edit"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import { FileTime } from "../../src/file/time"
|
||||
import { SessionID, MessageID } from "../../src/session/schema"
|
||||
|
||||
const ctx = {
|
||||
sessionID: "test-edit-session",
|
||||
messageID: "",
|
||||
sessionID: SessionID.make("ses_test-edit-session"),
|
||||
messageID: MessageID.make(""),
|
||||
callID: "",
|
||||
agent: "build",
|
||||
abort: AbortSignal.any([]),
|
||||
|
||||
@@ -4,10 +4,11 @@ import type { Tool } from "../../src/tool/tool"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { assertExternalDirectory } from "../../src/tool/external-directory"
|
||||
import type { PermissionNext } from "../../src/permission/next"
|
||||
import { SessionID, MessageID } from "../../src/session/schema"
|
||||
|
||||
const baseCtx: Omit<Tool.Context, "ask"> = {
|
||||
sessionID: "test",
|
||||
messageID: "",
|
||||
sessionID: SessionID.make("ses_test"),
|
||||
messageID: MessageID.make(""),
|
||||
callID: "",
|
||||
agent: "build",
|
||||
abort: AbortSignal.any([]),
|
||||
|
||||
@@ -3,10 +3,11 @@ import path from "path"
|
||||
import { GrepTool } from "../../src/tool/grep"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import { SessionID, MessageID } from "../../src/session/schema"
|
||||
|
||||
const ctx = {
|
||||
sessionID: "test",
|
||||
messageID: "",
|
||||
sessionID: SessionID.make("ses_test"),
|
||||
messageID: MessageID.make(""),
|
||||
callID: "",
|
||||
agent: "build",
|
||||
abort: AbortSignal.any([]),
|
||||
|
||||
@@ -2,10 +2,11 @@ import { describe, expect, test, spyOn, beforeEach, afterEach } from "bun:test"
|
||||
import { z } from "zod"
|
||||
import { QuestionTool } from "../../src/tool/question"
|
||||
import * as QuestionModule from "../../src/question"
|
||||
import { SessionID, MessageID } from "../../src/session/schema"
|
||||
|
||||
const ctx = {
|
||||
sessionID: "test-session",
|
||||
messageID: "test-message",
|
||||
sessionID: SessionID.make("ses_test-session"),
|
||||
messageID: MessageID.make("test-message"),
|
||||
callID: "test-call",
|
||||
agent: "test-agent",
|
||||
abort: AbortSignal.any([]),
|
||||
|
||||
@@ -6,12 +6,13 @@ import { Filesystem } from "../../src/util/filesystem"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import { PermissionNext } from "../../src/permission/next"
|
||||
import { Agent } from "../../src/agent/agent"
|
||||
import { SessionID, MessageID } from "../../src/session/schema"
|
||||
|
||||
const FIXTURES_DIR = path.join(import.meta.dir, "fixtures")
|
||||
|
||||
const ctx = {
|
||||
sessionID: "test",
|
||||
messageID: "",
|
||||
sessionID: SessionID.make("ses_test"),
|
||||
messageID: MessageID.make(""),
|
||||
callID: "",
|
||||
agent: "build",
|
||||
abort: AbortSignal.any([]),
|
||||
|
||||
@@ -6,10 +6,11 @@ import type { Tool } from "../../src/tool/tool"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { SkillTool } from "../../src/tool/skill"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import { SessionID, MessageID } from "../../src/session/schema"
|
||||
|
||||
const baseCtx: Omit<Tool.Context, "ask"> = {
|
||||
sessionID: "test",
|
||||
messageID: "",
|
||||
sessionID: SessionID.make("ses_test"),
|
||||
messageID: MessageID.make(""),
|
||||
callID: "",
|
||||
agent: "build",
|
||||
abort: AbortSignal.any([]),
|
||||
|
||||
@@ -2,12 +2,13 @@ import { describe, expect, test } from "bun:test"
|
||||
import path from "path"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { WebFetchTool } from "../../src/tool/webfetch"
|
||||
import { SessionID, MessageID } from "../../src/session/schema"
|
||||
|
||||
const projectRoot = path.join(import.meta.dir, "../..")
|
||||
|
||||
const ctx = {
|
||||
sessionID: "test",
|
||||
messageID: "message",
|
||||
sessionID: SessionID.make("ses_test"),
|
||||
messageID: MessageID.make("message"),
|
||||
callID: "",
|
||||
agent: "build",
|
||||
abort: AbortSignal.any([]),
|
||||
|
||||
@@ -4,10 +4,11 @@ import fs from "fs/promises"
|
||||
import { WriteTool } from "../../src/tool/write"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import { SessionID, MessageID } from "../../src/session/schema"
|
||||
|
||||
const ctx = {
|
||||
sessionID: "test-write-session",
|
||||
messageID: "",
|
||||
sessionID: SessionID.make("ses_test-write-session"),
|
||||
messageID: MessageID.make(""),
|
||||
callID: "",
|
||||
agent: "build",
|
||||
abort: AbortSignal.any([]),
|
||||
|
||||
Reference in New Issue
Block a user