Skip to content

Commit 0b1ec45

Browse files
authored
fix(tui): show branch beside directory (#40500)
1 parent 9bd99b4 commit 0b1ec45

6 files changed

Lines changed: 101 additions & 7 deletions

File tree

packages/plugin/src/tui/context.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import type {
1919
SessionPendingInfo,
2020
ShellInfo,
2121
SkillInfo,
22+
VcsInfo,
2223
} from "@opencode-ai/client"
2324
import type { ResolvedTheme } from "@opencode-ai/theme/tui"
2425
import type { CliRenderer, KeyEvent, Renderable } from "@opentui/core"
@@ -113,6 +114,11 @@ export interface Data {
113114
default(): LocationRef
114115
sync(location?: LocationRef): Promise<void>
115116
invalidate(location?: LocationRef): void
117+
readonly vcs: {
118+
info(location?: LocationRef): VcsInfo | undefined
119+
sync(location?: LocationRef): Promise<void>
120+
invalidate(location?: LocationRef): void
121+
}
116122
readonly agent: LocationCollection<AgentInfo>
117123
readonly command: LocationCollection<CommandInfo>
118124
readonly integration: LocationCollection<IntegrationInfo>

packages/tui/src/component/prompt/index.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1329,12 +1329,17 @@ export function Prompt(props: PromptProps) {
13291329
const locationLabel = createMemo(() => {
13301330
if (!props.sessionID) {
13311331
// No session yet: show where the next session will be created.
1332-
const directory = currentLocation.ref?.directory ?? data.location.default().directory
1333-
return abbreviateHome(directory, paths.home)
1332+
const location = currentLocation.ref ?? data.location.default()
1333+
const directory = abbreviateHome(location.directory, paths.home)
1334+
const branch = data.location.vcs.info(location)?.branch.current
1335+
return branch ? `${directory}:${branch}` : directory
13341336
}
13351337
if (status() !== "idle") return
1336-
const directory = data.session.get(props.sessionID)?.location.directory
1337-
return directory ? abbreviateHome(directory, paths.home) : undefined
1338+
const location = data.session.get(props.sessionID)?.location
1339+
if (!location) return
1340+
const directory = abbreviateHome(location.directory, paths.home)
1341+
const branch = data.location.vcs.info(location)?.branch.current
1342+
return branch ? `${directory}:${branch}` : directory
13381343
})
13391344

13401345
const spinnerDef = createMemo(() => {

packages/tui/src/context/data.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import type {
2727
SessionPendingInfo,
2828
ShellInfo,
2929
SkillInfo,
30+
VcsInfo,
3031
OpenCodeEvent,
3132
WebSearchProvider,
3233
} from "@opencode-ai/client"
@@ -49,6 +50,7 @@ type ShellWithLocation = ShellInfo & { readonly location: LocationRef }
4950

5051
type LocationData = {
5152
info?: LocationGetOutput
53+
vcs?: VcsInfo
5254
agent?: AgentInfo[]
5355
command?: CommandInfo[]
5456
integration?: IntegrationInfo[]
@@ -339,6 +341,17 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
339341
result.location.skill.invalidate(event.location)
340342
void result.location.skill.sync(event.location)
341343
break
344+
case "vcs.branch.updated":
345+
setStore("location", locationKey(event.location ?? defaultLocation()), (data) => ({
346+
...data,
347+
vcs: {
348+
branch: {
349+
...data?.vcs?.branch,
350+
current: event.data.branch,
351+
},
352+
},
353+
}))
354+
break
342355
case "session.agent.selected":
343356
if (store.session.info[event.data.sessionID])
344357
setStore("session", "info", event.data.sessionID, "agent", event.data.agent)
@@ -1152,6 +1165,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
11521165
})
11531166
const location = ref ?? defaultLocation()
11541167
await Promise.all([
1168+
result.location.vcs.sync(location),
11551169
result.location.agent.sync(location),
11561170
result.location.command.sync(location),
11571171
result.location.integration.sync(location),
@@ -1168,6 +1182,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
11681182
invalidate(ref?: LocationRef) {
11691183
const location = ref ?? defaultLocation()
11701184
sync.invalidate(`location:${locationKey(location)}`)
1185+
result.location.vcs.invalidate(location)
11711186
result.location.agent.invalidate(location)
11721187
result.location.command.invalidate(location)
11731188
result.location.integration.invalidate(location)
@@ -1180,6 +1195,22 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
11801195
result.shell.invalidate(location)
11811196
result.session.form.invalidate("global", location)
11821197
},
1198+
vcs: {
1199+
info(location?: LocationRef) {
1200+
return store.location[locationKey(location ?? defaultLocation())]?.vcs
1201+
},
1202+
sync(ref?: LocationRef) {
1203+
const location = ref ?? defaultLocation()
1204+
return sync.run(`location.vcs:${locationKey(location)}`, async () => {
1205+
const response = await client.api.vcs.get({ location: locationQuery(location) })
1206+
const key = locationKey(response.location)
1207+
setStore("location", key, { ...store.location[key], vcs: response.data })
1208+
})
1209+
},
1210+
invalidate(ref?: LocationRef) {
1211+
sync.invalidate(`location.vcs:${locationKey(ref ?? defaultLocation())}`)
1212+
},
1213+
},
11831214
agent: {
11841215
list(location?: LocationRef) {
11851216
return store.location[locationKey(location ?? defaultLocation())]?.agent
@@ -1377,6 +1408,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
13771408
setStore("location", key, { ...store.location[key], info: location })
13781409
})
13791410
.catch((error) => console.error("Failed to preload location", error))
1411+
void result.location.vcs.sync().catch((error) => console.error("Failed to preload VCS info", error))
13801412
void result.project.sync().catch((error) => console.error("Failed to preload projects", error))
13811413
return
13821414
}

packages/tui/src/feature-plugins/sidebar/footer.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@ import { createMemo, Show } from "solid-js"
33
import { FilePath } from "../../ui/file-path"
44

55
function View(props: { context: Plugin.Context }) {
6-
const directory = createMemo(() =>
7-
props.context.location ? props.context.ui.format.path(props.context.location.directory) : undefined,
8-
)
6+
const directory = createMemo(() => {
7+
if (!props.context.location) return undefined
8+
const value = props.context.ui.format.path(props.context.location.directory)
9+
const branch = props.context.data.location.vcs.info(props.context.location)?.branch.current
10+
return branch ? `${value}:${branch}` : value
11+
})
912
return (
1013
<Show when={directory()}>
1114
{(value) => <FilePath value={value()} maxWidth={38} fg={props.context.theme.text.subdued} />}

packages/tui/test/cli/tui/data.test.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,49 @@ test("does not preload session summaries into the data context", async () => {
106106
}
107107
})
108108

109+
test("syncs VCS info and applies branch updates", async () => {
110+
const events = createEventStream()
111+
const calls = createFetch((url) => {
112+
if (url.pathname !== "/api/vcs") return undefined
113+
return json({
114+
location: { directory, project: { id: "proj_test", directory: worktree, canonical: worktree } },
115+
data: { branch: { current: "main", default: "main" } },
116+
})
117+
}, events)
118+
let data!: ReturnType<typeof useData>
119+
120+
function Probe() {
121+
data = useData()
122+
return <box />
123+
}
124+
125+
const app = await testRender(() => (
126+
<TestTuiContexts>
127+
<ClientProvider api={createApi(calls.fetch)}>
128+
<ProjectProvider>
129+
<DataProvider>
130+
<Probe />
131+
</DataProvider>
132+
</ProjectProvider>
133+
</ClientProvider>
134+
</TestTuiContexts>
135+
))
136+
137+
try {
138+
await wait(() => data.location.vcs.info()?.branch.current === "main")
139+
emitEvent(events, {
140+
id: "evt_vcs_branch",
141+
created: Date.now(),
142+
type: "vcs.branch.updated",
143+
data: { branch: "feature" },
144+
})
145+
await wait(() => data.location.vcs.info()?.branch.current === "feature")
146+
expect(data.location.vcs.info()?.branch).toEqual({ current: "feature", default: "main" })
147+
} finally {
148+
app.renderer.destroy()
149+
}
150+
})
151+
109152
test("proactively syncs project metadata newest first", async () => {
110153
const events = createEventStream()
111154
const calls = createFetch((url) => {

packages/tui/test/fixture/tui-client.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,11 @@ export function createFetch(override?: FetchHandler, events?: ReturnType<typeof
9595
if (url.pathname === "/path") return json({ home: "", state: "", config: "", worktree, directory })
9696
if (url.pathname === "/api/location")
9797
return json({ directory, project: { id: "proj_test", directory: worktree, canonical: worktree } })
98+
if (url.pathname === "/api/vcs")
99+
return json({
100+
location: { directory, project: { id: "proj_test", directory: worktree, canonical: worktree } },
101+
data: { branch: { current: "main", default: "main" } },
102+
})
98103
if (url.pathname === "/api/fs/list")
99104
return json({ location: { directory, project: { id: "proj_test", directory: worktree, canonical: worktree } }, data: [] })
100105
if (url.pathname === "/api/project/current") return json({ id: "proj_test", directory: worktree })

0 commit comments

Comments
 (0)