mirror of
https://github.com/sst/opencode.git
synced 2025-12-23 10:11:41 +00:00
show current git branch in tui (#4765)
Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
parent
d95f724303
commit
09bc8d9ca4
7 changed files with 193 additions and 2 deletions
|
|
@ -478,7 +478,10 @@ function App() {
|
|||
<text fg={theme.textMuted}>v{Installation.VERSION}</text>
|
||||
</box>
|
||||
<box paddingLeft={1} paddingRight={1}>
|
||||
<text fg={theme.textMuted}>{process.cwd().replace(Global.Path.home, "~")}</text>
|
||||
<text fg={theme.textMuted}>
|
||||
{process.cwd().replace(Global.Path.home, "~")}
|
||||
{sync.data.vcs?.vcs?.branch ? `:${sync.data.vcs.vcs.branch}` : ""}
|
||||
</text>
|
||||
</box>
|
||||
</box>
|
||||
<Show when={false}>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import type {
|
|||
SessionStatus,
|
||||
ProviderListResponse,
|
||||
ProviderAuthMethod,
|
||||
VcsInfo,
|
||||
} from "@opencode-ai/sdk"
|
||||
import { createStore, produce, reconcile } from "solid-js/store"
|
||||
import { useSDK } from "@tui/context/sdk"
|
||||
|
|
@ -59,6 +60,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||
[key: string]: McpStatus
|
||||
}
|
||||
formatter: FormatterStatus[]
|
||||
vcs: VcsInfo | undefined
|
||||
}>({
|
||||
provider_next: {
|
||||
all: [],
|
||||
|
|
@ -82,6 +84,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||
lsp: [],
|
||||
mcp: {},
|
||||
formatter: [],
|
||||
vcs: undefined,
|
||||
})
|
||||
|
||||
const sdk = useSDK()
|
||||
|
|
@ -238,6 +241,11 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||
sdk.client.lsp.status().then((x) => setStore("lsp", x.data!))
|
||||
break
|
||||
}
|
||||
|
||||
case "vcs.changed": {
|
||||
setStore("vcs", "vcs", { branch: event.properties.branch })
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -276,6 +284,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||
sdk.client.formatter.status().then((x) => setStore("formatter", x.data!)),
|
||||
sdk.client.session.status().then((x) => setStore("session_status", x.data!)),
|
||||
sdk.client.provider.auth().then((x) => setStore("provider_auth", x.data ?? {})),
|
||||
sdk.client.vcs.get().then((x) => setStore("vcs", x.data)),
|
||||
]).then(() => {
|
||||
setStore("status", "complete")
|
||||
})
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ import { Format } from "../format"
|
|||
import { LSP } from "../lsp"
|
||||
import { FileWatcher } from "../file/watcher"
|
||||
import { File } from "../file"
|
||||
import { Flag } from "../flag/flag"
|
||||
import { Project } from "./project"
|
||||
import { Bus } from "../bus"
|
||||
import { Command } from "../command"
|
||||
import { Instance } from "./instance"
|
||||
import { Vcs } from "./vcs"
|
||||
import { Log } from "@/util/log"
|
||||
import { ShareNext } from "@/share/share-next"
|
||||
|
||||
|
|
@ -21,6 +21,7 @@ export async function InstanceBootstrap() {
|
|||
await LSP.init()
|
||||
FileWatcher.init()
|
||||
File.init()
|
||||
Vcs.init()
|
||||
|
||||
Bus.subscribe(Command.Event.Executed, async (payload) => {
|
||||
if (payload.properties.name === Command.Default.INIT) {
|
||||
|
|
|
|||
86
packages/opencode/src/project/vcs.ts
Normal file
86
packages/opencode/src/project/vcs.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import { $ } from "bun"
|
||||
import { watch, type FSWatcher } from "fs"
|
||||
import path from "path"
|
||||
import z from "zod"
|
||||
import { Log } from "@/util/log"
|
||||
import { Bus } from "@/bus"
|
||||
import { Instance } from "./instance"
|
||||
|
||||
const log = Log.create({ service: "vcs" })
|
||||
|
||||
export namespace Vcs {
|
||||
export const Event = {
|
||||
Changed: Bus.event(
|
||||
"vcs.changed",
|
||||
z.object({
|
||||
branch: z.string().optional(),
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
async function currentBranch() {
|
||||
return $`git rev-parse --abbrev-ref HEAD`
|
||||
.quiet()
|
||||
.nothrow()
|
||||
.cwd(Instance.worktree)
|
||||
.text()
|
||||
.then((x) => x.trim())
|
||||
.catch(() => undefined)
|
||||
}
|
||||
|
||||
const state = Instance.state(
|
||||
async () => {
|
||||
if (Instance.project.vcs !== "git") {
|
||||
return { branch: async () => undefined, watcher: undefined }
|
||||
}
|
||||
let current = await currentBranch()
|
||||
log.info("initialized", { branch: current })
|
||||
|
||||
const gitDir = await $`git rev-parse --git-dir`
|
||||
.quiet()
|
||||
.nothrow()
|
||||
.cwd(Instance.worktree)
|
||||
.text()
|
||||
.then((x) => x.trim())
|
||||
.catch(() => undefined)
|
||||
if (!gitDir) {
|
||||
log.warn("failed to resolve git directory")
|
||||
return { branch: async () => current, watcher: undefined }
|
||||
}
|
||||
|
||||
const gitHead = path.join(gitDir, "HEAD")
|
||||
let watcher: FSWatcher | undefined
|
||||
// we should probably centralize file watching (see watcher.ts)
|
||||
// but parcel still marked experimental rn
|
||||
try {
|
||||
watcher = watch(gitHead, async () => {
|
||||
const next = await currentBranch()
|
||||
if (next !== current) {
|
||||
log.info("branch changed", { from: current, to: next })
|
||||
current = next
|
||||
Bus.publish(Event.Changed, { branch: next })
|
||||
}
|
||||
})
|
||||
log.info("watching", { path: gitHead })
|
||||
} catch (e) {
|
||||
log.warn("failed to watch git HEAD", { error: e })
|
||||
}
|
||||
|
||||
return {
|
||||
branch: async () => current,
|
||||
watcher,
|
||||
}
|
||||
},
|
||||
async (state) => {
|
||||
state.watcher?.close()
|
||||
},
|
||||
)
|
||||
|
||||
export async function init() {
|
||||
return state()
|
||||
}
|
||||
|
||||
export async function branch() {
|
||||
return await state().then((s) => s.branch())
|
||||
}
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@ import { MessageV2 } from "../session/message-v2"
|
|||
import { TuiRoute } from "./tui"
|
||||
import { Permission } from "../permission"
|
||||
import { Instance } from "../project/instance"
|
||||
import { Vcs } from "../project/vcs"
|
||||
import { Agent } from "../agent/agent"
|
||||
import { Auth } from "../auth"
|
||||
import { Command } from "../command"
|
||||
|
|
@ -365,6 +366,47 @@ export namespace Server {
|
|||
})
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/vcs",
|
||||
describeRoute({
|
||||
description: "Get VCS info for the current instance",
|
||||
operationId: "vcs.get",
|
||||
responses: {
|
||||
200: {
|
||||
description: "VCS info",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(
|
||||
z
|
||||
.object({
|
||||
worktree: z.string(),
|
||||
directory: z.string(),
|
||||
projectID: z.string(),
|
||||
vcs: z
|
||||
.object({
|
||||
branch: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.meta({
|
||||
ref: "VcsInfo",
|
||||
}),
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
const branch = await Vcs.branch()
|
||||
return c.json({
|
||||
worktree: Instance.worktree,
|
||||
directory: Instance.directory,
|
||||
projectID: Instance.project.id,
|
||||
vcs: Instance.project.vcs ? { branch } : undefined,
|
||||
})
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/session",
|
||||
describeRoute({
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ import type {
|
|||
InstanceDisposeResponses,
|
||||
PathGetData,
|
||||
PathGetResponses,
|
||||
VcsGetData,
|
||||
VcsGetResponses,
|
||||
SessionListData,
|
||||
SessionListResponses,
|
||||
SessionCreateData,
|
||||
|
|
@ -311,6 +313,18 @@ class Path extends _HeyApiClient {
|
|||
}
|
||||
}
|
||||
|
||||
class Vcs extends _HeyApiClient {
|
||||
/**
|
||||
* Get VCS info for the current instance
|
||||
*/
|
||||
public get<ThrowOnError extends boolean = false>(options?: Options<VcsGetData, ThrowOnError>) {
|
||||
return (options?.client ?? this._client).get<VcsGetResponses, unknown, ThrowOnError>({
|
||||
url: "/vcs",
|
||||
...options,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class Session extends _HeyApiClient {
|
||||
/**
|
||||
* List all sessions
|
||||
|
|
@ -995,6 +1009,7 @@ export class OpencodeClient extends _HeyApiClient {
|
|||
tool = new Tool({ client: this._client })
|
||||
instance = new Instance({ client: this._client })
|
||||
path = new Path({ client: this._client })
|
||||
vcs = new Vcs({ client: this._client })
|
||||
session = new Session({ client: this._client })
|
||||
command = new Command({ client: this._client })
|
||||
provider = new Provider({ client: this._client })
|
||||
|
|
|
|||
|
|
@ -589,6 +589,13 @@ export type EventSessionError = {
|
|||
}
|
||||
}
|
||||
|
||||
export type EventVcsChanged = {
|
||||
type: "vcs.changed"
|
||||
properties: {
|
||||
branch?: string
|
||||
}
|
||||
}
|
||||
|
||||
export type EventTuiPromptAppend = {
|
||||
type: "tui.prompt.append"
|
||||
properties: {
|
||||
|
|
@ -670,6 +677,7 @@ export type Event =
|
|||
| EventSessionDeleted
|
||||
| EventSessionDiff
|
||||
| EventSessionError
|
||||
| EventVcsChanged
|
||||
| EventTuiPromptAppend
|
||||
| EventTuiCommandExecute
|
||||
| EventTuiToastShow
|
||||
|
|
@ -1251,6 +1259,15 @@ export type Path = {
|
|||
directory: string
|
||||
}
|
||||
|
||||
export type VcsInfo = {
|
||||
worktree: string
|
||||
directory: string
|
||||
projectID: string
|
||||
vcs?: {
|
||||
branch: string
|
||||
}
|
||||
}
|
||||
|
||||
export type NotFoundError = {
|
||||
name: "NotFoundError"
|
||||
data: {
|
||||
|
|
@ -1687,6 +1704,24 @@ export type PathGetResponses = {
|
|||
|
||||
export type PathGetResponse = PathGetResponses[keyof PathGetResponses]
|
||||
|
||||
export type VcsGetData = {
|
||||
body?: never
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
}
|
||||
url: "/vcs"
|
||||
}
|
||||
|
||||
export type VcsGetResponses = {
|
||||
/**
|
||||
* VCS info
|
||||
*/
|
||||
200: VcsInfo
|
||||
}
|
||||
|
||||
export type VcsGetResponse = VcsGetResponses[keyof VcsGetResponses]
|
||||
|
||||
export type SessionListData = {
|
||||
body?: never
|
||||
path?: never
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue