fix(vscode): group context controls in task header and use distinct icons (#12287)

* fix(vscode): group context controls in task header and use distinct icons

* chore: update kilo-vscode visual regression baselines

---------

Co-authored-by: kilo-maintainer[bot] <kilo-maintainer[bot]@users.noreply.github.com>
This commit is contained in:
Johnny Eric Amancio
2026-07-16 18:16:10 +02:00
committed by GitHub
parent 64713d4816
commit bb75928749
9 changed files with 200 additions and 133 deletions
+6
View File
@@ -0,0 +1,6 @@
---
"@kilocode/kilo-ui": patch
"kilo-code": patch
---
Use distinct icons for context, project memory, and code indexing, and group context controls to preserve task titles in narrow VS Code sidebars.
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6a89d1a9a31f7de1780d772b7caf71d96f0b4b895df954c77dcd508a7febe7c9
size 1950
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9a864b6a5c0e88bc00292f8a38685acdf2ecdd9eb5d880a663ba830152b7280b
size 3523
oid sha256:431a0c12cb844b5d4e535cc4c7bf5546fd9e66a178b8023b00978d07c291a531
size 3919
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:08f1498ef61670dfec83a2de45f1b9d7008c6c9987705a39175eb990182ee5c2
size 6156
oid sha256:f68544cdf994abca3004e652911aa8edd77cf2bb3f91678e5dc6210022dff2f9
size 6188
+4
View File
@@ -3,6 +3,10 @@ import { splitProps } from "solid-js"
const icons: Record<string, { path: string; viewBox: string }> = {
memory: {
viewBox: "0 0 24 24",
path: `<path d="M2 7h4M2 12h4M2 17h4" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><rect width="16" height="20" x="4" y="2" rx="2" stroke="currentColor" stroke-width="2"/>`,
},
database: {
viewBox: "0 0 24 24",
path: `<ellipse cx="12" cy="5" rx="7" ry="3" stroke="currentColor" stroke-width="2"/><path d="M5 5v7c0 1.66 3.13 3 7 3s7-1.34 7-3V5M5 12v7c0 1.66 3.13 3 7 3s7-1.34 7-3v-7" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>`,
},
@@ -122,13 +122,6 @@ export const TaskHeader: Component<TaskHeaderProps> = (props) => {
: []),
]
})
const activitySummary = createMemo(() => {
if (memory.error()) return memory.error()!
if (!memory.enabled()) return language.t("chat.memory.project.disabled")
const lines = activityLines()
if (lines.length === 0) return language.t("chat.memory.activity.idle")
return [language.t("chat.memory.status.active"), ...lines].join(" · ")
})
const activityItems = createMemo(() =>
activity()
.flatMap((item) => {
@@ -157,6 +150,8 @@ export const TaskHeader: Component<TaskHeaderProps> = (props) => {
)
const activityTooltip = () => (
<>
<div data-slot="task-header-context-tooltip-title">{language.t("settings.context.title")}</div>
<div data-slot="task-header-context-tooltip-status">{memoryStatus()}</div>
{activitySummaryView()}
<Show when={memoryVerbose() && activityItems().length > 0}>
<div data-slot="task-header-memory-activity-list">
@@ -318,107 +313,117 @@ export const TaskHeader: Component<TaskHeaderProps> = (props) => {
<DeferredPopover
placement="bottom-end"
portal={false}
class="task-header-memory-popover"
class="task-header-context-popover"
triggerAs="button"
triggerProps={{
type: "button",
get class() {
return `task-header-memory-trigger${memory.enabled() ? " task-header-memory-trigger--enabled" : ""}`
},
class: "task-header-context-trigger",
get ["aria-label"]() {
return activitySummary()
return language.t("settings.context.title")
},
}}
trigger={
<>
<Icon name="memory" size="small" />
<Icon name="server" size="small" />
<Show when={memoryActive()}>
<span data-slot="task-header-memory-dot" />
</Show>
</>
}
>
<div data-slot="task-header-memory-menu">
<div data-slot="task-header-memory-status">{memoryStatus()}</div>
{activitySummaryView()}
<Show
when={memory.enabled()}
fallback={
<>
<div data-slot="task-header-context-menu">
<div data-slot="task-header-context-title">{language.t("settings.context.title")}</div>
<Show when={!props.readonly}>
<section data-slot="task-header-context-section">
<div data-slot="task-header-context-section-title">
<Icon name="compress" size="small" />
<span>{language.t("settings.context.compaction.title")}</span>
</div>
<div data-slot="task-header-context-actions">
<button
data-slot="task-header-memory-action"
disabled={memory.pending()}
onClick={() => memory.enable()}
data-slot="task-header-context-action"
disabled={!canCompact()}
onClick={() => session.compact()}
>
{language.t("chat.memory.enable")}
{language.t("command.session.compact")}
</button>
</>
}
>
<div data-slot="task-header-memory-actions">
<button
data-slot="task-header-memory-action"
disabled={memory.loading() || memory.pending()}
onClick={() => memory.showMemory()}
>
{language.t("chat.memory.inspect")}
</button>
<button
data-slot="task-header-memory-action"
disabled={memory.pending()}
onClick={() => memory.remember()}
>
{language.t("chat.memory.remember")}
</button>
<button
data-slot="task-header-memory-action"
disabled={memory.pending()}
onClick={() => memory.forget()}
>
{language.t("chat.memory.forget")}
</button>
<button
data-slot="task-header-memory-action"
disabled={memory.pending()}
onClick={() => memory.rebuild()}
>
{language.t("chat.memory.rebuild")}
</button>
<button
data-slot="task-header-memory-action"
disabled={memory.pending()}
onClick={() => memory.disable()}
>
{language.t("chat.memory.disable")}
</button>
</div>
<div data-slot="task-header-memory-verbose">
<span>{language.t("chat.memory.verbose")}</span>
<Switch
checked={memoryVerbose()}
disabled={memory.pending()}
hideLabel
onChange={(next) => memory.verbose(next ? "on" : "off")}
>
{language.t("chat.memory.verbose")}
</Switch>
</div>
</div>
</section>
</Show>
<section data-slot="task-header-context-section">
<div data-slot="task-header-context-section-title">
<Icon name="memory" size="small" />
<span>{language.t("settings.context.memory.title")}</span>
</div>
<div data-slot="task-header-memory-status">{memoryStatus()}</div>
{activitySummaryView()}
<div data-slot="task-header-context-actions">
<Show
when={memory.enabled()}
fallback={
<button
data-slot="task-header-context-action"
disabled={memory.pending()}
onClick={() => memory.enable()}
>
{language.t("chat.memory.enable")}
</button>
}
>
<button
data-slot="task-header-context-action"
disabled={memory.loading() || memory.pending()}
onClick={() => memory.showMemory()}
>
{language.t("chat.memory.inspect")}
</button>
<button
data-slot="task-header-context-action"
disabled={memory.pending()}
onClick={() => memory.remember()}
>
{language.t("chat.memory.remember")}
</button>
<button
data-slot="task-header-context-action"
disabled={memory.pending()}
onClick={() => memory.forget()}
>
{language.t("chat.memory.forget")}
</button>
<button
data-slot="task-header-context-action"
disabled={memory.pending()}
onClick={() => memory.rebuild()}
>
{language.t("chat.memory.rebuild")}
</button>
<button
data-slot="task-header-context-action"
disabled={memory.pending()}
onClick={() => memory.disable()}
>
{language.t("chat.memory.disable")}
</button>
</Show>
</div>
<Show when={memory.enabled()}>
<div data-slot="task-header-memory-verbose">
<span>{language.t("chat.memory.verbose")}</span>
<Switch
checked={memoryVerbose()}
disabled={memory.pending()}
hideLabel
onChange={(next) => memory.verbose(next ? "on" : "off")}
>
{language.t("chat.memory.verbose")}
</Switch>
</div>
</Show>
</section>
</div>
</DeferredPopover>
</Tooltip>
<Show when={!props.readonly}>
<Tooltip value={language.t("command.session.compact")} placement="bottom">
<IconButton
icon="compress"
size="small"
variant="ghost"
disabled={!canCompact()}
onClick={() => session.compact()}
aria-label={language.t("command.session.compact")}
/>
</Tooltip>
</Show>
<Show when={hasMessages()}>
<Tooltip value={language.t("chat.search.toggle")} placement="bottom">
<IconButton
@@ -208,7 +208,7 @@ const Settings: Component<SettingsProps> = (props) => {
</Tabs.Trigger>
<Show when={features().indexing}>
<Tabs.Trigger value="indexing" aria-label={language.t("settings.indexing.title")}>
<Icon name="server" />
<Icon name="database" />
<span class="label">{language.t("settings.indexing.title")}</span>
</Tabs.Trigger>
</Show>
@@ -1037,31 +1037,40 @@ const mockMemory: MemoryContextValue = {
forget: () => {},
}
const memoryHeader = (width: string) => {
const session = {
...mockSessionValue({ id: SESSION_ID, status: "idle" }),
messages: () => [{ id: "msg-001" }] as any[],
contextUsage: () => ({ tokens: 34300, percentage: 17 }),
costBreakdown: () => [{ label: "Session", cost: 0.64 }],
currentSession: () => ({
id: SESSION_ID,
title: "Integrate project memory",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
}),
}
return (
<StoryProviders sessionID={SESSION_ID} status="idle" noPadding>
<SessionContext.Provider value={session as any}>
<MemoryContext.Provider value={mockMemory}>
<div style={{ width }}>
<TaskHeader />
</div>
</MemoryContext.Provider>
</SessionContext.Provider>
</StoryProviders>
)
}
export const TaskHeaderWithMemory: Story = {
name: "TaskHeader — with memory enabled",
render: () => {
const session = {
...mockSessionValue({ id: SESSION_ID, status: "idle" }),
messages: () => [{ id: "msg-001" }] as any[],
currentSession: () => ({
id: SESSION_ID,
title: "Integrate project memory",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
}),
}
return (
<StoryProviders sessionID={SESSION_ID} status="idle" noPadding>
<SessionContext.Provider value={session as any}>
<MemoryContext.Provider value={mockMemory}>
<div style={{ width: "380px" }}>
<TaskHeader />
</div>
</MemoryContext.Provider>
</SessionContext.Provider>
</StoryProviders>
)
},
render: () => memoryHeader("380px"),
}
export const TaskHeaderWithMemory200: Story = {
name: "TaskHeader — with memory enabled 200",
render: () => memoryHeader("200px"),
}
const usageTokens = { input: 25_900_000, output: 52_000, reasoning: 4_100, cache: { read: 10_500_000, write: 80_000 } }
@@ -50,6 +50,12 @@
white-space: nowrap;
}
@media (max-width: 360px) {
[data-component="task-header"] {
padding-inline: 8px;
}
}
/* ============================================
Task Header Todos
============================================ */
@@ -467,7 +473,7 @@
font-variant-numeric: tabular-nums;
}
.task-header-memory-trigger {
.task-header-context-trigger {
all: unset;
position: relative;
display: inline-flex;
@@ -478,18 +484,10 @@
border-radius: 3px;
color: var(--vscode-foreground);
cursor: pointer;
opacity: 0.4;
transition: opacity 0.15s ease;
}
.task-header-memory-trigger--enabled {
color: var(--vscode-foreground);
opacity: 1;
}
.task-header-memory-trigger:hover {
.task-header-context-trigger:hover {
background: var(--vscode-toolbar-hoverBackground);
opacity: 1;
}
[data-slot="task-header-memory-dot"] {
@@ -502,7 +500,7 @@
background: var(--vscode-testing-iconPassed, #73c991);
}
.task-header-memory-popover {
.task-header-context-popover {
width: min(280px, calc(100vw - 16px));
max-width: calc(100vw - 16px);
white-space: normal;
@@ -515,20 +513,62 @@
overflow-wrap: break-word;
}
[data-slot="task-header-memory-menu"] {
[data-slot="task-header-context-tooltip-title"] {
margin-bottom: 4px;
color: var(--vscode-foreground);
font-weight: 600;
}
[data-slot="task-header-context-tooltip-status"] {
margin-bottom: 2px;
color: var(--vscode-foreground);
}
[data-slot="task-header-context-menu"] {
display: flex;
flex-direction: column;
gap: 8px;
padding: 8px;
}
[data-slot="task-header-context-title"] {
padding: 2px 4px 8px;
color: var(--vscode-foreground);
font-size: var(--kilo-font-size-12);
font-weight: 600;
}
[data-slot="task-header-context-section"] {
display: flex;
flex-direction: column;
gap: 6px;
padding: 6px 0;
}
[data-slot="task-header-context-section"] + [data-slot="task-header-context-section"] {
margin-top: 6px;
padding-top: 12px;
border-top: 1px solid var(--vscode-widget-border, var(--vscode-editorWidget-border));
}
[data-slot="task-header-context-section-title"] {
display: flex;
align-items: center;
gap: 4px;
padding: 0 4px;
color: var(--vscode-descriptionForeground);
font-size: var(--kilo-font-size-11);
font-weight: 600;
}
[data-slot="task-header-memory-status"] {
padding: 0 4px;
color: var(--vscode-foreground);
font-size: var(--kilo-font-size-11);
}
[data-slot="task-header-memory-activity"] {
min-width: 0;
padding: 0 4px;
color: var(--vscode-descriptionForeground);
font-size: var(--kilo-font-size-11);
white-space: normal;
@@ -558,13 +598,13 @@
overflow-wrap: break-word;
}
[data-slot="task-header-memory-actions"] {
[data-slot="task-header-context-actions"] {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4px;
}
[data-slot="task-header-memory-action"] {
[data-slot="task-header-context-action"] {
border: 0;
border-radius: 3px;
padding: 4px 6px;
@@ -576,15 +616,15 @@
cursor: pointer;
}
[data-slot="task-header-memory-action"]:last-child:nth-child(odd) {
[data-slot="task-header-context-action"]:last-child:nth-child(odd) {
grid-column: 1 / -1;
}
[data-slot="task-header-memory-action"]:hover:not(:disabled) {
[data-slot="task-header-context-action"]:hover:not(:disabled) {
background: var(--vscode-list-hoverBackground);
}
[data-slot="task-header-memory-action"]:disabled {
[data-slot="task-header-context-action"]:disabled {
color: var(--vscode-disabledForeground);
cursor: default;
}