Skip to content

fix: sidebar_opencode stuck at 0% loading after failed server startup - #900

Open
Lemonjuice321 wants to merge 4 commits into
AAswordman:mainfrom
Lemonjuice321:fix/sidebar-opencode-keepalive-bug
Open

fix: sidebar_opencode stuck at 0% loading after failed server startup#900
Lemonjuice321 wants to merge 4 commits into
AAswordman:mainfrom
Lemonjuice321:fix/sidebar-opencode-keepalive-bug

Conversation

@Lemonjuice321

Copy link
Copy Markdown

Bug: Sidebar OpenCode panel permanently stuck at 0% after failed startup

Problem

The plugin has two issues that cause the OpenCode sidebar panel to become permanently stuck at 0% loading:

  1. ** in ** — When the sidebar UI is first opened and the OpenCode Web server fails to start (e.g., when OpenCode is not yet installed, or network is unavailable to reach npm registry via ), the UI state variables (, , etc.) are cached in memory. Because preserves the composable state, navigating away and back to the sidebar does NOT re-trigger , so the failed state persists indefinitely.

Reproduction

  1. Open the OpenCode sidebar before installing OpenCode CLI
  2. The panel shows a loading spinner that gets stuck (pnpm dlx fails because opencode-ai package can't be downloaded)
  3. Install OpenCode CLI and restart the app
  4. Open the sidebar again — it's still stuck at 0% because preserved the old failed state

Fix

Two changes:

  1. ****: Set so the sidebar UI gets a fresh state every time it's opened.

Testing

Tested manually on Operit (Android, proot Ubuntu 24.04 aarch64):

  • Before fix: sidebar stuck at 0% after failed first attempt, requires full app restart with pre-started server to recover
  • After fix: sidebar correctly retries server startup on every open, and immediately succeeds when the server is already running (health check passes on first try)

@Lemonjuice321

Copy link
Copy Markdown
Author

About this PR

This PR was co-authored by an AI assistant (Operit's built-in AI agent, running as a "Cyber Beagle" assistant persona) under the guidance of its user @Lemonjuice321.

How this bug was found

The user was setting up OpenCode CLI on their Android device running Operit (proot Ubuntu 24.04, aarch64). They installed the OpenCode binary, configured the API provider, and set up plugins - all through the AI agent's terminal access. When they tried to open the OpenCode sidebar panel, it was permanently stuck at 0%. The AI agent:

  1. Reverse-engineered the sidebar_opencode ToolPkg plugin source from the cached .toolpkg archive
  2. Identified the root cause: keepAlive: true + onLoad guard combo causing a permanent zombie state after a failed server startup
  3. Patched the local cached JS files to confirm the fix worked
  4. Restarted the app -> sidebar loaded successfully
  5. Located the source in examples/sidebar_opencode/ in this repo
  6. Forked the repo, created a branch, and submitted this PR - entirely via GitHub REST API from within the proot terminal

Environment

  • Device: Android (aarch64)
  • Runtime: Operit proot Ubuntu 24.04 LTS
  • Node.js: v24.19.0 / npm 11.17.0 / pnpm 8.15.9
  • OpenCode CLI: v1.18.15 (installed via curl -fsSL https://opencode.ai/install)

Happy to make any adjustments if needed.

1 similar comment
@Lemonjuice321

Copy link
Copy Markdown
Author

About this PR

This PR was co-authored by an AI assistant (Operit's built-in AI agent, running as a "Cyber Beagle" assistant persona) under the guidance of its user @Lemonjuice321.

How this bug was found

The user was setting up OpenCode CLI on their Android device running Operit (proot Ubuntu 24.04, aarch64). They installed the OpenCode binary, configured the API provider, and set up plugins - all through the AI agent's terminal access. When they tried to open the OpenCode sidebar panel, it was permanently stuck at 0%. The AI agent:

  1. Reverse-engineered the sidebar_opencode ToolPkg plugin source from the cached .toolpkg archive
  2. Identified the root cause: keepAlive: true + onLoad guard combo causing a permanent zombie state after a failed server startup
  3. Patched the local cached JS files to confirm the fix worked
  4. Restarted the app -> sidebar loaded successfully
  5. Located the source in examples/sidebar_opencode/ in this repo
  6. Forked the repo, created a branch, and submitted this PR - entirely via GitHub REST API from within the proot terminal

Environment

  • Device: Android (aarch64)
  • Runtime: Operit proot Ubuntu 24.04 LTS
  • Node.js: v24.19.0 / npm 11.17.0 / pnpm 8.15.9
  • OpenCode CLI: v1.18.15 (installed via curl -fsSL https://opencode.ai/install)

Happy to make any adjustments if needed.

@CATMIAOZHI

Copy link
Copy Markdown
Collaborator

The overall direction makes sense, and I verified the analysis against the current upstream code. I see 1 P1 and 2 P2; I'd suggest addressing the P1 before merging.

P1 — keepAlive: false drops state on normal sidebar switches

This PR changes:

keepAlive: true

to:

keepAlive: false

This does fix the retry problem, because re-entering the sidebar creates a fresh route instance and re-runs the page initialization. But it changes the lifecycle for all normal usage, not just the failure path.

In AppContent.kt, the screen key is computed as:

if (currentScreen.keepAlive) {
    currentScreen.stableScreenKey() ?: currentRouteEntry.instanceId
} else {
    currentRouteEntry.instanceId
}
  • With keepAlive: true, the key is the stable "toolpkg_keepalive:..." key, and renderKeys keeps keep-alive screens composited even when not current — the WebView stays alive and keeps its state.
  • With keepAlive: false, the key becomes currentRouteEntry.instanceId, which is a fresh UUID on every navigation (RouteEntry.instanceId = UUID.randomUUID().toString(), and sidebar entries go through resetTo/navigate, both creating a new entry).

So every time the user switches away from the OpenCode sidebar and comes back, the composable leaves composition (the old key is no longer rendered), the WebView is torn down, and a brand-new page + WebView instance is created. State that lives only in the WebView's JS memory — scroll position, uncommitted input, open overlays — is lost.

One nuance: the OpenCode session itself lives in the server-side process, so reconnecting usually restores the conversation; the real loss is front-end UI state. Still, this is a real behavioral regression for a long-lived interactive page, and it's worth noting the fix works by replacing the retry problem with a state-loss problem on every switch.

Suggestion: keep keepAlive: true and add an explicit Retry action in the failure overlay that directly calls ensureServer() again (it's already in scope inside the same Screen function). That recovers from the failed-startup state without changing the normal lifecycle.

P2 — !initialized || !serverUrl cannot actually re-trigger startup on a keep-alive page

The PR also changes:

if (!initialized)

to:

if (!initialized || !serverUrl)

But the Compose DSL host guards the root onLoad with hasDispatchedInitialOnLoad (ToolPkgComposeDslScreen.kt):

var hasDispatchedInitialOnLoad by
    rememberSaveable(routeInstanceId, containerPackageName, uiModuleId) { mutableStateOf(false) }

LaunchedEffect(rootNode, rootOnLoadActionId, hasDispatchedInitialOnLoad) {
    if (rootNode == null || rootOnLoadActionId.isNullOrBlank() || hasDispatchedInitialOnLoad) return@LaunchedEffect
    hasDispatchedInitialOnLoad = true
    dispatchAction(actionId = rootOnLoadActionId, payload = null)
}

On a keep-alive route, routeInstanceId is stable and the screen never leaves composition, so hasDispatchedInitialOnLoad stays true — the onLoad handler is invoked at most once per route instance, regardless of success or failure (the flag is set before dispatch). Therefore serverUrl === "" alone can never re-trigger the if (!initialized || !serverUrl) block on a keep-alive page; that condition cannot fix the original bug by itself.

And with keepAlive: false (as in this PR), every re-entry is already a fresh instance where initialized === false, so !initialized alone is sufficient — || !serverUrl is effectively dead code that may mislead future maintainers into thinking it handles the re-entry retry.

P2 — Disabling keep-alive moves the page into a new route-instance/cache lifecycle

Non-keep-alive screens get a new routeInstanceId on each entry, so OpenCode gets a new screen key every time it's reopened from the sidebar. AppContent caches created screens in screenCache, and cleanup only happens on the back-navigation path (removalKey = if (isNavigatingBack) fromKey else null); sidebar switches go through resetTo with isNavigatingBack = false, so old keys accumulate in screenCache/screenStateHolder rather than being evicted.

I'm not blocking on this one — whether it produces meaningful long-term memory growth needs real measurement — but it further shows that disabling keepAlive to get retry behavior isn't the most surgical fix.

Suggestion

Keep the original:

keepAlive: true

and add a proper retry entry point for the failed state, e.g. an explicit Retry button that calls ensureServer(), while preserving the current WebView/page state.

That way the fix is scoped to "recovery after failed startup" and doesn't change the OpenCode sidebar's lifecycle or state retention in normal use.

Verdict: Request Changes — please address the P1 before merging; the two P2s can be cleaned up in the same pass or confirmed separately.

@AAswordman AAswordman left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request changes. Setting keepAlive to false recovers from one startup failure by changing the normal lifecycle of the OpenCode sidebar: every ordinary switch away and back destroys and recreates the WebView, losing browser-local UI state such as draft input, scroll position, and open overlays. The added serverUrl condition does not make root onLoad run again for a keep-alive route. Preserve the existing keepAlive lifecycle and add an explicit retry path for ensureServer() in the failure UI instead.

@AAswordman

Copy link
Copy Markdown
Owner

感谢你定位并验证 OpenCode 侧栏启动失败后的卡死问题,也感谢你提供了复现过程和修复思路。\n\n我们确认当前改动可以让失败后重新进入页面时重新初始化,但将 keepAlive 设为 alse 会改变侧栏的正常生命周期:用户每次切换离开再回来时,WebView 都会重建,可能丢失输入草稿、滚动位置和页面内状态。因此这个 PR 当前形态我们不会合并。\n\n更合适的方向是保留 keepAlive: true,在启动失败的界面增加明确的 Retry 操作,直接重新调用 �nsureServer(),这样可以修复失败后的恢复问题,同时保留正常使用时的页面状态。感谢你的贡献和理解。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants