Skip to content

Commit b4e4c3f

Browse files
committed
wip: snapshot
1 parent ba676e7 commit b4e4c3f

8 files changed

Lines changed: 145 additions & 47 deletions

File tree

packages/opencode/package.json

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,29 +26,20 @@
2626
},
2727
"dependencies": {
2828
"@clack/prompts": "0.11.0",
29-
"@flystorage/file-storage": "1.1.0",
30-
"@flystorage/local-fs": "1.1.0",
31-
"@hono/zod-validator": "0.5.0",
3229
"@modelcontextprotocol/sdk": "1.15.1",
3330
"@openauthjs/openauth": "0.4.3",
34-
"@standard-schema/spec": "1.0.0",
3531
"ai": "catalog:",
3632
"decimal.js": "10.5.0",
3733
"diff": "8.0.2",
38-
"env-paths": "3.0.0",
3934
"hono": "4.7.10",
4035
"hono-openapi": "0.4.8",
4136
"isomorphic-git": "1.32.1",
4237
"open": "10.1.2",
4338
"remeda": "2.22.3",
44-
"ts-lsp-client": "1.0.3",
4539
"turndown": "7.2.0",
4640
"vscode-jsonrpc": "8.2.1",
47-
"vscode-languageclient": "8",
4841
"xdg-basedir": "5.1.0",
4942
"yargs": "18.0.0",
50-
"zod": "catalog:",
51-
"zod-openapi": "4.2.4",
52-
"zod-validation-error": "3.5.2"
43+
"zod": "catalog:"
5344
}
5445
}

packages/opencode/src/cli/cmd/debug/snapshot.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ import { cmd } from "../cmd"
44

55
export const SnapshotCommand = cmd({
66
command: "snapshot",
7-
builder: (yargs) => yargs.command(SnapshotCreateCommand).command(SnapshotRestoreCommand).demandCommand(),
7+
builder: (yargs) => yargs.command(CreateCommand).command(RestoreCommand).command(DiffCommand).demandCommand(),
88
async handler() {},
99
})
1010

11-
export const SnapshotCreateCommand = cmd({
11+
const CreateCommand = cmd({
1212
command: "create",
1313
async handler() {
1414
await bootstrap({ cwd: process.cwd() }, async () => {
@@ -18,7 +18,7 @@ export const SnapshotCreateCommand = cmd({
1818
},
1919
})
2020

21-
export const SnapshotRestoreCommand = cmd({
21+
const RestoreCommand = cmd({
2222
command: "restore <commit>",
2323
builder: (yargs) =>
2424
yargs.positional("commit", {
@@ -33,3 +33,20 @@ export const SnapshotRestoreCommand = cmd({
3333
})
3434
},
3535
})
36+
37+
export const DiffCommand = cmd({
38+
command: "diff <commit>",
39+
describe: "diff",
40+
builder: (yargs) =>
41+
yargs.positional("commit", {
42+
type: "string",
43+
description: "commit",
44+
demandOption: true,
45+
}),
46+
async handler(args) {
47+
await bootstrap({ cwd: process.cwd() }, async () => {
48+
const diff = await Snapshot.diff("test", args.commit)
49+
console.log(diff)
50+
})
51+
},
52+
})

packages/opencode/src/session/index.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -696,6 +696,15 @@ export namespace Session {
696696
})
697697
switch (value.type) {
698698
case "start":
699+
const snapshot = await Snapshot.create(assistantMsg.sessionID)
700+
if (snapshot)
701+
await updatePart({
702+
id: Identifier.ascending("part"),
703+
messageID: assistantMsg.id,
704+
sessionID: assistantMsg.sessionID,
705+
type: "snapshot",
706+
snapshot,
707+
})
699708
break
700709

701710
case "tool-input-start":
@@ -751,6 +760,15 @@ export namespace Session {
751760
},
752761
})
753762
delete toolCalls[value.toolCallId]
763+
const snapshot = await Snapshot.create(assistantMsg.sessionID)
764+
if (snapshot)
765+
await updatePart({
766+
id: Identifier.ascending("part"),
767+
messageID: assistantMsg.id,
768+
sessionID: assistantMsg.sessionID,
769+
type: "snapshot",
770+
snapshot,
771+
})
754772
}
755773
break
756774
}
@@ -771,6 +789,15 @@ export namespace Session {
771789
},
772790
})
773791
delete toolCalls[value.toolCallId]
792+
const snapshot = await Snapshot.create(assistantMsg.sessionID)
793+
if (snapshot)
794+
await updatePart({
795+
id: Identifier.ascending("part"),
796+
messageID: assistantMsg.id,
797+
sessionID: assistantMsg.sessionID,
798+
type: "snapshot",
799+
snapshot,
800+
})
774801
}
775802
break
776803
}

packages/opencode/src/session/message-v2.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@ export namespace MessageV2 {
7979
messageID: z.string(),
8080
})
8181

82+
export const SnapshotPart = PartBase.extend({
83+
type: z.literal("snapshot"),
84+
snapshot: z.string(),
85+
})
86+
8287
export const TextPart = PartBase.extend({
8388
type: z.literal("text"),
8489
text: z.string(),
@@ -154,7 +159,7 @@ export namespace MessageV2 {
154159
export type User = z.infer<typeof User>
155160

156161
export const Part = z
157-
.discriminatedUnion("type", [TextPart, FilePart, ToolPart, StepStartPart, StepFinishPart])
162+
.discriminatedUnion("type", [TextPart, FilePart, ToolPart, StepStartPart, StepFinishPart, SnapshotPart])
158163
.openapi({
159164
ref: "Part",
160165
})

packages/opencode/src/snapshot/index.ts

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,8 @@ export namespace Snapshot {
99
const log = Log.create({ service: "snapshot" })
1010

1111
export async function create(sessionID: string) {
12-
return
1312
log.info("creating snapshot")
1413
const app = App.info()
15-
const git = gitdir(sessionID)
1614

1715
// not a git repo, check if too big to snapshot
1816
if (!app.git) {
@@ -24,6 +22,7 @@ export namespace Snapshot {
2422
if (files.length > 1000) return
2523
}
2624

25+
const git = gitdir(sessionID)
2726
if (await fs.mkdir(git, { recursive: true })) {
2827
await $`git init`
2928
.env({
@@ -39,23 +38,27 @@ export namespace Snapshot {
3938
await $`git --git-dir ${git} add .`.quiet().cwd(app.path.cwd).nothrow()
4039
log.info("added files")
4140

42-
const result =
43-
await $`git --git-dir ${git} commit --allow-empty -m "snapshot" --author="opencode <mail@opencode.ai>"`
44-
.quiet()
45-
.cwd(app.path.cwd)
46-
.nothrow()
47-
log.info("commit")
41+
const result = await $`git --git-dir ${git} commit -m "snapshot" --author="opencode <mail@opencode.ai>"`
42+
.quiet()
43+
.cwd(app.path.cwd)
44+
.nothrow()
4845

4946
const match = result.stdout.toString().match(/\[.+ ([a-f0-9]+)\]/)
5047
if (!match) return
5148
return match![1]
5249
}
5350

54-
export async function restore(sessionID: string, commit: string) {
55-
log.info("restore", { commit })
51+
export async function restore(sessionID: string, snapshot: string) {
52+
log.info("restore", { commit: snapshot })
5653
const app = App.info()
5754
const git = gitdir(sessionID)
58-
await $`git --git-dir=${git} checkout ${commit} --force`.quiet().cwd(app.path.root)
55+
await $`git --git-dir=${git} checkout ${snapshot} --force`.quiet().cwd(app.path.root)
56+
}
57+
58+
export async function diff(sessionID: string, commit: string) {
59+
const git = gitdir(sessionID)
60+
const result = await $`git --git-dir=${git} diff -R ${commit}`.quiet().cwd(App.info().path.root)
61+
return result.stdout.toString("utf8")
5962
}
6063

6164
function gitdir(sessionID: string) {

packages/tui/sdk/.stats.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
configured_endpoints: 22
2-
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-9bdc593eab163d2165321716af6a4c13253792ad409420790ed6196da4178d3a.yml
3-
openapi_spec_hash: c687f53ada739d315e2e7056df93d999
2+
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-05150c78e0e6e97b0ce97ed685ebcf1cb01dc839beccb99e9d3ead5b783cfd47.yml
3+
openapi_spec_hash: 833a5b6d53d98dc2beac2c4c394b20d5
44
config_hash: 3695cfc829cfaae14490850b4a1ed282

packages/tui/sdk/session.go

Lines changed: 66 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,7 @@ type Part struct {
605605
Cost float64 `json:"cost"`
606606
Filename string `json:"filename"`
607607
Mime string `json:"mime"`
608+
Snapshot string `json:"snapshot"`
608609
// This field can have the runtime type of [ToolPartState].
609610
State interface{} `json:"state"`
610611
Synthetic bool `json:"synthetic"`
@@ -629,6 +630,7 @@ type partJSON struct {
629630
Cost apijson.Field
630631
Filename apijson.Field
631632
Mime apijson.Field
633+
Snapshot apijson.Field
632634
State apijson.Field
633635
Synthetic apijson.Field
634636
Text apijson.Field
@@ -657,49 +659,92 @@ func (r *Part) UnmarshalJSON(data []byte) (err error) {
657659
// for more type safety.
658660
//
659661
// Possible runtime types of the union are [TextPart], [FilePart], [ToolPart],
660-
// [StepStartPart], [StepFinishPart].
662+
// [StepStartPart], [StepFinishPart], [PartObject].
661663
func (r Part) AsUnion() PartUnion {
662664
return r.union
663665
}
664666

665-
// Union satisfied by [TextPart], [FilePart], [ToolPart], [StepStartPart] or
666-
// [StepFinishPart].
667+
// Union satisfied by [TextPart], [FilePart], [ToolPart], [StepStartPart],
668+
// [StepFinishPart] or [PartObject].
667669
type PartUnion interface {
668670
implementsPart()
669671
}
670672

671673
func init() {
672674
apijson.RegisterUnion(
673675
reflect.TypeOf((*PartUnion)(nil)).Elem(),
674-
"type",
676+
"",
675677
apijson.UnionVariant{
676-
TypeFilter: gjson.JSON,
677-
Type: reflect.TypeOf(TextPart{}),
678-
DiscriminatorValue: "text",
678+
TypeFilter: gjson.JSON,
679+
Type: reflect.TypeOf(TextPart{}),
679680
},
680681
apijson.UnionVariant{
681-
TypeFilter: gjson.JSON,
682-
Type: reflect.TypeOf(FilePart{}),
683-
DiscriminatorValue: "file",
682+
TypeFilter: gjson.JSON,
683+
Type: reflect.TypeOf(FilePart{}),
684684
},
685685
apijson.UnionVariant{
686-
TypeFilter: gjson.JSON,
687-
Type: reflect.TypeOf(ToolPart{}),
688-
DiscriminatorValue: "tool",
686+
TypeFilter: gjson.JSON,
687+
Type: reflect.TypeOf(ToolPart{}),
689688
},
690689
apijson.UnionVariant{
691-
TypeFilter: gjson.JSON,
692-
Type: reflect.TypeOf(StepStartPart{}),
693-
DiscriminatorValue: "step-start",
690+
TypeFilter: gjson.JSON,
691+
Type: reflect.TypeOf(StepStartPart{}),
694692
},
695693
apijson.UnionVariant{
696-
TypeFilter: gjson.JSON,
697-
Type: reflect.TypeOf(StepFinishPart{}),
698-
DiscriminatorValue: "step-finish",
694+
TypeFilter: gjson.JSON,
695+
Type: reflect.TypeOf(StepFinishPart{}),
696+
},
697+
apijson.UnionVariant{
698+
TypeFilter: gjson.JSON,
699+
Type: reflect.TypeOf(PartObject{}),
699700
},
700701
)
701702
}
702703

704+
type PartObject struct {
705+
ID string `json:"id,required"`
706+
MessageID string `json:"messageID,required"`
707+
SessionID string `json:"sessionID,required"`
708+
Snapshot string `json:"snapshot,required"`
709+
Type PartObjectType `json:"type,required"`
710+
JSON partObjectJSON `json:"-"`
711+
}
712+
713+
// partObjectJSON contains the JSON metadata for the struct [PartObject]
714+
type partObjectJSON struct {
715+
ID apijson.Field
716+
MessageID apijson.Field
717+
SessionID apijson.Field
718+
Snapshot apijson.Field
719+
Type apijson.Field
720+
raw string
721+
ExtraFields map[string]apijson.Field
722+
}
723+
724+
func (r *PartObject) UnmarshalJSON(data []byte) (err error) {
725+
return apijson.UnmarshalRoot(data, r)
726+
}
727+
728+
func (r partObjectJSON) RawJSON() string {
729+
return r.raw
730+
}
731+
732+
func (r PartObject) implementsPart() {}
733+
734+
type PartObjectType string
735+
736+
const (
737+
PartObjectTypeSnapshot PartObjectType = "snapshot"
738+
)
739+
740+
func (r PartObjectType) IsKnown() bool {
741+
switch r {
742+
case PartObjectTypeSnapshot:
743+
return true
744+
}
745+
return false
746+
}
747+
703748
type PartType string
704749

705750
const (
@@ -708,11 +753,12 @@ const (
708753
PartTypeTool PartType = "tool"
709754
PartTypeStepStart PartType = "step-start"
710755
PartTypeStepFinish PartType = "step-finish"
756+
PartTypeSnapshot PartType = "snapshot"
711757
)
712758

713759
func (r PartType) IsKnown() bool {
714760
switch r {
715-
case PartTypeText, PartTypeFile, PartTypeTool, PartTypeStepStart, PartTypeStepFinish:
761+
case PartTypeText, PartTypeFile, PartTypeTool, PartTypeStepStart, PartTypeStepFinish, PartTypeSnapshot:
716762
return true
717763
}
718764
return false

sdks/github/sst-env.d.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/* This file is auto-generated by SST. Do not edit. */
2+
/* tslint:disable */
3+
/* eslint-disable */
4+
/* deno-fmt-ignore-file */
5+
6+
/// <reference path="../../sst-env.d.ts" />
7+
8+
import "sst"
9+
export {}

0 commit comments

Comments
 (0)