Просмотр исходного кода

feat(tui): move logging to server logs

adamdottv 7 месяцев назад
Родитель
Сommit
ca8ce88354

+ 18 - 5
packages/opencode/src/file/index.ts

@@ -11,6 +11,19 @@ import { Log } from "../util/log"
 export namespace File {
   const log = Log.create({ service: "file" })
 
+  export const Info = z
+    .object({
+      path: z.string(),
+      added: z.number().int(),
+      removed: z.number().int(),
+      status: z.enum(["added", "deleted", "modified"]),
+    })
+    .openapi({
+      ref: "File",
+    })
+
+  export type Info = z.infer<typeof Info>
+
   export const Event = {
     Edited: Bus.event(
       "file.edited",
@@ -26,14 +39,14 @@ export namespace File {
 
     const diffOutput = await $`git diff --numstat HEAD`.cwd(app.path.cwd).quiet().nothrow().text()
 
-    const changedFiles = []
+    const changedFiles: Info[] = []
 
     if (diffOutput.trim()) {
       const lines = diffOutput.trim().split("\n")
       for (const line of lines) {
         const [added, removed, filepath] = line.split("\t")
         changedFiles.push({
-          file: filepath,
+          path: filepath,
           added: added === "-" ? 0 : parseInt(added, 10),
           removed: removed === "-" ? 0 : parseInt(removed, 10),
           status: "modified",
@@ -50,7 +63,7 @@ export namespace File {
           const content = await Bun.file(path.join(app.path.root, filepath)).text()
           const lines = content.split("\n").length
           changedFiles.push({
-            file: filepath,
+            path: filepath,
             added: lines,
             removed: 0,
             status: "added",
@@ -68,7 +81,7 @@ export namespace File {
       const deletedFiles = deletedOutput.trim().split("\n")
       for (const filepath of deletedFiles) {
         changedFiles.push({
-          file: filepath,
+          path: filepath,
           added: 0,
           removed: 0, // Could get original line count but would require another git command
           status: "deleted",
@@ -78,7 +91,7 @@ export namespace File {
 
     return changedFiles.map((x) => ({
       ...x,
-      file: path.relative(app.path.cwd, path.join(app.path.root, x.file)),
+      path: path.relative(app.path.cwd, path.join(app.path.root, x.path)),
     }))
   }
 

+ 20 - 18
packages/opencode/src/file/ripgrep.ts

@@ -34,25 +34,27 @@ export namespace Ripgrep {
 
   export const Match = z.object({
     type: z.literal("match"),
-    data: z.object({
-      path: z.object({
-        text: z.string(),
-      }),
-      lines: z.object({
-        text: z.string(),
-      }),
-      line_number: z.number(),
-      absolute_offset: z.number(),
-      submatches: z.array(
-        z.object({
-          match: z.object({
-            text: z.string(),
-          }),
-          start: z.number(),
-          end: z.number(),
+    data: z
+      .object({
+        path: z.object({
+          text: z.string(),
         }),
-      ),
-    }),
+        lines: z.object({
+          text: z.string(),
+        }),
+        line_number: z.number(),
+        absolute_offset: z.number(),
+        submatches: z.array(
+          z.object({
+            match: z.object({
+              text: z.string(),
+            }),
+            start: z.number(),
+            end: z.number(),
+          }),
+        ),
+      })
+      .openapi({ ref: "Match" }),
   })
 
   const End = z.object({

+ 1 - 1
packages/opencode/src/lsp/index.ts

@@ -28,7 +28,7 @@ export namespace LSP {
       }),
     })
     .openapi({
-      ref: "LSP.Symbol",
+      ref: "Symbol",
     })
   export type Symbol = z.infer<typeof Symbol>
 

+ 47 - 10
packages/opencode/src/server/server.ts

@@ -621,16 +621,7 @@ export namespace Server {
               description: "File status",
               content: {
                 "application/json": {
-                  schema: resolver(
-                    z
-                      .object({
-                        file: z.string(),
-                        added: z.number().int(),
-                        removed: z.number().int(),
-                        status: z.enum(["added", "deleted", "modified"]),
-                      })
-                      .array(),
-                  ),
+                  schema: resolver(File.Info.array()),
                 },
               },
             },
@@ -641,6 +632,52 @@ export namespace Server {
           return c.json(content)
         },
       )
+      .post(
+        "/log",
+        describeRoute({
+          description: "Write a log entry to the server logs",
+          responses: {
+            200: {
+              description: "Log entry written successfully",
+              content: {
+                "application/json": {
+                  schema: resolver(z.boolean()),
+                },
+              },
+            },
+          },
+        }),
+        zValidator(
+          "json",
+          z.object({
+            service: z.string().openapi({ description: "Service name for the log entry" }),
+            level: z.enum(["info", "error", "warn"]).openapi({ description: "Log level" }),
+            message: z.string().openapi({ description: "Log message" }),
+            extra: z
+              .record(z.string(), z.any())
+              .optional()
+              .openapi({ description: "Additional metadata for the log entry" }),
+          }),
+        ),
+        async (c) => {
+          const { service, level, message, extra } = c.req.valid("json")
+          const logger = Log.create({ service })
+
+          switch (level) {
+            case "info":
+              logger.info(message, extra)
+              break
+            case "error":
+              logger.error(message, extra)
+              break
+            case "warn":
+              logger.warn(message, extra)
+              break
+          }
+
+          return c.json(true)
+        },
+      )
 
     return result
   }

+ 30 - 2
packages/opencode/src/util/log.ts

@@ -2,6 +2,23 @@ import path from "path"
 import fs from "fs/promises"
 import { Global } from "../global"
 export namespace Log {
+  export type Logger = {
+    info(message?: any, extra?: Record<string, any>): void
+    error(message?: any, extra?: Record<string, any>): void
+    warn(message?: any, extra?: Record<string, any>): void
+    tag(key: string, value: string): Logger
+    clone(): Logger
+    time(
+      message: string,
+      extra?: Record<string, any>,
+    ): {
+      stop(): void
+      [Symbol.dispose](): void
+    }
+  }
+
+  const loggers = new Map<string, Logger>()
+
   export const Default = create({ service: "default" })
 
   export interface Options {
@@ -9,7 +26,6 @@ export namespace Log {
   }
 
   let logpath = ""
-
   export function file() {
     return logpath
   }
@@ -47,6 +63,14 @@ export namespace Log {
   export function create(tags?: Record<string, any>) {
     tags = tags || {}
 
+    const service = tags["service"]
+    if (service && typeof service === "string") {
+      const cached = loggers.get(service)
+      if (cached) {
+        return cached
+      }
+    }
+
     function build(message: any, extra?: Record<string, any>) {
       const prefix = Object.entries({
         ...tags,
@@ -60,7 +84,7 @@ export namespace Log {
       last = next.getTime()
       return [next.toISOString().split(".")[0], "+" + diff + "ms", prefix, message].filter(Boolean).join(" ") + "\n"
     }
-    const result = {
+    const result: Logger = {
       info(message?: any, extra?: Record<string, any>) {
         process.stderr.write("INFO  " + build(message, extra))
       },
@@ -96,6 +120,10 @@ export namespace Log {
       },
     }
 
+    if (service && typeof service === "string") {
+      loggers.set(service, result)
+    }
+
     return result
   }
 }

+ 6 - 24
packages/tui/cmd/opencode/main.go

@@ -5,7 +5,6 @@ import (
 	"encoding/json"
 	"log/slog"
 	"os"
-	"path/filepath"
 	"strings"
 
 	tea "github.com/charmbracelet/bubbletea/v2"
@@ -15,6 +14,7 @@ import (
 	"github.com/sst/opencode/internal/app"
 	"github.com/sst/opencode/internal/clipboard"
 	"github.com/sst/opencode/internal/tui"
+	"github.com/sst/opencode/internal/util"
 )
 
 var Version = "dev"
@@ -39,33 +39,15 @@ func main() {
 		os.Exit(1)
 	}
 
-	logfile := filepath.Join(appInfo.Path.Data, "log", "tui.log")
-	if _, err := os.Stat(filepath.Dir(logfile)); os.IsNotExist(err) {
-		err := os.MkdirAll(filepath.Dir(logfile), 0755)
-		if err != nil {
-			slog.Error("Failed to create log directory", "error", err)
-			os.Exit(1)
-		}
-	}
-	file, err := os.Create(logfile)
-	if err != nil {
-		slog.Error("Failed to create log file", "error", err)
-		os.Exit(1)
-	}
-	defer file.Close()
-	logger := slog.New(slog.NewTextHandler(file, &slog.HandlerOptions{Level: slog.LevelDebug}))
-	slog.SetDefault(logger)
-
-	slog.Debug("TUI launched", "app", appInfo)
-
 	httpClient := opencode.NewClient(
 		option.WithBaseURL(url),
 	)
 
-	if err != nil {
-		slog.Error("Failed to create client", "error", err)
-		os.Exit(1)
-	}
+	apiHandler := util.NewAPILogHandler(httpClient, "tui", slog.LevelDebug)
+	logger := slog.New(apiHandler)
+	slog.SetDefault(logger)
+
+	slog.Debug("TUI launched", "app", appInfo)
 
 	go func() {
 		err = clipboard.Init()

+ 2 - 2
packages/tui/internal/completions/files-folders.go

@@ -42,7 +42,7 @@ func (cg *filesAndFoldersContextGroup) getGitFiles() []dialog.CompletionItemI {
 		})
 
 		for _, file := range files {
-			title := file.File
+			title := file.Path
 			if file.Added > 0 {
 				title += green(" +" + strconv.Itoa(int(file.Added)))
 			}
@@ -51,7 +51,7 @@ func (cg *filesAndFoldersContextGroup) getGitFiles() []dialog.CompletionItemI {
 			}
 			item := dialog.NewCompletionItem(dialog.CompletionItem{
 				Title: title,
-				Value: file.File,
+				Value: file.Path,
 			})
 			items = append(items, item)
 		}

+ 131 - 0
packages/tui/internal/util/apilogger.go

@@ -0,0 +1,131 @@
+package util
+
+import (
+	"context"
+	"log/slog"
+	"sync"
+
+	opencode "github.com/sst/opencode-sdk-go"
+)
+
+// APILogHandler is a slog.Handler that sends logs to the opencode API
+type APILogHandler struct {
+	client  *opencode.Client
+	service string
+	level   slog.Level
+	attrs   []slog.Attr
+	groups  []string
+	mu      sync.Mutex
+}
+
+// NewAPILogHandler creates a new APILogHandler
+func NewAPILogHandler(client *opencode.Client, service string, level slog.Level) *APILogHandler {
+	return &APILogHandler{
+		client:  client,
+		service: service,
+		level:   level,
+		attrs:   make([]slog.Attr, 0),
+		groups:  make([]string, 0),
+	}
+}
+
+// Enabled reports whether the handler handles records at the given level.
+func (h *APILogHandler) Enabled(_ context.Context, level slog.Level) bool {
+	return level >= h.level
+}
+
+// Handle handles the Record.
+func (h *APILogHandler) Handle(ctx context.Context, r slog.Record) error {
+	// Convert slog level to API level
+	var apiLevel opencode.AppLogParamsLevel
+	switch r.Level {
+	case slog.LevelDebug, slog.LevelInfo:
+		apiLevel = opencode.AppLogParamsLevelInfo
+	case slog.LevelWarn:
+		apiLevel = opencode.AppLogParamsLevelWarn
+	case slog.LevelError:
+		apiLevel = opencode.AppLogParamsLevelError
+	default:
+		apiLevel = opencode.AppLogParamsLevelInfo
+	}
+
+	// Build extra fields
+	extra := make(map[string]any)
+
+	// Add handler attributes
+	h.mu.Lock()
+	for _, attr := range h.attrs {
+		extra[attr.Key] = attr.Value.Any()
+	}
+	h.mu.Unlock()
+
+	// Add record attributes
+	r.Attrs(func(attr slog.Attr) bool {
+		extra[attr.Key] = attr.Value.Any()
+		return true
+	})
+
+	// Send log to API
+	params := opencode.AppLogParams{
+		Service: opencode.F(h.service),
+		Level:   opencode.F(apiLevel),
+		Message: opencode.F(r.Message),
+	}
+
+	if len(extra) > 0 {
+		params.Extra = opencode.F(extra)
+	}
+
+	// Use a goroutine to avoid blocking the logger
+	go func() {
+		_, err := h.client.App.Log(context.Background(), params)
+		if err != nil {
+			// Fallback: we can't log the error using slog as it would create a loop
+			// TODO: fallback file?
+		}
+	}()
+
+	return nil
+}
+
+// WithAttrs returns a new Handler whose attributes consist of
+// both the receiver's attributes and the arguments.
+func (h *APILogHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
+	h.mu.Lock()
+	defer h.mu.Unlock()
+
+	newHandler := &APILogHandler{
+		client:  h.client,
+		service: h.service,
+		level:   h.level,
+		attrs:   make([]slog.Attr, len(h.attrs)+len(attrs)),
+		groups:  make([]string, len(h.groups)),
+	}
+
+	copy(newHandler.attrs, h.attrs)
+	copy(newHandler.attrs[len(h.attrs):], attrs)
+	copy(newHandler.groups, h.groups)
+
+	return newHandler
+}
+
+// WithGroup returns a new Handler with the given group appended to
+// the receiver's existing groups.
+func (h *APILogHandler) WithGroup(name string) slog.Handler {
+	h.mu.Lock()
+	defer h.mu.Unlock()
+
+	newHandler := &APILogHandler{
+		client:  h.client,
+		service: h.service,
+		level:   h.level,
+		attrs:   make([]slog.Attr, len(h.attrs)),
+		groups:  make([]string, len(h.groups)+1),
+	}
+
+	copy(newHandler.attrs, h.attrs)
+	copy(newHandler.groups, h.groups)
+	newHandler.groups[len(h.groups)] = name
+
+	return newHandler
+}

+ 4 - 4
packages/tui/sdk/.stats.yml

@@ -1,4 +1,4 @@
-configured_endpoints: 20
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-c06a9b8d8284683e8350fdd3eceff0b5756877f7b67e974acd565409b67d32a0.yml
-openapi_spec_hash: 5933bca0c79177065374ac724a6bc986
-config_hash: de53ecf98e1038f2cc2fd273b582f082
+configured_endpoints: 21
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-3ae2247ea9674e156e5ad818e13d8cd8622737ee1b95fdcde23ebf50963df13c.yml
+openapi_spec_hash: 3075cca003eb61c035d3eb5891a6c38c
+config_hash: a2751b16a52007a1e12967ab4aa3729f

+ 7 - 6
packages/tui/sdk/api.md

@@ -24,31 +24,32 @@ Methods:
 
 - <code title="get /app">client.App.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppService.Get">Get</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#App">App</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
 - <code title="post /app/init">client.App.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppService.Init">Init</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
+- <code title="post /log">client.App.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppService.Log">Log</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppLogParams">AppLogParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
 
 # Find
 
 Response Types:
 
-- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindSymbolsResponse">FindSymbolsResponse</a>
-- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindTextResponse">FindTextResponse</a>
+- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Match">Match</a>
+- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Symbol">Symbol</a>
 
 Methods:
 
 - <code title="get /find/file">client.Find.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindService.Files">Files</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, query <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindFilesParams">FindFilesParams</a>) ([]<a href="https://pkg.go.dev/builtin#string">string</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
-- <code title="get /find/symbol">client.Find.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindService.Symbols">Symbols</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, query <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindSymbolsParams">FindSymbolsParams</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindSymbolsResponse">FindSymbolsResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
-- <code title="get /find">client.Find.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindService.Text">Text</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, query <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindTextParams">FindTextParams</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindTextResponse">FindTextResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
+- <code title="get /find/symbol">client.Find.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindService.Symbols">Symbols</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, query <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindSymbolsParams">FindSymbolsParams</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Symbol">Symbol</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
+- <code title="get /find">client.Find.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindService.Text">Text</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, query <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindTextParams">FindTextParams</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Match">Match</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
 
 # File
 
 Response Types:
 
+- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#File">File</a>
 - <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FileReadResponse">FileReadResponse</a>
-- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FileStatusResponse">FileStatusResponse</a>
 
 Methods:
 
 - <code title="get /file">client.File.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FileService.Read">Read</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, query <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FileReadParams">FileReadParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FileReadResponse">FileReadResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
-- <code title="get /file/status">client.File.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FileService.Status">Status</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FileStatusResponse">FileStatusResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
+- <code title="get /file/status">client.File.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FileService.Status">Status</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#File">File</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
 
 # Config
 

+ 41 - 0
packages/tui/sdk/app.go

@@ -7,6 +7,7 @@ import (
 	"net/http"
 
 	"github.com/sst/opencode-sdk-go/internal/apijson"
+	"github.com/sst/opencode-sdk-go/internal/param"
 	"github.com/sst/opencode-sdk-go/internal/requestconfig"
 	"github.com/sst/opencode-sdk-go/option"
 )
@@ -46,6 +47,14 @@ func (r *AppService) Init(ctx context.Context, opts ...option.RequestOption) (re
 	return
 }
 
+// Write a log entry to the server logs
+func (r *AppService) Log(ctx context.Context, body AppLogParams, opts ...option.RequestOption) (res *bool, err error) {
+	opts = append(r.Options[:], opts...)
+	path := "log"
+	err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...)
+	return
+}
+
 type App struct {
 	Git      bool    `json:"git,required"`
 	Hostname string  `json:"hostname,required"`
@@ -121,3 +130,35 @@ func (r *AppTime) UnmarshalJSON(data []byte) (err error) {
 func (r appTimeJSON) RawJSON() string {
 	return r.raw
 }
+
+type AppLogParams struct {
+	// Log level
+	Level param.Field[AppLogParamsLevel] `json:"level,required"`
+	// Log message
+	Message param.Field[string] `json:"message,required"`
+	// Service name for the log entry
+	Service param.Field[string] `json:"service,required"`
+	// Additional metadata for the log entry
+	Extra param.Field[map[string]interface{}] `json:"extra"`
+}
+
+func (r AppLogParams) MarshalJSON() (data []byte, err error) {
+	return apijson.MarshalRoot(r)
+}
+
+// Log level
+type AppLogParamsLevel string
+
+const (
+	AppLogParamsLevelInfo  AppLogParamsLevel = "info"
+	AppLogParamsLevelError AppLogParamsLevel = "error"
+	AppLogParamsLevelWarn  AppLogParamsLevel = "warn"
+)
+
+func (r AppLogParamsLevel) IsKnown() bool {
+	switch r {
+	case AppLogParamsLevelInfo, AppLogParamsLevelError, AppLogParamsLevelWarn:
+		return true
+	}
+	return false
+}

+ 29 - 0
packages/tui/sdk/app_test.go

@@ -56,3 +56,32 @@ func TestAppInit(t *testing.T) {
 		t.Fatalf("err should be nil: %s", err.Error())
 	}
 }
+
+func TestAppLogWithOptionalParams(t *testing.T) {
+	t.Skip("skipped: tests are disabled for the time being")
+	baseURL := "http://localhost:4010"
+	if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
+		baseURL = envURL
+	}
+	if !testutil.CheckTestServer(t, baseURL) {
+		return
+	}
+	client := opencode.NewClient(
+		option.WithBaseURL(baseURL),
+	)
+	_, err := client.App.Log(context.TODO(), opencode.AppLogParams{
+		Level:   opencode.F(opencode.AppLogParamsLevelInfo),
+		Message: opencode.F("message"),
+		Service: opencode.F("service"),
+		Extra: opencode.F(map[string]interface{}{
+			"foo": "bar",
+		}),
+	})
+	if err != nil {
+		var apierr *opencode.Error
+		if errors.As(err, &apierr) {
+			t.Log(string(apierr.DumpRequest(true)))
+		}
+		t.Fatalf("err should be nil: %s", err.Error())
+	}
+}

+ 37 - 37
packages/tui/sdk/config.go

@@ -396,71 +396,71 @@ func (r configProviderModelsLimitJSON) RawJSON() string {
 
 type Keybinds struct {
 	// Exit the application
-	AppExit string `json:"app_exit"`
+	AppExit string `json:"app_exit,required"`
 	// Show help dialog
-	AppHelp string `json:"app_help"`
+	AppHelp string `json:"app_help,required"`
 	// Open external editor
-	EditorOpen string `json:"editor_open"`
+	EditorOpen string `json:"editor_open,required"`
 	// Close file
-	FileClose string `json:"file_close"`
-	// Toggle split/unified diff
-	FileDiffToggle string `json:"file_diff_toggle"`
+	FileClose string `json:"file_close,required"`
+	// Split/unified diff
+	FileDiffToggle string `json:"file_diff_toggle,required"`
 	// List files
-	FileList string `json:"file_list"`
+	FileList string `json:"file_list,required"`
 	// Search file
-	FileSearch string `json:"file_search"`
+	FileSearch string `json:"file_search,required"`
 	// Clear input field
-	InputClear string `json:"input_clear"`
+	InputClear string `json:"input_clear,required"`
 	// Insert newline in input
-	InputNewline string `json:"input_newline"`
+	InputNewline string `json:"input_newline,required"`
 	// Paste from clipboard
-	InputPaste string `json:"input_paste"`
+	InputPaste string `json:"input_paste,required"`
 	// Submit input
-	InputSubmit string `json:"input_submit"`
+	InputSubmit string `json:"input_submit,required"`
 	// Leader key for keybind combinations
-	Leader string `json:"leader"`
+	Leader string `json:"leader,required"`
 	// Copy message
-	MessagesCopy string `json:"messages_copy"`
+	MessagesCopy string `json:"messages_copy,required"`
 	// Navigate to first message
-	MessagesFirst string `json:"messages_first"`
+	MessagesFirst string `json:"messages_first,required"`
 	// Scroll messages down by half page
-	MessagesHalfPageDown string `json:"messages_half_page_down"`
+	MessagesHalfPageDown string `json:"messages_half_page_down,required"`
 	// Scroll messages up by half page
-	MessagesHalfPageUp string `json:"messages_half_page_up"`
+	MessagesHalfPageUp string `json:"messages_half_page_up,required"`
 	// Navigate to last message
-	MessagesLast string `json:"messages_last"`
+	MessagesLast string `json:"messages_last,required"`
 	// Toggle layout
-	MessagesLayoutToggle string `json:"messages_layout_toggle"`
+	MessagesLayoutToggle string `json:"messages_layout_toggle,required"`
 	// Navigate to next message
-	MessagesNext string `json:"messages_next"`
+	MessagesNext string `json:"messages_next,required"`
 	// Scroll messages down by one page
-	MessagesPageDown string `json:"messages_page_down"`
+	MessagesPageDown string `json:"messages_page_down,required"`
 	// Scroll messages up by one page
-	MessagesPageUp string `json:"messages_page_up"`
+	MessagesPageUp string `json:"messages_page_up,required"`
 	// Navigate to previous message
-	MessagesPrevious string `json:"messages_previous"`
+	MessagesPrevious string `json:"messages_previous,required"`
 	// Revert message
-	MessagesRevert string `json:"messages_revert"`
+	MessagesRevert string `json:"messages_revert,required"`
 	// List available models
-	ModelList string `json:"model_list"`
-	// Initialize project configuration
-	ProjectInit string `json:"project_init"`
-	// Toggle compact mode for session
-	SessionCompact string `json:"session_compact"`
+	ModelList string `json:"model_list,required"`
+	// Create/update AGENTS.md
+	ProjectInit string `json:"project_init,required"`
+	// Compact the session
+	SessionCompact string `json:"session_compact,required"`
 	// Interrupt current session
-	SessionInterrupt string `json:"session_interrupt"`
+	SessionInterrupt string `json:"session_interrupt,required"`
 	// List all sessions
-	SessionList string `json:"session_list"`
+	SessionList string `json:"session_list,required"`
 	// Create a new session
-	SessionNew string `json:"session_new"`
+	SessionNew string `json:"session_new,required"`
 	// Share current session
-	SessionShare string `json:"session_share"`
+	SessionShare string `json:"session_share,required"`
 	// Unshare current session
-	SessionUnshare string `json:"session_unshare"`
+	SessionUnshare string `json:"session_unshare,required"`
 	// List available themes
-	ThemeList string `json:"theme_list"`
-	// Show tool details
-	ToolDetails string       `json:"tool_details"`
+	ThemeList string `json:"theme_list,required"`
+	// Toggle tool details
+	ToolDetails string       `json:"tool_details,required"`
 	JSON        keybindsJSON `json:"-"`
 }
 

+ 4 - 2
packages/tui/sdk/event.go

@@ -916,14 +916,16 @@ func (r eventListResponseEventSessionErrorJSON) RawJSON() string {
 func (r EventListResponseEventSessionError) implementsEventListResponse() {}
 
 type EventListResponseEventSessionErrorProperties struct {
-	Error EventListResponseEventSessionErrorPropertiesError `json:"error"`
-	JSON  eventListResponseEventSessionErrorPropertiesJSON  `json:"-"`
+	Error     EventListResponseEventSessionErrorPropertiesError `json:"error"`
+	SessionID string                                            `json:"sessionID"`
+	JSON      eventListResponseEventSessionErrorPropertiesJSON  `json:"-"`
 }
 
 // eventListResponseEventSessionErrorPropertiesJSON contains the JSON metadata for
 // the struct [EventListResponseEventSessionErrorProperties]
 type eventListResponseEventSessionErrorPropertiesJSON struct {
 	Error       apijson.Field
+	SessionID   apijson.Field
 	raw         string
 	ExtraFields map[string]apijson.Field
 }

+ 37 - 38
packages/tui/sdk/file.go

@@ -42,89 +42,88 @@ func (r *FileService) Read(ctx context.Context, query FileReadParams, opts ...op
 }
 
 // Get file status
-func (r *FileService) Status(ctx context.Context, opts ...option.RequestOption) (res *[]FileStatusResponse, err error) {
+func (r *FileService) Status(ctx context.Context, opts ...option.RequestOption) (res *[]File, err error) {
 	opts = append(r.Options[:], opts...)
 	path := "file/status"
 	err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
 	return
 }
 
-type FileReadResponse struct {
-	Content string               `json:"content,required"`
-	Type    FileReadResponseType `json:"type,required"`
-	JSON    fileReadResponseJSON `json:"-"`
+type File struct {
+	Added   int64      `json:"added,required"`
+	Path    string     `json:"path,required"`
+	Removed int64      `json:"removed,required"`
+	Status  FileStatus `json:"status,required"`
+	JSON    fileJSON   `json:"-"`
 }
 
-// fileReadResponseJSON contains the JSON metadata for the struct
-// [FileReadResponse]
-type fileReadResponseJSON struct {
-	Content     apijson.Field
-	Type        apijson.Field
+// fileJSON contains the JSON metadata for the struct [File]
+type fileJSON struct {
+	Added       apijson.Field
+	Path        apijson.Field
+	Removed     apijson.Field
+	Status      apijson.Field
 	raw         string
 	ExtraFields map[string]apijson.Field
 }
 
-func (r *FileReadResponse) UnmarshalJSON(data []byte) (err error) {
+func (r *File) UnmarshalJSON(data []byte) (err error) {
 	return apijson.UnmarshalRoot(data, r)
 }
 
-func (r fileReadResponseJSON) RawJSON() string {
+func (r fileJSON) RawJSON() string {
 	return r.raw
 }
 
-type FileReadResponseType string
+type FileStatus string
 
 const (
-	FileReadResponseTypeRaw   FileReadResponseType = "raw"
-	FileReadResponseTypePatch FileReadResponseType = "patch"
+	FileStatusAdded    FileStatus = "added"
+	FileStatusDeleted  FileStatus = "deleted"
+	FileStatusModified FileStatus = "modified"
 )
 
-func (r FileReadResponseType) IsKnown() bool {
+func (r FileStatus) IsKnown() bool {
 	switch r {
-	case FileReadResponseTypeRaw, FileReadResponseTypePatch:
+	case FileStatusAdded, FileStatusDeleted, FileStatusModified:
 		return true
 	}
 	return false
 }
 
-type FileStatusResponse struct {
-	Added   int64                    `json:"added,required"`
-	File    string                   `json:"file,required"`
-	Removed int64                    `json:"removed,required"`
-	Status  FileStatusResponseStatus `json:"status,required"`
-	JSON    fileStatusResponseJSON   `json:"-"`
+type FileReadResponse struct {
+	Content string               `json:"content,required"`
+	Type    FileReadResponseType `json:"type,required"`
+	JSON    fileReadResponseJSON `json:"-"`
 }
 
-// fileStatusResponseJSON contains the JSON metadata for the struct
-// [FileStatusResponse]
-type fileStatusResponseJSON struct {
-	Added       apijson.Field
-	File        apijson.Field
-	Removed     apijson.Field
-	Status      apijson.Field
+// fileReadResponseJSON contains the JSON metadata for the struct
+// [FileReadResponse]
+type fileReadResponseJSON struct {
+	Content     apijson.Field
+	Type        apijson.Field
 	raw         string
 	ExtraFields map[string]apijson.Field
 }
 
-func (r *FileStatusResponse) UnmarshalJSON(data []byte) (err error) {
+func (r *FileReadResponse) UnmarshalJSON(data []byte) (err error) {
 	return apijson.UnmarshalRoot(data, r)
 }
 
-func (r fileStatusResponseJSON) RawJSON() string {
+func (r fileReadResponseJSON) RawJSON() string {
 	return r.raw
 }
 
-type FileStatusResponseStatus string
+type FileReadResponseType string
 
 const (
-	FileStatusResponseStatusAdded    FileStatusResponseStatus = "added"
-	FileStatusResponseStatusDeleted  FileStatusResponseStatus = "deleted"
-	FileStatusResponseStatusModified FileStatusResponseStatus = "modified"
+	FileReadResponseTypeRaw   FileReadResponseType = "raw"
+	FileReadResponseTypePatch FileReadResponseType = "patch"
 )
 
-func (r FileStatusResponseStatus) IsKnown() bool {
+func (r FileReadResponseType) IsKnown() bool {
 	switch r {
-	case FileStatusResponseStatusAdded, FileStatusResponseStatusDeleted, FileStatusResponseStatusModified:
+	case FileReadResponseTypeRaw, FileReadResponseTypePatch:
 		return true
 	}
 	return false

+ 159 - 50
packages/tui/sdk/find.go

@@ -42,7 +42,7 @@ func (r *FindService) Files(ctx context.Context, query FindFilesParams, opts ...
 }
 
 // Find workspace symbols
-func (r *FindService) Symbols(ctx context.Context, query FindSymbolsParams, opts ...option.RequestOption) (res *[]FindSymbolsResponse, err error) {
+func (r *FindService) Symbols(ctx context.Context, query FindSymbolsParams, opts ...option.RequestOption) (res *[]Symbol, err error) {
 	opts = append(r.Options[:], opts...)
 	path := "find/symbol"
 	err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...)
@@ -50,27 +50,24 @@ func (r *FindService) Symbols(ctx context.Context, query FindSymbolsParams, opts
 }
 
 // Find text in files
-func (r *FindService) Text(ctx context.Context, query FindTextParams, opts ...option.RequestOption) (res *[]FindTextResponse, err error) {
+func (r *FindService) Text(ctx context.Context, query FindTextParams, opts ...option.RequestOption) (res *[]Match, err error) {
 	opts = append(r.Options[:], opts...)
 	path := "find"
 	err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...)
 	return
 }
 
-type FindSymbolsResponse = interface{}
-
-type FindTextResponse struct {
-	AbsoluteOffset float64                    `json:"absolute_offset,required"`
-	LineNumber     float64                    `json:"line_number,required"`
-	Lines          FindTextResponseLines      `json:"lines,required"`
-	Path           FindTextResponsePath       `json:"path,required"`
-	Submatches     []FindTextResponseSubmatch `json:"submatches,required"`
-	JSON           findTextResponseJSON       `json:"-"`
+type Match struct {
+	AbsoluteOffset float64         `json:"absolute_offset,required"`
+	LineNumber     float64         `json:"line_number,required"`
+	Lines          MatchLines      `json:"lines,required"`
+	Path           MatchPath       `json:"path,required"`
+	Submatches     []MatchSubmatch `json:"submatches,required"`
+	JSON           matchJSON       `json:"-"`
 }
 
-// findTextResponseJSON contains the JSON metadata for the struct
-// [FindTextResponse]
-type findTextResponseJSON struct {
+// matchJSON contains the JSON metadata for the struct [Match]
+type matchJSON struct {
 	AbsoluteOffset apijson.Field
 	LineNumber     apijson.Field
 	Lines          apijson.Field
@@ -80,66 +77,63 @@ type findTextResponseJSON struct {
 	ExtraFields    map[string]apijson.Field
 }
 
-func (r *FindTextResponse) UnmarshalJSON(data []byte) (err error) {
+func (r *Match) UnmarshalJSON(data []byte) (err error) {
 	return apijson.UnmarshalRoot(data, r)
 }
 
-func (r findTextResponseJSON) RawJSON() string {
+func (r matchJSON) RawJSON() string {
 	return r.raw
 }
 
-type FindTextResponseLines struct {
-	Text string                    `json:"text,required"`
-	JSON findTextResponseLinesJSON `json:"-"`
+type MatchLines struct {
+	Text string         `json:"text,required"`
+	JSON matchLinesJSON `json:"-"`
 }
 
-// findTextResponseLinesJSON contains the JSON metadata for the struct
-// [FindTextResponseLines]
-type findTextResponseLinesJSON struct {
+// matchLinesJSON contains the JSON metadata for the struct [MatchLines]
+type matchLinesJSON struct {
 	Text        apijson.Field
 	raw         string
 	ExtraFields map[string]apijson.Field
 }
 
-func (r *FindTextResponseLines) UnmarshalJSON(data []byte) (err error) {
+func (r *MatchLines) UnmarshalJSON(data []byte) (err error) {
 	return apijson.UnmarshalRoot(data, r)
 }
 
-func (r findTextResponseLinesJSON) RawJSON() string {
+func (r matchLinesJSON) RawJSON() string {
 	return r.raw
 }
 
-type FindTextResponsePath struct {
-	Text string                   `json:"text,required"`
-	JSON findTextResponsePathJSON `json:"-"`
+type MatchPath struct {
+	Text string        `json:"text,required"`
+	JSON matchPathJSON `json:"-"`
 }
 
-// findTextResponsePathJSON contains the JSON metadata for the struct
-// [FindTextResponsePath]
-type findTextResponsePathJSON struct {
+// matchPathJSON contains the JSON metadata for the struct [MatchPath]
+type matchPathJSON struct {
 	Text        apijson.Field
 	raw         string
 	ExtraFields map[string]apijson.Field
 }
 
-func (r *FindTextResponsePath) UnmarshalJSON(data []byte) (err error) {
+func (r *MatchPath) UnmarshalJSON(data []byte) (err error) {
 	return apijson.UnmarshalRoot(data, r)
 }
 
-func (r findTextResponsePathJSON) RawJSON() string {
+func (r matchPathJSON) RawJSON() string {
 	return r.raw
 }
 
-type FindTextResponseSubmatch struct {
-	End   float64                         `json:"end,required"`
-	Match FindTextResponseSubmatchesMatch `json:"match,required"`
-	Start float64                         `json:"start,required"`
-	JSON  findTextResponseSubmatchJSON    `json:"-"`
+type MatchSubmatch struct {
+	End   float64              `json:"end,required"`
+	Match MatchSubmatchesMatch `json:"match,required"`
+	Start float64              `json:"start,required"`
+	JSON  matchSubmatchJSON    `json:"-"`
 }
 
-// findTextResponseSubmatchJSON contains the JSON metadata for the struct
-// [FindTextResponseSubmatch]
-type findTextResponseSubmatchJSON struct {
+// matchSubmatchJSON contains the JSON metadata for the struct [MatchSubmatch]
+type matchSubmatchJSON struct {
 	End         apijson.Field
 	Match       apijson.Field
 	Start       apijson.Field
@@ -147,32 +141,147 @@ type findTextResponseSubmatchJSON struct {
 	ExtraFields map[string]apijson.Field
 }
 
-func (r *FindTextResponseSubmatch) UnmarshalJSON(data []byte) (err error) {
+func (r *MatchSubmatch) UnmarshalJSON(data []byte) (err error) {
 	return apijson.UnmarshalRoot(data, r)
 }
 
-func (r findTextResponseSubmatchJSON) RawJSON() string {
+func (r matchSubmatchJSON) RawJSON() string {
 	return r.raw
 }
 
-type FindTextResponseSubmatchesMatch struct {
-	Text string                              `json:"text,required"`
-	JSON findTextResponseSubmatchesMatchJSON `json:"-"`
+type MatchSubmatchesMatch struct {
+	Text string                   `json:"text,required"`
+	JSON matchSubmatchesMatchJSON `json:"-"`
 }
 
-// findTextResponseSubmatchesMatchJSON contains the JSON metadata for the struct
-// [FindTextResponseSubmatchesMatch]
-type findTextResponseSubmatchesMatchJSON struct {
+// matchSubmatchesMatchJSON contains the JSON metadata for the struct
+// [MatchSubmatchesMatch]
+type matchSubmatchesMatchJSON struct {
 	Text        apijson.Field
 	raw         string
 	ExtraFields map[string]apijson.Field
 }
 
-func (r *FindTextResponseSubmatchesMatch) UnmarshalJSON(data []byte) (err error) {
+func (r *MatchSubmatchesMatch) UnmarshalJSON(data []byte) (err error) {
+	return apijson.UnmarshalRoot(data, r)
+}
+
+func (r matchSubmatchesMatchJSON) RawJSON() string {
+	return r.raw
+}
+
+type Symbol struct {
+	Kind     float64        `json:"kind,required"`
+	Location SymbolLocation `json:"location,required"`
+	Name     string         `json:"name,required"`
+	JSON     symbolJSON     `json:"-"`
+}
+
+// symbolJSON contains the JSON metadata for the struct [Symbol]
+type symbolJSON struct {
+	Kind        apijson.Field
+	Location    apijson.Field
+	Name        apijson.Field
+	raw         string
+	ExtraFields map[string]apijson.Field
+}
+
+func (r *Symbol) UnmarshalJSON(data []byte) (err error) {
+	return apijson.UnmarshalRoot(data, r)
+}
+
+func (r symbolJSON) RawJSON() string {
+	return r.raw
+}
+
+type SymbolLocation struct {
+	Range SymbolLocationRange `json:"range,required"`
+	Uri   string              `json:"uri,required"`
+	JSON  symbolLocationJSON  `json:"-"`
+}
+
+// symbolLocationJSON contains the JSON metadata for the struct [SymbolLocation]
+type symbolLocationJSON struct {
+	Range       apijson.Field
+	Uri         apijson.Field
+	raw         string
+	ExtraFields map[string]apijson.Field
+}
+
+func (r *SymbolLocation) UnmarshalJSON(data []byte) (err error) {
+	return apijson.UnmarshalRoot(data, r)
+}
+
+func (r symbolLocationJSON) RawJSON() string {
+	return r.raw
+}
+
+type SymbolLocationRange struct {
+	End   SymbolLocationRangeEnd   `json:"end,required"`
+	Start SymbolLocationRangeStart `json:"start,required"`
+	JSON  symbolLocationRangeJSON  `json:"-"`
+}
+
+// symbolLocationRangeJSON contains the JSON metadata for the struct
+// [SymbolLocationRange]
+type symbolLocationRangeJSON struct {
+	End         apijson.Field
+	Start       apijson.Field
+	raw         string
+	ExtraFields map[string]apijson.Field
+}
+
+func (r *SymbolLocationRange) UnmarshalJSON(data []byte) (err error) {
+	return apijson.UnmarshalRoot(data, r)
+}
+
+func (r symbolLocationRangeJSON) RawJSON() string {
+	return r.raw
+}
+
+type SymbolLocationRangeEnd struct {
+	Character float64                    `json:"character,required"`
+	Line      float64                    `json:"line,required"`
+	JSON      symbolLocationRangeEndJSON `json:"-"`
+}
+
+// symbolLocationRangeEndJSON contains the JSON metadata for the struct
+// [SymbolLocationRangeEnd]
+type symbolLocationRangeEndJSON struct {
+	Character   apijson.Field
+	Line        apijson.Field
+	raw         string
+	ExtraFields map[string]apijson.Field
+}
+
+func (r *SymbolLocationRangeEnd) UnmarshalJSON(data []byte) (err error) {
+	return apijson.UnmarshalRoot(data, r)
+}
+
+func (r symbolLocationRangeEndJSON) RawJSON() string {
+	return r.raw
+}
+
+type SymbolLocationRangeStart struct {
+	Character float64                      `json:"character,required"`
+	Line      float64                      `json:"line,required"`
+	JSON      symbolLocationRangeStartJSON `json:"-"`
+}
+
+// symbolLocationRangeStartJSON contains the JSON metadata for the struct
+// [SymbolLocationRangeStart]
+type symbolLocationRangeStartJSON struct {
+	Character   apijson.Field
+	Line        apijson.Field
+	raw         string
+	ExtraFields map[string]apijson.Field
+}
+
+func (r *SymbolLocationRangeStart) UnmarshalJSON(data []byte) (err error) {
 	return apijson.UnmarshalRoot(data, r)
 }
 
-func (r findTextResponseSubmatchesMatchJSON) RawJSON() string {
+func (r symbolLocationRangeStartJSON) RawJSON() string {
 	return r.raw
 }
 

+ 1 - 1
packages/tui/sdk/packages/ssestream/ssestream.go

@@ -29,7 +29,7 @@ func NewDecoder(res *http.Response) Decoder {
 		decoder = t(res.Body)
 	} else {
 		scn := bufio.NewScanner(res.Body)
-		scn.Buffer(nil, (bufio.MaxScanTokenSize<<4)*10)
+		scn.Buffer(nil, bufio.MaxScanTokenSize<<9)
 		decoder = &eventStreamDecoder{rc: res.Body, scn: scn}
 	}
 	return decoder

+ 2 - 2
packages/tui/sdk/scripts/lint

@@ -7,5 +7,5 @@ cd "$(dirname "$0")/.."
 echo "==> Running Go build"
 go build .
 
-# Compile the tests but don't run them
-go test -c .
+echo "==> Checking tests compile"
+go test -run=^$ .

+ 6 - 0
stainless.yml

@@ -51,14 +51,20 @@ resources:
     methods:
       get: get /app
       init: post /app/init
+      log: post /log
 
   find:
+    models:
+      match: Match
+      symbol: Symbol
     methods:
       text: get /find
       files: get /find/file
       symbols: get /find/symbol
 
   file:
+    models:
+      file: File
     methods:
       read: get /file
       status: get /file/status