Skip to content

fix(tui): TicketRef renders inline so text-wrap layout is correct (#250)#22

Merged
terrxo merged 1 commit into
devfrom
feat/250-ticket-ref-wrap
May 27, 2026
Merged

fix(tui): TicketRef renders inline so text-wrap layout is correct (#250)#22
terrxo merged 1 commit into
devfrom
feat/250-ticket-ref-wrap

Conversation

@terrxo

@terrxo terrxo commented May 27, 2026

Copy link
Copy Markdown

Fixes anomalyco#250: #N ticket refs were landing on the wrong visual line in assistant messages with multiple inline refs.

Bug

In Nik's screenshot, refs like #227, #243, #246 appeared on the wrong line, floating at end-of-unrelated-line, or duplicate-rendered. Visible examples:

  • ...awaiting next dispatch.#227#227 lands at end of the NEXT line
  • #163 amendments → re-review → surface to you #243#243 floats with no source reason
  • Paragraph ending with #246#246 duplicate-rendered

Root cause

packages/opencode/src/cli/cmd/tui/routes/session/index.tsx::TextPart rendered each paragraph segment as a separate widget under <box flexDirection="row" flexWrap="wrap">:

<box flexDirection="row" flexWrap="wrap">
  <For each={segments()}>
    {(seg) => seg.kind === "text"
      ? <markdown content={seg.text} />        // block-level widget
      : <TicketRef id={seg.id} />              // block-level <text> widget
    }
  </For>
</box>

Every segment — whether a <markdown> block or the <text>-based TicketRef — became its own flex item. flexWrap="wrap" then reordered items per-item (each flex item is atomic to the flex algorithm), not per-character. So the refs ended up wherever the flex layout placed them, not where the source text put them.

Fix

Render a paragraph containing #N refs as a single <text> block with inline <span> (text segments) and <a href={hivemind-url}> (refs) children. Both are OpenTUI TextNodeRenderables — they participate in the parent <text>'s character-level wrap math, so refs flow correctly with surrounding prose.

<text fg={theme.markdownText}>
  <For each={segments()}>
    {(seg) => seg.kind === "text"
      ? <span>{seg.text}</span>
      : <TicketRef id={seg.id} />              // now an inline <a>
    }
  </For>
</text>

TicketRef itself was reshaped from a top-level <text> widget into an inline <a href={HIVEMIND_UI_BASE}/tasks/${id}>. The terminal emulator (iTerm, Ghostty, Kitty, modern Terminal.app) renders it as a real OSC-8 hyperlink — ⌘-click opens hivemind-ui at /tasks/, preserving the click affordance.

Trade-offs (acknowledged in the original splitOnTicketRefs comment)

  • Inline markdown formatting (bold/italic/code) is dropped inside paragraphs that contain a #N ref. Plain-text paragraphs (no refs) keep the fast-path through full <markdown> rendering. This was the trade-off the original code documented; the previous flex-row implementation didn't honor it because each markdown block was its own block-level item.
  • Hover-card preview is gone. OpenTUI TextNode (<a>/<span>) doesn't accept its own mouse event listeners — they're part of the parent <text>'s render. The <TicketHoverCard> component is left mounted in app.tsx but no longer triggered (harmless dead path). A separate ticket can reintroduce a hover preview via a coordinated overlay anchored to <text>'s cursor position.

Verified

  • bun typecheck clean
  • All 50 TUI tests pass (test/cli/cmd/tui/)
  • 399 of 401 cli tests pass; 1 unrelated pre-existing failure in help-snapshots.test.ts (opencode→gruntcode rebrand, not introduced by this PR — confirmed by git stash then re-running)

Cross-references

What this does NOT touch

  • Hover-card component + context (kept as-is, dead path)
  • Markdown renderer (the <markdown> fast-path for ref-free paragraphs is unchanged)
  • Click handler semantics (still opens hivemind-ui in default browser, just via OSC-8 instead of open() call)

…omalyco#250)

Refs (anomalyco#229, anomalyco#243, anomalyco#246, ...) were landing on the wrong visual line in
assistant messages with multiple inline refs. Root cause: the rendering
path split each paragraph into per-segment widgets and laid them out
under `<box flexDirection="row" flexWrap="wrap">`. Every segment —
whether a markdown chunk or a TicketRef `<text>` — became its own flex
item, and the flex-wrap engine reordered items per-item instead of
flowing characters per-line. Result: `anomalyco#227` lands at end-of-next-line,
`anomalyco#243` floats at end of an unrelated line, `anomalyco#246` duplicate-renders.

Fix: render a paragraph containing #N refs as a single `<text>` block
with inline `<span>` (text) and `<a href=...>` (ref) children. Both
are OpenTUI `TextNodeRenderable`s — they participate in the parent
`<text>`'s character-level wrap math, so refs flow correctly with
surrounding prose.

Trade-off (consistent with the original splitOnTicketRefs comment):
paragraphs that contain a #N ref no longer render inline markdown
formatting (bold/italic/code). Plain-text paragraphs (the fast-path)
keep full markdown rendering as before. This was the trade-off the
original code documented; the previous flex-row implementation didn't
honor it because each markdown block was its own block-level item.

Hover-card preview is dropped for now: OpenTUI TextNode (`<a>`, `<span>`)
doesn't accept its own mouse-event listeners — they're part of the
parent `<text>`'s render. The terminal still renders `#N` as a real
OSC-8 hyperlink (⌘-click in iTerm opens hivemind-ui at /tasks/<id>),
which preserves the click affordance. The hover-card component itself
is left mounted but unused; reintroducing the hover via a coordinated
overlay anchored to TextRenderable cursor coords is out of scope here.

Closes anomalyco#250.

Verified:
- bun typecheck clean
- All 50 TUI tests pass (test/cli/cmd/tui/)
- All 399 cli tests pass (1 unrelated pre-existing help-snapshot failure
  from the opencode→gruntcode rebrand, not introduced here)
@github-actions

Copy link
Copy Markdown

This PR doesn't fully meet our contributing guidelines and PR template.

What needs to be fixed:

  • PR description is missing required template sections. Please use the PR template.

Please edit this PR description to address the above within 2 hours, or it will be automatically closed.

If you believe this was flagged incorrectly, please let a maintainer know.

@terrxo terrxo merged commit 1eaf65f into dev May 27, 2026
3 of 10 checks passed
@terrxo terrxo deleted the feat/250-ticket-ref-wrap branch May 27, 2026 13:26
terrxo added a commit that referenced this pull request May 28, 2026
…omalyco#250) (#22)

Refs (anomalyco#229, anomalyco#243, anomalyco#246, ...) were landing on the wrong visual line in
assistant messages with multiple inline refs. Root cause: the rendering
path split each paragraph into per-segment widgets and laid them out
under `<box flexDirection="row" flexWrap="wrap">`. Every segment —
whether a markdown chunk or a TicketRef `<text>` — became its own flex
item, and the flex-wrap engine reordered items per-item instead of
flowing characters per-line. Result: `anomalyco#227` lands at end-of-next-line,
`anomalyco#243` floats at end of an unrelated line, `anomalyco#246` duplicate-renders.

Fix: render a paragraph containing #N refs as a single `<text>` block
with inline `<span>` (text) and `<a href=...>` (ref) children. Both
are OpenTUI `TextNodeRenderable`s — they participate in the parent
`<text>`'s character-level wrap math, so refs flow correctly with
surrounding prose.

Trade-off (consistent with the original splitOnTicketRefs comment):
paragraphs that contain a #N ref no longer render inline markdown
formatting (bold/italic/code). Plain-text paragraphs (the fast-path)
keep full markdown rendering as before. This was the trade-off the
original code documented; the previous flex-row implementation didn't
honor it because each markdown block was its own block-level item.

Hover-card preview is dropped for now: OpenTUI TextNode (`<a>`, `<span>`)
doesn't accept its own mouse-event listeners — they're part of the
parent `<text>`'s render. The terminal still renders `#N` as a real
OSC-8 hyperlink (⌘-click in iTerm opens hivemind-ui at /tasks/<id>),
which preserves the click affordance. The hover-card component itself
is left mounted but unused; reintroducing the hover via a coordinated
overlay anchored to TextRenderable cursor coords is out of scope here.

Closes anomalyco#250.

Verified:
- bun typecheck clean
- All 50 TUI tests pass (test/cli/cmd/tui/)
- All 399 cli tests pass (1 unrelated pre-existing help-snapshot failure
  from the opencode→gruntcode rebrand, not introduced here)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Context7 MCP not working (Invalid schema for function 'context7_get-library-docs')

1 participant