Skip to content

Commit 15315be

Browse files
fix(session,tui,run,core): wave 6 residual closure — transport, info-only reads, handler table
- F-113: subagent-tab snapshot computed only when trace requested (cli/cmd/run/stream.transport.ts) - F-054: currentModel via findMessageInfo info-only read (session/prompt.ts) - F-088: SessionMessageUpdater static handler table, zero per-event closures (core/session/message-updater.ts) - F-079: shared contextUsage selector; three TUI call sites deduped (tui/util/context-usage.ts) - F-119: pending MCP OAuth transport deleted on failure (mcp/index.ts) Design-blocked with written rationale (audit/waves/lane-residuals-{A,B}-wave-6.md): F-047 timeout, F-118 bulk publish, F-099 Location widening, F-093 request-context dedup, F-037 per-type shutdown, F-063 write deadline, F-067/N-4 exclude cache. Gates: 161 tests 0 fail, typecheck x3 clean, prettier clean.
1 parent c1fa943 commit 15315be

8 files changed

Lines changed: 360 additions & 334 deletions

File tree

‎packages/core/src/session/message-updater.ts‎

Lines changed: 314 additions & 298 deletions
Large diffs are not rendered by default.

‎packages/opencode/src/cli/cmd/run/stream.transport.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -899,7 +899,10 @@ function createLayer(input: StreamInput) {
899899

900900
trackBlocker(event)
901901

902-
const prev = event.type === "message.part.updated" ? listSubagentTabs(state.subagent) : undefined
902+
// `prev` is only consumed by `traceTabs`, which is a no-op when tracing is
903+
// disabled (the default), so skip the copy+sort entirely in that case.
904+
const prev =
905+
input.trace && event.type === "message.part.updated" ? listSubagentTabs(state.subagent) : undefined
903906
const next = reduceSessionData({
904907
data: state.data,
905908
event,

‎packages/opencode/src/mcp/index.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -930,7 +930,10 @@ const layer = Layer.effect(
930930
}),
931931
)
932932

933-
if (error) return { status: "failed", error: `OAuth completion failed: ${error}` } satisfies Status
933+
if (error) {
934+
pendingOAuthTransports.delete(mcpName)
935+
return { status: "failed", error: `OAuth completion failed: ${error}` } satisfies Status
936+
}
934937

935938
yield* Effect.promise(() => pending.provider?.commit() ?? Promise.resolve())
936939
yield* auth.clearCodeVerifier(mcpName)

‎packages/opencode/src/session/prompt.ts‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -653,10 +653,8 @@ const layer = Layer.effect(
653653
...(current.model.variant && current.model.variant !== "default" ? { variant: current.model.variant } : {}),
654654
}
655655
}
656-
const match = yield* sessions
657-
.findMessage(sessionID, (m) => m.info.role === "user" && !!m.info.model)
658-
.pipe(Effect.orDie)
659-
if (Option.isSome(match) && match.value.info.role === "user") return match.value.info.model
656+
const match = yield* sessions.findMessageInfo(sessionID, (m) => m.role === "user" && !!m.model).pipe(Effect.orDie)
657+
if (Option.isSome(match) && match.value.role === "user") return match.value.model
660658
return yield* provider.defaultModel().pipe(Effect.orDie)
661659
})
662660

‎packages/tui/src/component/prompt/index.tsx‎

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,9 @@ import { usePromptStash } from "../../prompt/stash"
3737
import { DialogStash } from "../dialog-stash"
3838
import { type AutocompleteRef, Autocomplete } from "./autocomplete"
3939
import { useRenderer, useTerminalDimensions, type JSX } from "@opentui/solid"
40-
import type { AssistantMessage, FilePart, UserMessage } from "@opencode-ai/sdk/v2"
40+
import type { FilePart, UserMessage } from "@opencode-ai/sdk/v2"
4141
import { Locale } from "../../util/locale"
42+
import { contextUsage } from "../../util/context-usage"
4243
import { errorMessage } from "../../util/error"
4344
import { formatDuration } from "../../util/format"
4445
import { createColors, createFrames } from "../../ui/spinner"
@@ -266,19 +267,13 @@ export function Prompt(props: PromptProps) {
266267
const usage = createMemo(() => {
267268
if (!props.sessionID) return
268269
const session = sync.session.get(props.sessionID)
269-
const msg = sync.data.message[props.sessionID] ?? []
270-
const last = msg.findLast((item): item is AssistantMessage => item.role === "assistant" && item.tokens.output > 0)
271-
if (!last) return
270+
const found = contextUsage(sync.data.message[props.sessionID] ?? [], sync.data.provider)
271+
if (!found || found.tokens <= 0) return
272272

273-
const tokens =
274-
last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write
275-
if (tokens <= 0) return
276-
277-
const model = sync.data.provider.find((item) => item.id === last.providerID)?.models[last.modelID]
278-
const pct = model?.limit.context ? `${Math.round((tokens / model.limit.context) * 100)}%` : undefined
273+
const pct = found.percent === null ? undefined : `${found.percent}%`
279274
const cost = session?.cost ?? 0
280275
return {
281-
context: pct ? `${Locale.number(tokens)} (${pct})` : Locale.number(tokens),
276+
context: pct ? `${Locale.number(found.tokens)} (${pct})` : Locale.number(found.tokens),
282277
cost: cost > 0 ? money.format(cost) : undefined,
283278
}
284279
})

‎packages/tui/src/feature-plugins/sidebar/context.tsx‎

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import type { AssistantMessage } from "@opencode-ai/sdk/v2"
21
import type { TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui"
32
import type { BuiltinTuiPlugin } from "../builtins"
43
import { createMemo } from "solid-js"
4+
import { contextUsage } from "../../util/context-usage"
55

66
const id = "internal:sidebar-context"
77

@@ -17,20 +17,17 @@ function View(props: { api: TuiPluginApi; session_id: string }) {
1717
const cost = createMemo(() => session()?.cost ?? 0)
1818

1919
const state = createMemo(() => {
20-
const last = msg().findLast((item): item is AssistantMessage => item.role === "assistant" && item.tokens.output > 0)
21-
if (!last) {
20+
const found = contextUsage(msg(), props.api.state.provider)
21+
if (!found) {
2222
return {
2323
tokens: 0,
2424
percent: null,
2525
}
2626
}
2727

28-
const tokens =
29-
last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write
30-
const model = props.api.state.provider.find((item) => item.id === last.providerID)?.models[last.modelID]
3128
return {
32-
tokens,
33-
percent: model?.limit.context ? Math.round((tokens / model.limit.context) * 100) : null,
29+
tokens: found.tokens,
30+
percent: found.percent,
3431
}
3532
})
3633

‎packages/tui/src/routes/session/subagent-footer.tsx‎

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import { useRouteData } from "../../context/route"
33
import { useSync } from "../../context/sync"
44
import { useTheme } from "../../context/theme"
55
import { SplitBorder } from "../../ui/border"
6-
import type { AssistantMessage } from "@opencode-ai/sdk/v2"
76
import { Locale } from "../../util/locale"
7+
import { contextUsage } from "../../util/context-usage"
88
import { useTerminalDimensions } from "@opentui/solid"
99
import { useCommandShortcut, useOpencodeKeymap } from "../../keymap"
1010

@@ -36,20 +36,14 @@ export function SubagentFooter() {
3636
})
3737

3838
const usage = createMemo(() => {
39-
const msg = messages()
40-
const last = msg.findLast((item): item is AssistantMessage => item.role === "assistant" && item.tokens.output > 0)
41-
if (!last) return
39+
const found = contextUsage(messages(), sync.data.provider)
40+
if (!found || found.tokens <= 0) return
4241

43-
const tokens =
44-
last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write
45-
if (tokens <= 0) return
46-
47-
const model = sync.data.provider.find((item) => item.id === last.providerID)?.models[last.modelID]
48-
const pct = model?.limit.context ? `${Math.round((tokens / model.limit.context) * 100)}%` : undefined
42+
const pct = found.percent === null ? undefined : `${found.percent}%`
4943
const cost = session()?.cost ?? 0
5044

5145
return {
52-
context: pct ? `${Locale.number(tokens)} (${pct})` : Locale.number(tokens),
46+
context: pct ? `${Locale.number(found.tokens)} (${pct})` : Locale.number(found.tokens),
5347
cost: cost > 0 ? money.format(cost) : undefined,
5448
}
5549
})
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import type { AssistantMessage, Message, Provider } from "@opencode-ai/sdk/v2"
2+
3+
export type ContextUsage = {
4+
readonly tokens: number
5+
readonly percent: number | null
6+
}
7+
8+
export function contextUsage(
9+
messages: ReadonlyArray<Message>,
10+
providers: ReadonlyArray<Provider>,
11+
): ContextUsage | undefined {
12+
const last = messages.findLast(
13+
(item): item is AssistantMessage => item.role === "assistant" && item.tokens.output > 0,
14+
)
15+
if (!last) return
16+
const tokens =
17+
last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write
18+
const model = providers.find((item) => item.id === last.providerID)?.models[last.modelID]
19+
return { tokens, percent: model?.limit.context ? Math.round((tokens / model.limit.context) * 100) : null }
20+
}

0 commit comments

Comments
 (0)