summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--packages/opencode/src/cli/cmd/tui.ts10
-rw-r--r--packages/opencode/src/id/id.ts6
-rw-r--r--packages/opencode/src/session/index.ts154
-rw-r--r--packages/sdk/go/.release-please-manifest.json2
-rw-r--r--packages/sdk/go/.stats.yml4
-rw-r--r--packages/sdk/go/CHANGELOG.md8
-rw-r--r--packages/sdk/go/README.md2
-rw-r--r--packages/sdk/go/app.go70
-rw-r--r--packages/sdk/go/config.go71
-rw-r--r--packages/sdk/go/event.go75
-rw-r--r--packages/sdk/go/file.go89
-rw-r--r--packages/sdk/go/internal/version.go2
-rw-r--r--packages/sdk/go/session.go8
-rw-r--r--packages/tui/internal/app/app.go3
-rw-r--r--packages/tui/internal/components/chat/editor.go3
-rw-r--r--packages/tui/internal/components/chat/messages.go6
-rw-r--r--packages/tui/internal/tui/tui.go38
17 files changed, 406 insertions, 145 deletions
diff --git a/packages/opencode/src/cli/cmd/tui.ts b/packages/opencode/src/cli/cmd/tui.ts
index 2011c26cb..86b9f31b8 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 4e3ba9d48..f2b961e45 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 6b93a1275..21eaa3be8 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<string, AbortController>()
- const autoCompacting = new Map<string, boolean>()
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 64f3cdd64..76d5538a4 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 79ec5d889..db9407052 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 c0ea4a073..fad7e6837 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 780111828..0fe3d32bf 100644
--- a/packages/sdk/go/README.md
+++ b/packages/sdk/go/README.md
@@ -24,7 +24,7 @@ Or to pin the version:
<!-- x-release-please-start-version -->
```sh
-go get -u 'github.com/sst/[email protected]'
+go get -u 'github.com/sst/[email protected]'
```
<!-- x-release-please-end -->
diff --git a/packages/sdk/go/app.go b/packages/sdk/go/app.go
index 53a8aeb4a..62d86f93f 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 5469fb29e..b0473f844 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 2f44f907e..00ba202cc 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()
@@ -195,6 +198,11 @@ func init() {
},
apijson.UnionVariant{
TypeFilter: gjson.JSON,
+ Type: reflect.TypeOf(EventListResponseEventSessionCompacted{}),
+ DiscriminatorValue: "session.compacted",
+ },
+ apijson.UnionVariant{
+ TypeFilter: gjson.JSON,
Type: reflect.TypeOf(EventListResponseEventServerConnected{}),
DiscriminatorValue: "server.connected",
},
@@ -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 3e1b2f42f..bc36075f1 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 3c8392e9d..0e818c5b2 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 88d71b575..6696e0faf 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 4b333a93c..85b21165c 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 907734a12..121993927 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 053d538a4..8e36d99c1 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 62a647a14..69d023315 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