summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDax Raad <[email protected]>2025-07-14 14:44:47 -0400
committerDax Raad <[email protected]>2025-07-14 15:29:08 -0400
commitb4e4c3f662fc262755b989cacc2e3845418b1d34 (patch)
treeae5a1e09b46309fa5aee82ef9ff7eba831b97a7a
parentba676e7ae095a6e2089b2b061a1ec8f3cffd4e42 (diff)
downloadopencode-b4e4c3f662fc262755b989cacc2e3845418b1d34.tar.gz
opencode-b4e4c3f662fc262755b989cacc2e3845418b1d34.zip
wip: snapshot
-rw-r--r--packages/opencode/package.json11
-rw-r--r--packages/opencode/src/cli/cmd/debug/snapshot.ts23
-rw-r--r--packages/opencode/src/session/index.ts27
-rw-r--r--packages/opencode/src/session/message-v2.ts7
-rw-r--r--packages/opencode/src/snapshot/index.ts25
-rw-r--r--packages/tui/sdk/.stats.yml4
-rw-r--r--packages/tui/sdk/session.go86
-rw-r--r--sdks/github/sst-env.d.ts9
8 files changed, 145 insertions, 47 deletions
diff --git a/packages/opencode/package.json b/packages/opencode/package.json
index c5b7e4ba9..423100b7b 100644
--- a/packages/opencode/package.json
+++ b/packages/opencode/package.json
@@ -26,29 +26,20 @@
},
"dependencies": {
"@clack/prompts": "0.11.0",
- "@flystorage/file-storage": "1.1.0",
- "@flystorage/local-fs": "1.1.0",
- "@hono/zod-validator": "0.5.0",
"@modelcontextprotocol/sdk": "1.15.1",
"@openauthjs/openauth": "0.4.3",
- "@standard-schema/spec": "1.0.0",
"ai": "catalog:",
"decimal.js": "10.5.0",
"diff": "8.0.2",
- "env-paths": "3.0.0",
"hono": "4.7.10",
"hono-openapi": "0.4.8",
"isomorphic-git": "1.32.1",
"open": "10.1.2",
"remeda": "2.22.3",
- "ts-lsp-client": "1.0.3",
"turndown": "7.2.0",
"vscode-jsonrpc": "8.2.1",
- "vscode-languageclient": "8",
"xdg-basedir": "5.1.0",
"yargs": "18.0.0",
- "zod": "catalog:",
- "zod-openapi": "4.2.4",
- "zod-validation-error": "3.5.2"
+ "zod": "catalog:"
}
}
diff --git a/packages/opencode/src/cli/cmd/debug/snapshot.ts b/packages/opencode/src/cli/cmd/debug/snapshot.ts
index a06048c63..48d7f91e6 100644
--- a/packages/opencode/src/cli/cmd/debug/snapshot.ts
+++ b/packages/opencode/src/cli/cmd/debug/snapshot.ts
@@ -4,11 +4,11 @@ import { cmd } from "../cmd"
export const SnapshotCommand = cmd({
command: "snapshot",
- builder: (yargs) => yargs.command(SnapshotCreateCommand).command(SnapshotRestoreCommand).demandCommand(),
+ builder: (yargs) => yargs.command(CreateCommand).command(RestoreCommand).command(DiffCommand).demandCommand(),
async handler() {},
})
-export const SnapshotCreateCommand = cmd({
+const CreateCommand = cmd({
command: "create",
async handler() {
await bootstrap({ cwd: process.cwd() }, async () => {
@@ -18,7 +18,7 @@ export const SnapshotCreateCommand = cmd({
},
})
-export const SnapshotRestoreCommand = cmd({
+const RestoreCommand = cmd({
command: "restore <commit>",
builder: (yargs) =>
yargs.positional("commit", {
@@ -33,3 +33,20 @@ export const SnapshotRestoreCommand = cmd({
})
},
})
+
+export const DiffCommand = cmd({
+ command: "diff <commit>",
+ describe: "diff",
+ builder: (yargs) =>
+ yargs.positional("commit", {
+ type: "string",
+ description: "commit",
+ demandOption: true,
+ }),
+ async handler(args) {
+ await bootstrap({ cwd: process.cwd() }, async () => {
+ const diff = await Snapshot.diff("test", args.commit)
+ console.log(diff)
+ })
+ },
+})
diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts
index 18c01a9f9..e2f456997 100644
--- a/packages/opencode/src/session/index.ts
+++ b/packages/opencode/src/session/index.ts
@@ -696,6 +696,15 @@ export namespace Session {
})
switch (value.type) {
case "start":
+ const snapshot = await Snapshot.create(assistantMsg.sessionID)
+ if (snapshot)
+ await updatePart({
+ id: Identifier.ascending("part"),
+ messageID: assistantMsg.id,
+ sessionID: assistantMsg.sessionID,
+ type: "snapshot",
+ snapshot,
+ })
break
case "tool-input-start":
@@ -751,6 +760,15 @@ export namespace Session {
},
})
delete toolCalls[value.toolCallId]
+ const snapshot = await Snapshot.create(assistantMsg.sessionID)
+ if (snapshot)
+ await updatePart({
+ id: Identifier.ascending("part"),
+ messageID: assistantMsg.id,
+ sessionID: assistantMsg.sessionID,
+ type: "snapshot",
+ snapshot,
+ })
}
break
}
@@ -771,6 +789,15 @@ export namespace Session {
},
})
delete toolCalls[value.toolCallId]
+ const snapshot = await Snapshot.create(assistantMsg.sessionID)
+ if (snapshot)
+ await updatePart({
+ id: Identifier.ascending("part"),
+ messageID: assistantMsg.id,
+ sessionID: assistantMsg.sessionID,
+ type: "snapshot",
+ snapshot,
+ })
}
break
}
diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts
index 353fdcec3..0031f6eac 100644
--- a/packages/opencode/src/session/message-v2.ts
+++ b/packages/opencode/src/session/message-v2.ts
@@ -79,6 +79,11 @@ export namespace MessageV2 {
messageID: z.string(),
})
+ export const SnapshotPart = PartBase.extend({
+ type: z.literal("snapshot"),
+ snapshot: z.string(),
+ })
+
export const TextPart = PartBase.extend({
type: z.literal("text"),
text: z.string(),
@@ -154,7 +159,7 @@ export namespace MessageV2 {
export type User = z.infer<typeof User>
export const Part = z
- .discriminatedUnion("type", [TextPart, FilePart, ToolPart, StepStartPart, StepFinishPart])
+ .discriminatedUnion("type", [TextPart, FilePart, ToolPart, StepStartPart, StepFinishPart, SnapshotPart])
.openapi({
ref: "Part",
})
diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts
index 608eb89c7..447e28ed5 100644
--- a/packages/opencode/src/snapshot/index.ts
+++ b/packages/opencode/src/snapshot/index.ts
@@ -9,10 +9,8 @@ export namespace Snapshot {
const log = Log.create({ service: "snapshot" })
export async function create(sessionID: string) {
- return
log.info("creating snapshot")
const app = App.info()
- const git = gitdir(sessionID)
// not a git repo, check if too big to snapshot
if (!app.git) {
@@ -24,6 +22,7 @@ export namespace Snapshot {
if (files.length > 1000) return
}
+ const git = gitdir(sessionID)
if (await fs.mkdir(git, { recursive: true })) {
await $`git init`
.env({
@@ -39,23 +38,27 @@ export namespace Snapshot {
await $`git --git-dir ${git} add .`.quiet().cwd(app.path.cwd).nothrow()
log.info("added files")
- const result =
- await $`git --git-dir ${git} commit --allow-empty -m "snapshot" --author="opencode <[email protected]>"`
- .quiet()
- .cwd(app.path.cwd)
- .nothrow()
- log.info("commit")
+ const result = await $`git --git-dir ${git} commit -m "snapshot" --author="opencode <[email protected]>"`
+ .quiet()
+ .cwd(app.path.cwd)
+ .nothrow()
const match = result.stdout.toString().match(/\[.+ ([a-f0-9]+)\]/)
if (!match) return
return match![1]
}
- export async function restore(sessionID: string, commit: string) {
- log.info("restore", { commit })
+ export async function restore(sessionID: string, snapshot: string) {
+ log.info("restore", { commit: snapshot })
const app = App.info()
const git = gitdir(sessionID)
- await $`git --git-dir=${git} checkout ${commit} --force`.quiet().cwd(app.path.root)
+ await $`git --git-dir=${git} checkout ${snapshot} --force`.quiet().cwd(app.path.root)
+ }
+
+ export async function diff(sessionID: string, commit: string) {
+ const git = gitdir(sessionID)
+ const result = await $`git --git-dir=${git} diff -R ${commit}`.quiet().cwd(App.info().path.root)
+ return result.stdout.toString("utf8")
}
function gitdir(sessionID: string) {
diff --git a/packages/tui/sdk/.stats.yml b/packages/tui/sdk/.stats.yml
index c2069e9c5..5aeba9efe 100644
--- a/packages/tui/sdk/.stats.yml
+++ b/packages/tui/sdk/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 22
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-9bdc593eab163d2165321716af6a4c13253792ad409420790ed6196da4178d3a.yml
-openapi_spec_hash: c687f53ada739d315e2e7056df93d999
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-05150c78e0e6e97b0ce97ed685ebcf1cb01dc839beccb99e9d3ead5b783cfd47.yml
+openapi_spec_hash: 833a5b6d53d98dc2beac2c4c394b20d5
config_hash: 3695cfc829cfaae14490850b4a1ed282
diff --git a/packages/tui/sdk/session.go b/packages/tui/sdk/session.go
index e76ab7f90..c91de85bd 100644
--- a/packages/tui/sdk/session.go
+++ b/packages/tui/sdk/session.go
@@ -605,6 +605,7 @@ type Part struct {
Cost float64 `json:"cost"`
Filename string `json:"filename"`
Mime string `json:"mime"`
+ Snapshot string `json:"snapshot"`
// This field can have the runtime type of [ToolPartState].
State interface{} `json:"state"`
Synthetic bool `json:"synthetic"`
@@ -629,6 +630,7 @@ type partJSON struct {
Cost apijson.Field
Filename apijson.Field
Mime apijson.Field
+ Snapshot apijson.Field
State apijson.Field
Synthetic apijson.Field
Text apijson.Field
@@ -657,13 +659,13 @@ func (r *Part) UnmarshalJSON(data []byte) (err error) {
// for more type safety.
//
// Possible runtime types of the union are [TextPart], [FilePart], [ToolPart],
-// [StepStartPart], [StepFinishPart].
+// [StepStartPart], [StepFinishPart], [PartObject].
func (r Part) AsUnion() PartUnion {
return r.union
}
-// Union satisfied by [TextPart], [FilePart], [ToolPart], [StepStartPart] or
-// [StepFinishPart].
+// Union satisfied by [TextPart], [FilePart], [ToolPart], [StepStartPart],
+// [StepFinishPart] or [PartObject].
type PartUnion interface {
implementsPart()
}
@@ -671,35 +673,78 @@ type PartUnion interface {
func init() {
apijson.RegisterUnion(
reflect.TypeOf((*PartUnion)(nil)).Elem(),
- "type",
+ "",
apijson.UnionVariant{
- TypeFilter: gjson.JSON,
- Type: reflect.TypeOf(TextPart{}),
- DiscriminatorValue: "text",
+ TypeFilter: gjson.JSON,
+ Type: reflect.TypeOf(TextPart{}),
},
apijson.UnionVariant{
- TypeFilter: gjson.JSON,
- Type: reflect.TypeOf(FilePart{}),
- DiscriminatorValue: "file",
+ TypeFilter: gjson.JSON,
+ Type: reflect.TypeOf(FilePart{}),
},
apijson.UnionVariant{
- TypeFilter: gjson.JSON,
- Type: reflect.TypeOf(ToolPart{}),
- DiscriminatorValue: "tool",
+ TypeFilter: gjson.JSON,
+ Type: reflect.TypeOf(ToolPart{}),
},
apijson.UnionVariant{
- TypeFilter: gjson.JSON,
- Type: reflect.TypeOf(StepStartPart{}),
- DiscriminatorValue: "step-start",
+ TypeFilter: gjson.JSON,
+ Type: reflect.TypeOf(StepStartPart{}),
},
apijson.UnionVariant{
- TypeFilter: gjson.JSON,
- Type: reflect.TypeOf(StepFinishPart{}),
- DiscriminatorValue: "step-finish",
+ TypeFilter: gjson.JSON,
+ Type: reflect.TypeOf(StepFinishPart{}),
+ },
+ apijson.UnionVariant{
+ TypeFilter: gjson.JSON,
+ Type: reflect.TypeOf(PartObject{}),
},
)
}
+type PartObject struct {
+ ID string `json:"id,required"`
+ MessageID string `json:"messageID,required"`
+ SessionID string `json:"sessionID,required"`
+ Snapshot string `json:"snapshot,required"`
+ Type PartObjectType `json:"type,required"`
+ JSON partObjectJSON `json:"-"`
+}
+
+// partObjectJSON contains the JSON metadata for the struct [PartObject]
+type partObjectJSON struct {
+ ID apijson.Field
+ MessageID apijson.Field
+ SessionID apijson.Field
+ Snapshot apijson.Field
+ Type apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *PartObject) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r partObjectJSON) RawJSON() string {
+ return r.raw
+}
+
+func (r PartObject) implementsPart() {}
+
+type PartObjectType string
+
+const (
+ PartObjectTypeSnapshot PartObjectType = "snapshot"
+)
+
+func (r PartObjectType) IsKnown() bool {
+ switch r {
+ case PartObjectTypeSnapshot:
+ return true
+ }
+ return false
+}
+
type PartType string
const (
@@ -708,11 +753,12 @@ const (
PartTypeTool PartType = "tool"
PartTypeStepStart PartType = "step-start"
PartTypeStepFinish PartType = "step-finish"
+ PartTypeSnapshot PartType = "snapshot"
)
func (r PartType) IsKnown() bool {
switch r {
- case PartTypeText, PartTypeFile, PartTypeTool, PartTypeStepStart, PartTypeStepFinish:
+ case PartTypeText, PartTypeFile, PartTypeTool, PartTypeStepStart, PartTypeStepFinish, PartTypeSnapshot:
return true
}
return false
diff --git a/sdks/github/sst-env.d.ts b/sdks/github/sst-env.d.ts
new file mode 100644
index 000000000..b6a7e9066
--- /dev/null
+++ b/sdks/github/sst-env.d.ts
@@ -0,0 +1,9 @@
+/* This file is auto-generated by SST. Do not edit. */
+/* tslint:disable */
+/* eslint-disable */
+/* deno-fmt-ignore-file */
+
+/// <reference path="../../sst-env.d.ts" />
+
+import "sst"
+export {} \ No newline at end of file