diff --git a/crates/ide-core/src/workflow_settings.rs b/crates/ide-core/src/workflow_settings.rs
index dee76b931..03d0159e8 100644
--- a/crates/ide-core/src/workflow_settings.rs
+++ b/crates/ide-core/src/workflow_settings.rs
@@ -43,6 +43,7 @@ impl Default for VcsSettings {
pub struct GitSettings {
/// Show blame after the current editor line.
pub inline_blame: bool,
+ pub show_branch_in_project_view: bool,
/// Path to the git executable; empty = auto-detect.
pub executable: String,
/// Default of Update Project: `merge` | `rebase`.
@@ -74,6 +75,7 @@ impl Default for GitSettings {
fn default() -> Self {
Self {
inline_blame: false,
+ show_branch_in_project_view: false,
executable: String::new(),
update_method: "merge".into(),
auto_update_on_push_rejected: false,
diff --git a/src/bindings.ts b/src/bindings.ts
index 6b0a8044a..5e318a887 100644
--- a/src/bindings.ts
+++ b/src/bindings.ts
@@ -4732,6 +4732,7 @@ export type GitOperation =
export type GitSettings = {
/** Show blame after the current editor line. */
inlineBlame?: boolean,
+ showBranchInProjectView?: boolean,
/** Path to the git executable; empty = auto-detect. */
executable?: string,
/** Default of Update Project: `merge` | `rebase`. */
diff --git a/src/features/project-tree/ProjectView.module.css b/src/features/project-tree/ProjectView.module.css
index 4354bfc09..1e71b0bed 100644
--- a/src/features/project-tree/ProjectView.module.css
+++ b/src/features/project-tree/ProjectView.module.css
@@ -66,6 +66,22 @@
text-decoration-skip-ink: none;
text-underline-offset: 3px;
}
+.suffix {
+ display: inline-flex;
+ flex: 0 1 auto;
+ align-items: center;
+ gap: 4px;
+ min-width: 0;
+ margin-left: 6px;
+ overflow: hidden;
+ color: var(--fg-muted);
+ white-space: nowrap;
+}
+.suffixSegment {
+ display: inline-flex;
+ align-items: center;
+ gap: 1px;
+}
.detail {
min-width: 0;
margin-left: 4px;
diff --git a/src/features/project-tree/ProjectView.test.tsx b/src/features/project-tree/ProjectView.test.tsx
index 75df34880..eef702444 100644
--- a/src/features/project-tree/ProjectView.test.tsx
+++ b/src/features/project-tree/ProjectView.test.tsx
@@ -3,7 +3,12 @@ import { act, fireEvent, render, screen, waitFor, within } from "@testing-librar
import { emitLocalIdeEvent } from "@/platform/ipc";
import { installRepeatedDoubleClick } from "@/platform/doubleClick";
import { getContext } from "@/platform/context";
-import { provideEditorService, registerProjectViewRoot, type EditorService } from "@/workbench/contributions";
+import {
+ provideEditorService,
+ registerProjectViewRoot,
+ setFileDecorations,
+ type EditorService,
+} from "@/workbench/contributions";
import { activate } from "./index";
import { setOsClipboardForTests } from "./clipboard";
import { MemoryFs, setProjectFsForTests } from "./fs";
@@ -736,3 +741,35 @@ it("hides the path beside workspace roots when Show Root Paths is off, keeping i
expect(root?.getAttribute("title")).toBe("/a/service");
act(() => setViewOption("showRootPaths", true));
});
+
+it("shows a decoration's suffix after the root name, with icons and struck-out segments", async () => {
+ const { row } = setup();
+ await waitFor(() => expect(row("acme")).toBeTruthy());
+ act(() =>
+ setFileDecorations(
+ "test-suffix",
+ new Map([
+ [
+ ROOT,
+ {
+ suffixColor: "var(--accent)",
+ suffix: [
+ { text: "main" },
+ { text: "/" },
+ { text: "0", strike: true, label: "Up to date with origin/main" },
+ { text: "2", icon: "vcs-outgoing", label: "2 outgoing commits" },
+ ],
+ },
+ ],
+ ]),
+ ),
+ );
+ await waitFor(() => expect(row("acme").textContent).toContain("main/02"));
+ const root = row("acme");
+ expect(root.querySelector("s")?.textContent).toBe("0");
+ expect(root.querySelector("s")?.closest("[style]")?.getAttribute("style")).toContain("var(--accent)");
+ expect(within(root).getByLabelText("2 outgoing commits").querySelector("svg")).not.toBeNull();
+ expect(within(root).getByLabelText("Up to date with origin/main").textContent).toBe("0");
+ act(() => setFileDecorations("test-suffix", new Map()));
+ await waitFor(() => expect(row("acme").textContent).not.toContain("main/"));
+});
diff --git a/src/features/project-tree/ProjectView.tsx b/src/features/project-tree/ProjectView.tsx
index 4ac5280a4..da190740c 100644
--- a/src/features/project-tree/ProjectView.tsx
+++ b/src/features/project-tree/ProjectView.tsx
@@ -26,7 +26,12 @@ import { nativeDropPoint } from "@/platform/nativeDrop";
import type { RootFolder } from "@/platform/languageSettings";
import { useDetectedRoots, useEffectiveRoots } from "@/platform/projectRoots";
import { macStroke, strokeOf } from "@/platform/keymap";
-import { OWNS_FILE_DROP, prepareMenuGroups, useFileDecoration } from "@/workbench/contributions";
+import {
+ OWNS_FILE_DROP,
+ prepareMenuGroups,
+ useFileDecoration,
+ type DecorationSegment,
+} from "@/workbench/contributions";
import {
FILES_DRAG,
HighlightedText,
@@ -345,6 +350,7 @@ const Row = memo(function Row({
indices={matches}
tone="accent"
/>
+ {deco.suffix && }
{detail && {detail}}
{deco.badge && {deco.badge}}
>
@@ -353,6 +359,33 @@ const Row = memo(function Row({
);
});
+interface DecorationSuffixProps {
+ segments: readonly DecorationSegment[];
+ color?: string;
+}
+
+function DecorationSuffix({ segments, color }: DecorationSuffixProps) {
+ return (
+
+ {segments.map((s, i) => {
+ const text = s.strike ? {s.text} : s.text;
+ return (
+
+ {text}
+ {s.icon && }
+
+ );
+ })}
+
+ );
+}
+
// ---------------------------------------------------------------------------
// Member rows ("Show Members")
diff --git a/src/features/settings/pages/vcs.tsx b/src/features/settings/pages/vcs.tsx
index 239964c3d..6f33e1f78 100644
--- a/src/features/settings/pages/vcs.tsx
+++ b/src/features/settings/pages/vcs.tsx
@@ -494,6 +494,11 @@ export const VCS_PAGES: PageSpec[] = [
component: GitExecutableRow,
},
{ kind: "toggle", key: "vcs.git.inlineBlame", label: "Show inline blame for the current line" },
+ {
+ kind: "toggle",
+ key: "vcs.git.showBranchInProjectView",
+ label: "Show branch and incoming/outgoing commits in the Project view",
+ },
],
},
{
diff --git a/src/features/vcs/__tests__/rootSync.test.ts b/src/features/vcs/__tests__/rootSync.test.ts
new file mode 100644
index 000000000..4a7e7ae0e
--- /dev/null
+++ b/src/features/vcs/__tests__/rootSync.test.ts
@@ -0,0 +1,201 @@
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import type { BranchInfo } from "@/bindings";
+
+const ipc = vi.hoisted(() => ({ branchInfo: vi.fn() }));
+vi.mock("@/platform/ipc", async (original) => {
+ const real = await original();
+ return { ...real, commands: { ...real.commands, vcsBranchInfo: ipc.branchInfo } };
+});
+
+import { emitLocalIdeEvent } from "@/platform/ipc";
+import { useProject } from "@/platform/project";
+import { getFileDecoration } from "@/workbench/contributions";
+import { installRootSync, rootSyncSuffix } from "../rootSync";
+import { useVcs } from "../store";
+
+const info = (head: string | null, upstream: string | null, ahead = 0, behind = 0): BranchInfo => ({
+ head,
+ headCommit: "c1",
+ upstream,
+ ahead,
+ behind,
+ operation: null,
+});
+
+describe("rootSyncSuffix", () => {
+ it("strikes out zero when the branch matches its upstream", () => {
+ expect(rootSyncSuffix(info("main", "origin/main"))).toEqual([
+ { text: "main" },
+ { text: "/" },
+ { text: "0", strike: true, label: "Up to date with origin/main" },
+ ]);
+ });
+
+ it("shows outgoing then incoming commits with their icons", () => {
+ expect(rootSyncSuffix(info("main", "origin/main", 1, 3))).toEqual([
+ { text: "main" },
+ { text: "/" },
+ { text: "1", icon: "vcs-outgoing", label: "1 outgoing commits" },
+ { text: "3", icon: "vcs-incoming", label: "3 incoming commits" },
+ ]);
+ expect(rootSyncSuffix(info("main", "origin/main", 0, 2))).toEqual([
+ { text: "main" },
+ { text: "/" },
+ { text: "2", icon: "vcs-incoming", label: "2 incoming commits" },
+ ]);
+ });
+
+ it("shows only the branch without an upstream", () => {
+ expect(rootSyncSuffix(info("feature", null))).toEqual([{ text: "feature" }]);
+ });
+
+ it("shows nothing for a detached HEAD", () => {
+ expect(rootSyncSuffix(info(null, null))).toBeNull();
+ });
+});
+
+describe("installRootSync", () => {
+ const enable = (on: boolean) =>
+ useProject.setState({ settings: { vcs: { git: { showBranchInProjectView: on } } } } as never);
+ const gitChanged = () => emitLocalIdeEvent({ type: "vcsChanged", root: "/p" });
+ const settle = () => new Promise((resolve) => setTimeout(resolve, 400));
+ let dispose: () => void;
+
+ beforeEach(() => {
+ ipc.branchInfo.mockReset().mockImplementation(async (root: string) => ({
+ status: "ok",
+ data: root === "/p" ? info("main", "origin/main", 1, 0) : info("dev", null),
+ }));
+ useVcs.setState({ available: true, roots: ["/p", "/p/lib"] });
+ });
+
+ afterEach(() => {
+ dispose?.();
+ useProject.setState({ settings: null });
+ });
+
+ it("decorates every repository root when the setting is on", async () => {
+ enable(true);
+ dispose = installRootSync();
+ await vi.waitFor(() => expect(getFileDecoration("/p")?.suffix?.[2]?.text).toBe("1"));
+ expect(getFileDecoration("/p/lib")?.suffix).toEqual([{ text: "dev" }]);
+ expect(getFileDecoration("/p")?.suffixColor).toBe("var(--accent)");
+ expect(ipc.branchInfo).toHaveBeenCalledWith("/p");
+ expect(ipc.branchInfo).toHaveBeenCalledWith("/p/lib");
+ });
+
+ it("shows nothing and queries nothing while the setting is off", async () => {
+ enable(false);
+ dispose = installRootSync();
+ gitChanged();
+ await settle();
+ expect(ipc.branchInfo).not.toHaveBeenCalled();
+ expect(getFileDecoration("/p")?.suffix).toBeUndefined();
+ });
+
+ it("queries again after a change inside .git, such as a fetch, push or commit", async () => {
+ enable(true);
+ dispose = installRootSync();
+ await vi.waitFor(() => expect(getFileDecoration("/p")?.suffix?.[2]?.text).toBe("1"));
+ ipc.branchInfo.mockImplementation(async (root: string) => ({
+ status: "ok",
+ data: root === "/p" ? info("main", "origin/main", 1, 4) : info("dev", null),
+ }));
+ gitChanged();
+ await vi.waitFor(() => expect(getFileDecoration("/p")?.suffix?.[3]?.text).toBe("4"));
+ });
+
+ it("does not query git when only the status refreshes or worktree files change", async () => {
+ enable(true);
+ dispose = installRootSync();
+ await vi.waitFor(() => expect(ipc.branchInfo).toHaveBeenCalledTimes(2));
+ useVcs.setState((s) => ({ version: s.version + 1 }));
+ emitLocalIdeEvent({ type: "fsChanged", created: [], modified: ["/p/a.txt"], removed: [] } as never);
+ await settle();
+ expect(ipc.branchInfo).toHaveBeenCalledTimes(2);
+ });
+
+ it("queries the repositories again when the set of roots changes", async () => {
+ enable(true);
+ dispose = installRootSync();
+ await vi.waitFor(() => expect(ipc.branchInfo).toHaveBeenCalledTimes(2));
+ useVcs.setState({ roots: ["/p", "/p/lib", "/p/web"] });
+ await vi.waitFor(() => expect(ipc.branchInfo).toHaveBeenCalledWith("/p/web"));
+ });
+
+ it("clears the decorations when the setting is switched off", async () => {
+ enable(true);
+ dispose = installRootSync();
+ await vi.waitFor(() => expect(getFileDecoration("/p")?.suffix).toBeDefined());
+ enable(false);
+ await vi.waitFor(() => expect(getFileDecoration("/p")?.suffix).toBeUndefined());
+ });
+
+ it("skips a repository whose branch cannot be read", async () => {
+ ipc.branchInfo.mockImplementation(async (root: string) =>
+ root === "/p" ? { status: "error", error: "boom" } : { status: "ok", data: info("dev", null) },
+ );
+ enable(true);
+ dispose = installRootSync();
+ await vi.waitFor(() => expect(getFileDecoration("/p/lib")?.suffix).toEqual([{ text: "dev" }]));
+ expect(getFileDecoration("/p")?.suffix).toBeUndefined();
+ });
+
+ it("keeps a repository's previous suffix when a later read fails", async () => {
+ enable(true);
+ dispose = installRootSync();
+ await vi.waitFor(() => expect(getFileDecoration("/p")?.suffix?.[2]?.text).toBe("1"));
+ ipc.branchInfo.mockImplementation(async (root: string) =>
+ root === "/p" ? { status: "error", error: "index.lock" } : { status: "ok", data: info("dev", "origin/dev") },
+ );
+ gitChanged();
+ await vi.waitFor(() => expect(getFileDecoration("/p/lib")?.suffix?.[2]?.text).toBe("0"));
+ expect(getFileDecoration("/p")?.suffix?.[2]?.text).toBe("1");
+ });
+
+ it("runs one query round at a time and coalesces Git changes made meanwhile", async () => {
+ const pending: Array<() => void> = [];
+ ipc.branchInfo.mockImplementation(
+ (root: string) =>
+ new Promise((resolve) =>
+ pending.push(() => resolve({ status: "ok", data: info(root === "/p" ? "main" : "dev", null) })),
+ ),
+ );
+ enable(true);
+ dispose = installRootSync();
+ expect(ipc.branchInfo).toHaveBeenCalledTimes(2);
+ gitChanged();
+ await settle();
+ gitChanged();
+ await settle();
+ expect(ipc.branchInfo).toHaveBeenCalledTimes(2);
+ pending.splice(0).forEach((done) => done());
+ await vi.waitFor(() => expect(ipc.branchInfo).toHaveBeenCalledTimes(4));
+ pending.splice(0).forEach((done) => done());
+ await vi.waitFor(() => expect(getFileDecoration("/p")?.suffix).toEqual([{ text: "main" }]));
+ await settle();
+ expect(ipc.branchInfo).toHaveBeenCalledTimes(4);
+ });
+
+ it("does not publish results that arrive after disposal", async () => {
+ let finish = () => {};
+ ipc.branchInfo.mockImplementation(
+ () => new Promise((resolve) => (finish = () => resolve({ status: "ok", data: info("main", null) }))),
+ );
+ useVcs.setState({ roots: ["/p"] });
+ enable(true);
+ dispose = installRootSync();
+ dispose();
+ finish();
+ await new Promise((resolve) => setTimeout(resolve, 0));
+ expect(getFileDecoration("/p")?.suffix).toBeUndefined();
+ });
+
+ it("clears the decorations when the project has no repositories any more", async () => {
+ enable(true);
+ dispose = installRootSync();
+ await vi.waitFor(() => expect(getFileDecoration("/p")?.suffix).toBeDefined());
+ useVcs.setState({ available: false, roots: [] });
+ await vi.waitFor(() => expect(getFileDecoration("/p")?.suffix).toBeUndefined());
+ });
+});
diff --git a/src/features/vcs/index.ts b/src/features/vcs/index.ts
index d741b3013..e16d2cd3d 100644
--- a/src/features/vcs/index.ts
+++ b/src/features/vcs/index.ts
@@ -47,6 +47,7 @@ import { installFileConfirmations } from "./confirmations";
import { installIncomingCheck } from "./incoming";
import { closeLogTab, installGitLog, selectGitTab, useGitLog } from "./logStore";
import { installVcsStore, onConflictsAppeared, onConflictsCleared, useVcs } from "./store";
+import { installRootSync } from "./rootSync";
import { tx } from "@/platform/i18n";
/** The Resolve/Continue/Abort buttons exist in the title bar only while there is something to resolve. */
@@ -213,6 +214,7 @@ export function activate(): () => void {
useCommit.setState({ signOff: workflowSettings().vcs.git.signOff });
const disposers = [
installVcsStore(),
+ installRootSync(),
installGitLog(),
installDiagnosticsTracking(),
installPreparedMessage(),
diff --git a/src/features/vcs/rootSync.ts b/src/features/vcs/rootSync.ts
new file mode 100644
index 000000000..4a90223ff
--- /dev/null
+++ b/src/features/vcs/rootSync.ts
@@ -0,0 +1,100 @@
+import type { BranchInfo } from "@/bindings";
+import { call, commands, onIdeEvent } from "@/platform/ipc";
+import { tx } from "@/platform/i18n";
+import { onWorkflowSettingsChange, workflowSettings } from "@/platform/workflowSettings";
+import { setFileDecorations, type DecorationSegment, type FileDecoration } from "@/workbench/contributions";
+import { useVcs } from "./store";
+
+const PROVIDER = "vcs-root-sync";
+const GIT_CHANGE_DELAY_MS = 250;
+const SUFFIX_COLOR = "var(--accent)";
+
+export function rootSyncSuffix(info: BranchInfo): DecorationSegment[] | null {
+ const { head, upstream, ahead, behind } = info;
+ if (!head) return null;
+ if (!upstream) return [{ text: head }];
+ const counts: DecorationSegment[] = [];
+ if (ahead > 0) counts.push({ text: String(ahead), icon: "vcs-outgoing", label: tx`${ahead} outgoing commits` });
+ if (behind > 0) counts.push({ text: String(behind), icon: "vcs-incoming", label: tx`${behind} incoming commits` });
+ if (!counts.length) counts.push({ text: "0", strike: true, label: tx`Up to date with ${upstream}` });
+ return [{ text: head }, { text: "/" }, ...counts];
+}
+
+async function branchInfoOf(root: string): Promise<[string, BranchInfo] | null> {
+ try {
+ return [root, await call(commands.vcsBranchInfo(root))];
+ } catch {
+ return null;
+ }
+}
+
+function sameDecorations(a: ReadonlyMap, b: ReadonlyMap): boolean {
+ if (a.size !== b.size) return false;
+ for (const [root, d] of a) if (JSON.stringify(d) !== JSON.stringify(b.get(root))) return false;
+ return true;
+}
+
+export function installRootSync(): () => void {
+ let disposed = false;
+ let running = false;
+ let pending = false;
+ let shown: ReadonlyMap = new Map();
+
+ const publish = (next: ReadonlyMap) => {
+ if (disposed || sameDecorations(next, shown)) return;
+ shown = next;
+ setFileDecorations(PROVIDER, new Map(next));
+ };
+
+ const update = async () => {
+ const { available, roots } = useVcs.getState();
+ if (!available || !workflowSettings().vcs.git.showBranchInProjectView) return publish(new Map());
+ const infos = await Promise.all(roots.map(branchInfoOf));
+ const next = new Map();
+ roots.forEach((root, i) => {
+ const entry = infos[i];
+ const suffix = entry ? rootSyncSuffix(entry[1]) : shown.get(root)?.suffix;
+ if (suffix) next.set(root, { suffix, suffixColor: SUFFIX_COLOR });
+ });
+ publish(next);
+ };
+
+ const schedule = () => {
+ if (running) {
+ pending = true;
+ return;
+ }
+ running = true;
+ void (async () => {
+ try {
+ do {
+ pending = false;
+ await update();
+ } while (pending && !disposed);
+ } finally {
+ running = false;
+ }
+ })();
+ };
+
+ let timer: ReturnType | undefined;
+ const scheduleSoon = () => {
+ clearTimeout(timer);
+ timer = setTimeout(schedule, GIT_CHANGE_DELAY_MS);
+ };
+
+ const unsubVcs = useVcs.subscribe((s, prev) => {
+ if (s.available !== prev.available || s.roots.join("\n") !== prev.roots.join("\n")) schedule();
+ });
+ const offGit = onIdeEvent("vcsChanged", scheduleSoon);
+ const offSettings = onWorkflowSettingsChange((s) => s.vcs.git.showBranchInProjectView, schedule);
+ schedule();
+ return () => {
+ disposed = true;
+ clearTimeout(timer);
+ unsubVcs();
+ offGit();
+ offSettings();
+ setFileDecorations(PROVIDER, new Map());
+ };
+}
diff --git a/src/platform/clientSettings.ts b/src/platform/clientSettings.ts
index d795db34d..96acb44d0 100644
--- a/src/platform/clientSettings.ts
+++ b/src/platform/clientSettings.ts
@@ -83,6 +83,7 @@ export const CLIENT_SETTINGS_KEYS = [
"run.showOnStart",
"vcs.showDirtyDirectories",
"vcs.git.inlineBlame",
+ "vcs.git.showBranchInProjectView",
"vcs.git.showPushDialog",
"vcs.git.pushDialogOnlyProtected",
"vcs.git.combineStashAndShelf",
diff --git a/src/platform/locales/text/en.json b/src/platform/locales/text/en.json
index 753b1ec74..92b59f07c 100644
--- a/src/platform/locales/text/en.json
+++ b/src/platform/locales/text/en.json
@@ -983,5 +983,7 @@
"Import from Project": "Import from Project",
"No new MCP servers in the project's .mcp.json or .vscode/mcp.json.": "No new MCP servers in the project's .mcp.json or .vscode/mcp.json.",
"Imported 1 MCP server from the project.": "Imported 1 MCP server from the project.",
- "Imported {count} MCP servers from the project.": "Imported {count} MCP servers from the project."
+ "Imported {count} MCP servers from the project.": "Imported {count} MCP servers from the project.",
+ "Show branch and incoming/outgoing commits in the Project view": "Show branch and incoming/outgoing commits in the Project view",
+ "Up to date with {upstream}": "Up to date with {upstream}"
}
diff --git a/src/platform/locales/text/zh.json b/src/platform/locales/text/zh.json
index e0276b589..12c1feb86 100644
--- a/src/platform/locales/text/zh.json
+++ b/src/platform/locales/text/zh.json
@@ -7315,5 +7315,7 @@
"Import from Project": "从项目导入",
"No new MCP servers in the project's .mcp.json or .vscode/mcp.json.": "项目的 .mcp.json 和 .vscode/mcp.json 中没有新的 MCP 服务器。",
"Imported 1 MCP server from the project.": "已从项目导入 1 个 MCP 服务器。",
- "Imported {count} MCP servers from the project.": "已从项目导入 {count} 个 MCP 服务器。"
+ "Imported {count} MCP servers from the project.": "已从项目导入 {count} 个 MCP 服务器。",
+ "Show branch and incoming/outgoing commits in the Project view": "在项目视图中显示分支及传入/传出提交",
+ "Up to date with {upstream}": "已与 {upstream} 同步"
}
diff --git a/src/platform/workflowSettings.ts b/src/platform/workflowSettings.ts
index f7ae73712..972a4d211 100644
--- a/src/platform/workflowSettings.ts
+++ b/src/platform/workflowSettings.ts
@@ -19,6 +19,7 @@ export interface IssueLinkValid {
export interface GitSettingsValid {
inlineBlame: boolean;
+ showBranchInProjectView: boolean;
executable: string;
updateMethod: UpdateMethod;
autoUpdateOnPushRejected: boolean;
@@ -190,6 +191,7 @@ export const DEFAULT_WORKFLOW: WorkflowSettings = {
historyLimit: 1000,
git: {
inlineBlame: false,
+ showBranchInProjectView: false,
executable: "",
updateMethod: "merge",
autoUpdateOnPushRejected: false,
@@ -321,6 +323,7 @@ export function validateVcs(raw: unknown): VcsSettingsValid {
historyLimit: num(v.historyLimit, d.historyLimit, L.historyLimit, true),
git: {
inlineBlame: bool(g.inlineBlame, d.git.inlineBlame),
+ showBranchInProjectView: bool(g.showBranchInProjectView, d.git.showBranchInProjectView),
executable: text(g.executable, d.git.executable).trim(),
updateMethod: oneOf(g.updateMethod, ["merge", "rebase"] as const, d.git.updateMethod),
autoUpdateOnPushRejected: bool(g.autoUpdateOnPushRejected, d.git.autoUpdateOnPushRejected),
diff --git a/src/workbench/contributions.ts b/src/workbench/contributions.ts
index 931704c40..db25a20ad 100644
--- a/src/workbench/contributions.ts
+++ b/src/workbench/contributions.ts
@@ -826,9 +826,18 @@ export function showFindResults(source: FindResultsSource) {
// tree, editor tabs, Search Everywhere, etc. Providers (VCS status, problems)
// push decorations into this store; consumers read with `useFileDecoration`.
+export interface DecorationSegment {
+ text: string;
+ icon?: string;
+ strike?: boolean;
+ label?: string;
+}
+
export interface FileDecoration {
/** CSS colour, normally a token like `var(--vcs-modified)`. */
color?: string;
+ suffix?: readonly DecorationSegment[];
+ suffixColor?: string;
/** CSS colour tinting the editor tab (a data source's colour on its console). */
tint?: string;
/** Short badge text, e.g. a problem count. */