Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
e7529c6
feat(worktree): add handoff protocol contracts, channels, and error c…
Aug 6, 2026
c9bfb93
feat(agent): add handoff execution-CWD rebind capability gate
Aug 6, 2026
332d307
refactor(worktree): derive handoff direction/blocker unions from cons…
Aug 6, 2026
5e8ce6b
feat(worktree): add conflict-safe handoff execution
Aug 7, 2026
60d8c8c
feat(worktree): implement managed-to-current handoff confirm
Aug 7, 2026
f9c9f48
feat(worktree): implement hand-back handoff confirm
Aug 7, 2026
461bfd6
feat(worktree): reconcile interrupted handoffs with snapshot-backed r…
Aug 7, 2026
4a3e838
feat(agent): reconstruct and verify handoff runtimes before Send
Aug 7, 2026
1648f8a
feat(electron): add worktree handoff preview/confirm/recovery UI
Aug 7, 2026
ce0fc10
fix(electron): gate handoff actions on provider capability and polish…
Aug 7, 2026
642a174
docs(worktree): add conflict-safe handoff ADR and documentation
Aug 7, 2026
0d01586
test(e2e): add worktree v2 handoff UI UAT specs
Aug 7, 2026
07f2491
test(worktree): close AC-15 handoff blocker and state-transfer covera…
Aug 7, 2026
ad187ea
fix(worktree): make handoff UI UAT pass end-to-end in the real app
Aug 7, 2026
486adfa
fix(worktree): address review findings on conflict-safe handoff
Aug 7, 2026
54846ea
fix(worktree): journal source-cleaned intent before the destructive r…
Aug 7, 2026
d451b13
fix(e2e): require an exact assistant reply in the agent reply test
Aug 7, 2026
4e61fc3
fix(worktree): address CodeRabbit findings on conflict-safe handoff
Aug 7, 2026
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
3 changes: 3 additions & 0 deletions apps/electron/resources/release-notes/next.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@ This file accumulates release notes for the next unreleased version. PRs that ad
- **Share managed worktrees across sessions** — The workspace checkout control now offers **Existing worktree** for a new empty session: any ready managed worktree of the current workspace + repository can be selected, and the session binds to it as a shared owner without recreating or mutating the checkout. Every session bound to a worktree shows the same branch label; shared ownership shows as a Users icon and tooltip, and the existing deletion guards keep the checkout while any other session owns it ([#33](https://github.com/gannonh/kata-agents/issues/33), commit [ac0dd3b4](https://github.com/gannonh/kata-agents/commit/ac0dd3b4a41d81bef3c8c20353bc8b4cc2b3a4b1)).
- **Simplified managed worktree settings** — Worktrees settings now uses a compact delete-only list for active checkouts, labels the location **Worktree root**, and defaults **Automatically delete old worktrees** to off. The configurable **Auto-delete limit** still uses snapshot-first pruning so older worktrees remain recoverable on the owning server ([#41](https://github.com/gannonh/kata-agents/issues/41)).
- **Named managed worktrees and server-owned roots** — With Worktree V2 enabled, new worktrees accept a human-readable name that is normalized to lowercase kebab-case for the exact branch suffix and display name, while the Worktrees settings page configures a server-local materialization root without moving existing checkouts ([#40](https://github.com/gannonh/kata-agents/issues/40)).
- **Conflict-safe checkout handoff** — Sessions whose provider supports safe execution-CWD rebinding can move between the current checkout and a managed worktree: **Hand off to new worktree**, **Hand off to current checkout**, and **Hand back to worktree** preview the exact source/destination state, typed blockers, and cleanup before confirming; interrupted handoffs surface a snapshot-backed recovery state with a **Recover** action that rolls back exactly the journaled steps. The transcript and provider identity never move ([#42](https://github.com/gannonh/kata-agents/issues/42)).
## Improvements

## Bug Fixes

- **Worktree handoff fence fixes** — Dismissing the handoff preview dialog now releases the pending transaction (the session no longer stays fenced after Cancel), a session whose handoff is stuck in recovery-required can be deleted as the escape hatch, an interrupted handoff that already rebound the provider runtime is rebound back to the source checkout during recovery, and a failed execution-CWD proof no longer leaves the session stuck in a processing state ([#42](https://github.com/gannonh/kata-agents/issues/42)).

- **Worktree V2 recovery and protection accuracy** — Sessions bound to a removed worktree now show the exact lifecycle recovery state (Snapshotted, Restoring, …) with the worktree name instead of a generic “missing” badge; manual deletion is rejected server-side while any owner is active or flagged; retries of failed removals are governed by the verified snapshot when the checkout is no longer inspectable; the inventory refresh control renders a translated label ([#41](https://github.com/gannonh/kata-agents/issues/41)).

- **Git branch badge refresh** — Workspace badges now rediscover the live branch when switching sessions that share a working directory, instead of retaining the previously selected session's branch ([#32](https://github.com/gannonh/kata-agents/pull/32), commit [ddd37f03](https://github.com/gannonh/kata-agents/commit/ddd37f03b6292595c5409cae91b2249d88cd8337)).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1701,6 +1701,7 @@ export const ChatDisplay = React.forwardRef<ChatDisplayHandle, ChatDisplayProps>
return (
<div
key={turnKey}
data-testid="assistant-turn"
ref={el => { if (el) turnRefs.current.set(turnKey, el); else turnRefs.current.delete(turnKey) }}
className={cn(
"pt-2",
Expand Down Expand Up @@ -2157,7 +2158,7 @@ function ErrorMessage({ message, onOpenUrl, sessionId, onRetry }: { message: Mes
})

return (
<div className="flex justify-start mt-4">
<div className="flex justify-start mt-4" data-testid="chat-error-message">
{/* Subtle bg (3% opacity) + tinted shadow for softer error appearance */}
<div
className="max-w-[80%] shadow-tinted rounded-[8px] pl-5 pr-4 pt-2 pb-2.5 break-words"
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/**
* HandoffAction — the handoff surface for a bound session.
*
* `HandoffButton` offers the structurally-possible directions for the session's
* checkout (current-to-managed / managed-to-current / hand-back) and opens the
* preview/confirm dialog; `HandoffRecoveryBadge` surfaces an active
* (pending/recovery) handoff transaction so the user can open recovery.
*
* The server remains authoritative: a structurally possible direction still
* returns a typed blocker (unsupported provider, dirty destination, shared
* owners, …) which the dialog renders.
*/

import * as React from 'react'
import { useTranslation } from 'react-i18next'
import { ArrowLeftRight, Loader2, TriangleAlert } from 'lucide-react'

import type { WorktreeHandoffDirection, WorktreeHandoffStatus } from '@kata-sh/shared/protocol'

import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { Tooltip, TooltipContent, TooltipTrigger } from '@kata-sh/ui'

import { handoffDirectionsForCheckout } from '../input/handoff-controls'
import { HandoffDialog } from './HandoffDialog'

export interface HandoffActionProps {
sessionId: string
/** Client-visible checkout metadata for direction gating. */
checkout: { mode: 'current' | 'managed-worktree' } | undefined
/** Persisted handoff runtime state (armed → a prior handoff completed). */
handoffRuntimeState?: string | null
/** Server-derived: provider advertises safe execution-CWD rebinding (AC-1). */
handoffCapable?: boolean
/** True when the session's workspace is owned by a remote server. */
isRemoteWorkspace?: boolean
}

/** Small control that opens the handoff dialog for a chosen direction. */
export function HandoffButton({
sessionId,
checkout,
handoffRuntimeState,
handoffCapable,
isRemoteWorkspace,
}: HandoffActionProps) {
const { t } = useTranslation()
const [direction, setDirection] = React.useState<WorktreeHandoffDirection | null>(null)

// AC-1: handoff controls appear only for a provider adapter advertising safe
// execution-CWD rebinding. The server's typed blocker remains the backstop.
if (handoffCapable !== true) return null

const directions = handoffDirectionsForCheckout(checkout, handoffRuntimeState)
if (directions.length === 0) return null

const directionLabel = (dir: WorktreeHandoffDirection): string => {
switch (dir) {
case 'current-to-managed':
return t('git.handoff.direction.current-to-managed')
case 'managed-to-current':
return t('git.handoff.direction.managed-to-current')
case 'hand-back':
return t('git.handoff.direction.hand-back')
}
}

return (
<>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
data-testid="handoff-open-button"
variant="ghost"
size="sm"
className="h-7 gap-1 px-2 text-[12px] text-muted-foreground hover:text-foreground"
>
<ArrowLeftRight className="h-3.5 w-3.5" />
{t('git.handoff.action')}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="min-w-[200px]">
{directions.map((dir) => (
<DropdownMenuItem key={dir} data-testid={`handoff-direction-${dir}`} onSelect={() => setDirection(dir)}>
{directionLabel(dir)}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>

{direction && (
<HandoffDialog
open
sessionId={sessionId}
direction={direction}
isRemoteWorkspace={isRemoteWorkspace}
onOpenChange={(open) => {
if (!open) setDirection(null)
}}
/>
)}
</>
)
}

/**
* Polls HANDOFF_STATUS for the bound session and renders a recovery affordance
* while a transaction is pending/recovery-required. Quiet when inactive.
*/
export function HandoffRecoveryBadge({
sessionId,
onRecover,
}: {
sessionId: string
/** Called with the active status so the caller can open recovery UI. */
onRecover: (status: Extract<WorktreeHandoffStatus, { active: true }>) => void
}) {
const { t } = useTranslation()
const [status, setStatus] = React.useState<WorktreeHandoffStatus>({ active: false })
const [checking, setChecking] = React.useState(false)

React.useEffect(() => {
let cancelled = false
const poll = async () => {
setChecking(true)
try {
const next = await window.electronAPI.handoffStatus({ sessionId })
if (!cancelled) setStatus(next)
} catch {
// Transient server/unreachable states are quiet; the next poll retries.
} finally {
if (!cancelled) setChecking(false)
}
}
void poll()
const interval = window.setInterval(poll, 15_000)
return () => {
cancelled = true
window.clearInterval(interval)
}
}, [sessionId])

if (!status.active) return null

return (
<Tooltip>
<TooltipTrigger asChild>
<Button
data-testid="handoff-recovery-badge"
variant="ghost"
size="sm"
className="h-7 gap-1 px-2 text-[12px] text-rose-600 hover:text-rose-500 dark:text-rose-400"
onClick={() => onRecover(status)}
>
{checking ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<TriangleAlert className="h-3.5 w-3.5" />
)}
{t('git.handoff.statusActive')}
</Button>
</TooltipTrigger>
<TooltipContent>{t('git.handoff.recoveryHint')}</TooltipContent>
</Tooltip>
)
}
Loading
Loading