Skip to content

Wrapped sidebar todos overlap and consume the next section gap #57529

Open

Description

@ozandogrultan

Description

Long todo descriptions can overlap in the sidebar and consume the blank row before the next section. The Todo container can shrink below the height of its wrapped text.

Plugins

Reproduced with the native Todo component and a synthetic following section; no third-party plugin code is required.

OpenCode version

2.1.1; also reproduced with the component on current dev.

Steps to reproduce

  1. Use a narrow sidebar with four long todo descriptions that wrap onto multiple lines.
  2. Place another section with a long list after Todo in sidebar.content.
  3. Observe overlapping todo text and no blank row before the following section. Short, unwrapped descriptions retain the gap.

The regression test in the linked PR renders the native component at widths 40 and 50 with 3, 4, and 8 todos, including active and completed states.

Screenshot

Before (left), after preserving the Todo container's height (right):

Synthetic Todo layout before and after

Rendered from an isolated OpenCode 2.1.1 terminal recording with synthetic text, not an OS screenshot. No personal sessions or configuration are included.

Terminal

Isolated xterm-256color PTY; also reproduced with the OpenTUI test renderer.

Activity

  1. opencode-agent commented on Oct 20, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed report. I reproduced this on 2.1.1, and the same script shows it on the latest dev (3fdb13bc).

    Steps:

    • Turn on experimental.todo. Have the model write four long todos that wrap in the 42-column sidebar.
    • Add a local TUI plugin that appends a section after Todo in sidebar.content. The section is a box that can grow and shrink (flexGrow={1} minHeight={0}) with a header, a "search…" line and a scroll box of 67 rows, similar to your "Sessions (67)" example.

    The Todo section then shrinks below the height of its wrapped text. The first todo loses its last line, stray letters land inside the next todo ("Addrintegrationetestsrcovering"), the next section's header is drawn over "search…", and the blank row before it disappears. The screenshot and recording below show this, and the script below reproduces it.

    In my runs it only happened when the following section could shrink. With a plain long list after Todo, or with only the built-in sections, the todos wrapped correctly and the blank row was kept. Your proposed fix is in #57530.

    recording.mp4

    Screenshot

    Reproduction script (drive.ts)
    // Repro for anomalyco/opencode#57529: wrapped sidebar todos overlap.
    import { Effect } from "effect"
    import { Llm, OpenCodeDriver } from "opencode-drive"
    
    const todos = [
      { content: "Refactor the authentication middleware so that tokens are refreshed before expiry", status: "completed" },
      { content: "Add integration tests covering the new session restore flow across reconnects", status: "in_progress" },
      { content: "Update the configuration documentation to describe every new sidebar option", status: "pending" },
      { content: "Audit the error handling in the provider adapters for consistent retry behaviour", status: "pending" },
    ]
    
    // A synthetic sidebar section placed after Todo: a flexible section with a long, scrollable list
    // (like a session list plugin). With it, the native Todo section shrinks below its wrapped text.
    const followingPlugin = `/** @jsxImportSource @opentui/solid */
    import { Plugin } from "@opencode/plugin/tui"
    export default Plugin.define({
      id: "repro.following",
      setup(context) {
        context.ui.slot({
          append: "sidebar.content",
          render: () => (
            <box flexGrow={1} minHeight={0}>
              <text fg={context.theme.text.base}><b>Sessions (67)</b></text>
              <text fg={context.theme.text.muted}>search…</text>
              <scrollbox flexGrow={1} minHeight={0}>
                {Array.from({ length: 67 }, (_, i) => <text fg={context.theme.text.muted}>Session {i}</text>)}
              </scrollbox>
            </box>
          ),
        })
      },
    })
    `
    
    export default OpenCodeDriver.use(
      {
        project: { files: { ".opencode/plugins/following/tui.tsx": followingPlugin } },
        ...(process.env.OC_DEV ? { opencode: { dev: process.env.OC_DEV } } : {}),
        config: { experimental: { todo: true } },
        tui: { recording: true, viewport: { cols: 130, rows: 40 } },
      },
      ({ ui, llm }) =>
        Effect.gen(function* () {
          yield* llm.queue(
            Llm.toolCall({ index: 0, id: "call_todo", name: "todowrite", input: { todos } }),
            Llm.finish("tool-calls"),
          )
          const updated = todos.map((t, i) => ({ ...t, status: i === 0 ? "in_progress" : "pending" }))
          yield* llm.queue(
            Llm.toolCall({ index: 0, id: "call_todo2", name: "todowrite", input: { todos: updated } }),
            Llm.finish("tool-calls"),
          )
          yield* llm.queue(Llm.text("Todo list created."))
          yield* ui.submit("Plan the work")
          yield* ui.waitFor("Todo list created.").pipe(Effect.ignore)
          yield* Effect.sleep("1500 millis")
          yield* ui.screenshot("sidebar-todos")
          const frame: any = yield* ui.capture()
          yield* Effect.log("\n" + frame.lines.map((l: any) => l.spans.map((s: any) => s.text).join("")).join("\n"))
        }),
    )

    Run it with opencode-drive: opencode-drive run ./drive.ts. It drives opencode from your PATH; set OC_DEV to the path of an OpenCode checkout to run that from source instead.

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions