fix(agent-manager): support diff sidebar with terminals

This commit is contained in:
marius-kilocode
2026-05-14 14:09:12 +02:00
parent ddf3ea6915
commit da9292fbcc
8 changed files with 141 additions and 72 deletions
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Support viewing Agent Manager terminal tabs alongside the right diff sidebar, and send review comments to the active terminal.
@@ -1528,6 +1528,7 @@ const AgentManagerContent: Component = () => {
const openReviewTab = () => {
const sel = selection()
if (sel === null) return
terms.setActiveId(undefined)
setSidePanel(null)
setReviewOpenForContext(sel, true)
setReviewActive(true)
@@ -3007,76 +3008,73 @@ const AgentManagerContent: Component = () => {
/>
</Show>
<Show when={!contextEmpty() && !history()}>
{/* Stacking container: the terminal layer is an absolutely-
positioned overlay on top of the chat. This sub-container
is position:relative so the overlay does not cover the
tab bar above (which lives outside this stack). */}
{/* Terminal overlay is scoped to the main pane so it does not cover the tab bar or side panel. */}
<div class="am-detail-stack">
{/* Terminal tabs are kept mounted regardless of visibility so
output keeps streaming across worktree switches without
detaching xterm from the paint tree. */}
{renderTerminalLayer({ state: terms })}
{/* Chat + side diff panel (hidden when review tab or terminal tab is active) */}
{/* Chat/terminal + side diff panel. Keep it mounted under the
review tab so live xterm canvases never leave the paint tree. */}
<div
class={`am-detail-content ${sidePanel() !== null ? "am-detail-split" : ""}`}
style={{ display: reviewActive() || terms.activeId() ? "none" : undefined }}
class={`am-detail-content ${sidePanel() !== null ? "am-detail-split" : ""} ${reviewActive() ? "am-detail-content-hidden" : ""}`}
>
<div class="am-chat-wrapper">
<ChatView
onSelectSession={(id) => {
if (localSessionIDs().includes(id)) {
session.selectSession(id)
if (selection() === null) setSelection(LOCAL)
return
}
// Navigate to owning worktree instead of forcing into local mode
if (worktreeSessionIds().has(id)) {
const ms = managedSessions().find((s) => s.id === id)
if (ms?.worktreeId) {
selectWorktree(ms.worktreeId)
<div class={`am-main-pane ${terms.activeId() ? "am-main-pane-terminal-active" : ""}`}>
{/* Keep terminal tabs mounted so output streams across worktree switches. */}
{renderTerminalLayer({ state: terms })}
<div class="am-chat-wrapper">
<ChatView
onSelectSession={(id) => {
if (localSessionIDs().includes(id)) {
session.selectSession(id)
setReviewActive(false)
if (selection() === null) setSelection(LOCAL)
return
}
}
openLocally(id)
}}
onShowHistory={() => setHistory(true)}
onForkMessage={readOnly() ? undefined : handleForkSession}
readonly={readOnly()}
continueInWorktree={selection() === LOCAL}
promptBoxId={`agent-manager:${selection() ?? "unassigned"}`}
pendingSessionID={selection() === LOCAL ? activePendingId() : undefined}
/>
<Show when={readOnly()}>
<div class="am-readonly-banner">
<Icon name="branch" size="small" />
<span class="am-readonly-text">{t("agentManager.session.readonly")}</span>
<Button
variant="secondary"
size="small"
onClick={() => {
if (!loaded()) return
const sid = session.currentSessionID()
if (!sid) return
openLocally(sid)
}}
>
{t("agentManager.session.openLocally")}
</Button>
<Button
variant="primary"
size="small"
onClick={() => {
if (!loaded()) return
const sid = session.currentSessionID()
if (sid) vscode.postMessage({ type: "agentManager.promoteSession", sessionId: sid })
}}
>
{t("agentManager.session.openInWorktree")}
</Button>
</div>
</Show>
// Navigate to owning worktree instead of forcing into local mode
if (worktreeSessionIds().has(id)) {
const ms = managedSessions().find((s) => s.id === id)
if (ms?.worktreeId) {
selectWorktree(ms.worktreeId)
session.selectSession(id)
setReviewActive(false)
return
}
}
openLocally(id)
}}
onShowHistory={() => setHistory(true)}
onForkMessage={readOnly() ? undefined : handleForkSession}
readonly={readOnly()}
continueInWorktree={selection() === LOCAL}
promptBoxId={`agent-manager:${selection() ?? "unassigned"}`}
pendingSessionID={selection() === LOCAL ? activePendingId() : undefined}
/>
<Show when={readOnly()}>
<div class="am-readonly-banner">
<Icon name="branch" size="small" />
<span class="am-readonly-text">{t("agentManager.session.readonly")}</span>
<Button
variant="secondary"
size="small"
onClick={() => {
if (!loaded()) return
const sid = session.currentSessionID()
if (!sid) return
openLocally(sid)
}}
>
{t("agentManager.session.openLocally")}
</Button>
<Button
variant="primary"
size="small"
onClick={() => {
if (!loaded()) return
const sid = session.currentSessionID()
if (sid) vscode.postMessage({ type: "agentManager.promoteSession", sessionId: sid })
}}
>
{t("agentManager.session.openInWorktree")}
</Button>
</div>
</Show>
</div>
</div>
<Show when={sidePanel() !== null}>
<div class="am-diff-resize" style={{ width: `${diffWidth()}px` }}>
@@ -3121,6 +3119,7 @@ const AgentManagerContent: Component = () => {
}}
onRevertFile={revertCtl.revert}
revertingFiles={revertCtl.reverting()}
activeTerminalId={terms.activeId()}
/>
</Show>
</div>
@@ -3151,6 +3150,7 @@ const AgentManagerContent: Component = () => {
}}
onRevertFile={revertCtl.revert}
revertingFiles={revertCtl.reverting()}
activeTerminalId={terms.activeId()}
onClose={closeReviewTab}
/>
</div>
@@ -54,6 +54,7 @@ interface DiffPanelProps {
onOpenFile?: (relativePath: string, line?: number) => void
onRevertFile?: (file: string) => void
revertingFiles?: Set<string>
activeTerminalId?: string
}
export const DiffPanel: Component<DiffPanelProps> = (props) => {
@@ -305,6 +306,7 @@ export const DiffPanel: Component<DiffPanelProps> = (props) => {
deleteComment,
cancelDraft,
labels: labels(),
activeTerminalId: props.activeTerminalId,
})
}
@@ -328,7 +330,14 @@ export const DiffPanel: Component<DiffPanelProps> = (props) => {
const all = comments()
if (all.length === 0) return
window.dispatchEvent(
new MessageEvent("message", { data: { type: "appendReviewComments", comments: all, autoSend: true } }),
new MessageEvent("message", {
data: {
type: props.activeTerminalId ? "appendReviewCommentsToTerminal" : "appendReviewComments",
comments: all,
autoSend: true,
targetTerminalId: props.activeTerminalId,
},
}),
)
preserveScroll(() => setComments([]))
props.onSendAll?.()
@@ -59,6 +59,7 @@ interface FullScreenDiffViewProps {
onOpenFile?: (relativePath: string, line?: number) => void
onRevertFile?: (file: string) => void
revertingFiles?: Set<string>
activeTerminalId?: string
/** Defaults to true. Hides the per-file Revert action when false. */
canRevert?: boolean
/** Defaults to true. Disables comment creation and "Send all" when false. */
@@ -320,6 +321,7 @@ export const FullScreenDiffView: Component<FullScreenDiffViewProps> = (props) =>
deleteComment,
cancelDraft,
labels: labels(),
activeTerminalId: props.activeTerminalId,
})
}
@@ -336,7 +338,14 @@ export const FullScreenDiffView: Component<FullScreenDiffViewProps> = (props) =>
const all = comments()
if (all.length === 0) return
window.dispatchEvent(
new MessageEvent("message", { data: { type: "appendReviewComments", comments: all, autoSend: true } }),
new MessageEvent("message", {
data: {
type: props.activeTerminalId ? "appendReviewCommentsToTerminal" : "appendReviewComments",
comments: all,
autoSend: true,
targetTerminalId: props.activeTerminalId,
},
}),
)
preserveScroll(() => setComments([]))
props.onSendAll?.()
@@ -1650,16 +1650,38 @@ body.am-wt-dragging-active * {
flex-direction: column;
flex: 1;
min-height: 0;
min-width: 0;
isolation: isolate;
}
.am-detail-content-hidden {
position: absolute;
inset: 0;
opacity: 0;
pointer-events: none;
z-index: 0;
}
.am-detail-split {
flex-direction: row;
}
.am-detail-split .am-chat-wrapper {
.am-main-pane {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
min-width: 0;
position: relative;
isolation: isolate;
}
.am-main-pane-terminal-active .am-chat-wrapper {
opacity: 0;
pointer-events: none;
}
.am-detail-split .am-main-pane {
border-right: 1px solid var(--border-weak-base);
}
@@ -4331,9 +4353,9 @@ body.vscode-high-contrast-light {
* subtree and stop xterm's internal rAF render loop, causing the
* "press Enter to see content" bug on worktree switches.
*
* Stack: position:relative container hosting the chat, review, and
* terminal layer. Sits below the tab bar inside `.am-detail`.
* Layer: absolute-positioned overlay covering the stack.
* Stack: position:relative container hosting the main pane, review,
* and side panel. Sits below the tab bar inside `.am-detail`.
* Layer: absolute-positioned overlay covering the main pane.
* `pointer-events: none` when no terminal is active, letting
* clicks fall through to the chat behind it.
* Slots: absolute-positioned and stacked inside the layer; only the
@@ -4355,7 +4377,7 @@ body.vscode-high-contrast-light {
opacity: 0;
pointer-events: none;
background: var(--vscode-terminal-background, #1e1e1e);
z-index: 1;
z-index: 30;
/* Force a dedicated compositor layer so opacity flips do not re-lay-out
the xterm canvases underneath. */
will-change: opacity;
@@ -35,6 +35,7 @@ interface AnnotationHandlers {
deleteComment: (id: string) => void
cancelDraft: () => void
labels: AnnotationLabels
activeTerminalId?: string
}
function focusWhenConnected(el: HTMLTextAreaElement): void {
@@ -269,7 +270,12 @@ export function buildReviewAnnotation(
makeActionButton(handlers.labels.sendToChat, makeIcon("M1 1l14 7-14 7V9l10-1L1 7z"), () => {
window.dispatchEvent(
new MessageEvent("message", {
data: { type: "appendReviewComments", comments: [comment], autoSend: true },
data: {
type: handlers.activeTerminalId ? "appendReviewCommentsToTerminal" : "appendReviewComments",
comments: [comment],
autoSend: true,
targetTerminalId: handlers.activeTerminalId,
},
}),
)
handlers.deleteComment(comment.id)
@@ -19,6 +19,8 @@ import { UnicodeGraphemesAddon } from "@xterm/addon-unicode-graphemes"
import "@xterm/xterm/css/xterm.css"
import { useVSCode } from "../../src/context/vscode"
import { useLanguage } from "../../src/context/language"
import { formatReviewCommentsMarkdown } from "../../src/utils/review-comment-markdown"
import type { ReviewComment } from "../../src/types/messages"
interface Props {
terminalId: string
@@ -279,6 +281,14 @@ export const TerminalTab: Component<Props> = (props) => {
pendingFrame = requestAnimationFrame(runRepaint)
}
const fontSub = vscode.onMessage((message) => {
if (message.type === "appendReviewCommentsToTerminal") {
if (message.targetTerminalId !== props.terminalId) return
const comments = (message as { comments?: ReviewComment[] }).comments
if (!Array.isArray(comments) || comments.length === 0) return
term.paste(`${formatReviewCommentsMarkdown(comments)}\n`)
return
}
const size =
message.type === "fontSizeChanged" ? message.fontSize : message.type === "ready" ? message.fontSize : undefined
if (size === undefined) return
@@ -243,6 +243,13 @@ export interface AppendReviewCommentsMessage {
autoSend?: boolean
}
export interface AppendReviewCommentsToTerminalMessage {
type: "appendReviewCommentsToTerminal"
comments: ReviewComment[]
autoSend?: boolean
targetTerminalId: string
}
export interface TriggerTaskMessage {
type: "triggerTask"
text: string
@@ -958,6 +965,7 @@ export type ExtensionMessage =
| SetChatBoxMessage
| AppendChatBoxMessage
| AppendReviewCommentsMessage
| AppendReviewCommentsToTerminalMessage
| TriggerTaskMessage
| VariantsLoadedMessage
| CloudSessionDataLoadedMessage