fix(agent-manager): compact diff review controls

This commit is contained in:
marius-kilocode
2026-07-30 10:58:50 +02:00
parent 74a30e017b
commit c94d6645a3
45 changed files with 396 additions and 108 deletions
@@ -92,7 +92,11 @@ export class DiffSourceCatalog implements vscode.Disposable {
build(id: string, ctx: PanelContext): DiffSource {
const opts = { dir: () => ctx.dir, strictDir: ctx.strictDir, git: ctx.git, log: ctx.log }
if (id === WORKSPACE_SOURCE_ID) {
return createWorktreeDiffSource({ ...opts, baseBranchOverride: ctx.baseBranchOverride, baseBranch: ctx.baseBranch })
return createWorktreeDiffSource({
...opts,
baseBranchOverride: ctx.baseBranchOverride,
baseBranch: ctx.baseBranch,
})
}
if (id === STAGED_SOURCE_ID) return createStagedDiffSource(opts)
@@ -109,13 +113,21 @@ export class DiffSourceCatalog implements vscode.Disposable {
if (id.startsWith(SESSION_PREFIX)) {
const sessionId = id.slice(SESSION_PREFIX.length)
if (!sessionId) throw new Error(`DiffSourceCatalog.build: empty session id in "${id}"`)
return createSessionDiffSource(sessionId, this.sessionFetch, ctx.dir ?? ctx.workspaceRoot, this.checkSnapshotsEnabled)
return createSessionDiffSource(
sessionId,
this.sessionFetch,
ctx.dir ?? ctx.workspaceRoot,
this.checkSnapshotsEnabled,
)
}
throw new Error(`DiffSourceCatalog.build: unknown source id "${id}"`)
}
async listWorkspaceBranches(override: string | undefined, dir?: string): Promise<WorkspaceBranchesResult | undefined> {
async listWorkspaceBranches(
override: string | undefined,
dir?: string,
): Promise<WorkspaceBranchesResult | undefined> {
const root = dir ?? getWorkspaceRoot()
if (!root) return undefined
@@ -7,7 +7,6 @@ import { FileIcon } from "@kilocode/kilo-ui/file-icon"
import { DiffChanges } from "@kilocode/kilo-ui/diff-changes"
import { Icon } from "@kilocode/kilo-ui/icon"
import { Button } from "@kilocode/kilo-ui/button"
import { RadioGroup } from "@kilocode/kilo-ui/radio-group"
import { IconButton } from "@kilocode/kilo-ui/icon-button"
import { Spinner } from "@kilocode/kilo-ui/spinner"
import { Tooltip, TooltipKeybind } from "@kilocode/kilo-ui/tooltip"
@@ -15,6 +14,7 @@ import type { DiffLineAnnotation, AnnotationSide, SelectedLineRange } from "@pie
import type { WorktreeFileDiff } from "../src/types/messages"
import { KILO_FILE_PATH_MIME } from "../src/utils/path-mentions"
import { useLanguage } from "../src/context/language"
import { DiffStyleSelect } from "../diff-viewer/InlineSelect"
import { useVSCode } from "../src/context/vscode"
import { useServer } from "../src/context/server"
import { useProvider } from "../src/context/provider"
@@ -479,21 +479,18 @@ export const DiffPanel: Component<DiffPanelProps> = (props) => {
<div class="am-diff-panel" onKeyDown={handleKeyDown} onMouseDown={handleRootMouseDown} tabIndex={-1} ref={rootRef}>
<div class="am-diff-header">
<div class="am-diff-header-main">
<span class="am-diff-header-title">{t("session.review.change.other")}</span>
{/* Scope + base picker replace the static "Changes" title: it names
what you're looking at and is the primary control. Always shown,
so an empty scope can still be switched away from. */}
<Show when={props.lead}>{props.lead}</Show>
<Show when={props.diffs.length > 0}>
<>
<RadioGroup
options={["unified", "split"] as const}
current={props.diffStyle ?? "unified"}
size="small"
value={(style) => style}
label={(style) =>
style === "unified" ? t("ui.sessionReview.diffStyle.unified") : t("ui.sessionReview.diffStyle.split")
}
onSelect={(style) => {
if (!style) return
props.onDiffStyleChange?.(style)
}}
<DiffStyleSelect
value={props.diffStyle ?? "unified"}
onSelect={(style) => props.onDiffStyleChange?.(style)}
unifiedLabel={t("ui.sessionReview.diffStyle.unified")}
splitLabel={t("ui.sessionReview.diffStyle.split")}
title={t("ui.sessionReview.diffStyle.unified")}
/>
<span class="am-diff-header-stats">
<span>{t("session.review.filesChanged", { count: totals().files })}</span>
@@ -539,9 +536,6 @@ export const DiffPanel: Component<DiffPanelProps> = (props) => {
<IconButton icon="close" size="small" variant="ghost" label={t("common.close")} onClick={props.onClose} />
</div>
</div>
<Show when={props.lead}>
<div class="am-diff-scope">{props.lead}</div>
</Show>
<Show when={props.loading && props.diffs.length === 0}>
<div class="am-diff-loading">
@@ -43,12 +43,20 @@
.am-review-toolbar-left {
display: flex;
align-items: center;
gap: 12px;
gap: 10px;
flex: 1;
min-width: 0;
overflow: hidden;
}
/* Keep the radio group from being the tallest thing in the row so it matches
the 22px selector chips and the small ghost buttons. The inline scope/base
controls are styled in banners.css (non-am- prefixed, shared with the
standalone diff viewer). */
.am-review-toolbar [data-component="radio-group"] {
font-size: var(--font-size-small);
}
.am-review-toolbar-right {
display: flex;
align-items: center;
@@ -77,7 +85,7 @@
.am-review-toolbar-stats {
display: flex;
align-items: center;
flex: 1 1 auto;
flex: 0 100 auto;
gap: 8px;
font-size: var(--font-size-small);
color: var(--text-weak);
@@ -1717,44 +1717,58 @@ body.am-wt-dragging-active * {
display: flex;
align-items: center;
justify-content: space-between;
padding: 4px 4px 4px 12px;
gap: 6px;
padding: 4px 4px 4px 8px;
flex-shrink: 0;
border-bottom: 1px solid var(--border-weak-base);
position: relative;
z-index: 20;
background: var(--surface-base);
}
.am-diff-header-title {
font-size: var(--font-size-small);
font-weight: 500;
color: var(--text-weak);
}
.am-diff-scope {
display: flex;
align-items: center;
padding: 2px 4px 4px 12px;
flex-shrink: 0;
border-bottom: 1px solid var(--border-weak-base);
background: var(--surface-base);
/* Query container for the narrow-panel rules below and in banners.css. The
panel width is user-draggable, so the header adapts to its own width
rather than the viewport. */
container-type: inline-size;
container-name: am-diff-header;
}
.am-diff-header-main {
display: flex;
align-items: center;
gap: 10px;
gap: 6px;
flex: 1;
min-width: 0;
overflow: hidden;
}
.am-diff-header-stats {
display: flex;
align-items: center;
gap: 8px;
flex: 0 1 auto;
min-width: 0;
font-size: var(--font-size-small);
color: var(--text-weak);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Progressive disclosure, least important first. Thresholds are derived from the
measured control widths: scope 76px, base <=191px, diff style 76px, 6px gaps,
plus ~90px of action buttons and 12px padding. Each breakpoint fires while the
remaining set still fits, so nothing is ever clipped under the buttons.
The collapsed-files hint goes before the totals; both are also shown on the
worktree row and the diff toggle button, so no information is lost. */
@container am-diff-header (max-width: 560px) {
.am-diff-header-collapsed {
display: none;
}
}
@container am-diff-header (max-width: 520px) {
.am-diff-header-stats {
display: none;
}
}
.am-diff-header-adds {
@@ -40,7 +40,12 @@ export function isDiffScope(value: string): value is DiffScope {
*/
export function scopeDescriptors(ctx: string, hasSession: boolean): DiffSourceDescriptor[] {
const out: DiffSourceDescriptor[] = [
{ id: composeDiffId(ctx, "branch"), type: "workspace", group: "Git", capabilities: { revert: true, comments: true } },
{
id: composeDiffId(ctx, "branch"),
type: "workspace",
group: "Git",
capabilities: { revert: true, comments: true },
},
{ id: composeDiffId(ctx, "staged"), type: "staged", group: "Git", capabilities: { revert: false, comments: true } },
{
id: composeDiffId(ctx, "unstaged"),
+2 -1
View File
@@ -136,7 +136,8 @@ export const dict = {
"agentManager.diff.revertFile": "استعادة الملف",
"agentManager.diff.revertSuccess": "تم استعادة الملف",
"agentManager.diff.revertError": "فشل الاستعادة",
"agentManager.diff.applyBranchOnly": "لا يعمل تطبيق التغييرات إلا على فرق الفرع الكامل. انتقل إلى نطاق Branch لتطبيقها.",
"agentManager.diff.applyBranchOnly":
"لا يعمل تطبيق التغييرات إلا على فرق الفرع الكامل. انتقل إلى نطاق Branch لتطبيقها.",
"agentManager.open.button": "فتح",
"agentManager.open.tooltip": "فتح Worktree هذا في VS Code",
"agentManager.apply.globalButton": "تطبيق",
+2 -1
View File
@@ -139,7 +139,8 @@ export const dict = {
"agentManager.diff.revertFile": "Reverter arquivo",
"agentManager.diff.revertSuccess": "Arquivo revertido",
"agentManager.diff.revertError": "Falha ao reverter",
"agentManager.diff.applyBranchOnly": "Aplicar funciona apenas no diff completo da branch. Mude para o escopo Branch para aplicar.",
"agentManager.diff.applyBranchOnly":
"Aplicar funciona apenas no diff completo da branch. Mude para o escopo Branch para aplicar.",
"agentManager.open.button": "Abrir",
"agentManager.open.tooltip": "Abrir este Worktree no VS Code",
"agentManager.apply.globalButton": "Aplicar",
+2 -1
View File
@@ -139,7 +139,8 @@ export const dict = {
"agentManager.diff.revertFile": "Vrati datoteku",
"agentManager.diff.revertSuccess": "Datoteka vraćena",
"agentManager.diff.revertError": "Vraćanje neuspješno",
"agentManager.diff.applyBranchOnly": "Primijeni radi samo s kompletnim diffom grane. Prebacite se na opseg Branch da biste primijenili.",
"agentManager.diff.applyBranchOnly":
"Primijeni radi samo s kompletnim diffom grane. Prebacite se na opseg Branch da biste primijenili.",
"agentManager.open.button": "Otvori",
"agentManager.open.tooltip": "Otvori ovaj worktree u VS Code-u",
"agentManager.apply.globalButton": "Primijeni",
+2 -1
View File
@@ -140,7 +140,8 @@ export const dict = {
"agentManager.diff.revertFile": "Gendan fil",
"agentManager.diff.revertSuccess": "Fil gendannet",
"agentManager.diff.revertError": "Gendannelse fejlede",
"agentManager.diff.applyBranchOnly": "Anvend virker kun på hele Branch-diffen. Skift til Branch-området for at anvende.",
"agentManager.diff.applyBranchOnly":
"Anvend virker kun på hele Branch-diffen. Skift til Branch-området for at anvende.",
"agentManager.open.button": "Åbn",
"agentManager.open.tooltip": "Åbn dette Worktree i VS Code",
"agentManager.apply.globalButton": "Anvend",
@@ -140,7 +140,8 @@ export const dict = {
"agentManager.diff.revertFile": "Datei zurücksetzen",
"agentManager.diff.revertSuccess": "Datei zurückgesetzt",
"agentManager.diff.revertError": "Zurücksetzen fehlgeschlagen",
"agentManager.diff.applyBranchOnly": "Anwenden funktioniert nur für den vollständigen Branch-Diff. Wechsle zum Bereich Branch, um anzuwenden.",
"agentManager.diff.applyBranchOnly":
"Anwenden funktioniert nur für den vollständigen Branch-Diff. Wechsle zum Bereich Branch, um anzuwenden.",
"agentManager.open.button": "Öffnen",
"agentManager.open.tooltip": "Dieses Worktree in VS Code öffnen",
"agentManager.apply.globalButton": "Anwenden",
+2 -1
View File
@@ -139,7 +139,8 @@ export const dict = {
"agentManager.diff.revertFile": "Revertir archivo",
"agentManager.diff.revertSuccess": "Archivo revertido",
"agentManager.diff.revertError": "Error al revertir",
"agentManager.diff.applyBranchOnly": "Aplicar solo funciona con el diff completo de la rama. Cambia al ámbito Branch para aplicar.",
"agentManager.diff.applyBranchOnly":
"Aplicar solo funciona con el diff completo de la rama. Cambia al ámbito Branch para aplicar.",
"agentManager.open.button": "Abrir",
"agentManager.open.tooltip": "Abrir este Worktree en VS Code",
"agentManager.apply.globalButton": "Aplicar",
+2 -1
View File
@@ -139,7 +139,8 @@ export const dict = {
"agentManager.diff.revertFile": "Rétablir le fichier",
"agentManager.diff.revertSuccess": "Fichier rétabli",
"agentManager.diff.revertError": "Échec du rétablissement",
"agentManager.diff.applyBranchOnly": "Appliquer ne fonctionne que sur le diff complet de la branche. Passez à la portée Branch pour appliquer.",
"agentManager.diff.applyBranchOnly":
"Appliquer ne fonctionne que sur le diff complet de la branche. Passez à la portée Branch pour appliquer.",
"agentManager.open.button": "Ouvrir",
"agentManager.open.tooltip": "Ouvrir ce worktree dans VS Code",
"agentManager.apply.globalButton": "Appliquer",
+2 -1
View File
@@ -145,7 +145,8 @@ export const dict = {
"agentManager.diff.revertFile": "Ripristina file",
"agentManager.diff.revertSuccess": "File ripristinato",
"agentManager.diff.revertError": "Ripristino non riuscito",
"agentManager.diff.applyBranchOnly": "Applica funziona solo sul diff completo del branch. Passa all'ambito Branch per applicare.",
"agentManager.diff.applyBranchOnly":
"Applica funziona solo sul diff completo del branch. Passa all'ambito Branch per applicare.",
"agentManager.open.button": "Apri",
"agentManager.open.tooltip": "Apri questo worktree in VS Code",
"agentManager.apply.globalButton": "Applica",
+2 -1
View File
@@ -138,7 +138,8 @@ export const dict = {
"agentManager.diff.revertFile": "ファイルを元に戻す",
"agentManager.diff.revertSuccess": "ファイルを元に戻しました",
"agentManager.diff.revertError": "元に戻せませんでした",
"agentManager.diff.applyBranchOnly": "適用はブランチ全体の差分に対してのみ利用できます。適用するにはスコープを Branch に切り替えてください。",
"agentManager.diff.applyBranchOnly":
"適用はブランチ全体の差分に対してのみ利用できます。適用するにはスコープを Branch に切り替えてください。",
"agentManager.open.button": "開く",
"agentManager.open.tooltip": "このWorktreeをVS Codeで開く",
"agentManager.apply.globalButton": "適用",
+2 -1
View File
@@ -137,7 +137,8 @@ export const dict = {
"agentManager.diff.revertFile": "파일 되돌리기",
"agentManager.diff.revertSuccess": "파일이 되돌려졌습니다",
"agentManager.diff.revertError": "되돌리기 실패",
"agentManager.diff.applyBranchOnly": "적용은 전체 브랜치 diff에서만 작동합니다. 적용하려면 범위를 Branch로 전환하세요.",
"agentManager.diff.applyBranchOnly":
"적용은 전체 브랜치 diff에서만 작동합니다. 적용하려면 범위를 Branch로 전환하세요.",
"agentManager.open.button": "열기",
"agentManager.open.tooltip": "이 Worktree를 VS Code에서 열기",
"agentManager.apply.globalButton": "적용",
+2 -1
View File
@@ -144,7 +144,8 @@ export const dict = {
"agentManager.diff.revertFile": "Bestand terugzetten",
"agentManager.diff.revertSuccess": "Bestand teruggezet",
"agentManager.diff.revertError": "Terugzetten mislukt",
"agentManager.diff.applyBranchOnly": "Toepassen werkt alleen op de volledige branch-diff. Schakel naar het bereik Branch om toe te passen.",
"agentManager.diff.applyBranchOnly":
"Toepassen werkt alleen op de volledige branch-diff. Schakel naar het bereik Branch om toe te passen.",
"agentManager.open.button": "Openen",
"agentManager.open.tooltip": "Open deze worktree in VS Code",
"agentManager.apply.globalButton": "Toepassen",
+2 -1
View File
@@ -138,7 +138,8 @@ export const dict = {
"agentManager.diff.revertFile": "Cofnij plik",
"agentManager.diff.revertSuccess": "Plik cofnięty",
"agentManager.diff.revertError": "Cofanie nie powiodło się",
"agentManager.diff.applyBranchOnly": "Funkcja Zastosuj działa tylko z pełnym diffem brancha. Przełącz się na zakres Branch, aby zastosować.",
"agentManager.diff.applyBranchOnly":
"Funkcja Zastosuj działa tylko z pełnym diffem brancha. Przełącz się na zakres Branch, aby zastosować.",
"agentManager.open.button": "Otwórz",
"agentManager.open.tooltip": "Otwórz ten Worktree w VS Code",
"agentManager.apply.globalButton": "Zastosuj",
+2 -1
View File
@@ -139,7 +139,8 @@ export const dict = {
"agentManager.diff.revertFile": "Откатить файл",
"agentManager.diff.revertSuccess": "Файл откатан",
"agentManager.diff.revertError": "Ошибка отката",
"agentManager.diff.applyBranchOnly": "Применение работает только с полным diff ветки. Чтобы применить изменения, переключитесь на область Branch.",
"agentManager.diff.applyBranchOnly":
"Применение работает только с полным diff ветки. Чтобы применить изменения, переключитесь на область Branch.",
"agentManager.open.button": "Открыть",
"agentManager.open.tooltip": "Открыть этот Worktree в VS Code",
"agentManager.apply.globalButton": "Применить",
+2 -1
View File
@@ -134,7 +134,8 @@ export const dict = {
"agentManager.diff.revertFile": "ย้อนกลับไฟล์",
"agentManager.diff.revertSuccess": "ย้อนกลับไฟล์แล้ว",
"agentManager.diff.revertError": "ย้อนกลับล้มเหลว",
"agentManager.diff.applyBranchOnly": "นำไปใช้ได้เฉพาะกับ diff ของ Branch ทั้งหมดเท่านั้น สลับไปที่ขอบเขต Branch เพื่อใช้งาน",
"agentManager.diff.applyBranchOnly":
"นำไปใช้ได้เฉพาะกับ diff ของ Branch ทั้งหมดเท่านั้น สลับไปที่ขอบเขต Branch เพื่อใช้งาน",
"agentManager.open.button": "เปิด",
"agentManager.open.tooltip": "เปิด Worktree นี้ใน VS Code",
"agentManager.apply.globalButton": "นำไปใช้",
+2 -1
View File
@@ -145,7 +145,8 @@ export const dict = {
"agentManager.diff.revertFile": "Dosyayı geri al",
"agentManager.diff.revertSuccess": "Dosya geri alındı",
"agentManager.diff.revertError": "Geri alma başarısız",
"agentManager.diff.applyBranchOnly": "Uygula yalnızca tam Branch diff'inde çalışır. Uygulamak için Branch kapsamına geçin.",
"agentManager.diff.applyBranchOnly":
"Uygula yalnızca tam Branch diff'inde çalışır. Uygulamak için Branch kapsamına geçin.",
"agentManager.open.button": "Aç",
"agentManager.open.tooltip": "Bu worktree'yi VS Code'da aç",
"agentManager.apply.globalButton": "Uygula",
+2 -1
View File
@@ -146,7 +146,8 @@ export const dict = {
"agentManager.diff.revertFile": "Скасувати зміни файлу",
"agentManager.diff.revertSuccess": "Файл відновлено",
"agentManager.diff.revertError": "Не вдалося відновити",
"agentManager.diff.applyBranchOnly": "Застосування працює лише з повним diff гілки. Щоб застосувати зміни, перемкніться на область Branch.",
"agentManager.diff.applyBranchOnly":
"Застосування працює лише з повним diff гілки. Щоб застосувати зміни, перемкніться на область Branch.",
"agentManager.open.button": "Відкрити",
"agentManager.open.tooltip": "Відкрити це робоче дерево у VS Code",
"agentManager.apply.globalButton": "Застосувати",
@@ -96,7 +96,7 @@ export const BaseBranchPicker: Component<BaseBranchPickerProps> = (props) => {
placement="bottom-start"
flip
trigger={
<button class="am-selector-trigger diff-base-trigger" type="button">
<button class="am-selector-trigger diff-inline-trigger" type="button" title={triggerLabel()}>
<span class="am-selector-left">
<Show when={!props.currentBranch}>
<Icon name="branch" size="small" />
@@ -1,7 +1,8 @@
import { Show, type Component } from "solid-js"
import type { DiffSourceDescriptor } from "../../src/diff/sources/types"
import type { BranchInfo } from "../src/types/messages"
import { DiffPickerHeader } from "./DiffPickerHeader"
import { useLanguage } from "../src/context/language"
import { InlineSelect, type InlineOption } from "./InlineSelect"
import { BaseBranchPicker } from "./BaseBranchPicker"
interface DiffScopeControlsProps {
@@ -19,37 +20,61 @@ interface DiffScopeControlsProps {
currentBranch: string | undefined
onSelectBase: (branch: string | undefined) => void
/**
* Compact mode for the narrow Agent Manager side panel: hides the
* `current → base` prefix so only the picker trigger remains.
* Compact mode for the narrow side panel: drops the `current →` prefix and
* tightens the label caps so the row survives a user-shrunk panel.
*/
compact?: boolean
}
/**
* Composes the scope selector and base branch picker into one control row.
* Shared by the standalone Changes header and the two Agent Manager diff
* surfaces so all three render the identical controls.
* Scope selector plus base branch picker, sized to sit inline in a diff
* toolbar row. Shared by the Agent Manager side panel and review tab.
*
* Note this deliberately avoids `DiffPickerHeader`: that component is a
* full-width header band (`margin: 8px 12px`) and using it inside a toolbar
* inflates the row height and misaligns against the neighboring buttons.
*/
export const DiffScopeControls: Component<DiffScopeControlsProps> = (props) => {
const { t } = useLanguage()
const options = (): InlineOption<string>[] =>
props.descriptors.map((desc) => ({
value: desc.id,
label: t(`diffViewer.source.${desc.type}.label`),
group: t(desc.group === "Session" ? "diffViewer.group.session" : "diffViewer.group.git"),
}))
// The trigger's tooltip explains what the active scope actually shows,
// reusing the per-scope descriptions the standalone picker already has.
const title = () => {
const active = props.descriptors.find((desc) => desc.id === props.currentId)
if (!active) return ""
return t(`diffViewer.source.${active.type}.tooltip`)
}
return (
<DiffPickerHeader
descriptors={props.descriptors}
currentId={props.currentId}
onSelect={props.onSelectScope}
accessory={
<Show when={props.showBase}>
<BaseBranchPicker
branches={props.branches}
loading={props.branchesLoading}
defaultBranch={props.defaultBranch}
autoBase={props.autoBase}
currentBase={props.currentBase}
isAuto={props.isAuto}
currentBranch={props.compact ? undefined : props.currentBranch}
onSelect={props.onSelectBase}
/>
</Show>
}
/>
<span class="diff-scope-controls" classList={{ "diff-scope-controls-compact": props.compact }}>
<Show when={props.descriptors.length > 0}>
<InlineSelect
options={options()}
value={props.currentId}
onSelect={props.onSelectScope}
title={title()}
compact={props.compact}
/>
</Show>
<Show when={props.showBase}>
<BaseBranchPicker
branches={props.branches}
loading={props.branchesLoading}
defaultBranch={props.defaultBranch}
autoBase={props.autoBase}
currentBase={props.currentBase}
isAuto={props.isAuto}
currentBranch={props.compact ? undefined : props.currentBranch}
onSelect={props.onSelectBase}
/>
</Show>
</span>
)
}
@@ -0,0 +1,127 @@
import { type Component, For, Show, createSignal } from "solid-js"
import { Icon } from "@kilocode/kilo-ui/icon"
import { DeferredPopover } from "../src/components/shared/DeferredPopover"
export interface InlineOption<T extends string> {
value: T
label: string
/** Secondary text shown right-aligned in the menu row. */
hint?: string
/** Optional group heading; consecutive options sharing a group are grouped. */
group?: string
}
interface InlineSelectProps<T extends string> {
options: InlineOption<T>[]
value: T | undefined
onSelect: (value: T) => void
/** Trigger icon. Omit to keep the trigger as narrow as possible. */
icon?: string
/** Accessible name / tooltip text for the trigger. */
title: string
/** Caps the trigger label width so long values ellipsize instead of pushing. */
compact?: boolean
/** Extra class on the trigger, so narrow-width rules can target one control. */
class?: string
}
/**
* Compact dropdown sized for a diff toolbar row.
*
* Deliberately not kilo-ui's `Select`: that renders an input-sized control
* (32px, base font) which dwarfs the ghost buttons and radio group it sits
* next to. This mirrors the `am-selector-trigger` markup the branch pickers
* use, shrunk via `.diff-inline-trigger`, so every control in the row shares
* one height and font size.
*/
export function InlineSelect<T extends string>(props: InlineSelectProps<T>) {
const [open, setOpen] = createSignal(false)
const current = () => props.options.find((opt) => opt.value === props.value)
const label = () => current()?.label ?? ""
const choose = (value: T) => {
props.onSelect(value)
setOpen(false)
}
// Group heading renders only when it differs from the previous option's, so
// callers just order their options by group.
const heading = (index: number) => {
const group = props.options[index]?.group
if (!group) return undefined
if (index === 0) return group
return props.options[index - 1]?.group === group ? undefined : group
}
return (
<DeferredPopover
open={open()}
onOpenChange={setOpen}
placement="bottom-start"
flip
portal={false}
deferDismiss
class="am-dropdown diff-inline-menu"
trigger={
<button
class={`am-selector-trigger diff-inline-trigger${props.class ? ` ${props.class}` : ""}`}
type="button"
title={props.title}
>
<span class="am-selector-left">
<Show when={props.icon}>{(name) => <Icon name={name()} size="small" />}</Show>
<span class="am-selector-value">{label()}</span>
</span>
<span class="am-selector-right">
<Icon name="selector" size="small" />
</span>
</button>
}
>
<div class="am-dropdown-list">
<For each={props.options}>
{(opt, index) => (
<>
<Show when={heading(index())}>{(text) => <div class="diff-inline-group">{text()}</div>}</Show>
<button
class="am-branch-item"
classList={{ "am-branch-item-active": opt.value === props.value }}
type="button"
onClick={() => choose(opt.value)}
>
<span class="am-branch-item-left">
<span class="am-branch-item-name">{opt.label}</span>
</span>
<Show when={opt.hint}>
<span class="am-branch-hint">{opt.hint}</span>
</Show>
</button>
</>
)}
</For>
</div>
</DeferredPopover>
)
}
/** Compact unified/split picker. Replaces the wide radio group in tight rows. */
export const DiffStyleSelect: Component<{
value: "unified" | "split"
onSelect: (value: "unified" | "split") => void
unifiedLabel: string
splitLabel: string
title: string
}> = (props) => (
<InlineSelect
options={[
{ value: "unified", label: props.unifiedLabel },
{ value: "split", label: props.splitLabel },
]}
value={props.value}
onSelect={props.onSelect}
title={props.title}
class="diff-style-select"
compact
/>
)
-1
View File
@@ -296,7 +296,6 @@ export const dict = {
"session.tab.review": "مراجعة",
"session.review.filesChanged": "تم تغيير {{count}} ملفات",
"session.review.change.other": "تغييرات",
"session.review.loadingChanges": "جارٍ تحميل التغييرات...",
"session.review.noChanges": "لا توجد تغييرات",
"session.messages.loadingEarlier": "جارٍ تحميل الرسائل السابقة...",
-1
View File
@@ -306,7 +306,6 @@ export const dict = {
"session.tab.review": "Revisão",
"session.review.filesChanged": "{{count}} Arquivos Alterados",
"session.review.change.other": "Alterações",
"session.review.loadingChanges": "Carregando alterações...",
"session.review.noChanges": "Sem alterações",
"session.messages.loadingEarlier": "Carregando mensagens anteriores...",
-1
View File
@@ -304,7 +304,6 @@ export const dict = {
"session.tab.review": "Pregled",
"session.review.filesChanged": "Izmijenjeno {{count}} datoteka",
"session.review.change.other": "Izmjene",
"session.review.loadingChanges": "Učitavanje izmjena...",
"session.review.noChanges": "Nema izmjena",
-1
View File
@@ -303,7 +303,6 @@ export const dict = {
"session.tab.review": "Gennemgang",
"session.review.filesChanged": "{{count}} Filer ændret",
"session.review.change.other": "Ændringer",
"session.review.loadingChanges": "Indlæser ændringer...",
"session.review.noChanges": "Ingen ændringer",
"session.messages.loadingEarlier": "Indlæser tidligere beskeder...",
@@ -312,7 +312,6 @@ export const dict = {
"session.tab.review": "Überprüfung",
"session.review.filesChanged": "{{count}} Dateien geändert",
"session.review.change.other": "Änderungen",
"session.review.loadingChanges": "Lade Änderungen...",
"session.review.noChanges": "Keine Änderungen",
"session.messages.loadingEarlier": "Lade frühere Nachrichten...",
@@ -301,7 +301,6 @@ export const dict = {
"session.tab.review": "Review",
"session.review.filesChanged": "{{count}} Files Changed",
"session.review.change.other": "Changes",
"session.review.loadingChanges": "Loading changes...",
"session.review.noChanges": "No changes",
-1
View File
@@ -307,7 +307,6 @@ export const dict = {
"session.tab.review": "Revisión",
"session.review.filesChanged": "{{count}} Archivos Cambiados",
"session.review.change.other": "Cambios",
"session.review.loadingChanges": "Cargando cambios...",
"session.review.noChanges": "Sin cambios",
"session.messages.loadingEarlier": "Cargando mensajes anteriores...",
-1
View File
@@ -306,7 +306,6 @@ export const dict = {
"session.tab.review": "Revue",
"session.review.filesChanged": "{{count}} fichiers modifiés",
"session.review.change.other": "Modifications",
"session.review.loadingChanges": "Chargement des modifications...",
"session.review.noChanges": "Aucune modification",
"session.messages.loadingEarlier": "Chargement des messages précédents...",
-1
View File
@@ -219,7 +219,6 @@ export const dict = {
"ui.approval.source.default": "per impostazione predefinita",
"session.tab.review": "Revisione",
"session.review.filesChanged": "{{count}} file modificati",
"session.review.change.other": "Modifiche",
"session.review.loadingChanges": "Caricamento modifiche...",
"session.review.noChanges": "Nessuna modifica",
"session.messages.loadingEarlier": "Caricamento messaggi precedenti...",
-1
View File
@@ -303,7 +303,6 @@ export const dict = {
"session.tab.review": "レビュー",
"session.review.filesChanged": "{{count}} ファイル変更",
"session.review.change.other": "変更",
"session.review.loadingChanges": "変更を読み込み中...",
"session.review.noChanges": "変更なし",
"session.messages.loadingEarlier": "以前のメッセージを読み込み中...",
-1
View File
@@ -304,7 +304,6 @@ export const dict = {
"session.tab.review": "검토",
"session.review.filesChanged": "{{count}}개 파일 변경됨",
"session.review.change.other": "변경",
"session.review.loadingChanges": "변경 사항 로드 중...",
"session.review.noChanges": "변경 없음",
"session.messages.loadingEarlier": "이전 메시지 로드 중...",
-1
View File
@@ -307,7 +307,6 @@ export const dict = {
"session.tab.review": "Beoordelen",
"session.review.filesChanged": "{{count}} bestanden gewijzigd",
"session.review.change.other": "Wijzigingen",
"session.review.loadingChanges": "Wijzigingen laden...",
"session.review.noChanges": "Geen wijzigingen",
-1
View File
@@ -310,7 +310,6 @@ export const dict = {
"session.tab.review": "Gjennomgang",
"session.review.filesChanged": "{{count}} filer endret",
"session.review.change.other": "Endringer",
"session.review.loadingChanges": "Laster endringer...",
"session.review.noChanges": "Ingen endringer",
"session.messages.loadingEarlier": "Laster inn tidligere meldinger...",
-1
View File
@@ -304,7 +304,6 @@ export const dict = {
"session.tab.review": "Przegląd",
"session.review.filesChanged": "Zmieniono {{count}} plików",
"session.review.change.other": "Zmiany",
"session.review.loadingChanges": "Ładowanie zmian...",
"session.review.noChanges": "Brak zmian",
"session.messages.loadingEarlier": "Ładowanie wcześniejszych wiadomości...",
-1
View File
@@ -302,7 +302,6 @@ export const dict = {
"session.tab.review": "Обзор",
"session.review.filesChanged": "{{count}} файлов изменено",
"session.review.change.other": "Изменения",
"session.review.loadingChanges": "Загрузка изменений...",
"session.review.noChanges": "Нет изменений",
"session.messages.loadingEarlier": "Загрузка предыдущих сообщений...",
-1
View File
@@ -301,7 +301,6 @@ export const dict = {
"session.tab.review": "ตรวจสอบ",
"session.review.filesChanged": "{{count}} ไฟล์ที่เปลี่ยนแปลง",
"session.review.change.other": "การเปลี่ยนแปลง",
"session.review.loadingChanges": "กำลังโหลดการเปลี่ยนแปลง...",
"session.review.noChanges": "ไม่มีการเปลี่ยนแปลง",
-1
View File
@@ -302,7 +302,6 @@ export const dict = {
"session.tab.review": "İnceleme",
"session.review.filesChanged": "{{count}} Dosya Değişti",
"session.review.change.other": "Değişiklik",
"session.review.loadingChanges": "Değişiklikler yükleniyor...",
"session.review.noChanges": "Değişiklik yok",
-1
View File
@@ -306,7 +306,6 @@ export const dict = {
"session.tab.review": "Огляд",
"session.review.filesChanged": "{{count}} файлів змінено",
"session.review.change.other": "Змін",
"session.review.loadingChanges": "Завантаження змін...",
"session.review.noChanges": "Змін немає",
-1
View File
@@ -292,7 +292,6 @@ export const dict = {
"session.tab.review": "审查",
"session.review.filesChanged": "{{count}} 个文件变更",
"session.review.change.other": "更改",
"session.review.loadingChanges": "正在加载更改...",
"session.review.noChanges": "无更改",
"session.messages.loadingEarlier": "正在加载更早的消息...",
-1
View File
@@ -290,7 +290,6 @@ export const dict = {
"session.tab.review": "審查",
"session.review.filesChanged": "{{count}} 個檔案變更",
"session.review.change.other": "變更",
"session.review.loadingChanges": "正在載入變更...",
"session.review.noChanges": "沒有變更",
"session.messages.loadingEarlier": "正在載入更早的訊息...",
@@ -169,6 +169,119 @@
align-items: center;
gap: 6px;
min-width: 0;
flex-shrink: 1;
}
/* ============================================
Inline diff toolbar controls (scope, base, diff style)
============================================
These sit inside a toolbar row next to ghost buttons and a radio group, so
they must not carry header-band margins and must match the small control
height. `.am-selector-trigger` is a 32px full-width input-style control;
everything below shrinks it to a toolbar-sized chip. */
.diff-scope-controls {
display: inline-flex;
align-items: center;
gap: 6px;
min-width: 0;
flex-shrink: 1;
}
/* Two-class selector on purpose: `.am-selector-trigger` (agent-manager.css) is
a 32px full-width input-style control and loads after this file, so a single
`.diff-inline-trigger` class would lose the cascade tie and keep the tall
size. */
.am-selector-trigger.diff-inline-trigger {
width: auto;
height: 22px;
gap: 4px;
padding: 0 6px;
font-size: var(--kilo-font-size-12);
/* Tight line box: the inherited 19.5px line-height leaves no room inside a
22px chip and makes inline children look like they burst out of it. */
line-height: 20px;
/* Never squeeze: shrinking these to unreadable slivers looks broken. The
container queries below drop whole controls instead. */
flex-shrink: 0;
min-width: 0;
}
/* The "Default" badge is sized for dropdown rows (19.5px line box + padding =
21.5px), which fills a 22px chip edge to edge. Shrink it so it reads as a
badge inside the control instead of overflowing it. */
.am-selector-trigger.diff-inline-trigger .am-branch-badge {
padding: 0 4px;
line-height: 14px;
border-radius: 2px;
}
.am-selector-trigger.diff-inline-trigger .am-selector-right {
margin-left: 2px;
}
/* Long branch names ellipsize rather than pushing the row wider. The compact
variant (narrow side panel) caps tighter than the full-screen toolbar. */
.am-selector-trigger.diff-inline-trigger .am-selector-value {
max-width: 160px;
}
.diff-scope-controls-compact .am-selector-trigger.diff-inline-trigger .am-selector-value {
max-width: 92px;
}
/* The shared `.am-dropdown` rule pins popover width to the anchor width, which
would make these narrow triggers open an unreadably narrow menu. */
.diff-inline-menu[data-component="popover-content"] {
width: auto;
min-width: 200px;
}
.diff-inline-group {
padding: 6px 10px 2px;
font-size: var(--kilo-font-size-11);
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--text-weaker);
}
/* Inline controls inside the two Agent Manager diff toolbars. They must not add
vertical margin: each toolbar's height is its own padding plus the tallest
control, so a header-band margin here would visibly grow the row (this is
why these rows do not reuse `[data-component="diff-picker-header"]`). */
.am-review-toolbar-left > .diff-scope-controls,
.am-diff-header-main > .diff-scope-controls {
margin: 0;
min-width: 0;
flex-shrink: 0;
}
/* ============================================
Narrow side panel: progressive disclosure
============================================
The user can drag the diff side panel arbitrarily narrow. Flex shrinking
alone clips controls mid-glyph and slides them under the action buttons, so
whole controls are dropped instead, least important first. The scope select
always survives: it is the only way back out of an empty scope.
The container is declared on `.am-diff-header` in agent-manager.css (that
file only permits `am-`-prefixed selectors, so the `diff-`-prefixed rules
live here). */
/* Diff style has a home in the full-screen review, so it goes before base. */
@container am-diff-header (max-width: 470px) {
.am-diff-header-main .diff-style-select {
display: none;
}
}
@container am-diff-header (max-width: 390px) {
.am-diff-header-main .diff-base-picker {
display: none;
}
}
.diff-base-current {
@@ -193,12 +306,6 @@
line-height: 1;
}
.diff-base-trigger {
height: 24px;
font-size: var(--kilo-font-size-12);
padding: 0 8px;
}
/* ============================================
Diff Viewer Notice (e.g. snapshots disabled)
============================================ */