Skip to content

Commit 8df6e36

Browse files
zerone0xclaude
andcommitted
fix(tui): handle undefined agent during login/bootstrap transition
When logging in via Google or during bootstrap, sync.data.agent can be temporarily empty. This caused crashes when accessing local.agent.current().name. Changes: - Use safe initial value for agentStore.current - Add createEffect to reactively update current agent when agents load - Remove unsafe ! non-null assertion in current() method - Add null checks in move(), cycle(), cycleFavorite(), set() methods - Use optional chaining at all call sites Fixes anomalyco#7932, anomalyco#7931, anomalyco#7918 Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 087473b commit 8df6e36

File tree

3 files changed

+41
-17
lines changed

3 files changed

+41
-17
lines changed

packages/opencode/src/cli/cmd/tui/component/dialog-agent.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export function DialogAgent() {
2020
return (
2121
<DialogSelect
2222
title="Select agent"
23-
current={local.agent.current().name}
23+
current={local.agent.current()?.name ?? ""}
2424
options={options()}
2525
onSelect={(option) => {
2626
local.agent.set(option.value)

packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -530,7 +530,7 @@ export function Prompt(props: PromptProps) {
530530
if (store.mode === "shell") {
531531
sdk.client.session.shell({
532532
sessionID,
533-
agent: local.agent.current().name,
533+
agent: local.agent.current()?.name ?? "",
534534
model: {
535535
providerID: selectedModel.providerID,
536536
modelID: selectedModel.modelID,
@@ -551,7 +551,7 @@ export function Prompt(props: PromptProps) {
551551
sessionID,
552552
command: command.slice(1),
553553
arguments: args.join(" "),
554-
agent: local.agent.current().name,
554+
agent: local.agent.current()?.name ?? "",
555555
model: `${selectedModel.providerID}/${selectedModel.modelID}`,
556556
messageID,
557557
variant,
@@ -567,7 +567,7 @@ export function Prompt(props: PromptProps) {
567567
sessionID,
568568
...selectedModel,
569569
messageID,
570-
agent: local.agent.current().name,
570+
agent: local.agent.current()?.name ?? "",
571571
model: selectedModel,
572572
variant,
573573
parts: [
@@ -687,7 +687,7 @@ export function Prompt(props: PromptProps) {
687687
const highlight = createMemo(() => {
688688
if (keybind.leader) return theme.border
689689
if (store.mode === "shell") return theme.primary
690-
return local.agent.color(local.agent.current().name)
690+
return local.agent.color(local.agent.current()?.name ?? "")
691691
})
692692

693693
const showVariant = createMemo(() => {
@@ -698,7 +698,7 @@ export function Prompt(props: PromptProps) {
698698
})
699699

700700
const spinnerDef = createMemo(() => {
701-
const color = local.agent.color(local.agent.current().name)
701+
const color = local.agent.color(local.agent.current()?.name ?? "")
702702
return {
703703
frames: createFrames({
704704
color,
@@ -932,7 +932,7 @@ export function Prompt(props: PromptProps) {
932932
/>
933933
<box flexDirection="row" flexShrink={0} paddingTop={1} gap={1}>
934934
<text fg={highlight()}>
935-
{store.mode === "shell" ? "Shell" : Locale.titlecase(local.agent.current().name)}{" "}
935+
{store.mode === "shell" ? "Shell" : Locale.titlecase(local.agent.current()?.name ?? "")}{" "}
936936
</text>
937937
<Show when={store.mode === "normal"}>
938938
<box flexDirection="row" gap={1}>

packages/opencode/src/cli/cmd/tui/context/local.tsx

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,20 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
3838
const [agentStore, setAgentStore] = createStore<{
3939
current: string
4040
}>({
41-
current: agents()[0].name,
41+
// Use empty string as safe initial value - will be updated reactively when agents load
42+
current: agents()[0]?.name ?? "",
4243
})
44+
45+
// Reactively update current agent when agents list changes (e.g., after login/bootstrap)
46+
createEffect(() => {
47+
const list = agents()
48+
if (list.length === 0) return
49+
// If current is empty or no longer valid, set to first agent
50+
if (!agentStore.current || !list.some((x) => x.name === agentStore.current)) {
51+
setAgentStore("current", list[0].name)
52+
}
53+
})
54+
4355
const { theme } = useTheme()
4456
const colors = createMemo(() => [
4557
theme.secondary,
@@ -54,7 +66,9 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
5466
return agents()
5567
},
5668
current() {
57-
return agents().find((x) => x.name === agentStore.current)!
69+
const found = agents().find((x) => x.name === agentStore.current)
70+
// Return first agent as fallback if current not found (during loading/transition)
71+
return found ?? agents()[0]
5872
},
5973
set(name: string) {
6074
if (!agents().some((x) => x.name === name))
@@ -67,11 +81,13 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
6781
},
6882
move(direction: 1 | -1) {
6983
batch(() => {
70-
let next = agents().findIndex((x) => x.name === agentStore.current) + direction
71-
if (next < 0) next = agents().length - 1
72-
if (next >= agents().length) next = 0
73-
const value = agents()[next]
74-
setAgentStore("current", value.name)
84+
const list = agents()
85+
if (list.length === 0) return
86+
let next = list.findIndex((x) => x.name === agentStore.current) + direction
87+
if (next < 0) next = list.length - 1
88+
if (next >= list.length) next = 0
89+
const value = list[next]
90+
if (value) setAgentStore("current", value.name)
7591
})
7692
},
7793
color(name: string) {
@@ -179,6 +195,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
179195

180196
const currentModel = createMemo(() => {
181197
const a = agent.current()
198+
if (!a) return fallbackModel() ?? undefined
182199
return (
183200
getFirstValidModel(
184201
() => modelStore.model[a.name],
@@ -227,7 +244,9 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
227244
if (next >= recent.length) next = 0
228245
const val = recent[next]
229246
if (!val) return
230-
setModelStore("model", agent.current().name, { ...val })
247+
const currentAgent = agent.current()
248+
if (!currentAgent) return
249+
setModelStore("model", currentAgent.name, { ...val })
231250
},
232251
cycleFavorite(direction: 1 | -1) {
233252
const favorites = modelStore.favorite.filter((item) => isModelValid(item))
@@ -253,7 +272,9 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
253272
}
254273
const next = favorites[index]
255274
if (!next) return
256-
setModelStore("model", agent.current().name, { ...next })
275+
const currentAgent = agent.current()
276+
if (!currentAgent) return
277+
setModelStore("model", currentAgent.name, { ...next })
257278
const uniq = uniqueBy([next, ...modelStore.recent], (x) => `${x.providerID}/${x.modelID}`)
258279
if (uniq.length > 10) uniq.pop()
259280
setModelStore(
@@ -272,7 +293,9 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
272293
})
273294
return
274295
}
275-
setModelStore("model", agent.current().name, model)
296+
const currentAgent = agent.current()
297+
if (!currentAgent) return
298+
setModelStore("model", currentAgent.name, model)
276299
if (options?.recent) {
277300
const uniq = uniqueBy([model, ...modelStore.recent], (x) => `${x.providerID}/${x.modelID}`)
278301
if (uniq.length > 10) uniq.pop()
@@ -368,6 +391,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
368391
// Automatically update model when agent changes
369392
createEffect(() => {
370393
const value = agent.current()
394+
if (!value) return
371395
if (value.model) {
372396
if (isModelValid(value.model))
373397
model.set({

0 commit comments

Comments
 (0)