Skip to content

OpenCode embed exposure: chat→workbench link, resizable+full-width worker columns #596

@brendandebeasi

Description

@brendandebeasi

Summary

The OpenCode embed surfaces in the dashboard don't expose their content well. Three related issues:

1. Chat popup → workbench thread is a dead end

When OpenCode is shown in the worker-detail panel (small inline view of an active OpenCode session inside the AgentWorkers page), there's no way to escape into the full workbench experience for the same thread. Good for quick glance, bad for actual work.

Proposed: add an "Open in Workbench" link next to the existing OpenCodeDirectLink in the worker-detail panel header. Routes to a new /workbench/$thread sub-route filtered to that single thread.

2. Workbench WorkerColumns aren't resizable

Each WorkerColumn is a fixed w-[560px]. With multiple workers running, the screen partitions arbitrarily and the user can't widen the column they're actively reading.

Proposed: add drag handles between columns using the existing Resizable primitive (@spacedrive/primitives/Resizable, wraps react-resizable-layout). Persist widths to localStorage by column position, not worker id, so the layout is stable across worker churn.

3. Workbench columns don't fill the available width

The container is flex-1 gap-[10px] overflow-x-auto (Workbench.tsx:179) and each column is flex-shrink-0 w-[560px]. With N×560 < viewport width there's wasted horizontal space at the right edge; with N×560 > viewport, the page scrolls horizontally instead of letting columns share remaining space.

Proposed: flex columns to fill 100% of the workbench pane, with a per-column min-width (~360px) so they only fall back to horizontal scroll when there are genuinely too many. User-set widths from #2 take precedence.

Why this matters

The workbench is the primary workspace for multi-worker sessions. Friction here pushes users back to the worker-detail popup (read-only-ish, less capable), undercutting the workbench's whole purpose. Each issue compounds the others — without (3) you don't have the room to want (2), and without (2) the popup-vs-workbench tradeoff in (1) tilts even more toward "stay in the popup."


Implementation plan

Files involved

Concern File Lines (approx)
Worker-detail panel rendering OpenCode (the "chat popup") interface/src/routes/AgentWorkers.tsx 395–545 (WorkerDetail), 471 (OpenCodeDirectLink), 633 (impl)
Workbench multi-column layout interface/src/routes/Workbench.tsx 162–192
Per-column wrapper + fixed width interface/src/components/workbench/WorkerColumn.tsx 18 (w-[560px] flex-shrink-0)
Workbench routing interface/src/router.tsx 106 (path: "/workbench")
Resizable primitive @spacedrive/primitives/src/Resizable.tsx already shipped, wraps react-resizable-layout

Decisions

  • Routing for filtered workbench: sub-route /workbench/$thread, using TanStack Router's native History integration (back/forward/deep-link/refresh all work out of the box).
  • Resize implementation: JS-based react-resizable-layout via the existing Resizable primitive — gives a thin draggable divider between columns, more polished than native CSS resize.
  • Width persistence: keyed by column position (spacebot-workbench-column-widths = Record<positionIndex, number>), so layout is stable across worker churn.

Issue 1 — Open-in-Workbench link

Routing change in interface/src/router.tsx:

// Add a child route under /workbench
{
  path: "/workbench/$thread",
  component: function WorkbenchThreadPage() {
    const { thread } = useParams({ from: "/workbench/$thread" });
    return <Workbench filterThread={thread} />;
  },
}

Workbench accepts an optional filterThread prop that narrows the visible columns to that session id. When set, the sidebar shows a "Showing 1 of N workers — clear filter" affordance that links back to /workbench.

The link itself in AgentWorkers.tsx ~471:

{hasOpenCodeEmbed && detail.opencode_session_id && (
  <Link
    to="/workbench/$thread"
    params={{ thread: detail.opencode_session_id }}
    className={cx(badgeVariants({variant: "outline", size: "sm"}), "w-fit")}
  >
    Open in Workbench →
  </Link>
)}

UX touch (non-blocking): when workbench loads filtered to one thread, scroll that column into view + flash the border briefly so the user knows they landed on the right one.

Issue 2 — Resizable columns

Convert WorkerColumn's wrapper to participate in a horizontal Resizable chain. The primitive's useResizable hook returns a position value driven by drag; we feed the resulting widths into a Map<positionIndex, number> and persist on debounce.

Sketch (Workbench.tsx):

const [widths, setWidths] = useColumnWidths(); // localStorage-backed hook

return (
  <div className="flex flex-1 gap-[10px] min-w-0">
    {workers.map((worker, i) => (
      <ResizableColumn
        key={worker.id}
        index={i}
        width={widths[i]}
        onResize={(w) => setWidths({ ...widths, [i]: w })}
        isLast={i === workers.length - 1}
      >
        <WorkerColumn worker={worker} />
      </ResizableColumn>
    ))}
  </div>
);

useColumnWidths: reads/writes localStorage["spacebot-workbench-column-widths"] as JSON. Debounce writes (~250ms) to avoid spamming localStorage during drag. No migration needed — fresh feature.

Issue 3 — Fill width by default

Workbench.tsx:179:

- <div className="flex flex-1 gap-[10px] overflow-x-auto">
+ <div className="flex flex-1 gap-[10px] min-w-0 overflow-x-auto">

(Keep overflow-x-auto — only kicks in when columns' min-widths sum to more than viewport.)

WorkerColumn.tsx:18:

- <div className="flex h-full w-[560px] flex-shrink-0 flex-col overflow-hidden rounded-2xl border border-app-line bg-app-box">
+ <div className="flex h-full min-w-[360px] flex-1 flex-col overflow-hidden rounded-2xl border border-app-line bg-app-box">

When #2's resizer assigns an explicit width to a column, it takes precedence (flex-[0_0_<px>]).

Order to ship

  1. Issue 3 first (~1 file changed substantially, ~2-line diff in another). Visible win, low risk.
  2. Issue 2 second. Builds on Run release workflow on x86 and ARM runners #3's flex layout — resizer flips columns from flex-1 to explicit widths.
  3. Issue 1 last. Touches router.tsx + Workbench.tsx + AgentWorkers.tsx. Adds the new sub-route and the filterThread prop.

Each can ship as its own PR or combined.

Out of scope

  • Whether the popup-vs-workbench routing should remember per-thread preferences (separate UX question)
  • Vertical resizing within a column (worker output vs prompt-input) — separate
  • Custom user-authored workbench layouts / saved arrangements — bigger feature

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions