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 { 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,
})

View File

@@ -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

View File

@@ -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

View File

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

View File

@@ -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

View File

@@ -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)

View File

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

View File

@@ -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"`

View File

@@ -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"`

View File

@@ -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

View File

@@ -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 {

View File

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

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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"
}

View File

@@ -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

View File

@@ -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