Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions docs/spec/worktree-monitor.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,11 @@ Timeline replay tests verify status transitions against checked-in fixture data
A "+" button on each repo header opens a modal to create new worktrees without leaving the dashboard.

- **Name input** (auto-focused) + **source branch dropdown** (sorted: main > master > develop > dev* > alphabetical from dashboard worktrees)
- If `fork.ps1` (Windows) or `fork.sh` (Unix) exists in repo root, delegates to it with branch name as sole argument (runs from source worktree directory). Otherwise falls back to `git worktree add -b {name} {parentDir}/tm-{name}`.
- Modal shows creating animation, then auto-closes on success or displays error
- Treemon creates the worktree itself: it fetches the base branch from the upstream remote, then forks via `git worktree add -b {name} {parentDir}/tm-{name} {baseRef}`. `baseRef` prefers the remote-tracking ref `{remote}/{base}` — so a new worktree forks from the upstream tip rather than a possibly-stale local branch — falling back to the local `{base}` branch when no remote-tracking ref exists. No worktree needs the base checked out; fetch/remote failures fall back to whatever ref is available.
- After creation, an optional `post-fork.ps1` (Windows) / `post-fork.sh` (Unix) in the repo root runs **inside the new worktree**, receiving `{worktreePath} {sourceRepoRoot} {baseRef} {branchName}`. It is for setup only (symlinks, dependency install); a failure is reported as a non-fatal warning since the worktree already exists.
- Legacy `fork.ps1`/`fork.sh` scripts are **no longer executed** — Treemon now owns forking. If one is present, creation still succeeds but returns a warning to migrate setup steps into `post-fork.*`.
- Warnings (legacy fork script present, post-fork failure) flow back through `createWorktree` (`Result<string list, string>`) and are surfaced in the modal (UI) or console (CLI).
- Modal shows creating animation, then auto-closes on clean success, or shows warnings / error
- Server expedites worktree list refresh for the repo so the new card appears quickly

### Native Session Management
Expand Down
75 changes: 0 additions & 75 deletions fork.ps1

This file was deleted.

56 changes: 56 additions & 0 deletions post-fork.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Post-fork setup hook. Treemon creates the worktree itself, then runs this
# script inside the new worktree to wire up local-only dependencies.
#
# Treemon invokes it as:
# pwsh -NoProfile -File post-fork.ps1 <worktreePath> <sourceRepoRoot> <baseRef> <branch>
# with the working directory set to the new worktree.
param(
[Parameter(Position = 0)]
[string]$WorktreePath = (Get-Location).Path,
[Parameter(Position = 1)]
[string]$SourceRoot = $PSScriptRoot,
[Parameter(Position = 2)]
[string]$BaseRef = "",
[Parameter(Position = 3)]
[string]$Branch = ""
)

$ErrorActionPreference = "Stop"

Set-Location $WorktreePath

# Directory junctions (not symbolic links) so this works without elevation or
# Developer Mode — important because Treemon spawns this hook from the server
# process, where no one is present to approve a UAC prompt.
function New-RepoJunction {
param([string]$Name)

$target = Join-Path $SourceRoot $Name
if (-not (Test-Path $target)) {
Write-Host " No '$Name' in source repo, skipping."
return
}

Write-Host "Creating $Name junction..."
try {
New-Item -ItemType Junction -Path $Name -Target $target -ErrorAction Stop | Out-Null
Write-Host " Junction created successfully."
} catch {
Write-Host " Warning: Failed to create '$Name' junction: $($_.Exception.Message). Create it manually if needed."
}
}

New-RepoJunction ".claude"
New-RepoJunction "data"

if (Get-Command bd -ErrorAction SilentlyContinue) {
Write-Host "Initializing beads..."
bd init --skip-hooks --skip-merge-driver --no-daemon --quiet
} else {
Write-Host "Skipping beads init (bd not found)."
}

Write-Host "Installing npm dependencies..."
npm install

Write-Host "Done! Worktree ready at: $WorktreePath"
29 changes: 29 additions & 0 deletions review-worktree.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Creates a worktree for reviewing someone else's contribution: fetches a remote
# and forks a `review/<branch>` worktree that tracks `<remote>/<branch>`, then runs
# the standard post-fork setup. This is a manual workflow — Treemon's own
# "create worktree" feature no longer forks from arbitrary remotes.
#
# Usage:
# pwsh -File review-worktree.ps1 <branch> <remote>
param(
[Parameter(Mandatory = $true, Position = 0)]
[string]$Branch,
[Parameter(Mandatory = $true, Position = 1)]
[string]$Remote
)

$ErrorActionPreference = "Stop"

$repoRoot = $PSScriptRoot
$dirSafeBranch = $Branch -replace '/', '-'
$worktreePath = Join-Path (Split-Path $repoRoot -Parent) "tm-review-$dirSafeBranch"
$localBranch = "review/$Branch"

Write-Host "Fetching from $Remote..."
git fetch $Remote

Write-Host "Creating review worktree at $worktreePath (branch $localBranch tracking $Remote/$Branch)..."
git worktree add -b $localBranch $worktreePath "$Remote/$Branch"

# Reuse the same setup Treemon runs after creating a worktree.
& (Join-Path $repoRoot "post-fork.ps1") $worktreePath $repoRoot "$Remote/$Branch" $localBranch
20 changes: 12 additions & 8 deletions src/Cli/Program.fs
Original file line number Diff line number Diff line change
Expand Up @@ -164,14 +164,18 @@ let launchCmd =
let newCmd =
let handler (repo: string, branch: string, baseBranch: string, port: int option) =
withPort port (fun port ->
runApi
port
(fun api ->
api.createWorktree
{ RepoId = repo
BranchName = BranchName.create branch
BaseBranch = BranchName.create baseBranch })
$"Worktree created for branch '%s{branch}'")
tryCallServer port (fun api ->
let request =
{ RepoId = repo
BranchName = BranchName.create branch
BaseBranch = BranchName.create baseBranch }

match api.createWorktree request |> Async.RunSynchronously with
| Ok warnings ->
printfn $"✓ Worktree created for branch '%s{branch}'"
warnings |> List.iter (fun w -> eprintfn $"⚠ %s{w}")
0
| Error e -> eprintfn $"Error: %s{e}"; 1))

command "new" {
description "Create a new worktree"
Expand Down
101 changes: 59 additions & 42 deletions src/Client/CreateWorktreeModal.fs
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@ type ModalState =
| Open of CreateWorktreeForm
| Creating of RepoId
| CreateError of repoId: RepoId * message: string
| CreateWarning of repoId: RepoId * messages: string list

type Msg =
| OpenCreateWorktree of RepoId
| BranchesLoaded of Result<string list, exn>
| SetNewWorktreeName of string
| SetBaseBranch of string
| SubmitCreateWorktree
| CreateWorktreeCompleted of Result<unit, string>
| CreateWorktreeCompleted of Result<string list, string>
| CloseCreateModal

let repoId =
Expand All @@ -34,6 +35,7 @@ let repoId =
| Open form -> Some form.RepoId
| Creating repoId -> Some repoId
| CreateError (repoId, _) -> Some repoId
| CreateWarning (repoId, _) -> Some repoId

let isOpen =
function
Expand All @@ -49,54 +51,44 @@ let private just modal =
{ Modal = modal; RestoredFocus = None; RefreshWorktrees = false }, Cmd.none

let update (api: Lazy<IWorktreeApi>) (msg: Msg) (modal: ModalState) : UpdateResult * Cmd<Msg> =
match msg with
| OpenCreateWorktree rid ->
match msg, modal with
| OpenCreateWorktree rid, _ ->
{ Modal = LoadingBranches rid; RestoredFocus = None; RefreshWorktrees = false },
Cmd.OfAsync.either api.Value.getBranches (RepoId.value rid) (Ok >> BranchesLoaded) (Error >> BranchesLoaded)

| BranchesLoaded (Ok branches) ->
match modal with
| LoadingBranches rid ->
let baseBranch = branches |> List.tryHead |> Option.defaultValue ""
just (Open { RepoId = rid; Branches = branches; Name = ""; BaseBranch = baseBranch })
| _ -> just modal

| BranchesLoaded (Error _) ->
match modal with
| LoadingBranches rid -> just (CreateError (rid, "Failed to load branches"))
| _ -> just modal

| SetNewWorktreeName name ->
match modal with
| Open form -> just (Open { form with Name = name })
| _ -> just modal

| SetBaseBranch branch ->
match modal with
| Open form -> just (Open { form with BaseBranch = branch })
| _ -> just modal

| SubmitCreateWorktree ->
match modal with
| Open form when form.Name.Trim().Length > 0 ->
let request: CreateWorktreeRequest =
{ RepoId = RepoId.value form.RepoId
BranchName = BranchName.create (form.Name.Trim())
BaseBranch = BranchName.create form.BaseBranch }
{ Modal = Creating form.RepoId; RestoredFocus = None; RefreshWorktrees = false },
Cmd.OfAsync.perform api.Value.createWorktree request CreateWorktreeCompleted
| _ -> just modal

| CreateWorktreeCompleted (Ok _) ->
| BranchesLoaded (Ok branches), LoadingBranches rid ->
let baseBranch = branches |> List.tryHead |> Option.defaultValue ""
just (Open { RepoId = rid; Branches = branches; Name = ""; BaseBranch = baseBranch })
| BranchesLoaded (Ok _), _ -> just modal

| BranchesLoaded (Error _), LoadingBranches rid -> just (CreateError (rid, "Failed to load branches"))
| BranchesLoaded (Error _), _ -> just modal

| SetNewWorktreeName name, Open form -> just (Open { form with Name = name })
| SetNewWorktreeName _, _ -> just modal

| SetBaseBranch branch, Open form -> just (Open { form with BaseBranch = branch })
| SetBaseBranch _, _ -> just modal

| SubmitCreateWorktree, Open form when form.Name.Trim().Length > 0 ->
let request: CreateWorktreeRequest =
{ RepoId = RepoId.value form.RepoId
BranchName = BranchName.create (form.Name.Trim())
BaseBranch = BranchName.create form.BaseBranch }
{ Modal = Creating form.RepoId; RestoredFocus = None; RefreshWorktrees = false },
Cmd.OfAsync.perform api.Value.createWorktree request CreateWorktreeCompleted
| SubmitCreateWorktree, _ -> just modal

| CreateWorktreeCompleted (Ok warnings), Creating rid when not (List.isEmpty warnings) ->
{ Modal = CreateWarning (rid, warnings); RestoredFocus = None; RefreshWorktrees = true }, Cmd.none
| CreateWorktreeCompleted (Ok _), _ ->
let restored = repoId modal |> Option.map RepoHeader
{ Modal = Closed; RestoredFocus = restored; RefreshWorktrees = true }, Cmd.none

| CreateWorktreeCompleted (Error errorMsg) ->
match modal with
| Creating rid -> just (CreateError (rid, errorMsg))
| _ -> just modal
| CreateWorktreeCompleted (Error errorMsg), Creating rid -> just (CreateError (rid, errorMsg))
| CreateWorktreeCompleted (Error _), _ -> just modal

| CloseCreateModal ->
| CloseCreateModal, _ ->
let restored = repoId modal |> Option.map RepoHeader
{ Modal = Closed; RestoredFocus = restored; RefreshWorktrees = false }, Cmd.none

Expand Down Expand Up @@ -199,3 +191,28 @@ let view (dispatch: Msg -> unit) (modal: ModalState) =
]
]
]

| CreateWarning (_, messages) ->
modalOverlay dispatch true [
Html.div [
prop.className "modal-header warning"
prop.text "Worktree created — with warnings"
]
Html.div [
prop.className "modal-body"
prop.children (
messages
|> List.map (fun message ->
Html.div [ prop.className "modal-warning-message"; prop.text message ]))
]
Html.div [
prop.className "modal-footer"
prop.children [
Html.button [
prop.className "modal-btn cancel"
prop.onClick (fun _ -> dispatch CloseCreateModal)
prop.text "Close"
]
]
]
]
5 changes: 5 additions & 0 deletions src/Client/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,7 @@
padding: 14px 18px 0; font-weight: 600; font-size: 1.05em; color: #cdd6f4;
}
.modal-header.error { color: #f38ba8; }
.modal-header.warning { color: #f9e2af; }
.modal-body { padding: 14px 18px; display: flex; flex-direction: column; gap: 10px; }
.modal-body code { color: #89b4fa; font-family: 'Consolas', 'Cascadia Mono', monospace; }
.modal-body.creating {
Expand Down Expand Up @@ -409,6 +410,10 @@
color: #f38ba8; font-size: 0.9em; white-space: pre-wrap; word-break: break-word;
background: #1e1e2e; padding: 10px; border-radius: 6px;
}
.modal-warning-message {
color: #f9e2af; font-size: 0.9em; white-space: pre-wrap; word-break: break-word;
background: #1e1e2e; padding: 10px; border-radius: 6px;
}

.creating-dots::after {
content: '';
Expand Down
Loading
Loading