mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-19 10:02:04 +08:00
feat(vscode): add always-visible diff badge to sidebar (#7795)
Add a live worktree diff stats badge to the sidebar action row that shows current file changes (+additions -deletions). The badge uses GitStatsPoller for consistent polling with the Agent Manager, pauses when the sidebar is hidden, and collapses to an icon with a dot indicator when the sidebar is narrow.
This commit is contained in:
@@ -32,6 +32,9 @@ import {
|
||||
resolveWorkspaceDirectory,
|
||||
type SessionRefreshContext,
|
||||
} from "./kilo-provider-utils"
|
||||
import { GitOps } from "./agent-manager/GitOps"
|
||||
import { GitStatsPoller, type LocalStats } from "./agent-manager/GitStatsPoller"
|
||||
import { getWorkspaceRoot } from "./review-utils"
|
||||
import { MarketplaceService } from "./services/marketplace"
|
||||
import { resolveProjectDirectory } from "./project-directory"
|
||||
import { getBusySessionCount, seedSessionStatuses } from "./session-status"
|
||||
@@ -147,6 +150,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
private chatAutocomplete: ChatTextAreaAutocomplete | null = null
|
||||
private projectDirectory: string | null | undefined
|
||||
private slimEditMetadata = true
|
||||
/** Worktree diff stats poller for the sidebar badge — reuses GitStatsPoller (local stats only) */
|
||||
private statsPoller: GitStatsPoller | null = null
|
||||
private cachedStats: unknown = null
|
||||
|
||||
/** Optional interceptor called before the standard message handler.
|
||||
* Return null to consume the message, or return a (possibly transformed) message. */
|
||||
@@ -266,6 +272,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
data: profileData,
|
||||
})
|
||||
|
||||
// Re-send cached worktree stats so the badge renders immediately after webview reload.
|
||||
if (this.cachedStats) this.postMessage(this.cachedStats)
|
||||
|
||||
// Seed session status map so the Settings panel knows about already-running sessions.
|
||||
// Must run after webview is ready (postMessage is a no-op before that).
|
||||
void this.seedSessionStatusMap()
|
||||
@@ -303,6 +312,11 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
// Handle messages from webview (shared handler)
|
||||
this.setupWebviewMessageHandler(webviewView.webview)
|
||||
|
||||
// Pause stats polling when sidebar is hidden, resume when visible
|
||||
webviewView.onDidChangeVisibility(() => {
|
||||
this.statsPoller?.setEnabled(webviewView.visible)
|
||||
})
|
||||
|
||||
// Initialize connection to CLI backend
|
||||
this.initializeConnection()
|
||||
}
|
||||
@@ -1004,6 +1018,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
])
|
||||
this.sendNotificationSettings()
|
||||
|
||||
// Start polling worktree diff stats for the sidebar badge
|
||||
this.startStatsPolling()
|
||||
|
||||
console.log("[Kilo New] KiloProvider: ✅ initializeConnection completed successfully")
|
||||
} catch (error) {
|
||||
console.error("[Kilo New] KiloProvider: ❌ Failed to initialize connection:", error)
|
||||
@@ -2666,11 +2683,38 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
return this.marketplace
|
||||
}
|
||||
|
||||
// ── Worktree stats polling (sidebar diff badge) ──────────────────
|
||||
|
||||
private startStatsPolling(): void {
|
||||
this.statsPoller?.stop()
|
||||
const git = new GitOps({ log: () => {} })
|
||||
this.statsPoller = new GitStatsPoller({
|
||||
getWorktrees: () => [],
|
||||
getWorkspaceRoot: () => getWorkspaceRoot(),
|
||||
getClient: () => this.connectionService.getClient(),
|
||||
git,
|
||||
onStats: () => {},
|
||||
onLocalStats: (stats: LocalStats) => {
|
||||
const msg = {
|
||||
type: "worktreeStatsLoaded" as const,
|
||||
files: stats.files,
|
||||
additions: stats.additions,
|
||||
deletions: stats.deletions,
|
||||
}
|
||||
this.cachedStats = msg
|
||||
this.postMessage(msg)
|
||||
},
|
||||
log: () => {},
|
||||
})
|
||||
this.statsPoller.setEnabled(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose of the provider and clean up subscriptions.
|
||||
* Does NOT kill the server — that's the connection service's job.
|
||||
*/
|
||||
dispose(): void {
|
||||
this.statsPoller?.stop()
|
||||
this.unsubscribeEvent?.()
|
||||
this.unsubscribeState?.()
|
||||
this.unsubscribeNotificationDismiss?.()
|
||||
|
||||
@@ -183,17 +183,30 @@ export const ChatView: Component<ChatViewProps> = (props) => {
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Show>
|
||||
<Show when={isSidebar() && session.summary()?.files}>
|
||||
<Tooltip value="View file changes" placement="top" class="session-diff-wrapper">
|
||||
<Show when={isSidebar()}>
|
||||
<Tooltip
|
||||
value={
|
||||
session.worktreeStats()?.files
|
||||
? `${session.worktreeStats()!.files} file${session.worktreeStats()!.files > 1 ? "s" : ""} changed · +${session.worktreeStats()!.additions} -${session.worktreeStats()!.deletions}`
|
||||
: "No file changes"
|
||||
}
|
||||
placement="top"
|
||||
class="session-diff-wrapper"
|
||||
>
|
||||
<button
|
||||
class="session-diff-badge"
|
||||
classList={{
|
||||
"session-diff-badge--empty": !session.worktreeStats()?.files,
|
||||
"session-diff-badge--has-changes": !!session.worktreeStats()?.files,
|
||||
}}
|
||||
onClick={() => vscode.postMessage({ type: "openChanges" })}
|
||||
aria-label={language.t("command.session.show.changes")}
|
||||
>
|
||||
<Icon name="layers" size="small" />
|
||||
<span class="session-diff-files">{session.summary()!.files}f</span>
|
||||
<span class="session-diff-add">+{session.summary()!.additions}</span>
|
||||
<span class="session-diff-del">-{session.summary()!.deletions}</span>
|
||||
<Show when={session.worktreeStats()?.files}>
|
||||
<span class="session-diff-add">+{session.worktreeStats()!.additions}</span>
|
||||
<span class="session-diff-del">-{session.worktreeStats()!.deletions}</span>
|
||||
</Show>
|
||||
</button>
|
||||
</Tooltip>
|
||||
</Show>
|
||||
|
||||
@@ -154,6 +154,9 @@ interface SessionContextValue {
|
||||
revertedCount: Accessor<number>
|
||||
summary: Accessor<SessionInfo["summary"]>
|
||||
|
||||
// Live worktree diff stats (polled from CLI backend)
|
||||
worktreeStats: Accessor<{ files: number; additions: number; deletions: number } | undefined>
|
||||
|
||||
// Actions
|
||||
revertSession: (messageID: string) => void
|
||||
unrevertSession: () => void
|
||||
@@ -266,6 +269,11 @@ export const SessionProvider: ParentComponent = (props) => {
|
||||
// Cloud session preview state
|
||||
const [cloudPreviewId, setCloudPreviewId] = createSignal<string | null>(null)
|
||||
|
||||
// Live worktree diff stats from extension polling
|
||||
const [worktreeStats, setWorktreeStats] = createSignal<
|
||||
{ files: number; additions: number; deletions: number } | undefined
|
||||
>()
|
||||
|
||||
// Tracks optimistic messageIDs that haven't been confirmed by the server yet.
|
||||
// Prevents handleMessagesLoaded from wiping them when it replaces the array.
|
||||
const pendingOptimistic = new Map<string, Set<string>>()
|
||||
@@ -636,6 +644,10 @@ export const SessionProvider: ParentComponent = (props) => {
|
||||
})
|
||||
console.error("[Kilo New] Cloud session import failed:", message.error)
|
||||
break
|
||||
|
||||
case "worktreeStatsLoaded":
|
||||
setWorktreeStats({ files: message.files, additions: message.additions, deletions: message.deletions })
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1650,6 +1662,7 @@ export const SessionProvider: ParentComponent = (props) => {
|
||||
revert,
|
||||
revertedCount,
|
||||
summary,
|
||||
worktreeStats,
|
||||
revertSession,
|
||||
unrevertSession,
|
||||
sendMessage,
|
||||
|
||||
@@ -176,6 +176,7 @@ export function mockSessionValue(overrides?: {
|
||||
revert: () => undefined,
|
||||
revertedCount: () => 0,
|
||||
summary: () => undefined,
|
||||
worktreeStats: () => undefined,
|
||||
revertSession: noop,
|
||||
unrevertSession: noop,
|
||||
variantList: () => [],
|
||||
|
||||
@@ -174,21 +174,23 @@
|
||||
/* New Task Button */
|
||||
.new-task-button-wrapper {
|
||||
padding: 8px 12px 0;
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
.session-actions-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* All direct children share extra space equally, but start from their content size */
|
||||
.session-actions-row > * {
|
||||
flex: 1 1 0;
|
||||
min-width: fit-content;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Tooltip wrapper divs must fill their flex space, and buttons inside must fill the wrapper */
|
||||
/* Tooltip wrappers fill their flex slot, buttons fill the tooltip wrapper */
|
||||
.session-actions-row > [data-component="tooltip-trigger"] {
|
||||
display: flex;
|
||||
}
|
||||
@@ -197,15 +199,15 @@
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Diff stats tooltip wrapper — don't grow, push right */
|
||||
/* Diff wrapper also stretches evenly (same flex as buttons) */
|
||||
.session-diff-wrapper {
|
||||
flex: 0 0 auto !important;
|
||||
margin-left: auto;
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
/* Diff stats badge — matches Agent Manager diff toggle style */
|
||||
/* Diff stats badge — fills its flex slot, content centered */
|
||||
.session-diff-badge {
|
||||
display: inline-flex;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
@@ -219,6 +221,11 @@
|
||||
font-variant-numeric: tabular-nums;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.session-diff-badge--empty {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.session-diff-badge:hover {
|
||||
@@ -227,10 +234,8 @@
|
||||
|
||||
.session-diff-badge [data-component="icon"] {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.session-diff-files {
|
||||
color: var(--vscode-descriptionForeground);
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.session-diff-add {
|
||||
@@ -241,6 +246,31 @@
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
/* Collapse diff stats to icon-only when truly narrow */
|
||||
@container (max-width: 300px) {
|
||||
.session-diff-add,
|
||||
.session-diff-del {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.session-diff-badge {
|
||||
padding: 4px 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Dot indicator on the icon when collapsed and there are changes */
|
||||
.session-diff-badge--has-changes [data-component="icon"]::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
right: -2px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: #34d399;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Message List
|
||||
============================================ */
|
||||
|
||||
@@ -963,6 +963,14 @@ export interface AgentManagerLocalStatsMessage {
|
||||
stats: LocalGitStats
|
||||
}
|
||||
|
||||
// Sidebar: Live worktree diff stats (extension → webview)
|
||||
export interface WorktreeStatsLoadedMessage {
|
||||
type: "worktreeStatsLoaded"
|
||||
files: number
|
||||
additions: number
|
||||
deletions: number
|
||||
}
|
||||
|
||||
// Set the model for a session (extension → webview, used during multi-version creation)
|
||||
export interface AgentManagerSetSessionModelMessage {
|
||||
type: "agentManager.setSessionModel"
|
||||
@@ -1294,6 +1302,7 @@ export type ExtensionMessage =
|
||||
| RecentsLoadedMessage
|
||||
| LanguageChangedMessage
|
||||
| ContinueInWorktreeProgressMessage
|
||||
| WorktreeStatsLoadedMessage
|
||||
|
||||
// ============================================
|
||||
// Messages FROM webview TO extension
|
||||
|
||||
Reference in New Issue
Block a user