Git
The desktop app's git pickers and git bar. A branch and pull request menu, the where menu for a new session, a folder picker for a remote host with clone on send, the checkout's live status with sync, and the repository import, on a GitHost.
zidane-ui/git holds the desktop app's git pickers. BranchMenu switches between pull requests and branches, and starts a branch or a worktree. WherePicker chooses where a new session runs: this computer, a server or a shared machine, in the folder, a worktree or a sandbox. RemoteFolderPicker picks a folder on a remote host. GitBar shows where the checkout stands under a composer, and useImportRepo clones a repository. A GitHost provides the data and runs the actions. The previews below run on a fixture host.
Mount it#
import { createServerClient, createServerGitHost } from 'zidane-ui/adapters'
import { GitProvider, RemoteFolderPicker } from 'zidane-ui/git'
const client = createServerClient({ url, token })
const git = createServerGitHost({ client })
export function FolderStep({ folder, setFolder }) {
return (
<GitProvider host={git}>
<RemoteFolderPicker hostName="box" folder={folder} onChange={setFolder} />
</GitProvider>
)
}AppHostProvider (zidane-ui/app) puts host.git in a GitProvider for you. WherePicker reads no host: it shows the hosts, the environment kinds and the draft you pass.
The where menu#
WherePicker lists the places first: this computer, the saved servers and the machines shared with the draft's host. Then it lists the isolation kinds of the picked place. A kind that takes a branch (a worktree) adds a branch field. An empty field uses the suggested name. WhereMenu is the same menu without the pill, for a surface of your own.
The draft keeps only an explicit pick. resolveDraftEnvironment(descriptors, draft) turns it into the environment a session is created with: the pick while it is usable, else the default kind, else Local. withMachines(descriptors) orders a host's kinds before its machines' kinds and marks an offline machine's kinds unavailable. GitHost.environments lists the kinds when the host can. useEnvironmentChoices(cwd, hostId) reads them for a draft, withMachines applied, and reads them again on GitHost.subscribeEnvironments (the desktop app: a settings write or the window's focus; zidane-server: settings:changed and a reconnect).
The folder picker#
RemoteFolderPicker is a pill and its menu. RemoteFolderBrowser is the menu alone, for a surface of your own. The menu lists the roots, the folder's path (each step opens it), its folders and .., then Use. Type in its field to filter the folder it shows. Enter opens the one match. A text that starts with / is a path, and Enter opens it. When nothing matches, Enter searches the folders below by name (GitHost.searchFolders). New folder creates one in the folder shown (GitHost.createFolder) while the listing allows it (listing.allows.createFolders). A shared machine (machineId) gets the filter and the path only. A new session's composer on a server shows ServerScopeToggle (zidane-ui/chat/composer) instead, the project menu of this computer over the server's folders, with this browser behind "Select directory…".
Clone on send#
A new session on a server can start from a project on this computer. While no folder on the server is picked, useDraftTarget reads the project's origin (GitHost.originUrl) and lists the server's first root. resolveRemoteClone(origin, root) (zidane-ui/git/headless) says what a send would clone:
state | When | The pill shows |
|---|---|---|
clone | No folder of the repository's name in the first root | webheim · will clone |
exists | A folder of that name is there | webheim |
off | The server takes no clones (listing.allows.clone is false) | Default folder; the clone row shows cloning off, greyed out |
ServerScopeToggle shows it: pass draft.remoteClone and draft.cloning as clone and cloning. The clone is the menu's first row, picked while no folder is. A listing without allows gets no clone row. createServerGitHost and the desktop app fill allows from the hello's clone and createFolders flags, or, on an older server that sends neither, from the methods its hello lists; allows.searchFolders says whether the browser offers its search. On send, useSendPrompt clones first (GitHost.cloneOnServer) into <first root>/<repository name>, then creates the session there. A folder that already holds the same repository is used as it is. With cloning off, nothing is cloned and the session starts in the first root. When the clone fails, no session is made: the error shows on the draft and the composer keeps the text.
The branch menu#
BranchMenu opens from a control you pass as anchorRef. Pull requests waiting on your review come first, then yours, then the others, then the local branches. Each section shows a few rows until you expand it or type. Typing filters the loaded rows at once and asks the host to search every open pull request. A branch with an open pull request shows once, under it. A typed name that matches nothing can start a branch, or a worktree when you pass worktree.
A branch that another worktree has checked out is refused. The host names the worktree and the chats there. resolveChat picks the chat to offer and onOpenChat opens it.
The git bar#
GitBar is one quiet line under a composer: the branch (it opens BranchMenu, or the switcher you pass as BranchMenu), the commits to push and pull (SyncChip: a click syncs, or publishes a branch with no remote copy, after a confirmation confirmSync can turn off), the branch's open pull request, and the uncommitted changes (onShowChanges). In a narrow composer the extras step aside, the pull request first. It slides away for a chat without a folder and outside a git checkout, and holds its place, faded, while a folder it has not seen is read.
useGitBar(cwd, sessionId) is its data: status refreshed when the refs move (subscribeRefs), when the session's work lands (subscribeActivity), on focus and on a slow timer while the window has focus; branchPullRequest followed per branch. Facts are kept per folder, so another chat in the same folder shows them at once. syncAction, syncSummary and syncTooltip (zidane-ui/git/headless) are the chip's wording.
<GitBar cwd={folder} sessionId={id} running={running} onShowChanges={() => showDiffs()} />Import a repository#
useImportRepo() opens ImportRepoDialog on the modal layer and resolves with the cloned folder, or null. The preview under the field is the host's own parse (clone.parse), so it is what the host will clone. Once the field names a GitHub repository and holds still, a card shows what it is (clone.info): its preview image, the owner's avatar, stars, language, the branch a clone checks out and the description, as the skill browser shows a source. useImportRepo({ hostId }) clones on that saved server instead (cloneOnServer, into its first root), with the same card. The progress comes from clone.onProgress; Stop or Esc calls clone.cancel. pickFolder adds Change… for the parent folder. Without GitHost.clone it opens nothing: hide the control that calls it.
Pick a host#
| Host | From | Offers |
|---|---|---|
createServerGitHost({ client }) | zidane-ui/adapters | Remote folders (fs.listDirs, machines.listDirs), environment kinds (environments.list), worktree moves (sessions.moveEnvironment), and the checkout in a folder of the server's workspace roots: branches, pull requests, status and the branch's pull request (git.branches, git.pullRequests, git.status, git.branchPullRequest), switching, new branches, sync and the repository import (git.checkout, git.createBranch, git.sync, git.clone with its progress and git.cloneCancel). It also creates and searches the server's folders (fs.mkdir, fs.searchDirs), and fills listing.allows from the hello's createFolders and clone. Cloning, new folders and search need a chat token, the other writes an admin one. It leaves out originUrl and cloneOnServer: a browser has no project of its own to clone. subscribeRefs follows the server's watch of the checkout (git.watch, git:refs, taken again on each new connection). Each member is offered when the connection may call its method (build the host after the handshake). |
createFixtureGitHost(options) | zidane-ui/git/testing | Everything, in memory, for demos and tests |
| Your own | zidane-ui/git/headless | Any subset of the members below |
| Member | Gives |
|---|---|
branches(cwd), checkout(cwd, target, sessionId) | The branch menu. Without both, it renders nothing. |
pullRequests({ cwd, search, fresh }) | The pull request sections. { kind: 'unavailable' } shows a note; { kind: 'rate-limited', retryAt } says when it resumes. |
createBranch(cwd, name, sessionId) | "Start branch" for a typed name |
startWorktree(sessionId, branch) | "Start a worktree" and "Start as worktree" |
subscribeRefs(cwd, onChange) | An open menu refreshes when the refs move |
listFolders({ hostId, machineId, path }) | The remote folder picker. listing.allows says whether New folder and clone on send apply there |
createFolder({ hostId, parent, name }) | New folder in the remote folder picker |
searchFolders({ hostId, query, path }) | The remote folder picker's search of the folders below |
originUrl(cwd) | Clone on send: the origin of the local project |
cloneOnServer(hostId, input, parent) | Clone on send: the clone on the server |
environments({ cwd, hostId }) | The kinds the where menu shows |
subscribeEnvironments({ cwd, hostId }, onChange) | The kinds are read again when they may have changed |
status(cwd) | Where the checkout stands (branch, ahead and behind, changes) |
pickFolder() | A native folder dialog (the desktop app); Change… in the import dialog |
subscribeActivity(sessionId, onChange) | The git bar refreshes when the session's tool calls and terminal commands finish |
branchPullRequest(cwd, branch, fresh) | The git bar's pull request |
sync(cwd, sessionId) | The git bar's Sync and Publish. A refusal comes back as { ok: false, error, conflicts? } |
clone (parse, defaultParent, info, clone, onProgress, cancel) | The repository import, and its card (info) |
Errors come back as values: { code: 'failed', message } shows the host's words, { code: 'unavailable' } the gitUnavailable label.
Test your host#
import { describeGitHostContract } from 'zidane-ui/git/testing/contract'
describeGitHostContract('my host', backend => makeHarness(backend), ['listFolders', 'environments'])| Entry point | Holds |
|---|---|
zidane-ui/git | GitProvider, useGitHost, useGitLabels, BranchMenu, WherePicker, WhereMenu, RemoteFolderPicker, RemoteFolderBrowser, GitBar, SyncChip, useGitBar, ImportRepoDialog, useImportRepo, useEnvironmentChoices |
zidane-ui/git/headless | GitHost, the branch menu's model (switcherSections, switcherCreates, prMatches, switcherCacheOf), the draft model (resolveDraftEnvironment, resolveDraftDescriptor, withMachines, environmentKindIcon), clone on send (resolveRemoteClone), the sync chip's wording (syncAction, syncSummary, syncTooltip), the labels |
zidane-ui/git/testing | createFixtureGitHost, demo branches, pull requests, kinds, folders, origins, the branches' pull requests and a repository to import |
zidane-ui/git/testing/contract | describeGitHostContract |