Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-resumed-session-activity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---

Clear stale failures when an agent resumes, keep optional review suggestions from triggering attention, and explain Agent Manager status icons on hover. Keep progress spinners separate from attention warnings during worktree operations.
97 changes: 94 additions & 3 deletions packages/kilo-vscode/tests/fixtures/session-provider-activity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ Object.assign(globalThis, {
})

const { render } = await import("solid-js/web")
const { For } = await import("solid-js")
const { For, createSignal } = await import("solid-js")
const { WorktreeItem } = await import("../../webview-ui/agent-manager/WorktreeItem")
const { DragDropProvider, SortableProvider } = await import("@thisbeyond/solid-dnd")
const { renderTab } = await import("../../webview-ui/agent-manager/tab-rendering")
const { VSCodeProvider } = await import("../../webview-ui/src/context/vscode")
Expand Down Expand Up @@ -81,6 +82,8 @@ const language = {
}

const ref = { value: undefined as ReturnType<typeof useSession> | undefined }
const [operation, setOperation] = createSignal(false)
const [run, setRun] = createSignal(false)
const Probe = () => {
const session = useSession()
ref.value = session
Expand All @@ -105,6 +108,40 @@ const Probe = () => {
<SortableProvider ids={ids}>
<For each={ids}>{(id) => renderTab(id, deps)}</For>
</SortableProvider>
<WorktreeItem
worktree={{
id: "worktree",
path: "/test/worktree",
branch: "test",
parentBranch: "main",
createdAt: "2026-01-01T00:00:00.000Z",
}}
label="Recovery test"
active
pendingDelete={false}
busy={operation()}
activity={session.activityFor("root")}
runStatus={run() ? { worktreeId: "worktree", state: "running" } : undefined}
stale={false}
sessions={1}
grouped={false}
groupStart={false}
groupEnd={false}
groupSize={0}
renaming={false}
renameValue=""
closeKeybind=""
openKeybind=""
onClick={() => {}}
onDelete={() => {}}
onStartRename={() => {}}
onRenameInput={() => {}}
onCommitRename={() => {}}
onCancelRename={() => {}}
onRemoveStale={() => {}}
onCopyPath={() => {}}
onOpen={() => {}}
/>
</DragDropProvider>
)
}
Expand Down Expand Up @@ -145,12 +182,19 @@ const state = (id: string) => {
assert(value)
return value.activityFor(id)
}
const card = (expected: string) => {
const icon = host.querySelector('[data-sidebar-id="worktree"] .am-wt-icon')
assert(icon)
assert.equal(icon.getAttribute("data-activity"), expected)
assert.equal(!!icon.querySelector('[data-component="spinner"]'), expected === "busy" || expected === "retry")
}
const check = async (id: string, expected: string) => {
await settle()
step.value += 1
const value = ref.value
assert(value)
const actual = state(id)
if (id === "root") card(expected)
const tab = host.querySelector(`[data-tab-id="${id}"] [data-activity]`)
if (id === "root" || id === "background") assert(tab, `Missing rendered tab for ${id}`)
if (tab && tab.getAttribute("data-activity") !== expected) {
Expand Down Expand Up @@ -199,6 +243,14 @@ try {
value.setCurrentSessionID("root")
await check("root", "idle")
await check("background", "idle")
for (const update of [setOperation, setRun]) {
update(true)
await settle()
card("busy")
update(false)
await settle()
card("idle")
}

await emit({ type: "sessionStatus", sessionID: "background", status: "busy" })
await check("background", "busy")
Expand Down Expand Up @@ -240,6 +292,12 @@ try {
await check("root", "waiting")
await check("task-child", "waiting")
await check("task-grand", "waiting")
for (const update of [setOperation, setRun]) {
update(true)
await settle()
card("waiting")
update(false)
}
await emit({ type: "permissionError", permissionID: "permission", stale: true })
await check("root", "idle")
assert.equal(value.permissions().length, 0)
Expand Down Expand Up @@ -290,7 +348,9 @@ try {
type: "suggestionRequest",
suggestion: { id: "suggestion", sessionID: "task-grand", text: "Try this", actions: [] },
})
await check("root", "waiting")
await check("root", "idle")
await check("task-grand", "idle")
assert.equal(value.scopedSuggestions("root").length, 1)
await emit({ type: "suggestionResolved", requestID: "suggestion" })
await check("root", "idle")
assert.equal(value.suggestions().length, 0)
Expand All @@ -307,11 +367,17 @@ try {
await check("root", "idle")

await emit({ type: "sessionTurnClosed", sessionID: "root", reason: "completed" })
await emit({
type: "suggestionRequest",
suggestion: { id: "review", sessionID: "root", text: "Review the changes", actions: [] },
})
await check("root", "done")
assert.equal(value.suggestions().length, 1)
await emit({ type: "sessionStatus", sessionID: "root", status: "busy" })
await check("root", "busy")
await emit({ type: "sessionStatus", sessionID: "root", status: "idle" })
await check("root", "idle")
await emit({ type: "suggestionResolved", requestID: "review" })

await emit({ type: "sessionTurnClosed", sessionID: "root", reason: "completed" })
await check("root", "done")
Expand Down Expand Up @@ -370,14 +436,39 @@ try {
await check("root", "idle")
await emit({ type: "sessionError", eventID: "root-error", error: { name: "ProviderError" } })
await check("root", "error")
for (const update of [setOperation, setRun]) {
update(true)
await settle()
card("error")
update(false)
}
assert.equal(value.inUseFor("root"), false)
await emit({ type: "sessionTurnClosed", sessionID: "root", reason: "completed" })
await check("root", "error")

await emit({ type: "sessionStatus", sessionID: "root", status: "busy" })
await check("root", "busy")
for (const status of ["busy", "retry"] as const) {
await emit({ type: "sessionStatus", sessionID: "root", status })
await emit({
type: "sessionError",
sessionID: "root",
eventID: `root-${status}-error`,
error: { name: "ProviderError", message: "Request Entity Too Large" },
})
await check("root", "error")
await emit({ type: "sessionStatus", sessionID: "root", status })
await check("root", status)
assert.equal(value.closeReason(), undefined)
assert(value.messages().some((message) => message.sessionErrorID === `root-${status}-error`))
}
await emit({ type: "sessionTurnClosed", sessionID: "root", reason: "completed" })
await emit({ type: "sessionStatus", sessionID: "root", status: "idle" })
await check("root", "idle")
await check("root", "done")
await emit({ type: "sessionStatus", sessionID: "root", status: "busy" })
await emit({ type: "sessionError", sessionID: "root", eventID: "terminal-error", error: { name: "ProviderError" } })
await emit({ type: "sessionStatus", sessionID: "root", status: "idle" })
await check("root", "error")

await emit({
type: "questionRequest",
Expand Down
1 change: 1 addition & 0 deletions packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ const TSX_FILES = [
path.join(ROOT, "webview-ui/diff-virtual/DiffVirtualApp.tsx"),
// Shared components that consume agent-manager CSS classes (e.g. am-dropdown,
// am-branch-item) used by both the agent manager and the diff viewer.
path.join(ROOT, "webview-ui/src/components/shared/ActivityIcon.tsx"),
path.join(ROOT, "webview-ui/src/components/shared/BranchSelect.tsx"),
path.join(ROOT, "webview-ui/src/components/chat/TabDnd.tsx"),
path.join(ROOT, "webview-ui/diff-viewer/BaseBranchPicker.tsx"),
Expand Down
17 changes: 8 additions & 9 deletions packages/kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,14 @@
import { Component, For, Match, Show, Switch, createSignal } from "solid-js"
import { Icon } from "@kilocode/kilo-ui/icon"
import { IconButton } from "@kilocode/kilo-ui/icon-button"
import { Spinner } from "@kilocode/kilo-ui/spinner"
import { Tooltip, TooltipKeybind } from "@kilocode/kilo-ui/tooltip"
import { HoverCard } from "@kilocode/kilo-ui/hover-card"
import { ContextMenu } from "@kilocode/kilo-ui/context-menu"
import { Button } from "@kilocode/kilo-ui/button"
import type { WorktreeState, WorktreeGitStats, SectionState, RunStatus } from "../src/types/messages"
import type { PRStatus } from "../src/types/messages"
import { ActivityIcon } from "../src/components/shared/ActivityIcon"
import { label, running, type Activity } from "../src/utils/session-activity"
import { description, label, running, strongest, type Activity } from "../src/utils/session-activity"
import { colorCss } from "./section-colors"
import { useLanguage } from "../src/context/language"
import { formatRelativeDate } from "../src/utils/date"
Expand Down Expand Up @@ -157,7 +156,7 @@ export const WorktreeItem: Component<WorktreeItemProps> = (props) => {
const { t } = useLanguage()
const [hovered, setHovered] = createSignal(false)
const [overClose, setOverClose] = createSignal(false)
const state = () => props.activity
const state = () => strongest([props.activity, props.busy || props.runStatus?.state === "running" ? "busy" : "idle"])
const blocked = () =>
props.busy ||
props.blocked ||
Expand Down Expand Up @@ -209,12 +208,7 @@ export const WorktreeItem: Component<WorktreeItemProps> = (props) => {
onClick={() => props.onClick()}
>
<div class="am-wt-icon" data-activity={state()} aria-label={t(label(state()))}>
<Show
when={!props.busy && props.runStatus?.state !== "running"}
fallback={<Spinner class="am-worktree-spinner" />}
>
<ActivityIcon state={state()} idle={<Icon name="branch" size="small" />} />
</Show>
<ActivityIcon state={state()} idle={<Icon name="branch" size="small" />} />
Comment thread
marius-kilocode marked this conversation as resolved.
</div>
<div class="am-wt-content">
{/* Row 1: label + stale badge + stats/hover-actions overlay */}
Expand Down Expand Up @@ -412,6 +406,11 @@ export const WorktreeItem: Component<WorktreeItemProps> = (props) => {
<span class="am-hover-card-keybind">{props.navHint}</span>
</Show>
</div>
<Show when={state() !== "idle"}>
<div class="am-hover-card-divider" />
<div class="am-hover-card-label">{t(label(state()))}</div>
<div class="am-hover-card-note">{t(description(state()))}</div>
</Show>
<div class="am-hover-card-divider" />
<div class="am-hover-card-row">
<span class="am-hover-card-row-label">{t("agentManager.hoverCard.worktree")}</span>
Expand Down
100 changes: 52 additions & 48 deletions packages/kilo-vscode/webview-ui/src/components/chat/SessionTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { IconButton } from "@kilocode/kilo-ui/icon-button"
import { TooltipKeybind } from "@kilocode/kilo-ui/tooltip"
import { Show, type Component, type JSX } from "solid-js"
import { ActivityIcon } from "../shared/ActivityIcon"
import type { Activity } from "../../utils/session-activity"
import { description, type Activity } from "../../utils/session-activity"
import { useLanguage } from "../../context/language"

export const SessionTab: Component<{
title: string
Expand All @@ -22,56 +23,59 @@ export const SessionTab: Component<{
onMiddleClick?: (event: MouseEvent) => void
onKeyDown?: JSX.EventHandlerUnion<HTMLDivElement, KeyboardEvent>
onClose: () => void
}> = (props) => (
<div class={`am-tab ${props.active ? "am-tab-active" : ""}`} data-activity={props.state}>
<div
class="am-tab-target"
role={props.role}
aria-selected={props.selected}
aria-keyshortcuts={props.keyShortcuts}
tabIndex={props.tabIndex}
onClick={props.onSelect}
onMouseDown={props.onMiddleClick}
onKeyDown={props.onKeyDown}
>
}> = (props) => {
const { t } = useLanguage()
return (
<div class={`am-tab ${props.active ? "am-tab-active" : ""}`} data-activity={props.state}>
<div
class="am-tab-target"
role={props.role}
aria-selected={props.selected}
aria-keyshortcuts={props.keyShortcuts}
tabIndex={props.tabIndex}
onClick={props.onSelect}
onMouseDown={props.onMiddleClick}
onKeyDown={props.onKeyDown}
>
<TooltipKeybind
title={props.state === "idle" ? props.title : `${props.title}: ${t(description(props.state))}`}
keybind={props.keybind ?? ""}
placement="bottom"
gutter={8}
class="am-tab-tooltip"
openDelay={0}
>
<span class="am-tab-title">
<Show when={props.state !== "idle"}>
<span class="am-tab-icon" data-activity={props.state} aria-label={props.stateLabel}>
<ActivityIcon state={props.state} />
</span>
</Show>
<span class="am-tab-label">{props.title}</span>
</span>
</TooltipKeybind>
</div>
<TooltipKeybind
title={props.title}
keybind={props.keybind ?? ""}
placement="bottom"
title={props.closeTitle}
keybind={props.closeKeybind ?? ""}
placement="top"
gutter={8}
class="am-tab-tooltip"
class="am-tab-close-wrap"
openDelay={0}
>
<span class="am-tab-title">
<Show when={props.state !== "idle"}>
<span class="am-tab-icon" data-activity={props.state} aria-label={props.stateLabel}>
<ActivityIcon state={props.state} />
</span>
</Show>
<span class="am-tab-label">{props.title}</span>
</span>
<IconButton
icon="close-small"
size="small"
variant="ghost"
aria-label={props.closeLabel}
tabIndex={props.closeTabIndex}
class="am-tab-close"
onClick={(event) => {
event.stopPropagation()
props.onClose()
}}
/>
</TooltipKeybind>
</div>
<TooltipKeybind
title={props.closeTitle}
keybind={props.closeKeybind ?? ""}
placement="top"
gutter={8}
class="am-tab-close-wrap"
openDelay={0}
>
<IconButton
icon="close-small"
size="small"
variant="ghost"
aria-label={props.closeLabel}
tabIndex={props.closeTabIndex}
class="am-tab-close"
onClick={(event) => {
event.stopPropagation()
props.onClose()
}}
/>
</TooltipKeybind>
</div>
)
)
}
5 changes: 2 additions & 3 deletions packages/kilo-vscode/webview-ui/src/context/session.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1518,9 +1518,8 @@ export const SessionProvider: ParentComponent = (props) => {
? { type: "offline", message: message ?? "" }
: { type: newStatus }
setStatusMap(sessionID, info)
// Track busy start time and discard the previous turn's terminal state.
if (newStatus === "busy" || newStatus === "retry") clearClose(sessionID)
if (prev === "idle" && newStatus !== "idle") {
clearClose(sessionID)
if (!busySinceMap[sessionID]) setBusySinceMap(sessionID, Date.now())
}
if (newStatus === "idle") {
Expand Down Expand Up @@ -1781,7 +1780,7 @@ export const SessionProvider: ParentComponent = (props) => {
parents: lineage().parents,
statuses: statusMap,
outcomes: closeMap,
blocked: [...permissions(), ...questions().filter((item) => item.blocking !== false), ...suggestions()].map(
blocked: [...permissions(), ...questions().filter((item) => item.blocking !== false)].map(
(item) => item.sessionID,
),
submitting: Object.keys(submissionMap),
Expand Down
7 changes: 7 additions & 0 deletions packages/kilo-vscode/webview-ui/src/i18n/ar.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading