mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-01 04:46:43 +08:00
fix(agent-manager): support diff sidebar with terminals
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user