Slice 9: git_checkout and create_branch (menu + MCP)
git boundary Checkout/CreateBranch; service GitCheckout/GitCreateBranch (activity detail names the branch; gitAction takes an ok-detail). HTTP /api/repo/git ops checkout + create-branch (branch field). MCP tools git_checkout and create_branch. <repo-menu> gains Switch branch… and New branch… (prompt for name). Service test covers create+switch and existing-branch failure. Checkout is not destructive - git refuses if it would overwrite changes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -365,9 +365,9 @@ obeys the safety rules (§1.4).
|
|||||||
the tool handlers (§1.7). Expected tools (grow as features land):
|
the tool handlers (§1.7). Expected tools (grow as features land):
|
||||||
- Read: `list_repos`, `get_repo`, `get_active_project`, `get_activity`,
|
- Read: `list_repos`, `get_repo`, `get_active_project`, `get_activity`,
|
||||||
`get_pending_switch`, `list_prs`.
|
`get_pending_switch`, `list_prs`.
|
||||||
- Act: `git_fetch`, `git_pull`, `git_push`, `git_commit`,
|
- Act: `git_fetch`, `git_pull`, `git_push`, `git_commit`, `git_checkout`,
|
||||||
`git_discard_changes`, `merge_and_cleanup_pr`, `set_active_project`,
|
`create_branch`, `git_discard_changes`, `merge_and_cleanup_pr`,
|
||||||
`ack_switch`. (More — `git_checkout`, `create_branch`, `create_pr` — as they land.)
|
`set_active_project`, `ack_switch`. (More — `create_pr` — as they land.)
|
||||||
- **A tool's result type must be a struct, never a bare slice/map/scalar.** The
|
- **A tool's result type must be a struct, never a bare slice/map/scalar.** The
|
||||||
go-sdk infers each tool's `outputSchema` from its handler's result type, and MCP
|
go-sdk infers each tool's `outputSchema` from its handler's result type, and MCP
|
||||||
structured output must be a JSON **object** (`type: "object"`). A handler that
|
structured output must be a JSON **object** (`type: "object"`). A handler that
|
||||||
|
|||||||
@@ -217,3 +217,16 @@ Append-only running history of all changes (AGENT.md §9.1). Newest last.
|
|||||||
set; `http.extraheader` present; `git_fetch` via the app returned ok.
|
set; `http.extraheader` present; `git_fetch` via the app returned ok.
|
||||||
- **Security note:** the token is written to the container's ephemeral gitconfig
|
- **Security note:** the token is written to the container's ephemeral gitconfig
|
||||||
and passed in a `git config` argv — acceptable for a localhost dev container.
|
and passed in a `git config` argv — acceptable for a localhost dev container.
|
||||||
|
|
||||||
|
## 2026-09-20 — Slice 9: git_checkout + create_branch
|
||||||
|
- **What:** git boundary `Checkout` (switch existing branch) and `CreateBranch`
|
||||||
|
(git checkout -b). Service `GitCheckout`/`GitCreateBranch` (feed detail names
|
||||||
|
the branch; `gitAction` now takes an ok-detail). HTTP `/api/repo/git` gained
|
||||||
|
ops `checkout` and `create-branch` (+`branch` field). MCP tools `git_checkout`
|
||||||
|
and `create_branch`. `<repo-menu>` gained "Switch branch…" and "New branch…"
|
||||||
|
(prompt for the name). Service test covers create+switch+existing-branch-fails.
|
||||||
|
- **Why:** Round out the everyday git commands in both front doors (§1.7).
|
||||||
|
- **Affects:** `internal/git`, `internal/service` (+test), `internal/mcp`,
|
||||||
|
`cmd/server/main.go`, `components/repo-menu`, `web/templates/help.html`,
|
||||||
|
`AGENT.md` (§8.1). Checkout isn't §1.4-destructive — git refuses if it would
|
||||||
|
overwrite uncommitted changes.
|
||||||
|
|||||||
@@ -195,6 +195,7 @@ func main() {
|
|||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
Op string `json:"op"`
|
Op string `json:"op"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
|
Branch string `json:"branch"`
|
||||||
}
|
}
|
||||||
if err := c.Bind(&body); err != nil {
|
if err := c.Bind(&body); err != nil {
|
||||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid body"})
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid body"})
|
||||||
@@ -213,6 +214,10 @@ func main() {
|
|||||||
out, err = svc.GitCommit(ctx, activity.ActorUser, body.Path, body.Message)
|
out, err = svc.GitCommit(ctx, activity.ActorUser, body.Path, body.Message)
|
||||||
case "discard":
|
case "discard":
|
||||||
out, err = svc.GitDiscard(ctx, activity.ActorUser, body.Path)
|
out, err = svc.GitDiscard(ctx, activity.ActorUser, body.Path)
|
||||||
|
case "checkout":
|
||||||
|
out, err = svc.GitCheckout(ctx, activity.ActorUser, body.Path, body.Branch)
|
||||||
|
case "create-branch":
|
||||||
|
out, err = svc.GitCreateBranch(ctx, activity.ActorUser, body.Path, body.Branch)
|
||||||
default:
|
default:
|
||||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "unknown op: " + body.Op})
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "unknown op: " + body.Op})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ const ITEMS = [
|
|||||||
{ cmd: 'push', label: 'Publish', hint: 'push' },
|
{ cmd: 'push', label: 'Publish', hint: 'push' },
|
||||||
{ cmd: 'fetch', label: 'Check for updates', hint: 'fetch' },
|
{ cmd: 'fetch', label: 'Check for updates', hint: 'fetch' },
|
||||||
{ cmd: 'commit', label: 'Save my work…', hint: 'commit' },
|
{ cmd: 'commit', label: 'Save my work…', hint: 'commit' },
|
||||||
|
{ cmd: 'checkout', label: 'Switch branch…', hint: 'checkout' },
|
||||||
|
{ cmd: 'newbranch', label: 'New branch…', hint: 'branch' },
|
||||||
{ sep: true },
|
{ sep: true },
|
||||||
{ cmd: 'active', label: 'Set as active project' },
|
{ cmd: 'active', label: 'Set as active project' },
|
||||||
{ cmd: 'handoff', label: 'Ask Claude to switch here' },
|
{ cmd: 'handoff', label: 'Ask Claude to switch here' },
|
||||||
@@ -91,6 +93,16 @@ class RepoMenu extends HTMLElement {
|
|||||||
if (msg && msg.trim()) await this.#git('commit', { message: msg });
|
if (msg && msg.trim()) await this.#git('commit', { message: msg });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'checkout': {
|
||||||
|
const b = window.prompt(`Switch ${name} to which existing branch?`);
|
||||||
|
if (b && b.trim()) await this.#git('checkout', { branch: b.trim() });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'newbranch': {
|
||||||
|
const b = window.prompt(`New branch name in ${name}:`);
|
||||||
|
if (b && b.trim()) await this.#git('create-branch', { branch: b.trim() });
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 'discard': {
|
case 'discard': {
|
||||||
const ok = window.confirm(
|
const ok = window.confirm(
|
||||||
`Discard ALL uncommitted changes in ${name}?\n\n` +
|
`Discard ALL uncommitted changes in ${name}?\n\n` +
|
||||||
|
|||||||
@@ -11,10 +11,11 @@ event. Safe commands run on click; the destructive one confirms first (§1.4).
|
|||||||
- **Listens:** `repo:contextmenu` on `document` — `detail: { repo, x, y }`
|
- **Listens:** `repo:contextmenu` on `document` — `detail: { repo, x, y }`
|
||||||
(dispatched by `<repo-list>` on right-click). Shows the menu at (x, y).
|
(dispatched by `<repo-list>` on right-click). Shows the menu at (x, y).
|
||||||
- **Commands → endpoints:**
|
- **Commands → endpoints:**
|
||||||
- Get latest / Publish / Check for updates / Save my work… / Discard all
|
- Get latest / Publish / Check for updates / Save my work… / Switch branch… /
|
||||||
changes… → `POST /api/repo/git {path, op, message?}` (op: pull/push/fetch/
|
New branch… / Discard all changes… → `POST /api/repo/git {path, op,
|
||||||
commit/discard). "Save my work…" prompts for a message; "Discard all
|
message?, branch?}` (op: pull/push/fetch/commit/checkout/create-branch/
|
||||||
changes…" confirms (destructive).
|
discard). "Save my work…" prompts for a message; "Switch branch…"/"New
|
||||||
|
branch…" prompt for the branch; "Discard all changes…" confirms (destructive).
|
||||||
- Set as active project → `POST /api/active-project`.
|
- Set as active project → `POST /api/active-project`.
|
||||||
- Ask Claude to switch here → `POST /api/switch` (the handoff request).
|
- Ask Claude to switch here → `POST /api/switch` (the handoff request).
|
||||||
- Copy path → clipboard.
|
- Copy path → clipboard.
|
||||||
@@ -23,6 +24,8 @@ event. Safe commands run on click; the destructive one confirms first (§1.4).
|
|||||||
## History
|
## History
|
||||||
- 2026-09-20: created — slice 6; plain-language git commands + coordination
|
- 2026-09-20: created — slice 6; plain-language git commands + coordination
|
||||||
actions, backed by the shared service layer (same ops Claude gets via MCP).
|
actions, backed by the shared service layer (same ops Claude gets via MCP).
|
||||||
|
- 2026-09-20: added "Switch branch…" (checkout) and "New branch…" (create-branch),
|
||||||
|
both prompting for the branch name (slice 9).
|
||||||
|
|
||||||
## Notes / gotchas
|
## Notes / gotchas
|
||||||
- Results aren't shown inline; they appear in `<activity-feed>` (each op records a
|
- Results aren't shown inline; they appear in `<activity-feed>` (each op records a
|
||||||
|
|||||||
@@ -155,6 +155,18 @@ func (c *CLI) DiscardAll(ctx context.Context, dir string) (string, error) {
|
|||||||
return c.run(ctx, dir, "reset", "--hard", "HEAD")
|
return c.run(ctx, dir, "reset", "--hard", "HEAD")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Checkout switches to an existing branch. Git refuses if uncommitted changes
|
||||||
|
// would be overwritten, so this is not destructive — the error is surfaced.
|
||||||
|
func (c *CLI) Checkout(ctx context.Context, dir, branch string) (string, error) {
|
||||||
|
return c.run(ctx, dir, "checkout", branch)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateBranch creates a new branch from the current HEAD and switches to it
|
||||||
|
// (git checkout -b). Fails if the branch already exists.
|
||||||
|
func (c *CLI) CreateBranch(ctx context.Context, dir, name string) (string, error) {
|
||||||
|
return c.run(ctx, dir, "checkout", "-b", name)
|
||||||
|
}
|
||||||
|
|
||||||
// Branch is a local branch and its upstream, if any.
|
// Branch is a local branch and its upstream, if any.
|
||||||
type Branch struct {
|
type Branch struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
|||||||
@@ -92,6 +92,18 @@ type gitActionOutput struct {
|
|||||||
Output string `json:"output" jsonschema:"the git command output (may be empty)"`
|
Output string `json:"output" jsonschema:"the git command output (may be empty)"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// gitCheckoutInput selects a branch to switch to.
|
||||||
|
type gitCheckoutInput struct {
|
||||||
|
Path string `json:"path" jsonschema:"absolute path of the repository, from list_repos"`
|
||||||
|
Branch string `json:"branch" jsonschema:"the existing branch to switch to"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// createBranchInput names a new branch to create.
|
||||||
|
type createBranchInput struct {
|
||||||
|
Path string `json:"path" jsonschema:"absolute path of the repository, from list_repos"`
|
||||||
|
Name string `json:"name" jsonschema:"the new branch name to create and switch to"`
|
||||||
|
}
|
||||||
|
|
||||||
// NewServer builds the MCP server and registers the (currently read-only) tools.
|
// NewServer builds the MCP server and registers the (currently read-only) tools.
|
||||||
func NewServer(svc *service.Service, version string) *mcpsdk.Server {
|
func NewServer(svc *service.Service, version string) *mcpsdk.Server {
|
||||||
s := mcpsdk.NewServer(&mcpsdk.Implementation{
|
s := mcpsdk.NewServer(&mcpsdk.Implementation{
|
||||||
@@ -253,6 +265,28 @@ func NewServer(svc *service.Service, version string) *mcpsdk.Server {
|
|||||||
return nil, gitActionOutput{Output: out}, nil
|
return nil, gitActionOutput{Output: out}, nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
mcpsdk.AddTool(s, &mcpsdk.Tool{
|
||||||
|
Name: "git_checkout",
|
||||||
|
Description: "Switch a repository to an existing branch. Git refuses if uncommitted changes would be overwritten (the error is returned). Path is from list_repos.",
|
||||||
|
}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in gitCheckoutInput) (*mcpsdk.CallToolResult, gitActionOutput, error) {
|
||||||
|
out, err := svc.GitCheckout(ctx, activity.ActorClaude, in.Path, in.Branch)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gitActionOutput{}, err
|
||||||
|
}
|
||||||
|
return nil, gitActionOutput{Output: out}, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
mcpsdk.AddTool(s, &mcpsdk.Tool{
|
||||||
|
Name: "create_branch",
|
||||||
|
Description: "Create a new branch from the current HEAD and switch to it (git checkout -b). Fails if the branch already exists. Path is from list_repos.",
|
||||||
|
}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in createBranchInput) (*mcpsdk.CallToolResult, gitActionOutput, error) {
|
||||||
|
out, err := svc.GitCreateBranch(ctx, activity.ActorClaude, in.Path, in.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gitActionOutput{}, err
|
||||||
|
}
|
||||||
|
return nil, gitActionOutput{Output: out}, nil
|
||||||
|
})
|
||||||
|
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ func (s *Service) MergeAndCleanup(ctx context.Context, actor activity.Actor, rep
|
|||||||
// gitAction runs one mutating git op through the boundary, records the outcome
|
// gitAction runs one mutating git op through the boundary, records the outcome
|
||||||
// on the activity feed, and refreshes the repo in the index on success. Callers
|
// on the activity feed, and refreshes the repo in the index on success. Callers
|
||||||
// are responsible for §1.4 confirmation of destructive ops (e.g. discard).
|
// are responsible for §1.4 confirmation of destructive ops (e.g. discard).
|
||||||
func (s *Service) gitAction(ctx context.Context, actor activity.Actor, repoPath, kind string, run func(dir string) (string, error)) (string, error) {
|
func (s *Service) gitAction(ctx context.Context, actor activity.Actor, repoPath, kind, okDetail string, run func(dir string) (string, error)) (string, error) {
|
||||||
base, ok := s.index.Get(filepath.Clean(repoPath))
|
base, ok := s.index.Get(filepath.Clean(repoPath))
|
||||||
if !ok {
|
if !ok {
|
||||||
return "", fmt.Errorf("unknown repository %q", repoPath)
|
return "", fmt.Errorf("unknown repository %q", repoPath)
|
||||||
@@ -172,7 +172,10 @@ func (s *Service) gitAction(ctx context.Context, actor activity.Actor, repoPath,
|
|||||||
s.feed.Record(actor, kind, base.Path, "failed: "+err.Error())
|
s.feed.Record(actor, kind, base.Path, "failed: "+err.Error())
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
s.feed.Record(actor, kind, base.Path, "ok")
|
if okDetail == "" {
|
||||||
|
okDetail = "ok"
|
||||||
|
}
|
||||||
|
s.feed.Record(actor, kind, base.Path, okDetail)
|
||||||
if s.refresh != nil {
|
if s.refresh != nil {
|
||||||
s.refresh(ctx, base.Path)
|
s.refresh(ctx, base.Path)
|
||||||
}
|
}
|
||||||
@@ -182,19 +185,19 @@ func (s *Service) gitAction(ctx context.Context, actor activity.Actor, repoPath,
|
|||||||
// GitFetch, GitPull, GitPush, GitCommit, GitDiscard are the mutating commands
|
// GitFetch, GitPull, GitPush, GitCommit, GitDiscard are the mutating commands
|
||||||
// the right-click menu (and, later, MCP) invoke.
|
// the right-click menu (and, later, MCP) invoke.
|
||||||
func (s *Service) GitFetch(ctx context.Context, actor activity.Actor, repoPath string) (string, error) {
|
func (s *Service) GitFetch(ctx context.Context, actor activity.Actor, repoPath string) (string, error) {
|
||||||
return s.gitAction(ctx, actor, repoPath, "git-fetch", func(d string) (string, error) {
|
return s.gitAction(ctx, actor, repoPath, "git-fetch", "ok", func(d string) (string, error) {
|
||||||
return "", s.git.Fetch(ctx, d)
|
return "", s.git.Fetch(ctx, d)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) GitPull(ctx context.Context, actor activity.Actor, repoPath string) (string, error) {
|
func (s *Service) GitPull(ctx context.Context, actor activity.Actor, repoPath string) (string, error) {
|
||||||
return s.gitAction(ctx, actor, repoPath, "git-pull", func(d string) (string, error) {
|
return s.gitAction(ctx, actor, repoPath, "git-pull", "ok", func(d string) (string, error) {
|
||||||
return s.git.Pull(ctx, d)
|
return s.git.Pull(ctx, d)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) GitPush(ctx context.Context, actor activity.Actor, repoPath string) (string, error) {
|
func (s *Service) GitPush(ctx context.Context, actor activity.Actor, repoPath string) (string, error) {
|
||||||
return s.gitAction(ctx, actor, repoPath, "git-push", func(d string) (string, error) {
|
return s.gitAction(ctx, actor, repoPath, "git-push", "ok", func(d string) (string, error) {
|
||||||
return s.git.Push(ctx, d)
|
return s.git.Push(ctx, d)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -203,18 +206,38 @@ func (s *Service) GitCommit(ctx context.Context, actor activity.Actor, repoPath,
|
|||||||
if strings.TrimSpace(message) == "" {
|
if strings.TrimSpace(message) == "" {
|
||||||
return "", fmt.Errorf("a commit message is required")
|
return "", fmt.Errorf("a commit message is required")
|
||||||
}
|
}
|
||||||
return s.gitAction(ctx, actor, repoPath, "git-commit", func(d string) (string, error) {
|
return s.gitAction(ctx, actor, repoPath, "git-commit", "ok", func(d string) (string, error) {
|
||||||
return s.git.Commit(ctx, d, message)
|
return s.git.Commit(ctx, d, message)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// GitDiscard is DESTRUCTIVE (§1.4) — the caller must confirm with the user first.
|
// GitDiscard is DESTRUCTIVE (§1.4) — the caller must confirm with the user first.
|
||||||
func (s *Service) GitDiscard(ctx context.Context, actor activity.Actor, repoPath string) (string, error) {
|
func (s *Service) GitDiscard(ctx context.Context, actor activity.Actor, repoPath string) (string, error) {
|
||||||
return s.gitAction(ctx, actor, repoPath, "git-discard", func(d string) (string, error) {
|
return s.gitAction(ctx, actor, repoPath, "git-discard", "ok", func(d string) (string, error) {
|
||||||
return s.git.DiscardAll(ctx, d)
|
return s.git.DiscardAll(ctx, d)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GitCheckout switches to an existing branch.
|
||||||
|
func (s *Service) GitCheckout(ctx context.Context, actor activity.Actor, repoPath, branch string) (string, error) {
|
||||||
|
if strings.TrimSpace(branch) == "" {
|
||||||
|
return "", fmt.Errorf("a branch name is required")
|
||||||
|
}
|
||||||
|
return s.gitAction(ctx, actor, repoPath, "git-checkout", "switched to "+branch, func(d string) (string, error) {
|
||||||
|
return s.git.Checkout(ctx, d, branch)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GitCreateBranch creates a new branch from HEAD and switches to it.
|
||||||
|
func (s *Service) GitCreateBranch(ctx context.Context, actor activity.Actor, repoPath, name string) (string, error) {
|
||||||
|
if strings.TrimSpace(name) == "" {
|
||||||
|
return "", fmt.Errorf("a branch name is required")
|
||||||
|
}
|
||||||
|
return s.gitAction(ctx, actor, repoPath, "git-create-branch", "created "+name, func(d string) (string, error) {
|
||||||
|
return s.git.CreateBranch(ctx, d, name)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// resolveForge maps a repo path to (owner, repo) on the configured forge host via
|
// resolveForge maps a repo path to (owner, repo) on the configured forge host via
|
||||||
// its git remotes, preferring "origin".
|
// its git remotes, preferring "origin".
|
||||||
func (s *Service) resolveForge(ctx context.Context, repoPath string) (owner, repo string, err error) {
|
func (s *Service) resolveForge(ctx context.Context, repoPath string) (owner, repo string, err error) {
|
||||||
|
|||||||
@@ -65,6 +65,24 @@ func TestGitActions(t *testing.T) {
|
|||||||
t.Fatalf("a.txt = %q, want restored to \"one\"", got)
|
t.Fatalf("a.txt = %q, want restored to \"one\"", got)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create a branch (switches to it), then switch back to main.
|
||||||
|
if _, err := svc.GitCreateBranch(ctx, activity.ActorUser, repoPath, "feature-x"); err != nil {
|
||||||
|
t.Fatalf("GitCreateBranch: %v", err)
|
||||||
|
}
|
||||||
|
if st, _ := svc.GetRepo(repoPath); st.Branch != "feature-x" {
|
||||||
|
t.Fatalf("branch = %q, want feature-x", st.Branch)
|
||||||
|
}
|
||||||
|
if _, err := svc.GitCheckout(ctx, activity.ActorUser, repoPath, "main"); err != nil {
|
||||||
|
t.Fatalf("GitCheckout: %v", err)
|
||||||
|
}
|
||||||
|
if st, _ := svc.GetRepo(repoPath); st.Branch != "main" {
|
||||||
|
t.Fatalf("branch = %q, want main", st.Branch)
|
||||||
|
}
|
||||||
|
// Creating an existing branch fails.
|
||||||
|
if _, err := svc.GitCreateBranch(ctx, activity.ActorUser, repoPath, "feature-x"); err == nil {
|
||||||
|
t.Fatalf("expected error creating an existing branch")
|
||||||
|
}
|
||||||
|
|
||||||
// The feed recorded the successful actions.
|
// The feed recorded the successful actions.
|
||||||
kinds := map[string]bool{}
|
kinds := map[string]bool{}
|
||||||
for _, e := range feed.Events(0) {
|
for _, e := range feed.Events(0) {
|
||||||
|
|||||||
@@ -55,6 +55,8 @@
|
|||||||
<li><strong>Publish</strong> — push your commits.</li>
|
<li><strong>Publish</strong> — push your commits.</li>
|
||||||
<li><strong>Check for updates</strong> — fetch without changing your files.</li>
|
<li><strong>Check for updates</strong> — fetch without changing your files.</li>
|
||||||
<li><strong>Save my work…</strong> — commit everything (asks for a message).</li>
|
<li><strong>Save my work…</strong> — commit everything (asks for a message).</li>
|
||||||
|
<li><strong>Switch branch…</strong> — move to another existing branch.</li>
|
||||||
|
<li><strong>New branch…</strong> — create a branch and switch to it.</li>
|
||||||
<li><strong>Set as active project</strong> / <strong>Ask Claude to switch here</strong>.</li>
|
<li><strong>Set as active project</strong> / <strong>Ask Claude to switch here</strong>.</li>
|
||||||
<li><strong>Copy path</strong>.</li>
|
<li><strong>Copy path</strong>.</li>
|
||||||
<li><strong>Discard all changes…</strong> — throw away uncommitted edits
|
<li><strong>Discard all changes…</strong> — throw away uncommitted edits
|
||||||
|
|||||||
Reference in New Issue
Block a user