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 && ( + + )}