From 315cdd23e2e5db8aa894dfd8dfd42086dabbaeae Mon Sep 17 00:00:00 2001 From: indifferentketchup Date: Sat, 30 May 2026 21:26:07 +0000 Subject: [PATCH] feat: strip dcp-message-id tags from opencode output + reopen closed panes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two independent fixes: - opencode-server.ts: stripDcpTags() removes tags from text deltas before they reach the frame/DB. Applied to all three text paths (session.next.text.delta, message.part.delta text field, handleUpdatedPart text type). Reasoning/tool paths untouched. - useWorkspacePanes.ts: module-level closedPaneStack (capped at 10) captures pane kind + chatIds on removePane and removeTab auto-remove. reopenPane() pops the stack and re-attaches a new pane to the existing chat ids (chats survive pane close server-side). hasClosedPanes drives conditional render. - ChatTabBar.tsx: [+] is now instant new-tab (no dropdown); split-pane dropdown (Columns2 icon) opens Chat/Term/Code in a new pane; reopen button (RotateCcw icon) appears when closed panes exist. - Workspace.tsx: pass reopenPane + hasClosedPanes through to ChatTabBar. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/services/backends/opencode-server.ts | 14 ++++-- apps/web/src/components/ChatTabBar.tsx | 44 ++++++++++++++----- apps/web/src/components/Workspace.tsx | 9 ++-- apps/web/src/hooks/useWorkspacePanes.ts | 41 +++++++++++++++++ 4 files changed, 91 insertions(+), 17 deletions(-) diff --git a/apps/coder/src/services/backends/opencode-server.ts b/apps/coder/src/services/backends/opencode-server.ts index 3bc333b..babb29c 100644 --- a/apps/coder/src/services/backends/opencode-server.ts +++ b/apps/coder/src/services/backends/opencode-server.ts @@ -192,7 +192,8 @@ export class OpenCodeServerBackend implements AgentBackend { const st = this.byOpencodeId.get(p.sessionID); if (!st?.activeTurn) return; this.bumpActivity(st); - st.activeTurn.onEvent({ type: 'text', text: p.delta }); + const cleaned = stripDcpTags(p.delta); + if (cleaned) st.activeTurn.onEvent({ type: 'text', text: cleaned }); return; } case 'session.next.reasoning.delta': { @@ -264,7 +265,8 @@ export class OpenCodeServerBackend implements AgentBackend { st.activeTurn.onEvent({ type: 'reasoning', text: p.delta }); } else if (p.field === 'text') { st.streamedPartKeys.add(`text:${p.partID}`); - st.activeTurn.onEvent({ type: 'text', text: p.delta }); + const cleaned = stripDcpTags(p.delta); + if (cleaned) st.activeTurn.onEvent({ type: 'text', text: cleaned }); } return; } @@ -301,7 +303,8 @@ export class OpenCodeServerBackend implements AgentBackend { st.partTypeById.set(part.id, part.type); const key = resolvePartDedupeKey(part, part.type); if (key && st.streamedPartKeys.delete(key)) return; // already streamed via delta - const text = part.text ?? ''; + const raw = part.text ?? ''; + const text = part.type === 'text' ? stripDcpTags(raw) : raw; if (text && part.time?.end != null) { turn.onEvent({ type: part.type, text }); } @@ -717,6 +720,11 @@ function sleep(ms: number): Promise { return new Promise((r) => setTimeout(r, ms)); } +/** Strip opencode-dcp plugin tags that render as literal text in the UI. */ +function stripDcpTags(s: string): string { + return s.replace(/[^<]*<\/dcp-message-id>/g, ''); +} + function errMsg(e: unknown): string { return e instanceof Error ? e.message : String(e); } diff --git a/apps/web/src/components/ChatTabBar.tsx b/apps/web/src/components/ChatTabBar.tsx index 05be688..7c84a94 100644 --- a/apps/web/src/components/ChatTabBar.tsx +++ b/apps/web/src/components/ChatTabBar.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { Code, History, MessageSquare, Plus, Terminal, X } from 'lucide-react'; +import { Code, Columns2, History, MessageSquare, Plus, RotateCcw, Terminal, X } from 'lucide-react'; import type { Chat, WorkspacePane } from '@/api/types'; import { StatusDot } from '@/components/StatusDot'; import { @@ -26,7 +26,9 @@ interface Props { onCloseOthers: (chatId: string) => void; onCloseToRight: (chatId: string) => void; onCloseAll: () => void; - onAddPane: (kind: 'chat' | 'terminal' | 'coder') => void; + onNewTab: () => void; + onSplitPane: (kind: 'chat' | 'terminal' | 'coder') => void; + onReopenPane?: () => void; onShowHistory: () => void; onRename: (chatId: string, name: string) => Promise; onRemovePane?: () => void; @@ -40,7 +42,9 @@ export function ChatTabBar({ onCloseOthers, onCloseToRight, onCloseAll, - onAddPane, + onNewTab, + onSplitPane, + onReopenPane, onShowHistory, onRename, onRemovePane, @@ -131,7 +135,7 @@ export function ChatTabBar({ - onAddPane('chat')}> + New chat @@ -170,29 +174,49 @@ export function ChatTabBar({ )}
+ - onAddPane('chat')}> + onSplitPane('chat')}> New BooChat - onAddPane('terminal')}> + onSplitPane('terminal')}> New BooTerm - onAddPane('coder')}> + onSplitPane('coder')}> New BooCode + {onReopenPane && ( + + )}