mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 19:11:03 +08:00
refactor(vscode): remove duplicate and unused UI code
This commit is contained in:
@@ -360,7 +360,7 @@ describe("Agent Manager Model Picker", () => {
|
||||
|
||||
describe("Agent Manager Worktree Actions", () => {
|
||||
it("opens the configuration dialog from the primary plus action", () => {
|
||||
const source = fs.readFileSync(path.join(ROOT, "webview-ui/agent-manager/WorktreeSectionActions.tsx"), "utf-8")
|
||||
const source = fs.readFileSync(path.join(ROOT, "webview-ui/agent-manager/ProjectActions.tsx"), "utf-8")
|
||||
const start = source.indexOf('<div class="am-split-button">')
|
||||
const end = source.indexOf("</div>", start)
|
||||
const actions = source.slice(start, end)
|
||||
|
||||
@@ -47,7 +47,7 @@ describe("planDisplayPath", () => {
|
||||
describe("plan_exit renderer uses openFile not openDiff (source)", () => {
|
||||
const ROOT = path.resolve(import.meta.dir, "../..")
|
||||
const FILE = path.join(ROOT, "webview-ui/src/components/chat/AssistantMessage.tsx")
|
||||
const TURN_FILE = path.join(ROOT, "webview-ui/src/components/chat/VscodeSessionTurn.tsx")
|
||||
const TURN_FILE = path.join(ROOT, "webview-ui/src/components/chat/TranscriptRow.tsx")
|
||||
const src = fs.readFileSync(FILE, "utf-8")
|
||||
const turnSrc = fs.readFileSync(TURN_FILE, "utf-8")
|
||||
|
||||
@@ -76,6 +76,7 @@ describe("plan_exit renderer uses openFile not openDiff (source)", () => {
|
||||
expect(src).not.toContain("Object.values(data.store.part ?? {}).flat()")
|
||||
expect(src).not.toContain("[...props.parts, ...all()]")
|
||||
expect(src).not.toContain("turnParts")
|
||||
expect(turnSrc).toContain("parts={row().parts as unknown as SDKPart[]}")
|
||||
expect(turnSrc).not.toContain("assistantMessages().flatMap")
|
||||
expect(turnSrc).not.toContain("turnParts={assistantParts()}")
|
||||
})
|
||||
|
||||
@@ -3,7 +3,7 @@ import fs from "node:fs"
|
||||
import path from "node:path"
|
||||
|
||||
const ROOT = path.resolve(import.meta.dir, "../..")
|
||||
const TURN_FILE = path.join(ROOT, "webview-ui/src/components/chat/VscodeSessionTurn.tsx")
|
||||
const TURN_FILE = path.join(ROOT, "webview-ui/src/components/chat/TranscriptRow.tsx")
|
||||
const PROVIDER_FILE = path.join(ROOT, "src/KiloProvider.ts")
|
||||
const BANNER_FILE = path.join(ROOT, "webview-ui/src/components/chat/RevertBanner.tsx")
|
||||
const SESSION_FILE = path.join(ROOT, "webview-ui/src/types/messages/sessions.ts")
|
||||
@@ -32,12 +32,12 @@ function exported(name: string) {
|
||||
|
||||
describe("message revert checkpoints", () => {
|
||||
it("keeps revert actions available after a session is already reverted", () => {
|
||||
expect(src).toMatch(/onRevert=\{\s*assistantMessages\(\)\.length > 0\s*\? \(\) =>/)
|
||||
expect(src).toMatch(/onRevert=\{\s*row\(\)\.answered\s*\? \(\) =>/)
|
||||
expect(src).not.toMatch(/onRevert=\{[\s\S]*?&& !session\.revert\(\)[\s\S]*?\? \(\) =>/)
|
||||
})
|
||||
|
||||
it("only marks revert disabled while the agent is busy", () => {
|
||||
expect(src).toMatch(/data-revert-disabled=\{\s*assistantMessages\(\)\.length > 0 && session\.status\(\) !== "idle"/)
|
||||
expect(src).toMatch(/data-revert-disabled=\{\s*row\(\)\.answered && session\.status\(\) !== "idle"/)
|
||||
expect(src).not.toMatch(/data-revert-disabled=\{[\s\S]*?!session\.revert\(\)/)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
buildFileAnnotations,
|
||||
clearReviewComposer,
|
||||
createReviewComposer,
|
||||
labels,
|
||||
reviewAnnotationSpeechKey,
|
||||
reviewComposerDraft,
|
||||
reviewComposerEdit,
|
||||
@@ -441,3 +442,19 @@ describe("getFilename", () => {
|
||||
expect(getFilename("src/components/Button.tsx")).toBe("Button.tsx")
|
||||
})
|
||||
})
|
||||
|
||||
it("shares review action labels and translates line numbers", () => {
|
||||
const value = labels((key, params) => (params ? `${key}:${params.line}` : key))
|
||||
expect(value).toMatchObject({
|
||||
placeholder: "agentManager.review.commentPlaceholder",
|
||||
cancel: "common.cancel",
|
||||
comment: "agentManager.review.commentAction",
|
||||
send: "prompt.action.send",
|
||||
save: "common.save",
|
||||
sendToChat: "agentManager.review.sendToChat",
|
||||
edit: "common.edit",
|
||||
delete: "common.delete",
|
||||
})
|
||||
expect(value.commentOnLine(3)).toBe("agentManager.review.commentOnLine:3")
|
||||
expect(value.editCommentOnLine(7)).toBe("agentManager.review.editCommentOnLine:7")
|
||||
})
|
||||
|
||||
@@ -48,7 +48,7 @@ import {
|
||||
reviewDraftSpeechKey,
|
||||
reviewEditSpeechKey,
|
||||
sendReviewComments,
|
||||
type AnnotationLabels,
|
||||
labels,
|
||||
type AnnotationMeta,
|
||||
type ReviewComposer,
|
||||
type ReviewDraft,
|
||||
@@ -130,18 +130,6 @@ export const DiffPanel: Component<DiffPanelProps> = (props) => {
|
||||
const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.userAgent)
|
||||
const sendAllKeybind = () =>
|
||||
isMac ? t("agentManager.review.sendAllShortcut.mac") : t("agentManager.review.sendAllShortcut.other")
|
||||
const labels = (): AnnotationLabels => ({
|
||||
commentOnLine: (line) => t("agentManager.review.commentOnLine", { line }),
|
||||
editCommentOnLine: (line) => t("agentManager.review.editCommentOnLine", { line }),
|
||||
placeholder: t("agentManager.review.commentPlaceholder"),
|
||||
cancel: t("common.cancel"),
|
||||
comment: t("agentManager.review.commentAction"),
|
||||
send: t("prompt.action.send"),
|
||||
save: t("common.save"),
|
||||
sendToChat: t("agentManager.review.sendToChat"),
|
||||
edit: t("common.edit"),
|
||||
delete: t("common.delete"),
|
||||
})
|
||||
const localComposer = createReviewComposer()
|
||||
const composer = () => props.composer ?? localComposer
|
||||
const [manualOpen, setManualOpen] = createSignal<Record<string, string[]>>({})
|
||||
@@ -437,7 +425,7 @@ export const DiffPanel: Component<DiffPanelProps> = (props) => {
|
||||
updateComment,
|
||||
deleteComment,
|
||||
cancelDraft,
|
||||
labels: labels(),
|
||||
labels: labels(t),
|
||||
activeTerminalId: props.activeTerminalId,
|
||||
speech: reviewSpeech,
|
||||
})
|
||||
@@ -465,16 +453,7 @@ export const DiffPanel: Component<DiffPanelProps> = (props) => {
|
||||
const sendAllToChat = () => {
|
||||
const all = comments()
|
||||
if (all.length === 0) return
|
||||
window.dispatchEvent(
|
||||
new MessageEvent("message", {
|
||||
data: {
|
||||
type: props.activeTerminalId ? "appendReviewCommentsToTerminal" : "appendReviewComments",
|
||||
comments: all,
|
||||
autoSend: true,
|
||||
targetTerminalId: props.activeTerminalId,
|
||||
},
|
||||
}),
|
||||
)
|
||||
sendReviewComments(all, props.activeTerminalId)
|
||||
preserveScroll(() => setComments([]))
|
||||
props.onSendAll?.()
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { TooltipKeybind } from "@kilocode/kilo-ui/tooltip"
|
||||
import type { LanguageContextValue } from "../src/context/language"
|
||||
import { parseBindingTokens } from "./keybind-tokens"
|
||||
|
||||
interface Props {
|
||||
export interface WorktreeCreateProps {
|
||||
branch: string
|
||||
bindings: Record<string, string>
|
||||
loaded: boolean
|
||||
@@ -16,59 +16,62 @@ interface Props {
|
||||
onCreate: () => void
|
||||
onNew: () => void
|
||||
onSection: () => void
|
||||
onSettings: () => void
|
||||
}
|
||||
|
||||
export const ProjectActions: Component<Props> = (props) => (
|
||||
<div class="am-project-actions">
|
||||
<div class="am-split-button">
|
||||
<TooltipKeybind
|
||||
title={props.t("agentManager.shortcuts.advancedWorktree")}
|
||||
keybind={props.bindings.newWorktree ?? ""}
|
||||
export const WorktreeCreate: Component<WorktreeCreateProps> = (props) => (
|
||||
<div class="am-split-button">
|
||||
<TooltipKeybind
|
||||
title={props.t("agentManager.shortcuts.advancedWorktree")}
|
||||
keybind={props.bindings.newWorktree ?? ""}
|
||||
>
|
||||
<IconButton
|
||||
icon="plus"
|
||||
size="small"
|
||||
variant="ghost"
|
||||
label={props.t("agentManager.worktree.new")}
|
||||
onClick={props.onNew}
|
||||
disabled={!props.loaded}
|
||||
/>
|
||||
</TooltipKeybind>
|
||||
<DropdownMenu gutter={4} placement="bottom-end">
|
||||
<DropdownMenu.Trigger
|
||||
class="am-split-arrow"
|
||||
aria-label={props.t("agentManager.worktree.advancedOptions")}
|
||||
disabled={!props.loaded}
|
||||
>
|
||||
<IconButton
|
||||
icon="plus"
|
||||
size="small"
|
||||
variant="ghost"
|
||||
label={props.t("agentManager.worktree.new")}
|
||||
onClick={props.onNew}
|
||||
disabled={!props.loaded}
|
||||
/>
|
||||
</TooltipKeybind>
|
||||
<DropdownMenu gutter={4} placement="bottom-end">
|
||||
<DropdownMenu.Trigger
|
||||
class="am-split-arrow"
|
||||
aria-label={props.t("agentManager.worktree.advancedOptions")}
|
||||
disabled={!props.loaded}
|
||||
>
|
||||
<Icon name="chevron-down" size="small" />
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content class="am-split-menu">
|
||||
<DropdownMenu.Item onSelect={props.onCreate}>
|
||||
<span class="am-worktree-menu-gap" aria-hidden="true" />
|
||||
<DropdownMenu.ItemLabel class="am-worktree-menu-label">
|
||||
<span>{props.t("sidebar.session.newWorktree.from")}</span>
|
||||
<span class="am-worktree-menu-branch">
|
||||
<Icon name="branch" size="small" />
|
||||
<strong>{props.branch}</strong>
|
||||
</span>
|
||||
</DropdownMenu.ItemLabel>
|
||||
<span class="am-menu-shortcut">
|
||||
{parseBindingTokens(props.bindings.quickWorktree ?? "").map((token) => (
|
||||
<kbd class="am-menu-key">{token}</kbd>
|
||||
))}
|
||||
<Icon name="chevron-down" size="small" />
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content class="am-split-menu">
|
||||
<DropdownMenu.Item onSelect={props.onCreate}>
|
||||
<span class="am-worktree-menu-gap" aria-hidden="true" />
|
||||
<DropdownMenu.ItemLabel class="am-worktree-menu-label">
|
||||
<span>{props.t("sidebar.session.newWorktree.from")}</span>
|
||||
<span class="am-worktree-menu-branch">
|
||||
<Icon name="branch" size="small" />
|
||||
<strong>{props.branch}</strong>
|
||||
</span>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item onSelect={props.onSection}>
|
||||
<Icon name="plus" size="small" />
|
||||
<DropdownMenu.ItemLabel>{props.t("agentManager.worktree.newSection")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</DropdownMenu.ItemLabel>
|
||||
<span class="am-menu-shortcut">
|
||||
{parseBindingTokens(props.bindings.quickWorktree ?? "").map((token) => (
|
||||
<kbd class="am-menu-key">{token}</kbd>
|
||||
))}
|
||||
</span>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item onSelect={props.onSection}>
|
||||
<Icon name="plus" size="small" />
|
||||
<DropdownMenu.ItemLabel>{props.t("agentManager.worktree.newSection")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)
|
||||
|
||||
export const ProjectActions: Component<WorktreeCreateProps & { onSettings: () => void }> = (props) => (
|
||||
<div class="am-project-actions">
|
||||
<WorktreeCreate {...props} />
|
||||
<IconButton
|
||||
icon="settings-gear"
|
||||
size="small"
|
||||
|
||||
@@ -2,28 +2,18 @@
|
||||
|
||||
import type { Accessor, Component } from "solid-js"
|
||||
import { Show } from "solid-js"
|
||||
import { DropdownMenu } from "@kilocode/kilo-ui/dropdown-menu"
|
||||
import { Icon } from "@kilocode/kilo-ui/icon"
|
||||
import { IconButton } from "@kilocode/kilo-ui/icon-button"
|
||||
import { Tooltip, TooltipKeybind } from "@kilocode/kilo-ui/tooltip"
|
||||
import type { LanguageContextValue } from "../src/context/language"
|
||||
import { parseBindingTokens } from "./keybind-tokens"
|
||||
import { WorktreeCreate, type WorktreeCreateProps } from "./ProjectActions"
|
||||
import { SidebarSearchMenu, type SidebarSearchMenuRef } from "./SidebarSearchMenu"
|
||||
import type { SidebarSearchItem } from "./sidebar-search"
|
||||
|
||||
interface WorktreeSectionActionsProps {
|
||||
interface WorktreeSectionActionsProps extends WorktreeCreateProps {
|
||||
items: Accessor<SidebarSearchItem[]>
|
||||
current: Accessor<SidebarSearchItem | undefined>
|
||||
bindings: Record<string, string>
|
||||
branch: string
|
||||
git: boolean
|
||||
loaded: boolean
|
||||
t: LanguageContextValue["t"]
|
||||
onRef: (ref: SidebarSearchMenuRef) => void
|
||||
onSelect: (item: SidebarSearchItem) => void
|
||||
onCreate: () => void
|
||||
onNew: () => void
|
||||
onSection: () => void
|
||||
onShortcuts: () => void
|
||||
onSettings: () => void
|
||||
onHistory: () => void
|
||||
@@ -47,54 +37,7 @@ export const WorktreeSectionActions: Component<WorktreeSectionActionsProps> = (p
|
||||
onSelect={props.onSelect}
|
||||
/>
|
||||
<Show when={props.git}>
|
||||
<div class="am-split-button">
|
||||
<TooltipKeybind
|
||||
title={props.t("agentManager.shortcuts.advancedWorktree")}
|
||||
keybind={props.bindings.newWorktree ?? ""}
|
||||
>
|
||||
<IconButton
|
||||
icon="plus"
|
||||
size="small"
|
||||
variant="ghost"
|
||||
label={props.t("agentManager.worktree.new")}
|
||||
onClick={props.onNew}
|
||||
disabled={!props.loaded}
|
||||
/>
|
||||
</TooltipKeybind>
|
||||
<DropdownMenu gutter={4} placement="bottom-end">
|
||||
<DropdownMenu.Trigger
|
||||
class="am-split-arrow"
|
||||
aria-label={props.t("agentManager.worktree.advancedOptions")}
|
||||
disabled={!props.loaded}
|
||||
>
|
||||
<Icon name="chevron-down" size="small" />
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content class="am-split-menu">
|
||||
<DropdownMenu.Item onSelect={props.onCreate}>
|
||||
<span class="am-worktree-menu-gap" aria-hidden="true" />
|
||||
<DropdownMenu.ItemLabel class="am-worktree-menu-label">
|
||||
<span>{props.t("sidebar.session.newWorktree.from")}</span>
|
||||
<span class="am-worktree-menu-branch">
|
||||
<Icon name="branch" size="small" />
|
||||
<strong>{props.branch}</strong>
|
||||
</span>
|
||||
</DropdownMenu.ItemLabel>
|
||||
<span class="am-menu-shortcut">
|
||||
{parseBindingTokens(props.bindings.quickWorktree ?? "").map((token) => (
|
||||
<kbd class="am-menu-key">{token}</kbd>
|
||||
))}
|
||||
</span>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item onSelect={props.onSection}>
|
||||
<Icon name="plus" size="small" />
|
||||
<DropdownMenu.ItemLabel>{props.t("agentManager.worktree.newSection")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<WorktreeCreate {...props} />
|
||||
<TooltipKeybind
|
||||
title={props.t("agentManager.shortcuts.title")}
|
||||
keybind={props.bindings.showShortcuts ?? ""}
|
||||
|
||||
@@ -41,7 +41,7 @@ import {
|
||||
reviewDraftSpeechKey,
|
||||
reviewEditSpeechKey,
|
||||
sendReviewComments,
|
||||
type AnnotationLabels,
|
||||
labels,
|
||||
type AnnotationMeta,
|
||||
type ReviewComposer,
|
||||
type ReviewDraft,
|
||||
@@ -122,18 +122,6 @@ export const FullScreenDiffView: Component<FullScreenDiffViewProps> = (props) =>
|
||||
const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.userAgent)
|
||||
const sendAllKeybind = () =>
|
||||
isMac ? t("agentManager.review.sendAllShortcut.mac") : t("agentManager.review.sendAllShortcut.other")
|
||||
const labels = (): AnnotationLabels => ({
|
||||
commentOnLine: (line) => t("agentManager.review.commentOnLine", { line }),
|
||||
editCommentOnLine: (line) => t("agentManager.review.editCommentOnLine", { line }),
|
||||
placeholder: t("agentManager.review.commentPlaceholder"),
|
||||
cancel: t("common.cancel"),
|
||||
comment: t("agentManager.review.commentAction"),
|
||||
send: t("prompt.action.send"),
|
||||
save: t("common.save"),
|
||||
sendToChat: t("agentManager.review.sendToChat"),
|
||||
edit: t("common.edit"),
|
||||
delete: t("common.delete"),
|
||||
})
|
||||
const localComposer = createReviewComposer()
|
||||
const composer = () => props.composer ?? localComposer
|
||||
const [manualOpen, setManualOpen] = createSignal<Record<string, string[]>>({})
|
||||
@@ -437,7 +425,7 @@ export const FullScreenDiffView: Component<FullScreenDiffViewProps> = (props) =>
|
||||
updateComment,
|
||||
deleteComment,
|
||||
cancelDraft,
|
||||
labels: labels(),
|
||||
labels: labels(t),
|
||||
activeTerminalId: props.activeTerminalId,
|
||||
speech: reviewSpeech,
|
||||
})
|
||||
@@ -458,16 +446,7 @@ export const FullScreenDiffView: Component<FullScreenDiffViewProps> = (props) =>
|
||||
const sendAllToChat = () => {
|
||||
const all = comments()
|
||||
if (all.length === 0) return
|
||||
window.dispatchEvent(
|
||||
new MessageEvent("message", {
|
||||
data: {
|
||||
type: props.activeTerminalId ? "appendReviewCommentsToTerminal" : "appendReviewComments",
|
||||
comments: all,
|
||||
autoSend: true,
|
||||
targetTerminalId: props.activeTerminalId,
|
||||
},
|
||||
}),
|
||||
)
|
||||
sendReviewComments(all, props.activeTerminalId)
|
||||
preserveScroll(() => setComments([]))
|
||||
props.onSendAll?.()
|
||||
}
|
||||
|
||||
@@ -16,6 +16,21 @@ export interface AnnotationLabels {
|
||||
delete: string
|
||||
}
|
||||
|
||||
export function labels(t: (key: string, params?: Record<string, string | number>) => string): AnnotationLabels {
|
||||
return {
|
||||
commentOnLine: (line) => t("agentManager.review.commentOnLine", { line }),
|
||||
editCommentOnLine: (line) => t("agentManager.review.editCommentOnLine", { line }),
|
||||
placeholder: t("agentManager.review.commentPlaceholder"),
|
||||
cancel: t("common.cancel"),
|
||||
comment: t("agentManager.review.commentAction"),
|
||||
send: t("prompt.action.send"),
|
||||
save: t("common.save"),
|
||||
sendToChat: t("agentManager.review.sendToChat"),
|
||||
edit: t("common.edit"),
|
||||
delete: t("common.delete"),
|
||||
}
|
||||
}
|
||||
|
||||
// A draft is the active unsaved inline comment composer opened from the gutter.
|
||||
// It becomes a normal comment only after the user submits the textarea.
|
||||
export interface AnnotationMeta {
|
||||
|
||||
@@ -18,12 +18,12 @@ import {
|
||||
buildReviewAnnotation,
|
||||
createReviewComposer,
|
||||
sendReviewComments,
|
||||
type AnnotationLabels,
|
||||
labels,
|
||||
type AnnotationMeta,
|
||||
type ReviewComposer,
|
||||
type ReviewDraft,
|
||||
} from "../diff-viewer/review-annotations"
|
||||
import { lineCount } from "../diff-viewer/review-comments"
|
||||
import { getFilename, lineCount } from "../diff-viewer/review-comments"
|
||||
import { useLanguage } from "../src/context/language"
|
||||
|
||||
export interface DocumentPanelProps {
|
||||
@@ -43,10 +43,6 @@ export interface DocumentPanelProps {
|
||||
visible: Accessor<boolean>
|
||||
}
|
||||
|
||||
function pathName(file: string): string {
|
||||
return file.slice(file.lastIndexOf("/") + 1)
|
||||
}
|
||||
|
||||
function virtualDiff(file: string, content: string): WorktreeFileDiff {
|
||||
return {
|
||||
file,
|
||||
@@ -98,18 +94,6 @@ export const DocumentPanel: Component<DocumentPanelProps> = (props) => {
|
||||
const file = () => selected()?.file ?? ""
|
||||
const content = () => data()?.content ?? ""
|
||||
const diff = () => virtualDiff(file(), content())
|
||||
const labels = (): AnnotationLabels => ({
|
||||
commentOnLine: (line) => t("agentManager.review.commentOnLine", { line }),
|
||||
editCommentOnLine: (line) => t("agentManager.review.editCommentOnLine", { line }),
|
||||
placeholder: t("agentManager.review.commentPlaceholder"),
|
||||
cancel: t("common.cancel"),
|
||||
comment: t("agentManager.review.commentAction"),
|
||||
send: t("prompt.action.send"),
|
||||
save: t("common.save"),
|
||||
sendToChat: t("agentManager.review.sendToChat"),
|
||||
edit: t("common.edit"),
|
||||
delete: t("common.delete"),
|
||||
})
|
||||
|
||||
const updateComments = (next: ReviewComment[]) => props.onCommentsChange(next)
|
||||
const comments = () => props.comments.filter((item) => item.file === file())
|
||||
@@ -176,7 +160,7 @@ export const DocumentPanel: Component<DocumentPanelProps> = (props) => {
|
||||
updateComment,
|
||||
deleteComment,
|
||||
cancelDraft,
|
||||
labels: labels(),
|
||||
labels: labels(t),
|
||||
activeTerminalId: props.activeTerminalId,
|
||||
})
|
||||
const gutter = (range: SelectedLineRange) => {
|
||||
@@ -293,7 +277,7 @@ export const DocumentPanel: Component<DocumentPanelProps> = (props) => {
|
||||
<SortableClosableTab
|
||||
id={id}
|
||||
class="am-document-tab"
|
||||
label={pathName(tab.file)}
|
||||
label={getFilename(tab.file)}
|
||||
tooltip={tab.file}
|
||||
icon="open-file"
|
||||
iconNode={<FileIcon node={{ path: tab.file, type: "file" }} class="am-document-tab-icon" />}
|
||||
|
||||
@@ -3,9 +3,6 @@
|
||||
/**
|
||||
* MessageList component
|
||||
* Scrollable turn-based message list with virtualization.
|
||||
* Each user message is rendered as a VscodeSessionTurn — a custom component that
|
||||
* renders all assistant parts as a flat, verbose list with no context grouping,
|
||||
* and fully expands sub-agent (task tool) parts inline.
|
||||
* Shows recent sessions in the empty state for quick resumption.
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,210 +0,0 @@
|
||||
/**
|
||||
* VscodeSessionTurn component
|
||||
* Custom replacement for the upstream SessionTurn, designed for the VS Code sidebar.
|
||||
*
|
||||
* Key differences from upstream SessionTurn:
|
||||
* - No "Gathered context" grouping — each tool call is rendered individually
|
||||
* - Sub-agents are fully expanded inline via TaskToolExpanded
|
||||
* - No per-turn auto-scroll (MessageList handles it)
|
||||
* - Simpler flat structure without overflow containers
|
||||
*/
|
||||
|
||||
import { Component, createMemo, For, Show, createEffect } from "solid-js"
|
||||
import { UserMessageDisplay } from "@kilocode/kilo-ui/message-part"
|
||||
import { DiffChanges } from "@kilocode/kilo-ui/diff-changes"
|
||||
import { Icon } from "@kilocode/kilo-ui/icon"
|
||||
import { useData } from "@kilocode/kilo-ui/context/data"
|
||||
import { useI18n } from "@kilocode/kilo-ui/context/i18n"
|
||||
import { AssistantMessage } from "./AssistantMessage"
|
||||
import type {
|
||||
AssistantMessage as SDKAssistantMessage,
|
||||
Message as SDKMessage,
|
||||
Part as SDKPart,
|
||||
SnapshotFileDiff,
|
||||
} from "@kilocode/sdk/v2"
|
||||
import { ErrorDisplay } from "./ErrorDisplay"
|
||||
import { useServer } from "../../context/server"
|
||||
import { useSession } from "../../context/session"
|
||||
import { useLanguage } from "../../context/language"
|
||||
import { useVSCode } from "../../context/vscode"
|
||||
import { useFeedback } from "../../context/feedback"
|
||||
import { visibleError } from "../../context/session-errors"
|
||||
import type { ErrorDisplayProps } from "./ErrorDisplay"
|
||||
import type { Message as WebMessage } from "../../types/messages"
|
||||
|
||||
export interface VscodeTurn {
|
||||
id: string
|
||||
user: WebMessage
|
||||
assistant: WebMessage[]
|
||||
partial?: boolean
|
||||
}
|
||||
|
||||
interface VscodeSessionTurnProps {
|
||||
turn: VscodeTurn
|
||||
queued?: boolean
|
||||
onForkMessage?: (sessionId: string, messageId: string) => void
|
||||
}
|
||||
|
||||
export const VscodeSessionTurn: Component<VscodeSessionTurnProps> = (props) => {
|
||||
const data = useData()
|
||||
const i18n = useI18n()
|
||||
const server = useServer()
|
||||
const session = useSession()
|
||||
const language = useLanguage()
|
||||
const vscode = useVSCode()
|
||||
const feedback = useFeedback()
|
||||
|
||||
const emptyParts: SDKPart[] = []
|
||||
const emptyDiffs: SnapshotFileDiff[] = []
|
||||
|
||||
createEffect(() => {
|
||||
const turn = props.turn
|
||||
const ids = turn.partial ? turn.assistant.map((m) => m.id) : [turn.user.id, ...turn.assistant.map((m) => m.id)]
|
||||
session.hydrateParts(ids)
|
||||
})
|
||||
|
||||
const message = createMemo(() => props.turn.user as SDKMessage & { role: "user" })
|
||||
|
||||
const parts = createMemo(() => {
|
||||
const msg = message()
|
||||
return (data.store.part?.[msg.id] ?? emptyParts) as SDKPart[]
|
||||
})
|
||||
|
||||
const assistantMessages = createMemo(() => props.turn.assistant as SDKAssistantMessage[])
|
||||
|
||||
const interrupted = createMemo(() => assistantMessages().some((m) => m.error?.name === "MessageAbortedError"))
|
||||
|
||||
const error = createMemo(() => visibleError(assistantMessages(), session.isErrorHidden))
|
||||
|
||||
// Diffs from message summary
|
||||
const diffs = createMemo(() => {
|
||||
const rawDiffs = (message() as unknown as { summary?: { diffs?: unknown[] } } | undefined)?.summary?.diffs
|
||||
if (!rawDiffs?.length) return emptyDiffs
|
||||
const seen = new Set<string>()
|
||||
return (rawDiffs as SnapshotFileDiff[])
|
||||
.reduceRight<SnapshotFileDiff[]>((result, diff) => {
|
||||
const file = diff.file ?? ""
|
||||
if (seen.has(file)) return result
|
||||
seen.add(file)
|
||||
result.push(diff)
|
||||
return result
|
||||
}, [])
|
||||
.reverse()
|
||||
})
|
||||
|
||||
const openChanges = () => vscode.postMessage({ type: "openChanges", turnId: message().id })
|
||||
|
||||
// Copy part ID — the last text part from the last assistant message.
|
||||
// Synthetic parts (e.g. "Initializing snapshot…" from the slow-repo guard)
|
||||
// are transient status lines, not assistant output: they must never win
|
||||
// this lookup, otherwise the copy button renders beside the spinner
|
||||
// instead of the real response.
|
||||
const showAssistantCopyPartID = createMemo(() => {
|
||||
const msgs = assistantMessages()
|
||||
for (let i = msgs.length - 1; i >= 0; i--) {
|
||||
const msg = msgs[i]
|
||||
if (!msg) continue
|
||||
const msgParts = (data.store.part?.[msg.id] ?? emptyParts) as SDKPart[]
|
||||
for (let j = msgParts.length - 1; j >= 0; j--) {
|
||||
const part = msgParts[j]
|
||||
if (!part || part.type !== "text") continue
|
||||
if ((part as SDKPart & { synthetic?: boolean }).synthetic) continue
|
||||
if ((part as SDKPart & { text: string }).text?.trim()) return part.id
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
return (
|
||||
<Show when={message()}>
|
||||
{(msg) => (
|
||||
<div class="vscode-session-turn" data-message={msg().id}>
|
||||
{/* User message */}
|
||||
<Show when={!props.turn.partial}>
|
||||
<div
|
||||
class="vscode-session-turn-user"
|
||||
data-revert-disabled={assistantMessages().length > 0 && session.status() !== "idle" ? "" : undefined}
|
||||
title={
|
||||
assistantMessages().length > 0 && session.status() !== "idle"
|
||||
? language.t("revert.disabled.agentBusy")
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<UserMessageDisplay
|
||||
message={msg() as unknown as Parameters<typeof UserMessageDisplay>[0]["message"]}
|
||||
parts={parts() as unknown as Parameters<typeof UserMessageDisplay>[0]["parts"]}
|
||||
interrupted={interrupted()}
|
||||
queued={props.queued}
|
||||
onFork={props.onForkMessage ? () => props.onForkMessage?.(msg().sessionID, msg().id) : undefined}
|
||||
onRevert={
|
||||
assistantMessages().length > 0
|
||||
? () => {
|
||||
if (session.status() !== "idle") return
|
||||
session.revertSession(msg().id)
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{/* Assistant parts — flat list, no context grouping */}
|
||||
<Show when={assistantMessages().length > 0}>
|
||||
<div class="vscode-session-turn-assistant">
|
||||
<For each={assistantMessages()}>
|
||||
{(amsg) => (
|
||||
<AssistantMessage
|
||||
message={amsg}
|
||||
showAssistantCopyPartID={showAssistantCopyPartID()}
|
||||
feedback={{
|
||||
enabled: feedback.telemetryEnabled(),
|
||||
rating: feedback.getRating(amsg.id),
|
||||
onRate: (next) =>
|
||||
feedback.rate({
|
||||
messageID: amsg.id,
|
||||
sessionID: amsg.sessionID,
|
||||
parentMessageID: amsg.parentID,
|
||||
providerID: amsg.providerID,
|
||||
modelID: amsg.modelID,
|
||||
variant: (amsg as SDKAssistantMessage & { variant?: string }).variant,
|
||||
next,
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{/* Diff summary — shown after completion. Click opens the changes view. */}
|
||||
<Show when={diffs().length > 0 && server.gitInstalled()}>
|
||||
<div class="vscode-session-turn-diffs" data-component="session-turn">
|
||||
<button
|
||||
type="button"
|
||||
class="vscode-session-turn-diffs-trigger"
|
||||
onClick={openChanges}
|
||||
aria-label={i18n.t("ui.sessionReview.change.modified")}
|
||||
>
|
||||
<span data-slot="session-turn-diffs-label">{i18n.t("ui.sessionReview.change.modified")}</span>
|
||||
<span data-slot="session-turn-diffs-count">
|
||||
{diffs().length} {i18n.t(diffs().length === 1 ? "ui.common.file.one" : "ui.common.file.other")}
|
||||
</span>
|
||||
<span data-slot="session-turn-diffs-meta">
|
||||
<DiffChanges changes={diffs()} variant="bars" />
|
||||
</span>
|
||||
<span data-slot="session-turn-diffs-chevron" aria-hidden="true">
|
||||
<Icon name="chevron-right" size="small" />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{/* Error handling */}
|
||||
<Show when={error()}>
|
||||
{(err) => <ErrorDisplay error={err() as ErrorDisplayProps["error"]} onLogin={server.goToLogin} />}
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
@@ -166,7 +166,6 @@ interface SessionContextValue {
|
||||
isErrorHidden: (messageID: string) => boolean
|
||||
|
||||
// Move stashed parts into the reactive store for the given message IDs.
|
||||
// Called by VscodeSessionTurn when the virtualizer renders a turn.
|
||||
hydrateParts: (messageIDs: string[]) => void
|
||||
|
||||
// Todos for current session
|
||||
@@ -363,7 +362,7 @@ export const SessionProvider: ParentComponent = (props) => {
|
||||
const [pages, setPages] = createStore<Record<string, MessagePageState>>({})
|
||||
|
||||
// Parts stash: holds parts from messagesLoaded outside the reactive store
|
||||
// until a VscodeSessionTurn is rendered by the virtualizer and calls
|
||||
// until a TranscriptRowView is rendered by the virtualizer and calls
|
||||
// hydrateParts(). This avoids writing parts for off-screen messages into
|
||||
// the store, which would trigger expensive DOM work for invisible content.
|
||||
const stash = new PartStash()
|
||||
|
||||
@@ -11,7 +11,10 @@ import type { Meta, StoryObj } from "storybook-solidjs-vite"
|
||||
import type { AssistantMessage as SDKAssistantMessage, ReasoningPart, TextPart, ToolPart } from "@kilocode/sdk/v2"
|
||||
import { StoryProviders, defaultMockData, mockSessionValue } from "./StoryProviders"
|
||||
import { AssistantMessage } from "../components/chat/AssistantMessage"
|
||||
import { VscodeSessionTurn } from "../components/chat/VscodeSessionTurn"
|
||||
import { For } from "solid-js"
|
||||
import { TranscriptRowView } from "../components/chat/TranscriptRow"
|
||||
import { messageTurns } from "../context/session-queue"
|
||||
import { transcriptRows } from "../context/transcript-rows"
|
||||
import { ChatView } from "../components/chat/ChatView"
|
||||
import { Part } from "@kilocode/kilo-ui/message-part"
|
||||
import { registerVscodeToolOverrides } from "../components/chat/VscodeToolOverrides"
|
||||
@@ -1380,20 +1383,28 @@ export const DiffSummaryCollapsed: Story = {
|
||||
{
|
||||
id: USER_MSG_ID,
|
||||
sessionID: SESSION_ID,
|
||||
role: "user",
|
||||
role: "user" as const,
|
||||
createdAt: new Date(now - 10000).toISOString(),
|
||||
time: { created: now - 10000 },
|
||||
summary: { diffs: mockDiffs },
|
||||
},
|
||||
{ ...baseAssistantMessage, parentID: USER_MSG_ID },
|
||||
{ ...baseAssistantMessage, parentID: USER_MSG_ID, createdAt: new Date(now - 9000).toISOString() },
|
||||
],
|
||||
},
|
||||
part: {
|
||||
[USER_MSG_ID]: [
|
||||
{ id: "part-user-text", sessionID: SESSION_ID, messageID: USER_MSG_ID, type: "text", text: "Fix the bug" },
|
||||
{
|
||||
id: "part-user-text",
|
||||
sessionID: SESSION_ID,
|
||||
messageID: USER_MSG_ID,
|
||||
type: "text" as const,
|
||||
text: "Fix the bug",
|
||||
},
|
||||
],
|
||||
[ASST_MSG_ID]: [textPart],
|
||||
},
|
||||
}
|
||||
const parts = new Map(Object.entries(data.part))
|
||||
const session = {
|
||||
...mockSessionValue({ id: SESSION_ID, status: "idle" }),
|
||||
messages: () => data.message[SESSION_ID],
|
||||
@@ -1419,13 +1430,9 @@ export const DiffSummaryCollapsed: Story = {
|
||||
<ServerContext.Provider value={server as any}>
|
||||
<SessionContext.Provider value={session as any}>
|
||||
<div style={{ width: "380px", padding: "12px" }}>
|
||||
<VscodeSessionTurn
|
||||
turn={{
|
||||
id: USER_MSG_ID,
|
||||
user: data.message[SESSION_ID][0] as any,
|
||||
assistant: [data.message[SESSION_ID][1] as any],
|
||||
}}
|
||||
/>
|
||||
<For each={transcriptRows(messageTurns(data.message[SESSION_ID]), (id) => parts.get(id) ?? [])}>
|
||||
{(row) => <TranscriptRowView row={row} />}
|
||||
</For>
|
||||
</div>
|
||||
</SessionContext.Provider>
|
||||
</ServerContext.Provider>
|
||||
|
||||
Reference in New Issue
Block a user