mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-05 23:29:54 +00:00
fix(tui): show branch beside directory (#40500)
This commit is contained in:
parent
9bd99b4c91
commit
0b1ec457dc
6 changed files with 101 additions and 7 deletions
|
|
@ -19,6 +19,7 @@ import type {
|
|||
SessionPendingInfo,
|
||||
ShellInfo,
|
||||
SkillInfo,
|
||||
VcsInfo,
|
||||
} from "@opencode-ai/client"
|
||||
import type { ResolvedTheme } from "@opencode-ai/theme/tui"
|
||||
import type { CliRenderer, KeyEvent, Renderable } from "@opentui/core"
|
||||
|
|
@ -113,6 +114,11 @@ export interface Data {
|
|||
default(): LocationRef
|
||||
sync(location?: LocationRef): Promise<void>
|
||||
invalidate(location?: LocationRef): void
|
||||
readonly vcs: {
|
||||
info(location?: LocationRef): VcsInfo | undefined
|
||||
sync(location?: LocationRef): Promise<void>
|
||||
invalidate(location?: LocationRef): void
|
||||
}
|
||||
readonly agent: LocationCollection<AgentInfo>
|
||||
readonly command: LocationCollection<CommandInfo>
|
||||
readonly integration: LocationCollection<IntegrationInfo>
|
||||
|
|
|
|||
|
|
@ -1329,12 +1329,17 @@ export function Prompt(props: PromptProps) {
|
|||
const locationLabel = createMemo(() => {
|
||||
if (!props.sessionID) {
|
||||
// No session yet: show where the next session will be created.
|
||||
const directory = currentLocation.ref?.directory ?? data.location.default().directory
|
||||
return abbreviateHome(directory, paths.home)
|
||||
const location = currentLocation.ref ?? data.location.default()
|
||||
const directory = abbreviateHome(location.directory, paths.home)
|
||||
const branch = data.location.vcs.info(location)?.branch.current
|
||||
return branch ? `${directory}:${branch}` : directory
|
||||
}
|
||||
if (status() !== "idle") return
|
||||
const directory = data.session.get(props.sessionID)?.location.directory
|
||||
return directory ? abbreviateHome(directory, paths.home) : undefined
|
||||
const location = data.session.get(props.sessionID)?.location
|
||||
if (!location) return
|
||||
const directory = abbreviateHome(location.directory, paths.home)
|
||||
const branch = data.location.vcs.info(location)?.branch.current
|
||||
return branch ? `${directory}:${branch}` : directory
|
||||
})
|
||||
|
||||
const spinnerDef = createMemo(() => {
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import type {
|
|||
SessionPendingInfo,
|
||||
ShellInfo,
|
||||
SkillInfo,
|
||||
VcsInfo,
|
||||
OpenCodeEvent,
|
||||
WebSearchProvider,
|
||||
} from "@opencode-ai/client"
|
||||
|
|
@ -49,6 +50,7 @@ type ShellWithLocation = ShellInfo & { readonly location: LocationRef }
|
|||
|
||||
type LocationData = {
|
||||
info?: LocationGetOutput
|
||||
vcs?: VcsInfo
|
||||
agent?: AgentInfo[]
|
||||
command?: CommandInfo[]
|
||||
integration?: IntegrationInfo[]
|
||||
|
|
@ -339,6 +341,17 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
result.location.skill.invalidate(event.location)
|
||||
void result.location.skill.sync(event.location)
|
||||
break
|
||||
case "vcs.branch.updated":
|
||||
setStore("location", locationKey(event.location ?? defaultLocation()), (data) => ({
|
||||
...data,
|
||||
vcs: {
|
||||
branch: {
|
||||
...data?.vcs?.branch,
|
||||
current: event.data.branch,
|
||||
},
|
||||
},
|
||||
}))
|
||||
break
|
||||
case "session.agent.selected":
|
||||
if (store.session.info[event.data.sessionID])
|
||||
setStore("session", "info", event.data.sessionID, "agent", event.data.agent)
|
||||
|
|
@ -1152,6 +1165,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
})
|
||||
const location = ref ?? defaultLocation()
|
||||
await Promise.all([
|
||||
result.location.vcs.sync(location),
|
||||
result.location.agent.sync(location),
|
||||
result.location.command.sync(location),
|
||||
result.location.integration.sync(location),
|
||||
|
|
@ -1168,6 +1182,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
invalidate(ref?: LocationRef) {
|
||||
const location = ref ?? defaultLocation()
|
||||
sync.invalidate(`location:${locationKey(location)}`)
|
||||
result.location.vcs.invalidate(location)
|
||||
result.location.agent.invalidate(location)
|
||||
result.location.command.invalidate(location)
|
||||
result.location.integration.invalidate(location)
|
||||
|
|
@ -1180,6 +1195,22 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
result.shell.invalidate(location)
|
||||
result.session.form.invalidate("global", location)
|
||||
},
|
||||
vcs: {
|
||||
info(location?: LocationRef) {
|
||||
return store.location[locationKey(location ?? defaultLocation())]?.vcs
|
||||
},
|
||||
sync(ref?: LocationRef) {
|
||||
const location = ref ?? defaultLocation()
|
||||
return sync.run(`location.vcs:${locationKey(location)}`, async () => {
|
||||
const response = await client.api.vcs.get({ location: locationQuery(location) })
|
||||
const key = locationKey(response.location)
|
||||
setStore("location", key, { ...store.location[key], vcs: response.data })
|
||||
})
|
||||
},
|
||||
invalidate(ref?: LocationRef) {
|
||||
sync.invalidate(`location.vcs:${locationKey(ref ?? defaultLocation())}`)
|
||||
},
|
||||
},
|
||||
agent: {
|
||||
list(location?: LocationRef) {
|
||||
return store.location[locationKey(location ?? defaultLocation())]?.agent
|
||||
|
|
@ -1377,6 +1408,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
setStore("location", key, { ...store.location[key], info: location })
|
||||
})
|
||||
.catch((error) => console.error("Failed to preload location", error))
|
||||
void result.location.vcs.sync().catch((error) => console.error("Failed to preload VCS info", error))
|
||||
void result.project.sync().catch((error) => console.error("Failed to preload projects", error))
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,12 @@ import { createMemo, Show } from "solid-js"
|
|||
import { FilePath } from "../../ui/file-path"
|
||||
|
||||
function View(props: { context: Plugin.Context }) {
|
||||
const directory = createMemo(() =>
|
||||
props.context.location ? props.context.ui.format.path(props.context.location.directory) : undefined,
|
||||
)
|
||||
const directory = createMemo(() => {
|
||||
if (!props.context.location) return undefined
|
||||
const value = props.context.ui.format.path(props.context.location.directory)
|
||||
const branch = props.context.data.location.vcs.info(props.context.location)?.branch.current
|
||||
return branch ? `${value}:${branch}` : value
|
||||
})
|
||||
return (
|
||||
<Show when={directory()}>
|
||||
{(value) => <FilePath value={value()} maxWidth={38} fg={props.context.theme.text.subdued} />}
|
||||
|
|
|
|||
|
|
@ -106,6 +106,49 @@ test("does not preload session summaries into the data context", async () => {
|
|||
}
|
||||
})
|
||||
|
||||
test("syncs VCS info and applies branch updates", async () => {
|
||||
const events = createEventStream()
|
||||
const calls = createFetch((url) => {
|
||||
if (url.pathname !== "/api/vcs") return undefined
|
||||
return json({
|
||||
location: { directory, project: { id: "proj_test", directory: worktree, canonical: worktree } },
|
||||
data: { branch: { current: "main", default: "main" } },
|
||||
})
|
||||
}, events)
|
||||
let data!: ReturnType<typeof useData>
|
||||
|
||||
function Probe() {
|
||||
data = useData()
|
||||
return <box />
|
||||
}
|
||||
|
||||
const app = await testRender(() => (
|
||||
<TestTuiContexts>
|
||||
<ClientProvider api={createApi(calls.fetch)}>
|
||||
<ProjectProvider>
|
||||
<DataProvider>
|
||||
<Probe />
|
||||
</DataProvider>
|
||||
</ProjectProvider>
|
||||
</ClientProvider>
|
||||
</TestTuiContexts>
|
||||
))
|
||||
|
||||
try {
|
||||
await wait(() => data.location.vcs.info()?.branch.current === "main")
|
||||
emitEvent(events, {
|
||||
id: "evt_vcs_branch",
|
||||
created: Date.now(),
|
||||
type: "vcs.branch.updated",
|
||||
data: { branch: "feature" },
|
||||
})
|
||||
await wait(() => data.location.vcs.info()?.branch.current === "feature")
|
||||
expect(data.location.vcs.info()?.branch).toEqual({ current: "feature", default: "main" })
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
test("proactively syncs project metadata newest first", async () => {
|
||||
const events = createEventStream()
|
||||
const calls = createFetch((url) => {
|
||||
|
|
|
|||
|
|
@ -95,6 +95,11 @@ export function createFetch(override?: FetchHandler, events?: ReturnType<typeof
|
|||
if (url.pathname === "/path") return json({ home: "", state: "", config: "", worktree, directory })
|
||||
if (url.pathname === "/api/location")
|
||||
return json({ directory, project: { id: "proj_test", directory: worktree, canonical: worktree } })
|
||||
if (url.pathname === "/api/vcs")
|
||||
return json({
|
||||
location: { directory, project: { id: "proj_test", directory: worktree, canonical: worktree } },
|
||||
data: { branch: { current: "main", default: "main" } },
|
||||
})
|
||||
if (url.pathname === "/api/fs/list")
|
||||
return json({ location: { directory, project: { id: "proj_test", directory: worktree, canonical: worktree } }, data: [] })
|
||||
if (url.pathname === "/api/project/current") return json({ id: "proj_test", directory: worktree })
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue