diff --git a/apps/desktop/src/main/__tests__/composer-git-branch.test.ts b/apps/desktop/src/main/__tests__/composer-git-branch.test.ts
deleted file mode 100644
index 661f793c7b..0000000000
--- a/apps/desktop/src/main/__tests__/composer-git-branch.test.ts
+++ /dev/null
@@ -1,271 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { strict as assert } from 'node:assert';
-import { test, type TestContext } from 'node:test';
-import { parseHTML } from 'linkedom';
-import { act, createElement, type ReactElement } from 'react';
-import { createRoot } from 'react-dom/client';
-import type { GitBranchReadResult } from '@maka/core/git-review';
-import {
- createFakeWorkbarServices,
- useComposerGitBranch,
- WorkbarServicesProvider,
- type ComposerGitBranch,
- type WorkbarReviewService,
- type WorkbarServices,
-} from '../../renderer/features/workbar/testing.js';
-
-function installRenderer(t: TestContext) {
- const original = {
- document: globalThis.document,
- window: globalThis.window,
- IS_REACT_ACT_ENVIRONMENT: (globalThis as typeof globalThis & {
- IS_REACT_ACT_ENVIRONMENT?: boolean;
- }).IS_REACT_ACT_ENVIRONMENT,
- };
- const { document, window } = parseHTML('
');
- window.getComputedStyle = () =>
- ({ direction: 'ltr', getPropertyValue: () => '' }) as unknown as CSSStyleDeclaration;
- Object.assign(globalThis, { document, window, IS_REACT_ACT_ENVIRONMENT: true });
- t.after(() => {
- Object.assign(globalThis, original);
- });
- return document;
-}
-
-/** A review port whose `branch()` answers from a mutable value, counting calls. */
-function reviewPort(answer: () => GitBranchReadResult): {
- service: WorkbarReviewService;
- reads: () => number;
-} {
- let calls = 0;
- return {
- reads: () => calls,
- service: {
- read: async () => {
- throw new Error('not used');
- },
- branch: async () => {
- calls += 1;
- return answer();
- },
- subscribeSessionEvents: () => () => undefined,
- },
- };
-}
-
-function servicesWithReview(
- review: WorkbarReviewService,
- pty: { emit(sessionId: string): void } = { emit: () => undefined },
-): WorkbarServices {
- // The fake base satisfies every other port; `review` and the PTY stream are
- // the two this hook consumes.
- return createFakeWorkbarServices({
- review,
- terminal: {
- ...createFakeWorkbarServices().terminal,
- subscribePtyData: (handler) => {
- pty.emit = (sessionId) => handler({ sessionId, ref: 'r', sequence: 0, data: 'x' });
- return () => undefined;
- },
- },
- });
-}
-
-test('useComposerGitBranch re-reads on focus and visibility, and follows a branch change', async (t) => {
- const document = installRenderer(t);
- let current: GitBranchReadResult = {
- ok: true,
- snapshot: { branch: 'feature/old', shortSha: null },
- };
- const port = reviewPort(() => current);
- const container = document.querySelector('#root');
- assert.ok(container);
- const root = createRoot(container);
-
- let observed: ComposerGitBranch | undefined;
- function Probe(): ReactElement {
- observed = useComposerGitBranch('session-1');
- return createElement('span', null, observed?.name ?? observed?.shortSha ?? '');
- }
-
- await act(() =>
- root.render(
- createElement(WorkbarServicesProvider, {
- services: servicesWithReview(port.service),
- children: createElement(Probe),
- }),
- ),
- );
- assert.equal(observed?.name, 'feature/old', 'the first read must land');
- assert.equal(port.reads(), 1);
-
- // The branch changes under the app (e.g. the integrated terminal), and the
- // user returns to the window — the chip must follow, not stay silently stale.
- current = { ok: true, snapshot: { branch: 'feature/new', shortSha: null } };
- await act(() => {
- window.dispatchEvent(new window.Event('focus'));
- });
- await act(async () => {});
- assert.equal(observed?.name, 'feature/new', 'a focus must re-read the branch');
- assert.equal(port.reads(), 2);
-
- // A detached HEAD after a checkout is reported as the short sha.
- current = { ok: true, snapshot: { branch: null, shortSha: 'abc1234' } };
- await act(() => {
- document.dispatchEvent(new window.Event('visibilitychange'));
- });
- await act(async () => {});
- assert.equal(observed?.shortSha, 'abc1234', 'a visibility change must re-read too');
-
- await act(() => root.unmount());
-});
-
-test('useComposerGitBranch renders nothing (undefined), never an empty chip', async (t) => {
- const document = installRenderer(t);
- const port = reviewPort(() => ({ ok: false, isGitRepo: false }));
- const container = document.querySelector('#root');
- assert.ok(container);
- const root = createRoot(container);
-
- let observed: ComposerGitBranch | undefined | null = null;
- function Probe(): ReactElement {
- observed = useComposerGitBranch('session-1');
- return createElement('span');
- }
- await act(() =>
- root.render(
- createElement(WorkbarServicesProvider, {
- services: servicesWithReview(port.service),
- children: createElement(Probe),
- }),
- ),
- );
- assert.equal(observed, undefined, 'a non-repository must yield undefined, not a husk');
-
- // No session at all: still undefined, and no read is issued.
- function NoSessionProbe(): ReactElement {
- observed = useComposerGitBranch(undefined);
- return createElement('span');
- }
- await act(() =>
- root.render(
- createElement(WorkbarServicesProvider, {
- services: servicesWithReview(port.service),
- children: createElement(NoSessionProbe),
- }),
- ),
- );
- assert.equal(observed, undefined);
- // The non-repository read above already ran once; no session must add none.
- assert.equal(port.reads(), 1, 'no session must not issue a read');
-
- await act(() => root.unmount());
-});
-
-test('useComposerGitBranch re-reads after the session terminal goes quiet (the in-app case)', async (t) => {
- const document = installRenderer(t);
- let current: GitBranchReadResult = {
- ok: true,
- snapshot: { branch: 'feature/before', shortSha: null },
- };
- const port = reviewPort(() => current);
- const pty = { emit: (_sessionId: string) => undefined };
- const container = document.querySelector('#root');
- assert.ok(container);
- const root = createRoot(container);
-
- let observed: ComposerGitBranch | undefined;
- function Probe(): ReactElement {
- observed = useComposerGitBranch('session-1');
- return createElement('span');
- }
- await act(() =>
- root.render(
- createElement(WorkbarServicesProvider, {
- services: servicesWithReview(port.service, pty),
- children: createElement(Probe),
- }),
- ),
- );
- assert.equal(observed?.name, 'feature/before');
- assert.equal(port.reads(), 1);
-
- // A command typed in this session's integrated terminal: output arrives with
- // no window blur and no new shell run, so only the PTY signal can see it.
- current = { ok: true, snapshot: { branch: 'feature/after', shortSha: null } };
- await act(() => {
- pty.emit('session-1');
- });
- // The read is debounced until the output settles.
- await act(() => new Promise((resolve) => setTimeout(resolve, 600)));
- assert.equal(observed?.name, 'feature/after', 'the chip must follow an in-app branch change');
- assert.equal(port.reads(), 2);
-
- // Output from ANOTHER session must not touch this chip.
- current = { ok: true, snapshot: { branch: 'feature/other', shortSha: null } };
- await act(() => {
- pty.emit('session-2');
- });
- await act(() => new Promise((resolve) => setTimeout(resolve, 600)));
- assert.equal(observed?.name, 'feature/after', 'another session\'s terminal must not move this chip');
- assert.equal(port.reads(), 2);
-
- await act(() => root.unmount());
-});
-
-test('an unchanged branch costs no re-render when the terminal re-reads', async (t) => {
- const document = installRenderer(t);
- const port = reviewPort(() => ({
- ok: true,
- snapshot: { branch: 'feature/same', shortSha: null },
- }));
- const pty = { emit: (_sessionId: string) => undefined };
- const container = document.querySelector('#root');
- assert.ok(container);
- const root = createRoot(container);
-
- let renders = 0;
- function Probe(): ReactElement {
- renders += 1;
- useComposerGitBranch('session-1');
- return createElement('span');
- }
- await act(() =>
- root.render(
- createElement(WorkbarServicesProvider, {
- services: servicesWithReview(port.service, pty),
- children: createElement(Probe),
- }),
- ),
- );
- const afterFirst = renders;
-
- // A busy terminal re-reads on each quiet gap; the answer is identical, so the
- // hook must not repaint the composer for it.
- await act(() => {
- pty.emit('session-1');
- });
- await act(() => new Promise((resolve) => setTimeout(resolve, 600)));
- assert.ok(port.reads() > 1, 'the quiet gap did re-read');
- assert.equal(renders, afterFirst, 'an unchanged branch must not re-render');
-
- await act(() => root.unmount());
-});
diff --git a/apps/desktop/src/main/__tests__/git-review-main.test.ts b/apps/desktop/src/main/__tests__/git-review-main.test.ts
index cf66e10b07..b41b7c65b8 100644
--- a/apps/desktop/src/main/__tests__/git-review-main.test.ts
+++ b/apps/desktop/src/main/__tests__/git-review-main.test.ts
@@ -24,7 +24,7 @@ import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { promisify } from 'node:util';
import { after, describe, it } from 'node:test';
-import { readGitBranch, readGitReview } from '../git-review-main.js';
+import { readGitReview } from '../git-review-main.js';
const execFileAsync = promisify(execFile);
const roots = new Set();
@@ -170,29 +170,3 @@ async function git(root: string, ...args: string[]): Promise {
timeout: 10_000,
});
}
-
-describe('Git branch read (composer chip)', () => {
- it('reads the checked-out branch', async () => {
- const root = await repository();
- await git(root, 'checkout', '-b', 'feature/chip');
- assert.deepEqual(await readGitBranch(root), {
- ok: true,
- snapshot: { branch: 'feature/chip', shortSha: null },
- });
- });
-
- it('falls back to the short sha on a detached HEAD', async () => {
- const root = await repository();
- await git(root, 'checkout', '--detach', 'HEAD');
- const result = await readGitBranch(root);
- assert.equal(result.ok, true);
- if (!result.ok) return;
- assert.equal(result.snapshot.branch, null);
- assert.match(result.snapshot.shortSha ?? '', /^[0-9a-f]{7,}$/u);
- });
-
- it('reports a non-repository as isGitRepo: false, so no chip is drawn', async () => {
- const root = await temporaryRoot();
- assert.deepEqual(await readGitBranch(root), { ok: false, isGitRepo: false });
- });
-});
diff --git a/apps/desktop/src/main/git-review-main.ts b/apps/desktop/src/main/git-review-main.ts
index 98fbc78d68..a75b209c13 100644
--- a/apps/desktop/src/main/git-review-main.ts
+++ b/apps/desktop/src/main/git-review-main.ts
@@ -24,8 +24,6 @@ import { isAbsolute, relative, resolve } from 'node:path';
import { promisify } from 'node:util';
import { countDiffLineStats } from '@maka/core/unified-diff';
import {
- type GitBranchReadResult,
- type GitBranchSnapshot,
type GitReviewFile,
type GitReviewFileStatus,
type GitReviewReadResult,
@@ -43,35 +41,6 @@ export interface GitReviewCommandRunner {
(root: string, args: readonly string[]): Promise;
}
-/**
- * The working tree's Git branch, for the composer's branch chip. Three states:
- * not a repository (`ok: false, isGitRepo: false` — the caller renders nothing),
- * a detached HEAD (`branch: null`, `shortSha` set), or a named branch.
- *
- * Read-only and deliberately cheap: the review reader withholds the bulk of its
- * work, so this asks only what the chip prints.
- */
-export async function readGitBranch(
- cwd: string,
- runGit: GitReviewCommandRunner = runGitCommand,
-): Promise {
- try {
- const repositoryRoot = await resolveProjectRoot([cwd]);
- if (!(await resolveProjectGitInfo(repositoryRoot)).isGitRepo) {
- return { ok: false, isGitRepo: false };
- }
- const branch = cleanLine(await runGit(repositoryRoot, ['branch', '--show-current']));
- // A detached HEAD yields no branch name; its short commit is the honest
- // answer, and a repository with no commits yet has neither.
- const shortSha = branch === null
- ? cleanLine(await runGit(repositoryRoot, ['rev-parse', '--short', 'HEAD']).catch(() => ''))
- : null;
- return { ok: true, snapshot: { branch, shortSha } };
- } catch {
- return { ok: false, isGitRepo: true };
- }
-}
-
export async function readGitReview(
cwd: string,
source: GitReviewSource,
diff --git a/apps/desktop/src/main/runtime-host-workspace-ipc-main.ts b/apps/desktop/src/main/runtime-host-workspace-ipc-main.ts
index af647a36e2..f2f2d91ca6 100644
--- a/apps/desktop/src/main/runtime-host-workspace-ipc-main.ts
+++ b/apps/desktop/src/main/runtime-host-workspace-ipc-main.ts
@@ -20,7 +20,7 @@
import { stat } from 'node:fs/promises';
import type { GitReviewSource } from '@maka/core/git-review';
import type { DesktopRuntimeHostClient } from './runtime-host-client.js';
-import { readGitBranch, readGitReview } from './git-review-main.js';
+import { readGitReview } from './git-review-main.js';
import {
handleReconnectableRead,
type ReconnectableReadIpcMain,
@@ -44,21 +44,6 @@ export function registerRuntimeHostWorkspaceIpc(
if (!cwd) return { ok: false as const, reason: 'workspace_unavailable' as const };
return readGitReview(cwd, request.source, undefined, request.baseBranch);
});
-
- // The composer's branch chip: the same session → workspace resolution as the
- // review read above, but only the branch the chip prints.
- handleReconnectableRead(input.ipcMain, 'git:branch', async (_event, raw: unknown) => {
- const sessionId = readBranchRequest(raw);
- const cwd =
- input.allowLocalWorkspace === false ? null : await sessionWorkspace(input.client, sessionId);
- // An unavailable workspace is not a repository either: the chip stays off.
- if (!cwd) return { ok: false as const, isGitRepo: false };
- return readGitBranch(cwd);
- });
-}
-
-function readBranchRequest(value: unknown): string {
- return requiredString(requiredRecord(value, 'Git branch').sessionId, 'Session id');
}
async function sessionWorkspace(client: WorkspaceClient, sessionId: string): Promise {
diff --git a/apps/desktop/src/preload/bridge-contract.d.ts b/apps/desktop/src/preload/bridge-contract.d.ts
index 2cddb6b9b8..4e6688eb2f 100644
--- a/apps/desktop/src/preload/bridge-contract.d.ts
+++ b/apps/desktop/src/preload/bridge-contract.d.ts
@@ -85,7 +85,6 @@ import type {
AppUpdateStatus,
} from '../shared/app-update.js';
import type {
- GitBranchReadResult,
GitReviewReadResult,
GitReviewSource,
} from '@maka/core/git-review';
@@ -1478,8 +1477,6 @@ export interface MakaBridge {
source: GitReviewSource;
baseBranch?: string;
}): Promise;
- /** The working tree's branch (or short sha on a detached HEAD). */
- branch(input: { sessionId: string }): Promise;
};
goal: {
/** The session's current goal (null when none is set). */
diff --git a/apps/desktop/src/preload/preload.ts b/apps/desktop/src/preload/preload.ts
index fc42c7d750..b0072ecf24 100644
--- a/apps/desktop/src/preload/preload.ts
+++ b/apps/desktop/src/preload/preload.ts
@@ -183,7 +183,6 @@ import type {
import type { ThinkingLevel } from '@maka/core/model-thinking';
import type { E2eFixtureState } from '@maka/core/e2e-fixture';
import type {
- GitBranchReadResult,
GitReviewReadResult,
GitReviewSource,
} from '@maka/core/git-review';
@@ -2976,9 +2975,6 @@ const makaBridge = {
}): Promise {
return invokeSessionInput('git-review:read', input);
},
- branch(input: { sessionId: string }): Promise {
- return invokeSessionInput('git:branch', input);
- },
},
goal: {
get(sessionId: string): Promise {
diff --git a/apps/desktop/src/renderer/chat-composer-region.tsx b/apps/desktop/src/renderer/chat-composer-region.tsx
index d8f4ab5bae..91b55b8256 100644
--- a/apps/desktop/src/renderer/chat-composer-region.tsx
+++ b/apps/desktop/src/renderer/chat-composer-region.tsx
@@ -150,7 +150,6 @@ interface ChatComposerRegionProps
*/
children: (
usage: { readonly usageTokens: number; readonly contextWindow?: number } | undefined,
- gitBranch: { readonly name?: string; readonly shortSha?: string } | undefined,
) => ReactNode;
}>;
directoryComposerProps: Pick<
@@ -267,14 +266,12 @@ export function ChatComposerRegion({
// the anchor prop remains the reading it falls back to.
const renderComposer = (
liveContextUsage: { readonly usageTokens: number; readonly contextWindow?: number } | undefined,
- gitBranch: { name?: string; shortSha?: string } | undefined,
) => (
{(goalProjection) => (
- {(usage, gitBranch) => renderComposer(usage, gitBranch)}
+ {renderComposer}
) : (
- renderComposer(undefined, undefined)
+ renderComposer(undefined)
)}
>
);
diff --git a/apps/desktop/src/renderer/features/workbar/ports.ts b/apps/desktop/src/renderer/features/workbar/ports.ts
index 5744a0c8af..a7c1e6bc52 100644
--- a/apps/desktop/src/renderer/features/workbar/ports.ts
+++ b/apps/desktop/src/renderer/features/workbar/ports.ts
@@ -33,11 +33,7 @@ import type {
ArtifactTextReadResult,
} from '@maka/core/artifacts';
import type { BrowserState, BrowserViewRect } from '@maka/core/browser';
-import type {
- GitBranchReadResult,
- GitReviewReadResult,
- GitReviewSource,
-} from '@maka/core/git-review';
+import type { GitReviewReadResult, GitReviewSource } from '@maka/core/git-review';
import type { PermissionMode } from '@maka/core/permission';
import type { SandboxBoundaryResponse } from '@maka/core/sandbox-boundary';
import type { ClientCapabilityResponse } from '@maka/core/client-capability-grant';
@@ -71,8 +67,6 @@ export interface WorkbarReviewService {
source: GitReviewSource;
baseBranch?: string;
}): Promise;
- /** The working tree's branch (or short sha when detached), for the composer chip. */
- branch(sessionId: string): Promise;
subscribeSessionEvents(
sessionId: string,
handler: (event: SessionEvent) => void,
diff --git a/apps/desktop/src/renderer/features/workbar/testing.ts b/apps/desktop/src/renderer/features/workbar/testing.ts
index 6edb4cedc1..7c67e20586 100644
--- a/apps/desktop/src/renderer/features/workbar/testing.ts
+++ b/apps/desktop/src/renderer/features/workbar/testing.ts
@@ -22,7 +22,6 @@ import type { WorkbarServices } from './ports.js';
export { WorkbarServicesProvider } from './services-context.js';
export type {
WorkbarServices,
- WorkbarReviewService,
SessionTracePage,
SessionUsageSummary,
WorkbarIngestInput,
@@ -57,10 +56,6 @@ export * from './tools/terminal/session-terminal-frame.js';
export * from '../../application/contracts/session-inspector/use-session-trace.js';
export * from './controller/use-workbar-controller.js';
export { SideChatCloseConfirmation } from './ui/side-chat-close-confirmation.js';
-export {
- useComposerGitBranch,
- type ComposerGitBranch,
-} from './tools/composer-git-branch.js';
const noopSubscription = (): (() => void) => () => undefined;
@@ -78,7 +73,6 @@ export function createFakeWorkbarServices(
read: async () => {
throw new Error('Fake review.read is not configured');
},
- branch: async () => ({ ok: false, isGitRepo: false }),
subscribeSessionEvents: noopSubscription,
},
terminal: {
diff --git a/apps/desktop/src/renderer/features/workbar/tools/composer-git-branch.ts b/apps/desktop/src/renderer/features/workbar/tools/composer-git-branch.ts
deleted file mode 100644
index d3e0479b59..0000000000
--- a/apps/desktop/src/renderer/features/workbar/tools/composer-git-branch.ts
+++ /dev/null
@@ -1,162 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { useCallback, useEffect, useRef, useState } from 'react';
-import { useWorkbarServices } from '../services-context.js';
-
-/**
- * How long this session's PTY must be silent before the branch is re-read. Long
- * enough that a command's output burst settles into one read, short enough that
- * the chip has caught up by the time a person looks at it.
- */
-const PTY_QUIET_MS = 400;
-
-export interface ComposerGitBranch {
- readonly name?: string;
- readonly shortSha?: string;
-}
-
-/**
- * The branch the active Session's working tree is on, for the composer's branch
- * chip. `undefined` whenever there is nothing to show — no session, not a
- * repository, a failed read — so the chip renders nothing rather than an empty
- * husk.
- *
- * The read is re-taken, not frozen: a branch changes under the app, including
- * from the Desktop's own integrated terminal, so a value read once would go
- * silently stale — and a stale status readout is worse than an absent one,
- * because it still looks like a good value.
- *
- * Three triggers, because a branch changes in three places:
- * - the app was left and returned to (`focus`, `visibilitychange`);
- * - a command ran in this session's INTEGRATED terminal, which lives in the
- * same document — so neither window event fires. That terminal is a long-lived
- * PTY: `git checkout` produces no new shell run and no session event, only
- * output. The signal is therefore the output going quiet — a command has
- * finished when the PTY has been silent for a beat — and only for a run
- * belonging to THIS session.
- * - `sessionId` changing (a different session is a different working tree).
- *
- * `subscribeSessionEvents` would not cover the middle case: it carries the
- * model's transcript events (`tool_start`/`tool_result`), and a command typed by
- * a person is not one of those.
- *
- * The re-read is driven through refs, not through a state token: a busy terminal
- * spawns one read per quiet gap, and most of those answer the same branch. A
- * token held in state would repaint the composer on every one of them even when
- * nothing changed, so reads go through `read()` and a state update happens only
- * when the value actually differs.
- */
-export function useComposerGitBranch(
- sessionId: string | undefined,
-): ComposerGitBranch | undefined {
- const { review, terminal } = useWorkbarServices();
- const [branch, setBranch] = useState(undefined);
- // The last applied value, readable from a trigger without re-subscribing, and
- // the session the in-flight read belongs to (a late answer for a session the
- // user has left must not land).
- const branchRef = useRef(undefined);
- const sessionRef = useRef(sessionId);
- sessionRef.current = sessionId;
-
- // Set state only on a real change. Returning the caller is not enough on its
- // own here because these reads are not the render's own dependency; the guard
- // is what keeps an unchanged branch from repainting the composer.
- const apply = useCallback((next: ComposerGitBranch | undefined) => {
- const current = branchRef.current;
- if (current?.name === next?.name && current?.shortSha === next?.shortSha) return;
- branchRef.current = next;
- setBranch(next);
- }, []);
-
- const read = useCallback(() => {
- const id = sessionRef.current;
- if (!id) {
- apply(undefined);
- return;
- }
- void review
- .branch(id)
- .then((result) => {
- if (sessionRef.current !== id) return;
- apply(
- result.ok
- ? {
- ...(result.snapshot.branch !== null ? { name: result.snapshot.branch } : {}),
- ...(result.snapshot.branch === null && result.snapshot.shortSha !== null
- ? { shortSha: result.snapshot.shortSha }
- : {}),
- }
- : undefined,
- );
- })
- .catch(() => {
- if (sessionRef.current === id) apply(undefined);
- });
- }, [review, apply]);
-
- // The session's own read, taken whenever the session changes.
- useEffect(() => {
- if (!sessionId) {
- branchRef.current = undefined;
- setBranch(undefined);
- return;
- }
- read();
- }, [sessionId, read]);
-
- // Returning to the app is a branch change we cannot observe directly.
- useEffect(() => {
- const onFocus = () => read();
- // `visibilitychange` alone is enough: returning to a tab fires it, and the
- // hidden->visible transition is the only direction that can have missed a
- // change. Gating on `document.visibilityState` would trust a property some
- // embedders do not populate, and the extra read on a hide is harmless.
- const onVisibility = () => read();
- window.addEventListener('focus', onFocus);
- document.addEventListener('visibilitychange', onVisibility);
- return () => {
- window.removeEventListener('focus', onFocus);
- document.removeEventListener('visibilitychange', onVisibility);
- };
- }, [read]);
-
- // The integrated terminal: a persistent PTY in this same document, so a typed
- // command fires no window event and creates no shell run. Its OUTPUT is the
- // signal, debounced so a burst costs one read rather than one per chunk, and
- // scoped to this session so another session's terminal cannot move this chip.
- useEffect(() => {
- if (!sessionId) return;
- let quietTimer: ReturnType | undefined;
- const unsubscribe = terminal.subscribePtyData((event) => {
- if (event.sessionId !== sessionId) return;
- if (quietTimer !== undefined) clearTimeout(quietTimer);
- quietTimer = setTimeout(() => {
- quietTimer = undefined;
- read();
- }, PTY_QUIET_MS);
- });
- return () => {
- if (quietTimer !== undefined) clearTimeout(quietTimer);
- unsubscribe();
- };
- }, [sessionId, terminal, read]);
-
- return branch;
-}
diff --git a/apps/desktop/src/renderer/features/workbar/tools/inspector/live-context-usage-probe.tsx b/apps/desktop/src/renderer/features/workbar/tools/inspector/live-context-usage-probe.tsx
index 075b1ad7e3..e43864ef2d 100644
--- a/apps/desktop/src/renderer/features/workbar/tools/inspector/live-context-usage-probe.tsx
+++ b/apps/desktop/src/renderer/features/workbar/tools/inspector/live-context-usage-probe.tsx
@@ -21,12 +21,6 @@ import { useWorkbarServices } from '../../services-context.js';
import type { ReactElement, ReactNode } from 'react';
import type { LiveContextUsage } from '../../../../application/contracts/session-inspector/live-context-usage.js';
import { useLiveContextUsage } from '../../../../application/contracts/session-inspector/use-live-context-usage.js';
-import {
- useComposerGitBranch,
- type ComposerGitBranch,
-} from '../composer-git-branch.js';
-
-export type { ComposerGitBranch } from '../composer-git-branch.js';
/**
* Render-prop boundary for the composer context gauge (#4717).
@@ -42,10 +36,7 @@ export function LiveContextUsageProbe(props: {
readonly sessionId: string | undefined;
readonly model: string | undefined;
readonly providerType: string | undefined;
- readonly children: (
- usage: LiveContextUsage | undefined,
- gitBranch: ComposerGitBranch | undefined,
- ) => ReactNode;
+ readonly children: (usage: LiveContextUsage | undefined) => ReactNode;
}): ReactElement {
const { inspector } = useWorkbarServices();
const usage = useLiveContextUsage({
@@ -54,13 +45,5 @@ export function LiveContextUsageProbe(props: {
model: props.model,
providerType: props.providerType,
});
- // Two independent hooks (each in its own module), composed here only because
- // this is the single injection point the shell can offer: `app-shell.tsx` is
- // token-frozen by the architecture ratchet and `chat-composer-region.tsx` is
- // capability-frozen, so a second probe prop cannot be threaded through without
- // growing recorded debt. The branch logic is not coupled to the usage reading —
- // `useComposerGitBranch` is standalone and tested on its own; only the carrier
- // is shared.
- const gitBranch = useComposerGitBranch(props.sessionId);
- return <>{props.children(usage, gitBranch)}>;
+ return <>{props.children(usage)}>;
}
diff --git a/apps/desktop/src/renderer/platform/desktop/create-workbar-services.ts b/apps/desktop/src/renderer/platform/desktop/create-workbar-services.ts
index 06654bedef..8622be3fc4 100644
--- a/apps/desktop/src/renderer/platform/desktop/create-workbar-services.ts
+++ b/apps/desktop/src/renderer/platform/desktop/create-workbar-services.ts
@@ -108,7 +108,6 @@ export function createDesktopWorkbarServices(
popupMenu: (input) => bridge.appWindow.popupMenu(input),
review: {
read: (input) => bridge.gitReview.read(input),
- branch: (sessionId) => bridge.gitReview.branch({ sessionId }),
subscribeSessionEvents: (sessionId, handler) =>
bridge.sessions.subscribeEvents(sessionId, handler),
},
diff --git a/apps/desktop/src/renderer/styles/composer.css b/apps/desktop/src/renderer/styles/composer.css
index 13df3e7d05..70dc4523b5 100644
--- a/apps/desktop/src/renderer/styles/composer.css
+++ b/apps/desktop/src/renderer/styles/composer.css
@@ -338,34 +338,6 @@
max-width: min(320px, 92vw);
}
-/* The Git-branch readout beside the usage gauge in the composer footer. A
- ``, not a control — nothing to click — but it wears the same label type
- and pill geometry as the model chip. The cap is wider than the model chip's
- 180px so a usual branch (`feat/some-topic`) shows whole; only a genuinely long
- one ellipsizes, and the `title` carries the full text in that case. */
-.maka-composer-git-branch {
- font: var(--maka-text-label);
- height: var(--h-control-md);
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: var(--space-1);
- padding: 0 var(--space-2);
- border: var(--border-width-hairline) solid transparent;
- border-radius: var(--radius-pill);
- background: transparent;
- color: var(--muted-foreground);
- white-space: nowrap;
- max-width: min(360px, 40vw);
-}
-
-.maka-composer-git-branch-text {
- min-width: 0;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
/* Model chip (static fallback) still owns its own quiet geometry. */
.maka-composer-model-chip {
font: var(--maka-text-label);
diff --git a/apps/desktop/stories/session-workbar.stories.tsx b/apps/desktop/stories/session-workbar.stories.tsx
index 3567bb1d15..9d4973ab45 100644
--- a/apps/desktop/stories/session-workbar.stories.tsx
+++ b/apps/desktop/stories/session-workbar.stories.tsx
@@ -829,7 +829,6 @@ function bridge(options: {
if (options.reviewFail) throw new Error('读取变更失败:无法运行 git diff');
return options.review ?? { ok: true, snapshot: gitReviewSnapshot };
},
- branch: async () => ({ ok: true, snapshot: { branch: 'main', shortSha: null } }),
subscribeSessionEvents: unsubscribe,
},
terminal: {
diff --git a/packages/core/src/git-review.ts b/packages/core/src/git-review.ts
index f65f49ad07..9ca9ec90ac 100644
--- a/packages/core/src/git-review.ts
+++ b/packages/core/src/git-review.ts
@@ -61,23 +61,3 @@ export type GitReviewReadResult =
| 'invalid_base_branch'
| 'git_failed';
};
-
-/**
- * The working tree's Git branch, for the composer's branch chip. `branch: null`
- * with a `shortSha` means a detached HEAD; a repository with no commits yet has
- * neither. The chip is only rendered when the read succeeded.
- */
-export interface GitBranchSnapshot {
- branch: string | null;
- shortSha: string | null;
-}
-
-/**
- * `isGitRepo` distinguishes the two ways a read can fail: `false` is "this
- * directory is not a repository" (the caller renders nothing), while `true` is
- * "a repository, but the query failed" (nothing to show either, but the
- * distinction lets a caller decide whether to retry rather than hide for good).
- */
-export type GitBranchReadResult =
- | { ok: true; snapshot: GitBranchSnapshot }
- | { ok: false; isGitRepo: boolean };
diff --git a/packages/ui/src/__tests__/composer-context-usage.test.tsx b/packages/ui/src/__tests__/composer-context-usage.test.tsx
index 46c96d3951..9f6c229f40 100644
--- a/packages/ui/src/__tests__/composer-context-usage.test.tsx
+++ b/packages/ui/src/__tests__/composer-context-usage.test.tsx
@@ -147,72 +147,3 @@ test('the context usage share resolves declared, then metered, then metadata win
Object.assign(globalThis, original);
}
});
-
-test('the git branch chip shows the branch, the short sha when detached, and nothing without Git', async () => {
- const original = {
- document: globalThis.document,
- window: globalThis.window,
- IS_REACT_ACT_ENVIRONMENT: (globalThis as typeof globalThis & {
- IS_REACT_ACT_ENVIRONMENT?: boolean;
- }).IS_REACT_ACT_ENVIRONMENT,
- };
- const { document, window } = parseHTML('');
- window.getComputedStyle = () => ({
- direction: 'ltr',
- writingMode: 'horizontal-tb',
- getPropertyValue: () => '',
- }) as unknown as CSSStyleDeclaration;
- Object.assign(globalThis, { document, window, IS_REACT_ACT_ENVIRONMENT: true });
- const container = document.querySelector('#root');
- assert.ok(container);
- const root = createRoot(container);
-
- const render = (branch?: { name?: string; shortSha?: string }) =>
- root.render(
-
- undefined}
- onStop={() => undefined}
- />
- ,
- );
-
- // The chip is a readout, not a control: nothing under it may be a button, so
- // a click has nothing to land on.
- const branchEl = () => container.querySelector('.maka-composer-git-branch');
- const chipText = () => branchEl()?.textContent?.trim();
-
- try {
- // No `gitBranch` — a non-repository workspace — means no chip at all, not an
- // empty one: absence of Git is the normal case and must not leave a husk.
- await act(() => render(undefined));
- assert.equal(chipText(), undefined, 'no chip may render outside a Git repository');
-
- await act(() => render({ name: 'feature/chip' }));
- assert.equal(chipText(), 'feature/chip', 'a named branch must render its name');
- // The chip must be a readout, not a control: whatever carries the branch
- // text may not be (or contain) a button, so a click has nothing to land on.
- assert.equal(
- branchEl()?.closest('button'),
- null,
- 'the branch chip must not be a button — the branch is a readout, not an action',
- );
- assert.equal(
- branchEl()?.tagName,
- 'SPAN',
- 'the chip must be a plain span, not a control',
- );
- // The full branch must survive on the element a truncating row can still
- // name: `title` is what hover shows when the text had to be shortened.
- assert.match(branchEl()?.getAttribute('title') ?? '', /feature\/chip/u);
-
- // Detached HEAD: the branch name is unknown, so the short sha is the label.
- await act(() => render({ shortSha: 'abc1234' }));
- assert.equal(chipText(), 'abc1234', 'a detached HEAD must name itself through the short sha');
- assert.equal(branchEl()?.closest('button'), null, 'a detached HEAD readout must not be a button');
- } finally {
- await act(() => root.unmount());
- Object.assign(globalThis, original);
- }
-});
diff --git a/packages/ui/src/composer.tsx b/packages/ui/src/composer.tsx
index 32f512ea60..380f238c48 100644
--- a/packages/ui/src/composer.tsx
+++ b/packages/ui/src/composer.tsx
@@ -39,7 +39,6 @@ import {
ArrowUp,
CircleGauge,
FileText,
- GitBranch,
ListTodo,
MessagesSquare,
Network,
@@ -459,17 +458,6 @@ export const Composer = forwardRef<
/** Open the Host-owned trace surface for this readout. */
onOpen(): void;
};
- /**
- * The working tree's Git branch, beside the context-usage readout. Omitted
- * entirely when the session's directory is not a Git repository, so the
- * chip simply does not exist there rather than sitting empty.
- */
- gitBranch?: {
- /** The branch name, or `undefined` on a detached HEAD. */
- name?: string;
- /** The short commit sha, set only when `name` is absent. */
- shortSha?: string;
- };
/**
* Optional edit-and-resend banner above the composer. Desktop owns the
* revision draft; Composer only renders the notice + cancel affordance.
@@ -2336,7 +2324,6 @@ export const Composer = forwardRef<
/>
)}
{props.contextUsage ? : null}
- {props.gitBranch ? : null}
{/* The project decides where a NEW chat starts, which makes it a
parameter of this send like the model beside it — so it sits
@@ -2488,27 +2475,4 @@ function ContextUsageAction(props: {
);
}
-function GitBranchChip(props: { name?: string; shortSha?: string }) {
- const copy = getConversationCopy(useUiLocale()).messages.systemNotes;
- // A detached HEAD has no branch name; the short sha is the honest label. Only
- // one of the two is ever set (the host resolves it), and neither means the
- // repository state is unknown — the chip stays off rather than guess.
- const detached = props.name === undefined;
- if (detached && props.shortSha === undefined) return null;
- const label = props.name ?? props.shortSha!;
- // The visible text is the branch; `title` names what it is and carries the
- // full text, so a branch the row had to shorten is still readable on hover.
- const title = detached ? copy.gitBranchDetached(props.shortSha!) : copy.gitBranchLabel;
- // Readout, not a control: a ``, so there is nothing to click or tab to.
- // The class matches the ghost buttons beside it and widens past the model
- // chip's 180px cap, so a usual branch shows whole; only a genuinely long one
- // ellipsizes, and `title` carries it.
- return (
-
-
- {label}
-
- );
-}
-
-export type ComposerProps = ComponentProps;
\ No newline at end of file
+export type ComposerProps = ComponentProps;
diff --git a/packages/ui/src/conversation-copy.ts b/packages/ui/src/conversation-copy.ts
index e22bb14cdf..6fe41b2941 100644
--- a/packages/ui/src/conversation-copy.ts
+++ b/packages/ui/src/conversation-copy.ts
@@ -366,8 +366,6 @@ export interface ConversationCopy {
contextUsageNoWindow: (used: number) => string;
contextUsageUnavailable: string;
contextUsageOpen: string;
- gitBranchLabel: string;
- gitBranchDetached: (sha: string) => string;
stepLimit: string;
};
};
@@ -604,11 +602,9 @@ const CONVERSATION_COPY = {
`上下文窗口:已用 ${Math.round((used / window) * 100)}%(${formatCompactTokenCount(used)} / ${formatCompactTokenCount(window)} token)`,
contextUsageNoWindow: (used) =>
`已用 ${formatCompactTokenCount(used)} token;上下文窗口上限未知`,
- contextUsageUnavailable: '暂无用量数据',
- contextUsageOpen: '打开用量追踪',
- gitBranchLabel: '当前 Git 分支',
- gitBranchDetached: (sha: string) => `游离 HEAD(${sha})`,
- stepLimit: '已达到本轮工具步骤上限,任务可能尚未完成。发送“继续”即可接着处理。',
+ contextUsageUnavailable: '暂无用量数据',
+ contextUsageOpen: '打开用量追踪',
+ stepLimit: '已达到本轮工具步骤上限,任务可能尚未完成。发送“继续”即可接着处理。',
},
},
chat: {
@@ -767,8 +763,6 @@ const CONVERSATION_COPY = {
`已用 ${formatCompactTokenCount(used)} token;上下文視窗上限未知`,
contextUsageUnavailable: '暫無用量資料',
contextUsageOpen: '開啟用量追蹤',
- gitBranchLabel: '目前 Git 分支',
- gitBranchDetached: (sha: string) => `游離 HEAD(${sha})`,
stepLimit: '已達到本輪工具步驟上限,任務可能尚未完成。傳送“繼續”即可接著處理。',
},
},
@@ -954,8 +948,6 @@ const CONVERSATION_COPY = {
`This request used ${formatCompactTokenCount(used)} tokens; no context limit is available for this model.`,
contextUsageUnavailable: 'No usage data is available for this request.',
contextUsageOpen: 'Open usage trace',
- gitBranchLabel: 'Current Git branch',
- gitBranchDetached: (sha: string) => `detached HEAD (${sha})`,
stepLimit: 'Reached the configured step limit. The task may be incomplete. Send “continue” to resume.',
},
},