compaction improvements

This commit is contained in:
Dax Raad
2025-09-11 02:22:14 -04:00
parent c3a55c35bb
commit 4c94753eda
17 changed files with 406 additions and 145 deletions

View File

@@ -16,6 +16,7 @@ import { Ide } from "../../ide"
import { Flag } from "../../flag/flag" import { Flag } from "../../flag/flag"
import { Session } from "../../session" import { Session } from "../../session"
import { Instance } from "../../project/instance" import { Instance } from "../../project/instance"
import { $ } from "bun"
declare global { declare global {
const OPENCODE_TUI_PATH: string const OPENCODE_TUI_PATH: string
@@ -111,8 +112,7 @@ export const TuiCommand = cmd({
hostname: args.hostname, hostname: args.hostname,
}) })
let cmd = ["go", "run", "./main.go"] let cmd = [] as string[]
let cwd = Bun.fileURLToPath(new URL("../../../../tui/cmd/opencode", import.meta.url))
const tui = Bun.embeddedFiles.find((item) => (item as File).name.includes("tui")) as File const tui = Bun.embeddedFiles.find((item) => (item as File).name.includes("tui")) as File
if (tui) { if (tui) {
let binaryName = tui.name let binaryName = tui.name
@@ -125,9 +125,13 @@ export const TuiCommand = cmd({
await Bun.write(file, tui, { mode: 0o755 }) await Bun.write(file, tui, { mode: 0o755 })
await fs.chmod(binary, 0o755) await fs.chmod(binary, 0o755)
} }
cwd = process.cwd()
cmd = [binary] 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", { Log.Default.info("tui", {
cmd, cmd,
}) })

View File

@@ -30,7 +30,7 @@ export namespace Identifier {
function generateID(prefix: keyof typeof prefixes, descending: boolean, given?: string): string { function generateID(prefix: keyof typeof prefixes, descending: boolean, given?: string): string {
if (!given) { if (!given) {
return generateNewID(prefix, descending) return create(prefix, descending)
} }
if (!given.startsWith(prefixes[prefix])) { if (!given.startsWith(prefixes[prefix])) {
@@ -49,8 +49,8 @@ export namespace Identifier {
return result return result
} }
function generateNewID(prefix: keyof typeof prefixes, descending: boolean): string { export function create(prefix: keyof typeof prefixes, descending: boolean, timestamp?: number): string {
const currentTimestamp = Date.now() const currentTimestamp = timestamp ?? Date.now()
if (currentTimestamp !== lastTimestamp) { if (currentTimestamp !== lastTimestamp) {
lastTimestamp = currentTimestamp lastTimestamp = currentTimestamp

View File

@@ -86,6 +86,7 @@ export namespace Session {
time: z.object({ time: z.object({
created: z.number(), created: z.number(),
updated: z.number(), updated: z.number(),
compacting: z.number().optional(),
}), }),
revert: z revert: z
.object({ .object({
@@ -137,12 +138,17 @@ export namespace Session {
error: MessageV2.Assistant.shape.error, error: MessageV2.Assistant.shape.error,
}), }),
), ),
Compacted: Bus.event(
"session.compacted",
z.object({
sessionID: z.string(),
}),
),
} }
const state = Instance.state( const state = Instance.state(
() => { () => {
const pending = new Map<string, AbortController>() const pending = new Map<string, AbortController>()
const autoCompacting = new Map<string, boolean>()
const queued = new Map< const queued = new Map<
string, string,
{ {
@@ -156,7 +162,6 @@ export namespace Session {
return { return {
pending, pending,
autoCompacting,
queued, queued,
} }
}, },
@@ -714,24 +719,8 @@ export namespace Session {
})().then((x) => Provider.getModel(x.providerID, x.modelID)) })().then((x) => Provider.getModel(x.providerID, x.modelID))
let msgs = await messages(input.sessionID) 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 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) using abort = lock(input.sessionID)
const lastSummary = msgs.findLast((msg) => msg.info.role === "assistant" && msg.info.summary === true) const lastSummary = msgs.findLast((msg) => msg.info.role === "assistant" && msg.info.summary === true)
@@ -999,7 +988,38 @@ export namespace Session {
error: e, 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) const queue = (state().queued.get(input.sessionID) ?? []).filter((x) => !x.processed)
if (queue.length) { if (queue.length) {
for (const item of queue) { for (const item of queue) {
@@ -1756,10 +1776,22 @@ export namespace Session {
} }
export async function summarize(input: { sessionID: string; providerID: string; modelID: string }) { 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 msgs = await messages(input.sessionID)
const lastSummary = msgs.findLast((msg) => msg.info.role === "assistant" && msg.info.summary === true) const start = Math.max(
const filtered = msgs.filter((msg) => !lastSummary || msg.info.id >= lastSummary.info.id) 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 model = await Provider.getModel(input.providerID, input.modelID)
const system = [ const system = [
...SystemPrompt.summarize(model.providerID), ...SystemPrompt.summarize(model.providerID),
@@ -1767,36 +1799,8 @@ export namespace Session {
...(await SystemPrompt.custom()), ...(await SystemPrompt.custom()),
] ]
const next: MessageV2.Info = { const generated = await generateText({
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({
maxRetries: 10, maxRetries: 10,
abortSignal: abort.signal,
model: model.language, model: model.language,
messages: [ messages: [
...system.map( ...system.map(
@@ -1805,7 +1809,7 @@ export namespace Session {
content: x, content: x,
}), }),
), ),
...MessageV2.toModelMessage(filtered), ...MessageV2.toModelMessage(toSummarize),
{ {
role: "user", role: "user",
content: [ 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) Bus.publish(Event.Compacted, {
return result sessionID: input.sessionID,
})
return msg
} }
function isLocked(sessionID: string) { function isLocked(sessionID: string) {
@@ -1837,12 +1877,6 @@ export namespace Session {
log.info("unlocking", { sessionID }) log.info("unlocking", { sessionID })
state().pending.delete(sessionID) state().pending.delete(sessionID)
const isAutoCompacting = state().autoCompacting.get(sessionID) ?? false
if (isAutoCompacting) {
state().autoCompacting.delete(sessionID)
return
}
const session = await get(sessionID) const session = await get(sessionID)
if (session.parentID) return if (session.parentID) return

View File

@@ -1,3 +1,3 @@
{ {
".": "0.8.0" ".": "0.9.0"
} }

View File

@@ -1,4 +1,4 @@
configured_endpoints: 43 configured_endpoints: 43
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-97b61518d8666ea7cb310af04248e00bcf8dc9753ba3c7e84471df72b3232004.yml openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-46826ba8640557721614b0c9a3f1860681d825ca8d8b12869652fa25aacb0b4c.yml
openapi_spec_hash: a3500531973ad999c350b87c21aa3ab8 openapi_spec_hash: 33b8db6fde3021579b21325ce910197d
config_hash: 026ef000d34bf2f930e7b41e77d2d3ff config_hash: 026ef000d34bf2f930e7b41e77d2d3ff

View File

@@ -1,5 +1,13 @@
# Changelog # 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) ## 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) Full Changelog: [v0.7.0...v0.8.0](https://github.com/sst/opencode-sdk-go/compare/v0.7.0...v0.8.0)

View File

@@ -24,7 +24,7 @@ Or to pin the version:
<!-- x-release-please-start-version --> <!-- x-release-please-start-version -->
```sh ```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'
``` ```
<!-- x-release-please-end --> <!-- x-release-please-end -->

View File

@@ -60,6 +60,8 @@ type Model struct {
ReleaseDate string `json:"release_date,required"` ReleaseDate string `json:"release_date,required"`
Temperature bool `json:"temperature,required"` Temperature bool `json:"temperature,required"`
ToolCall bool `json:"tool_call,required"` ToolCall bool `json:"tool_call,required"`
Experimental bool `json:"experimental"`
Provider ModelProvider `json:"provider"`
JSON modelJSON `json:"-"` JSON modelJSON `json:"-"`
} }
@@ -75,6 +77,8 @@ type modelJSON struct {
ReleaseDate apijson.Field ReleaseDate apijson.Field
Temperature apijson.Field Temperature apijson.Field
ToolCall apijson.Field ToolCall apijson.Field
Experimental apijson.Field
Provider apijson.Field
raw string raw string
ExtraFields map[string]apijson.Field ExtraFields map[string]apijson.Field
} }
@@ -135,6 +139,26 @@ func (r modelLimitJSON) RawJSON() string {
return r.raw 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 { type Provider struct {
ID string `json:"id,required"` ID string `json:"id,required"`
Env []string `json:"env,required"` Env []string `json:"env,required"`

View File

@@ -1565,9 +1565,11 @@ type ConfigProviderModel struct {
ID string `json:"id"` ID string `json:"id"`
Attachment bool `json:"attachment"` Attachment bool `json:"attachment"`
Cost ConfigProviderModelsCost `json:"cost"` Cost ConfigProviderModelsCost `json:"cost"`
Experimental bool `json:"experimental"`
Limit ConfigProviderModelsLimit `json:"limit"` Limit ConfigProviderModelsLimit `json:"limit"`
Name string `json:"name"` Name string `json:"name"`
Options map[string]interface{} `json:"options"` Options map[string]interface{} `json:"options"`
Provider ConfigProviderModelsProvider `json:"provider"`
Reasoning bool `json:"reasoning"` Reasoning bool `json:"reasoning"`
ReleaseDate string `json:"release_date"` ReleaseDate string `json:"release_date"`
Temperature bool `json:"temperature"` Temperature bool `json:"temperature"`
@@ -1581,9 +1583,11 @@ type configProviderModelJSON struct {
ID apijson.Field ID apijson.Field
Attachment apijson.Field Attachment apijson.Field
Cost apijson.Field Cost apijson.Field
Experimental apijson.Field
Limit apijson.Field Limit apijson.Field
Name apijson.Field Name apijson.Field
Options apijson.Field Options apijson.Field
Provider apijson.Field
Reasoning apijson.Field Reasoning apijson.Field
ReleaseDate apijson.Field ReleaseDate apijson.Field
Temperature apijson.Field Temperature apijson.Field
@@ -1650,6 +1654,27 @@ func (r configProviderModelsLimitJSON) RawJSON() string {
return r.raw 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 { type ConfigProviderOptions struct {
APIKey string `json:"apiKey"` APIKey string `json:"apiKey"`
BaseURL string `json:"baseURL"` BaseURL string `json:"baseURL"`

View File

@@ -63,7 +63,8 @@ type EventListResponse struct {
// [EventListResponseEventSessionUpdatedProperties], // [EventListResponseEventSessionUpdatedProperties],
// [EventListResponseEventSessionDeletedProperties], // [EventListResponseEventSessionDeletedProperties],
// [EventListResponseEventSessionIdleProperties], // [EventListResponseEventSessionIdleProperties],
// [EventListResponseEventSessionErrorProperties], [interface{}]. // [EventListResponseEventSessionErrorProperties],
// [EventListResponseEventSessionCompactedProperties], [interface{}].
Properties interface{} `json:"properties,required"` Properties interface{} `json:"properties,required"`
Type EventListResponseType `json:"type,required"` Type EventListResponseType `json:"type,required"`
JSON eventListResponseJSON `json:"-"` JSON eventListResponseJSON `json:"-"`
@@ -105,6 +106,7 @@ func (r *EventListResponse) UnmarshalJSON(data []byte) (err error) {
// [EventListResponseEventPermissionReplied], [EventListResponseEventFileEdited], // [EventListResponseEventPermissionReplied], [EventListResponseEventFileEdited],
// [EventListResponseEventSessionUpdated], [EventListResponseEventSessionDeleted], // [EventListResponseEventSessionUpdated], [EventListResponseEventSessionDeleted],
// [EventListResponseEventSessionIdle], [EventListResponseEventSessionError], // [EventListResponseEventSessionIdle], [EventListResponseEventSessionError],
// [EventListResponseEventSessionCompacted],
// [EventListResponseEventServerConnected]. // [EventListResponseEventServerConnected].
func (r EventListResponse) AsUnion() EventListResponseUnion { func (r EventListResponse) AsUnion() EventListResponseUnion {
return r.union return r.union
@@ -118,7 +120,8 @@ func (r EventListResponse) AsUnion() EventListResponseUnion {
// [EventListResponseEventPermissionUpdated], // [EventListResponseEventPermissionUpdated],
// [EventListResponseEventPermissionReplied], [EventListResponseEventFileEdited], // [EventListResponseEventPermissionReplied], [EventListResponseEventFileEdited],
// [EventListResponseEventSessionUpdated], [EventListResponseEventSessionDeleted], // [EventListResponseEventSessionUpdated], [EventListResponseEventSessionDeleted],
// [EventListResponseEventSessionIdle], [EventListResponseEventSessionError] or // [EventListResponseEventSessionIdle], [EventListResponseEventSessionError],
// [EventListResponseEventSessionCompacted] or
// [EventListResponseEventServerConnected]. // [EventListResponseEventServerConnected].
type EventListResponseUnion interface { type EventListResponseUnion interface {
implementsEventListResponse() implementsEventListResponse()
@@ -193,6 +196,11 @@ func init() {
Type: reflect.TypeOf(EventListResponseEventSessionError{}), Type: reflect.TypeOf(EventListResponseEventSessionError{}),
DiscriminatorValue: "session.error", DiscriminatorValue: "session.error",
}, },
apijson.UnionVariant{
TypeFilter: gjson.JSON,
Type: reflect.TypeOf(EventListResponseEventSessionCompacted{}),
DiscriminatorValue: "session.compacted",
},
apijson.UnionVariant{ apijson.UnionVariant{
TypeFilter: gjson.JSON, TypeFilter: gjson.JSON,
Type: reflect.TypeOf(EventListResponseEventServerConnected{}), Type: reflect.TypeOf(EventListResponseEventServerConnected{}),
@@ -1108,6 +1116,66 @@ func (r EventListResponseEventSessionErrorType) IsKnown() bool {
return false 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 { type EventListResponseEventServerConnected struct {
Properties interface{} `json:"properties,required"` Properties interface{} `json:"properties,required"`
Type EventListResponseEventServerConnectedType `json:"type,required"` Type EventListResponseEventServerConnectedType `json:"type,required"`
@@ -1163,12 +1231,13 @@ const (
EventListResponseTypeSessionDeleted EventListResponseType = "session.deleted" EventListResponseTypeSessionDeleted EventListResponseType = "session.deleted"
EventListResponseTypeSessionIdle EventListResponseType = "session.idle" EventListResponseTypeSessionIdle EventListResponseType = "session.idle"
EventListResponseTypeSessionError EventListResponseType = "session.error" EventListResponseTypeSessionError EventListResponseType = "session.error"
EventListResponseTypeSessionCompacted EventListResponseType = "session.compacted"
EventListResponseTypeServerConnected EventListResponseType = "server.connected" EventListResponseTypeServerConnected EventListResponseType = "server.connected"
) )
func (r EventListResponseType) IsKnown() bool { func (r EventListResponseType) IsKnown() bool {
switch r { 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 true
} }
return false return false

View File

@@ -100,6 +100,7 @@ func (r FileStatus) IsKnown() bool {
} }
type FileNode struct { type FileNode struct {
Absolute string `json:"absolute,required"`
Ignored bool `json:"ignored,required"` Ignored bool `json:"ignored,required"`
Name string `json:"name,required"` Name string `json:"name,required"`
Path string `json:"path,required"` Path string `json:"path,required"`
@@ -109,6 +110,7 @@ type FileNode struct {
// fileNodeJSON contains the JSON metadata for the struct [FileNode] // fileNodeJSON contains the JSON metadata for the struct [FileNode]
type fileNodeJSON struct { type fileNodeJSON struct {
Absolute apijson.Field
Ignored apijson.Field Ignored apijson.Field
Name apijson.Field Name apijson.Field
Path apijson.Field Path apijson.Field
@@ -142,7 +144,8 @@ func (r FileNodeType) IsKnown() bool {
type FileReadResponse struct { type FileReadResponse struct {
Content string `json:"content,required"` Content string `json:"content,required"`
Type FileReadResponseType `json:"type,required"` Diff string `json:"diff"`
Patch FileReadResponsePatch `json:"patch"`
JSON fileReadResponseJSON `json:"-"` JSON fileReadResponseJSON `json:"-"`
} }
@@ -150,7 +153,8 @@ type FileReadResponse struct {
// [FileReadResponse] // [FileReadResponse]
type fileReadResponseJSON struct { type fileReadResponseJSON struct {
Content apijson.Field Content apijson.Field
Type apijson.Field Diff apijson.Field
Patch apijson.Field
raw string raw string
ExtraFields map[string]apijson.Field ExtraFields map[string]apijson.Field
} }
@@ -163,19 +167,64 @@ func (r fileReadResponseJSON) RawJSON() string {
return r.raw 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 ( // fileReadResponsePatchJSON contains the JSON metadata for the struct
FileReadResponseTypeRaw FileReadResponseType = "raw" // [FileReadResponsePatch]
FileReadResponseTypePatch FileReadResponseType = "patch" 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 { func (r *FileReadResponsePatch) UnmarshalJSON(data []byte) (err error) {
switch r { return apijson.UnmarshalRoot(data, r)
case FileReadResponseTypeRaw, FileReadResponseTypePatch: }
return true
} func (r fileReadResponsePatchJSON) RawJSON() string {
return false 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 { type FileListParams struct {

View File

@@ -2,4 +2,4 @@
package internal package internal
const PackageVersion = "0.8.0" // x-release-please-version const PackageVersion = "0.9.0" // x-release-please-version

View File

@@ -1334,6 +1334,7 @@ func (r sessionJSON) RawJSON() string {
type SessionTime struct { type SessionTime struct {
Created float64 `json:"created,required"` Created float64 `json:"created,required"`
Updated float64 `json:"updated,required"` Updated float64 `json:"updated,required"`
Compacting float64 `json:"compacting"`
JSON sessionTimeJSON `json:"-"` JSON sessionTimeJSON `json:"-"`
} }
@@ -1341,6 +1342,7 @@ type SessionTime struct {
type sessionTimeJSON struct { type sessionTimeJSON struct {
Created apijson.Field Created apijson.Field
Updated apijson.Field Updated apijson.Field
Compacting apijson.Field
raw string raw string
ExtraFields map[string]apijson.Field ExtraFields map[string]apijson.Field
} }

View File

@@ -653,6 +653,9 @@ func getDefaultModel(
} }
func (a *App) IsBusy() bool { func (a *App) IsBusy() bool {
if a.Session.Time.Compacting > 0 {
return true
}
if len(a.Messages) == 0 { if len(a.Messages) == 0 {
return false return false
} }

View File

@@ -385,6 +385,9 @@ func (m *editorComponent) Content() string {
} else if m.app.IsBusy() { } else if m.app.IsBusy() {
keyText := m.getInterruptKeyText() keyText := m.getInterruptKeyText()
status := "working" status := "working"
if m.app.Session.Time.Compacting > 0 {
status = "compacting"
}
if m.app.CurrentPermission.ID != "" { if m.app.CurrentPermission.ID != "" {
status = "waiting for permission" status = "waiting for permission"
} }

View File

@@ -365,6 +365,9 @@ func (m *messagesComponent) renderView() tea.Cmd {
lastAssistantMessage := "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" lastAssistantMessage := "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"
for _, msg := range slices.Backward(m.app.Messages) { for _, msg := range slices.Backward(m.app.Messages) {
if assistant, ok := msg.Info.(opencode.AssistantMessage); ok { if assistant, ok := msg.Info.(opencode.AssistantMessage); ok {
if assistant.Time.Completed > 0 {
break
}
lastAssistantMessage = assistant.ID lastAssistantMessage = assistant.ID
break break
} }
@@ -475,6 +478,9 @@ func (m *messagesComponent) renderView() tea.Cmd {
} }
case opencode.AssistantMessage: case opencode.AssistantMessage:
if casted.Summary {
continue
}
if casted.ID == m.app.Session.Revert.MessageID { if casted.ID == m.app.Session.Revert.MessageID {
reverted = true reverted = true
revertedMessageCount = 1 revertedMessageCount = 1

View File

@@ -592,10 +592,40 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} }
if matchIndex == -1 { 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(), Info: msg.Properties.Info.AsUnion(),
Parts: []opencode.PartUnion{}, 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: 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) slog.Error("Server error", "name", err.Name, "message", err.Data.Message)
return a, toast.NewErrorToast(err.Data.Message, toast.WithTitle(string(err.Name))) 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: case tea.WindowSizeMsg:
msg.Height -= 2 // Make space for the status bar msg.Height -= 2 // Make space for the status bar
a.width, a.height = msg.Width, msg.Height a.width, a.height = msg.Width, msg.Height