Dax Raad 7 месяцев назад
Родитель
Сommit
b4e4c3f662

+ 1 - 10
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:"
   }
 }

+ 20 - 3
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)
+    })
+  },
+})

+ 27 - 0
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
           }

+ 6 - 1
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",
     })

+ 14 - 11
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) {

+ 2 - 2
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

+ 66 - 20
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

+ 9 - 0
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 {}