diff --git a/packages/opencode/src/cli/cmd/tui.ts b/packages/opencode/src/cli/cmd/tui.ts index 2011c26c..86b9f31b 100644 --- a/packages/opencode/src/cli/cmd/tui.ts +++ b/packages/opencode/src/cli/cmd/tui.ts @@ -16,6 +16,7 @@ import { Ide } from "../../ide" import { Flag } from "../../flag/flag" import { Session } from "../../session" import { Instance } from "../../project/instance" +import { $ } from "bun" declare global { const OPENCODE_TUI_PATH: string @@ -111,8 +112,7 @@ export const TuiCommand = cmd({ hostname: args.hostname, }) - let cmd = ["go", "run", "./main.go"] - let cwd = Bun.fileURLToPath(new URL("../../../../tui/cmd/opencode", import.meta.url)) + let cmd = [] as string[] const tui = Bun.embeddedFiles.find((item) => (item as File).name.includes("tui")) as File if (tui) { let binaryName = tui.name @@ -125,9 +125,13 @@ export const TuiCommand = cmd({ await Bun.write(file, tui, { mode: 0o755 }) await fs.chmod(binary, 0o755) } - cwd = process.cwd() cmd = [binary] } + if (!tui) { + const dir = Bun.fileURLToPath(new URL("../../../../tui/cmd/opencode", import.meta.url)) + await $`go build -o ./dist/tui ./main.go`.cwd(dir) + cmd = [path.join(dir, "dist/tui")] + } Log.Default.info("tui", { cmd, }) diff --git a/packages/opencode/src/id/id.ts b/packages/opencode/src/id/id.ts index 4e3ba9d4..f2b961e4 100644 --- a/packages/opencode/src/id/id.ts +++ b/packages/opencode/src/id/id.ts @@ -30,7 +30,7 @@ export namespace Identifier { function generateID(prefix: keyof typeof prefixes, descending: boolean, given?: string): string { if (!given) { - return generateNewID(prefix, descending) + return create(prefix, descending) } if (!given.startsWith(prefixes[prefix])) { @@ -49,8 +49,8 @@ export namespace Identifier { return result } - function generateNewID(prefix: keyof typeof prefixes, descending: boolean): string { - const currentTimestamp = Date.now() + export function create(prefix: keyof typeof prefixes, descending: boolean, timestamp?: number): string { + const currentTimestamp = timestamp ?? Date.now() if (currentTimestamp !== lastTimestamp) { lastTimestamp = currentTimestamp diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index 6b93a127..21eaa3be 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -86,6 +86,7 @@ export namespace Session { time: z.object({ created: z.number(), updated: z.number(), + compacting: z.number().optional(), }), revert: z .object({ @@ -137,12 +138,17 @@ export namespace Session { error: MessageV2.Assistant.shape.error, }), ), + Compacted: Bus.event( + "session.compacted", + z.object({ + sessionID: z.string(), + }), + ), } const state = Instance.state( () => { const pending = new Map() - const autoCompacting = new Map() const queued = new Map< string, { @@ -156,7 +162,6 @@ export namespace Session { return { pending, - autoCompacting, queued, } }, @@ -714,24 +719,8 @@ export namespace Session { })().then((x) => Provider.getModel(x.providerID, x.modelID)) let msgs = await messages(input.sessionID) - const previous = msgs.filter((x) => x.info.role === "assistant").at(-1)?.info as MessageV2.Assistant const outputLimit = Math.min(model.info.limit.output, OUTPUT_TOKEN_MAX) || OUTPUT_TOKEN_MAX - // auto summarize if too long - if (previous && previous.tokens) { - const tokens = - previous.tokens.input + previous.tokens.cache.read + previous.tokens.cache.write + previous.tokens.output - if (model.info.limit.context && tokens > Math.max((model.info.limit.context - outputLimit) * 0.9, 0)) { - state().autoCompacting.set(input.sessionID, true) - - await summarize({ - sessionID: input.sessionID, - providerID: model.providerID, - modelID: model.info.id, - }) - return prompt(input) - } - } using abort = lock(input.sessionID) const lastSummary = msgs.findLast((msg) => msg.info.role === "assistant" && msg.info.summary === true) @@ -999,7 +988,38 @@ export namespace Session { error: e, }) }, - async prepareStep({ messages }) { + async prepareStep({ messages, steps }) { + // Auto compact if too long + const tokens = (() => { + if (steps.length) { + const previous = steps.at(-1) + if (previous) return getUsage(model.info, previous.usage, previous.providerMetadata).tokens + } + const msg = msgs.findLast((x) => x.info.role === "assistant")?.info as MessageV2.Assistant + if (msg && msg.tokens) { + return msg.tokens + } + })() + if (tokens) { + log.info("compact check", tokens) + const count = tokens.input + tokens.cache.read + tokens.cache.write + tokens.output + if (model.info.limit.context && count > Math.max((model.info.limit.context - outputLimit) * 0.9, 0)) { + log.info("compacting in prepareStep") + const summarized = await summarize({ + sessionID: input.sessionID, + providerID: model.providerID, + modelID: model.info.id, + }) + const msgs = await Session.messages(input.sessionID).then((x) => + x.filter((x) => x.info.id >= summarized.id), + ) + return { + messages: MessageV2.toModelMessage(msgs), + } + } + } + + // Add queued messages to the stream const queue = (state().queued.get(input.sessionID) ?? []).filter((x) => !x.processed) if (queue.length) { for (const item of queue) { @@ -1756,10 +1776,22 @@ export namespace Session { } export async function summarize(input: { sessionID: string; providerID: string; modelID: string }) { - using abort = lock(input.sessionID) + await update(input.sessionID, (draft) => { + draft.time.compacting = Date.now() + }) + await using _ = defer(async () => { + await update(input.sessionID, (draft) => { + draft.time.compacting = undefined + }) + }) const msgs = await messages(input.sessionID) - const lastSummary = msgs.findLast((msg) => msg.info.role === "assistant" && msg.info.summary === true) - const filtered = msgs.filter((msg) => !lastSummary || msg.info.id >= lastSummary.info.id) + const start = Math.max( + 0, + msgs.findLastIndex((msg) => msg.info.role === "assistant" && msg.info.summary === true), + ) + const split = start + Math.floor((msgs.length - start) / 2) + log.info("summarizing", { start, split }) + const toSummarize = msgs.slice(start, split) const model = await Provider.getModel(input.providerID, input.modelID) const system = [ ...SystemPrompt.summarize(model.providerID), @@ -1767,36 +1799,8 @@ export namespace Session { ...(await SystemPrompt.custom()), ] - const next: MessageV2.Info = { - id: Identifier.ascending("message"), - role: "assistant", - sessionID: input.sessionID, - system, - mode: "build", - path: { - cwd: Instance.directory, - root: Instance.worktree, - }, - summary: true, - cost: 0, - modelID: input.modelID, - providerID: model.providerID, - tokens: { - input: 0, - output: 0, - reasoning: 0, - cache: { read: 0, write: 0 }, - }, - time: { - created: Date.now(), - }, - } - await updateMessage(next) - - const processor = createProcessor(next, model.info) - const stream = streamText({ + const generated = await generateText({ maxRetries: 10, - abortSignal: abort.signal, model: model.language, messages: [ ...system.map( @@ -1805,7 +1809,7 @@ export namespace Session { content: x, }), ), - ...MessageV2.toModelMessage(filtered), + ...MessageV2.toModelMessage(toSummarize), { role: "user", content: [ @@ -1817,9 +1821,45 @@ export namespace Session { }, ], }) + const usage = getUsage(model.info, generated.usage, generated.providerMetadata) + const msg: MessageV2.Info = { + id: Identifier.create("message", false, toSummarize.at(-1)!.info.time.created + 1), + role: "assistant", + sessionID: input.sessionID, + system, + mode: "build", + path: { + cwd: Instance.directory, + root: Instance.worktree, + }, + summary: true, + cost: usage.cost, + tokens: usage.tokens, + modelID: input.modelID, + providerID: model.providerID, + time: { + created: Date.now(), + completed: Date.now(), + }, + } + await updateMessage(msg) + await updatePart({ + type: "text", + sessionID: input.sessionID, + messageID: msg.id, + id: Identifier.ascending("part"), + text: generated.text, + time: { + start: Date.now(), + end: Date.now(), + }, + }) - const result = await processor.process(stream) - return result + Bus.publish(Event.Compacted, { + sessionID: input.sessionID, + }) + + return msg } function isLocked(sessionID: string) { @@ -1837,12 +1877,6 @@ export namespace Session { log.info("unlocking", { sessionID }) state().pending.delete(sessionID) - const isAutoCompacting = state().autoCompacting.get(sessionID) ?? false - if (isAutoCompacting) { - state().autoCompacting.delete(sessionID) - return - } - const session = await get(sessionID) if (session.parentID) return diff --git a/packages/sdk/go/.release-please-manifest.json b/packages/sdk/go/.release-please-manifest.json index 64f3cdd6..76d5538a 100644 --- a/packages/sdk/go/.release-please-manifest.json +++ b/packages/sdk/go/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.8.0" + ".": "0.9.0" } diff --git a/packages/sdk/go/.stats.yml b/packages/sdk/go/.stats.yml index 79ec5d88..db940705 100644 --- a/packages/sdk/go/.stats.yml +++ b/packages/sdk/go/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 43 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-97b61518d8666ea7cb310af04248e00bcf8dc9753ba3c7e84471df72b3232004.yml -openapi_spec_hash: a3500531973ad999c350b87c21aa3ab8 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-46826ba8640557721614b0c9a3f1860681d825ca8d8b12869652fa25aacb0b4c.yml +openapi_spec_hash: 33b8db6fde3021579b21325ce910197d config_hash: 026ef000d34bf2f930e7b41e77d2d3ff diff --git a/packages/sdk/go/CHANGELOG.md b/packages/sdk/go/CHANGELOG.md index c0ea4a07..fad7e683 100644 --- a/packages/sdk/go/CHANGELOG.md +++ b/packages/sdk/go/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 0.9.0 (2025-09-10) + +Full Changelog: [v0.8.0...v0.9.0](https://github.com/sst/opencode-sdk-go/compare/v0.8.0...v0.9.0) + +### Features + +- **api:** api update ([2d3a28d](https://github.com/sst/opencode-sdk-go/commit/2d3a28df5657845aa4d73087e1737d1fc8c3ce1c)) + ## 0.8.0 (2025-09-01) Full Changelog: [v0.7.0...v0.8.0](https://github.com/sst/opencode-sdk-go/compare/v0.7.0...v0.8.0) diff --git a/packages/sdk/go/README.md b/packages/sdk/go/README.md index 78011182..0fe3d32b 100644 --- a/packages/sdk/go/README.md +++ b/packages/sdk/go/README.md @@ -24,7 +24,7 @@ Or to pin the version: ```sh -go get -u 'github.com/sst/opencode-sdk-go@v0.8.0' +go get -u 'github.com/sst/opencode-sdk-go@v0.9.0' ``` diff --git a/packages/sdk/go/app.go b/packages/sdk/go/app.go index 53a8aeb4..62d86f93 100644 --- a/packages/sdk/go/app.go +++ b/packages/sdk/go/app.go @@ -50,33 +50,37 @@ func (r *AppService) Providers(ctx context.Context, query AppProvidersParams, op } type Model struct { - ID string `json:"id,required"` - Attachment bool `json:"attachment,required"` - Cost ModelCost `json:"cost,required"` - Limit ModelLimit `json:"limit,required"` - Name string `json:"name,required"` - Options map[string]interface{} `json:"options,required"` - Reasoning bool `json:"reasoning,required"` - ReleaseDate string `json:"release_date,required"` - Temperature bool `json:"temperature,required"` - ToolCall bool `json:"tool_call,required"` - JSON modelJSON `json:"-"` + ID string `json:"id,required"` + Attachment bool `json:"attachment,required"` + Cost ModelCost `json:"cost,required"` + Limit ModelLimit `json:"limit,required"` + Name string `json:"name,required"` + Options map[string]interface{} `json:"options,required"` + Reasoning bool `json:"reasoning,required"` + ReleaseDate string `json:"release_date,required"` + Temperature bool `json:"temperature,required"` + ToolCall bool `json:"tool_call,required"` + Experimental bool `json:"experimental"` + Provider ModelProvider `json:"provider"` + JSON modelJSON `json:"-"` } // modelJSON contains the JSON metadata for the struct [Model] type modelJSON struct { - ID apijson.Field - Attachment apijson.Field - Cost apijson.Field - Limit apijson.Field - Name apijson.Field - Options apijson.Field - Reasoning apijson.Field - ReleaseDate apijson.Field - Temperature apijson.Field - ToolCall apijson.Field - raw string - ExtraFields map[string]apijson.Field + ID apijson.Field + Attachment apijson.Field + Cost apijson.Field + Limit apijson.Field + Name apijson.Field + Options apijson.Field + Reasoning apijson.Field + ReleaseDate apijson.Field + Temperature apijson.Field + ToolCall apijson.Field + Experimental apijson.Field + Provider apijson.Field + raw string + ExtraFields map[string]apijson.Field } func (r *Model) UnmarshalJSON(data []byte) (err error) { @@ -135,6 +139,26 @@ func (r modelLimitJSON) RawJSON() string { return r.raw } +type ModelProvider struct { + Npm string `json:"npm,required"` + JSON modelProviderJSON `json:"-"` +} + +// modelProviderJSON contains the JSON metadata for the struct [ModelProvider] +type modelProviderJSON struct { + Npm apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *ModelProvider) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r modelProviderJSON) RawJSON() string { + return r.raw +} + type Provider struct { ID string `json:"id,required"` Env []string `json:"env,required"` diff --git a/packages/sdk/go/config.go b/packages/sdk/go/config.go index 5469fb29..b0473f84 100644 --- a/packages/sdk/go/config.go +++ b/packages/sdk/go/config.go @@ -1562,34 +1562,38 @@ func (r configProviderJSON) RawJSON() string { } type ConfigProviderModel struct { - ID string `json:"id"` - Attachment bool `json:"attachment"` - Cost ConfigProviderModelsCost `json:"cost"` - Limit ConfigProviderModelsLimit `json:"limit"` - Name string `json:"name"` - Options map[string]interface{} `json:"options"` - Reasoning bool `json:"reasoning"` - ReleaseDate string `json:"release_date"` - Temperature bool `json:"temperature"` - ToolCall bool `json:"tool_call"` - JSON configProviderModelJSON `json:"-"` + ID string `json:"id"` + Attachment bool `json:"attachment"` + Cost ConfigProviderModelsCost `json:"cost"` + Experimental bool `json:"experimental"` + Limit ConfigProviderModelsLimit `json:"limit"` + Name string `json:"name"` + Options map[string]interface{} `json:"options"` + Provider ConfigProviderModelsProvider `json:"provider"` + Reasoning bool `json:"reasoning"` + ReleaseDate string `json:"release_date"` + Temperature bool `json:"temperature"` + ToolCall bool `json:"tool_call"` + JSON configProviderModelJSON `json:"-"` } // configProviderModelJSON contains the JSON metadata for the struct // [ConfigProviderModel] type configProviderModelJSON struct { - ID apijson.Field - Attachment apijson.Field - Cost apijson.Field - Limit apijson.Field - Name apijson.Field - Options apijson.Field - Reasoning apijson.Field - ReleaseDate apijson.Field - Temperature apijson.Field - ToolCall apijson.Field - raw string - ExtraFields map[string]apijson.Field + ID apijson.Field + Attachment apijson.Field + Cost apijson.Field + Experimental apijson.Field + Limit apijson.Field + Name apijson.Field + Options apijson.Field + Provider apijson.Field + Reasoning apijson.Field + ReleaseDate apijson.Field + Temperature apijson.Field + ToolCall apijson.Field + raw string + ExtraFields map[string]apijson.Field } func (r *ConfigProviderModel) UnmarshalJSON(data []byte) (err error) { @@ -1650,6 +1654,27 @@ func (r configProviderModelsLimitJSON) RawJSON() string { return r.raw } +type ConfigProviderModelsProvider struct { + Npm string `json:"npm,required"` + JSON configProviderModelsProviderJSON `json:"-"` +} + +// configProviderModelsProviderJSON contains the JSON metadata for the struct +// [ConfigProviderModelsProvider] +type configProviderModelsProviderJSON struct { + Npm apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *ConfigProviderModelsProvider) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r configProviderModelsProviderJSON) RawJSON() string { + return r.raw +} + type ConfigProviderOptions struct { APIKey string `json:"apiKey"` BaseURL string `json:"baseURL"` diff --git a/packages/sdk/go/event.go b/packages/sdk/go/event.go index 2f44f907..00ba202c 100644 --- a/packages/sdk/go/event.go +++ b/packages/sdk/go/event.go @@ -63,7 +63,8 @@ type EventListResponse struct { // [EventListResponseEventSessionUpdatedProperties], // [EventListResponseEventSessionDeletedProperties], // [EventListResponseEventSessionIdleProperties], - // [EventListResponseEventSessionErrorProperties], [interface{}]. + // [EventListResponseEventSessionErrorProperties], + // [EventListResponseEventSessionCompactedProperties], [interface{}]. Properties interface{} `json:"properties,required"` Type EventListResponseType `json:"type,required"` JSON eventListResponseJSON `json:"-"` @@ -105,6 +106,7 @@ func (r *EventListResponse) UnmarshalJSON(data []byte) (err error) { // [EventListResponseEventPermissionReplied], [EventListResponseEventFileEdited], // [EventListResponseEventSessionUpdated], [EventListResponseEventSessionDeleted], // [EventListResponseEventSessionIdle], [EventListResponseEventSessionError], +// [EventListResponseEventSessionCompacted], // [EventListResponseEventServerConnected]. func (r EventListResponse) AsUnion() EventListResponseUnion { return r.union @@ -118,7 +120,8 @@ func (r EventListResponse) AsUnion() EventListResponseUnion { // [EventListResponseEventPermissionUpdated], // [EventListResponseEventPermissionReplied], [EventListResponseEventFileEdited], // [EventListResponseEventSessionUpdated], [EventListResponseEventSessionDeleted], -// [EventListResponseEventSessionIdle], [EventListResponseEventSessionError] or +// [EventListResponseEventSessionIdle], [EventListResponseEventSessionError], +// [EventListResponseEventSessionCompacted] or // [EventListResponseEventServerConnected]. type EventListResponseUnion interface { implementsEventListResponse() @@ -193,6 +196,11 @@ func init() { Type: reflect.TypeOf(EventListResponseEventSessionError{}), DiscriminatorValue: "session.error", }, + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(EventListResponseEventSessionCompacted{}), + DiscriminatorValue: "session.compacted", + }, apijson.UnionVariant{ TypeFilter: gjson.JSON, Type: reflect.TypeOf(EventListResponseEventServerConnected{}), @@ -1108,6 +1116,66 @@ func (r EventListResponseEventSessionErrorType) IsKnown() bool { return false } +type EventListResponseEventSessionCompacted struct { + Properties EventListResponseEventSessionCompactedProperties `json:"properties,required"` + Type EventListResponseEventSessionCompactedType `json:"type,required"` + JSON eventListResponseEventSessionCompactedJSON `json:"-"` +} + +// eventListResponseEventSessionCompactedJSON contains the JSON metadata for the +// struct [EventListResponseEventSessionCompacted] +type eventListResponseEventSessionCompactedJSON struct { + Properties apijson.Field + Type apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *EventListResponseEventSessionCompacted) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r eventListResponseEventSessionCompactedJSON) RawJSON() string { + return r.raw +} + +func (r EventListResponseEventSessionCompacted) implementsEventListResponse() {} + +type EventListResponseEventSessionCompactedProperties struct { + SessionID string `json:"sessionID,required"` + JSON eventListResponseEventSessionCompactedPropertiesJSON `json:"-"` +} + +// eventListResponseEventSessionCompactedPropertiesJSON contains the JSON metadata +// for the struct [EventListResponseEventSessionCompactedProperties] +type eventListResponseEventSessionCompactedPropertiesJSON struct { + SessionID apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *EventListResponseEventSessionCompactedProperties) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r eventListResponseEventSessionCompactedPropertiesJSON) RawJSON() string { + return r.raw +} + +type EventListResponseEventSessionCompactedType string + +const ( + EventListResponseEventSessionCompactedTypeSessionCompacted EventListResponseEventSessionCompactedType = "session.compacted" +) + +func (r EventListResponseEventSessionCompactedType) IsKnown() bool { + switch r { + case EventListResponseEventSessionCompactedTypeSessionCompacted: + return true + } + return false +} + type EventListResponseEventServerConnected struct { Properties interface{} `json:"properties,required"` Type EventListResponseEventServerConnectedType `json:"type,required"` @@ -1163,12 +1231,13 @@ const ( EventListResponseTypeSessionDeleted EventListResponseType = "session.deleted" EventListResponseTypeSessionIdle EventListResponseType = "session.idle" EventListResponseTypeSessionError EventListResponseType = "session.error" + EventListResponseTypeSessionCompacted EventListResponseType = "session.compacted" EventListResponseTypeServerConnected EventListResponseType = "server.connected" ) func (r EventListResponseType) IsKnown() bool { switch r { - case EventListResponseTypeInstallationUpdated, EventListResponseTypeLspClientDiagnostics, EventListResponseTypeMessageUpdated, EventListResponseTypeMessageRemoved, EventListResponseTypeMessagePartUpdated, EventListResponseTypeMessagePartRemoved, EventListResponseTypePermissionUpdated, EventListResponseTypePermissionReplied, EventListResponseTypeFileEdited, EventListResponseTypeSessionUpdated, EventListResponseTypeSessionDeleted, EventListResponseTypeSessionIdle, EventListResponseTypeSessionError, EventListResponseTypeServerConnected: + case EventListResponseTypeInstallationUpdated, EventListResponseTypeLspClientDiagnostics, EventListResponseTypeMessageUpdated, EventListResponseTypeMessageRemoved, EventListResponseTypeMessagePartUpdated, EventListResponseTypeMessagePartRemoved, EventListResponseTypePermissionUpdated, EventListResponseTypePermissionReplied, EventListResponseTypeFileEdited, EventListResponseTypeSessionUpdated, EventListResponseTypeSessionDeleted, EventListResponseTypeSessionIdle, EventListResponseTypeSessionError, EventListResponseTypeSessionCompacted, EventListResponseTypeServerConnected: return true } return false diff --git a/packages/sdk/go/file.go b/packages/sdk/go/file.go index 3e1b2f42..bc36075f 100644 --- a/packages/sdk/go/file.go +++ b/packages/sdk/go/file.go @@ -100,15 +100,17 @@ func (r FileStatus) IsKnown() bool { } type FileNode struct { - Ignored bool `json:"ignored,required"` - Name string `json:"name,required"` - Path string `json:"path,required"` - Type FileNodeType `json:"type,required"` - JSON fileNodeJSON `json:"-"` + Absolute string `json:"absolute,required"` + Ignored bool `json:"ignored,required"` + Name string `json:"name,required"` + Path string `json:"path,required"` + Type FileNodeType `json:"type,required"` + JSON fileNodeJSON `json:"-"` } // fileNodeJSON contains the JSON metadata for the struct [FileNode] type fileNodeJSON struct { + Absolute apijson.Field Ignored apijson.Field Name apijson.Field Path apijson.Field @@ -141,16 +143,18 @@ func (r FileNodeType) IsKnown() bool { } type FileReadResponse struct { - Content string `json:"content,required"` - Type FileReadResponseType `json:"type,required"` - JSON fileReadResponseJSON `json:"-"` + Content string `json:"content,required"` + Diff string `json:"diff"` + Patch FileReadResponsePatch `json:"patch"` + JSON fileReadResponseJSON `json:"-"` } // fileReadResponseJSON contains the JSON metadata for the struct // [FileReadResponse] type fileReadResponseJSON struct { Content apijson.Field - Type apijson.Field + Diff apijson.Field + Patch apijson.Field raw string ExtraFields map[string]apijson.Field } @@ -163,19 +167,64 @@ func (r fileReadResponseJSON) RawJSON() string { return r.raw } -type FileReadResponseType string +type FileReadResponsePatch struct { + Hunks []FileReadResponsePatchHunk `json:"hunks,required"` + NewFileName string `json:"newFileName,required"` + OldFileName string `json:"oldFileName,required"` + Index string `json:"index"` + NewHeader string `json:"newHeader"` + OldHeader string `json:"oldHeader"` + JSON fileReadResponsePatchJSON `json:"-"` +} -const ( - FileReadResponseTypeRaw FileReadResponseType = "raw" - FileReadResponseTypePatch FileReadResponseType = "patch" -) +// fileReadResponsePatchJSON contains the JSON metadata for the struct +// [FileReadResponsePatch] +type fileReadResponsePatchJSON struct { + Hunks apijson.Field + NewFileName apijson.Field + OldFileName apijson.Field + Index apijson.Field + NewHeader apijson.Field + OldHeader apijson.Field + raw string + ExtraFields map[string]apijson.Field +} -func (r FileReadResponseType) IsKnown() bool { - switch r { - case FileReadResponseTypeRaw, FileReadResponseTypePatch: - return true - } - return false +func (r *FileReadResponsePatch) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r fileReadResponsePatchJSON) RawJSON() string { + return r.raw +} + +type FileReadResponsePatchHunk struct { + Lines []string `json:"lines,required"` + NewLines float64 `json:"newLines,required"` + NewStart float64 `json:"newStart,required"` + OldLines float64 `json:"oldLines,required"` + OldStart float64 `json:"oldStart,required"` + JSON fileReadResponsePatchHunkJSON `json:"-"` +} + +// fileReadResponsePatchHunkJSON contains the JSON metadata for the struct +// [FileReadResponsePatchHunk] +type fileReadResponsePatchHunkJSON struct { + Lines apijson.Field + NewLines apijson.Field + NewStart apijson.Field + OldLines apijson.Field + OldStart apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *FileReadResponsePatchHunk) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r fileReadResponsePatchHunkJSON) RawJSON() string { + return r.raw } type FileListParams struct { diff --git a/packages/sdk/go/internal/version.go b/packages/sdk/go/internal/version.go index 3c8392e9..0e818c5b 100644 --- a/packages/sdk/go/internal/version.go +++ b/packages/sdk/go/internal/version.go @@ -2,4 +2,4 @@ package internal -const PackageVersion = "0.8.0" // x-release-please-version +const PackageVersion = "0.9.0" // x-release-please-version diff --git a/packages/sdk/go/session.go b/packages/sdk/go/session.go index 88d71b57..6696e0fa 100644 --- a/packages/sdk/go/session.go +++ b/packages/sdk/go/session.go @@ -1332,15 +1332,17 @@ func (r sessionJSON) RawJSON() string { } type SessionTime struct { - Created float64 `json:"created,required"` - Updated float64 `json:"updated,required"` - JSON sessionTimeJSON `json:"-"` + Created float64 `json:"created,required"` + Updated float64 `json:"updated,required"` + Compacting float64 `json:"compacting"` + JSON sessionTimeJSON `json:"-"` } // sessionTimeJSON contains the JSON metadata for the struct [SessionTime] type sessionTimeJSON struct { Created apijson.Field Updated apijson.Field + Compacting apijson.Field raw string ExtraFields map[string]apijson.Field } diff --git a/packages/tui/internal/app/app.go b/packages/tui/internal/app/app.go index 4b333a93..85b21165 100644 --- a/packages/tui/internal/app/app.go +++ b/packages/tui/internal/app/app.go @@ -653,6 +653,9 @@ func getDefaultModel( } func (a *App) IsBusy() bool { + if a.Session.Time.Compacting > 0 { + return true + } if len(a.Messages) == 0 { return false } diff --git a/packages/tui/internal/components/chat/editor.go b/packages/tui/internal/components/chat/editor.go index 907734a1..12199392 100644 --- a/packages/tui/internal/components/chat/editor.go +++ b/packages/tui/internal/components/chat/editor.go @@ -385,6 +385,9 @@ func (m *editorComponent) Content() string { } else if m.app.IsBusy() { keyText := m.getInterruptKeyText() status := "working" + if m.app.Session.Time.Compacting > 0 { + status = "compacting" + } if m.app.CurrentPermission.ID != "" { status = "waiting for permission" } diff --git a/packages/tui/internal/components/chat/messages.go b/packages/tui/internal/components/chat/messages.go index 053d538a..8e36d99c 100644 --- a/packages/tui/internal/components/chat/messages.go +++ b/packages/tui/internal/components/chat/messages.go @@ -365,6 +365,9 @@ func (m *messagesComponent) renderView() tea.Cmd { lastAssistantMessage := "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" for _, msg := range slices.Backward(m.app.Messages) { if assistant, ok := msg.Info.(opencode.AssistantMessage); ok { + if assistant.Time.Completed > 0 { + break + } lastAssistantMessage = assistant.ID break } @@ -475,6 +478,9 @@ func (m *messagesComponent) renderView() tea.Cmd { } case opencode.AssistantMessage: + if casted.Summary { + continue + } if casted.ID == m.app.Session.Revert.MessageID { reverted = true revertedMessageCount = 1 diff --git a/packages/tui/internal/tui/tui.go b/packages/tui/internal/tui/tui.go index 62a647a1..69d02331 100644 --- a/packages/tui/internal/tui/tui.go +++ b/packages/tui/internal/tui/tui.go @@ -592,10 +592,40 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } if matchIndex == -1 { - a.app.Messages = append(a.app.Messages, app.Message{ + // Extract the new message ID + var newMessageID string + switch casted := msg.Properties.Info.AsUnion().(type) { + case opencode.UserMessage: + newMessageID = casted.ID + case opencode.AssistantMessage: + newMessageID = casted.ID + } + + // Find the correct insertion index by scanning backwards + // Most messages are added to the end, so start from the end + insertIndex := len(a.app.Messages) + for i := len(a.app.Messages) - 1; i >= 0; i-- { + var existingID string + switch casted := a.app.Messages[i].Info.(type) { + case opencode.UserMessage: + existingID = casted.ID + case opencode.AssistantMessage: + existingID = casted.ID + } + if existingID < newMessageID { + insertIndex = i + 1 + break + } + } + + // Create the new message + newMessage := app.Message{ Info: msg.Properties.Info.AsUnion(), Parts: []opencode.PartUnion{}, - }) + } + + // Insert at the correct position + a.app.Messages = append(a.app.Messages[:insertIndex], append([]app.Message{newMessage}, a.app.Messages[insertIndex:]...)...) } } case opencode.EventListResponseEventPermissionUpdated: @@ -627,6 +657,10 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { slog.Error("Server error", "name", err.Name, "message", err.Data.Message) return a, toast.NewErrorToast(err.Data.Message, toast.WithTitle(string(err.Name))) } + case opencode.EventListResponseEventSessionCompacted: + if msg.Properties.SessionID == a.app.Session.ID { + return a, toast.NewSuccessToast("Session compacted successfully") + } case tea.WindowSizeMsg: msg.Height -= 2 // Make space for the status bar a.width, a.height = msg.Width, msg.Height