feat: fork
This commit is contained in:
@@ -83,7 +83,7 @@ interface DragState {
|
||||
|
||||
type SessionTreeProps = Pick<
|
||||
WorkspaceBrowserProps,
|
||||
'useSessions' | 'startSession' | 'open' | 'insertSessionBefore'
|
||||
'useSessions' | 'startSession' | 'open' | 'forkSession' | 'insertSessionBefore'
|
||||
> & {
|
||||
workspaces: readonly WorkspaceView[]
|
||||
/** Live search filter owned by the browser root (the query outlives the tree). */
|
||||
@@ -98,7 +98,7 @@ type SessionTreeProps = Pick<
|
||||
|
||||
/** The scrolling session tree; unmounting at collapse settle drops the sessions subscription and expansion state. */
|
||||
function SessionTree({
|
||||
useSessions, startSession, open, workspaces, query,
|
||||
useSessions, startSession, open, forkSession, workspaces, query,
|
||||
onRenameRequest, onDeleteRequest, onSessionRename, insertSessionBefore,
|
||||
}: SessionTreeProps) {
|
||||
const list = useSessions(s => s)
|
||||
@@ -115,6 +115,24 @@ function SessionTree({
|
||||
if (current === undefined || currentGroup === undefined) return
|
||||
setExpandedProjects(l => (l.includes(currentGroup) ? l : [...l, currentGroup]))
|
||||
}, [current, currentGroup])
|
||||
// The selected session must be visible: unfold its ancestor chain (fork
|
||||
// lands the child under a possibly folded parent row).
|
||||
const currentAncestors = useMemo(() => {
|
||||
const chain: string[] = []
|
||||
let cursor = current === undefined ? undefined : list.byId[current]?.parentId
|
||||
while (cursor !== undefined && !chain.includes(cursor)) {
|
||||
chain.push(cursor)
|
||||
cursor = list.byId[cursor]?.parentId
|
||||
}
|
||||
return chain
|
||||
}, [current, list])
|
||||
useEffect(() => {
|
||||
if (currentAncestors.length === 0) return
|
||||
setExpandedSessions((l) => {
|
||||
const missing = currentAncestors.filter(id => !l.includes(id))
|
||||
return missing.length === 0 ? l : [...l, ...missing]
|
||||
})
|
||||
}, [currentAncestors])
|
||||
const groups = useMemo(
|
||||
() => deriveGroups(list, workspaces, { expandedProjects, expandedSessions, query }),
|
||||
[list, workspaces, expandedProjects, expandedSessions, query],
|
||||
@@ -195,6 +213,7 @@ function SessionTree({
|
||||
now={now}
|
||||
onOpen={open}
|
||||
onRename={onSessionRename}
|
||||
onFork={forkSession}
|
||||
onToggle={(id) => { setExpandedSessions(l => toggled(l, id)) }}
|
||||
drag={dragProps}
|
||||
/>
|
||||
@@ -209,7 +228,7 @@ function SessionTree({
|
||||
}
|
||||
|
||||
/** The flat "In one list" body: every session a top-level row, newest-first. */
|
||||
function FlatList({ useSessions, open, onSessionRename, query }: Pick<SessionTreeProps, 'useSessions' | 'open' | 'onSessionRename' | 'query'>) {
|
||||
function FlatList({ useSessions, open, forkSession, onSessionRename, query }: Pick<SessionTreeProps, 'useSessions' | 'open' | 'forkSession' | 'onSessionRename' | 'query'>) {
|
||||
const list = useSessions(s => s)
|
||||
const rows = useMemo(() => deriveFlat(list, { query }), [list, query])
|
||||
const now = Date.now()
|
||||
@@ -228,6 +247,7 @@ function FlatList({ useSessions, open, onSessionRename, query }: Pick<SessionTre
|
||||
now={now}
|
||||
onOpen={open}
|
||||
onRename={onSessionRename}
|
||||
onFork={forkSession}
|
||||
/* v8 ignore next -- required-prop filler: flat rows render no twist, so it never fires. */
|
||||
onToggle={() => {}}
|
||||
flat
|
||||
@@ -254,6 +274,7 @@ export function WorkspaceBrowser({
|
||||
startSession,
|
||||
open,
|
||||
renameSession,
|
||||
forkSession,
|
||||
renameWorkspace,
|
||||
deleteWorkspace,
|
||||
insertSessionBefore,
|
||||
@@ -460,13 +481,14 @@ export function WorkspaceBrowser({
|
||||
|
||||
{/* Always-mounted seat keeps the region's flex slot while the list
|
||||
itself is wide-only. */}
|
||||
<div className={css.listArea}>
|
||||
{wide && (groupBy === 'flat'
|
||||
? <FlatList useSessions={useSessions} open={open} onSessionRename={onSessionRename} query={query} />
|
||||
<div className={css.listArea}>
|
||||
{wide && (groupBy === 'flat'
|
||||
? <FlatList useSessions={useSessions} open={open} forkSession={forkSession} onSessionRename={onSessionRename} query={query} />
|
||||
: (
|
||||
<SessionTree
|
||||
useSessions={useSessions}
|
||||
onSessionRename={onSessionRename}
|
||||
forkSession={forkSession}
|
||||
workspaces={workspaces}
|
||||
startSession={startSession}
|
||||
open={open}
|
||||
|
||||
@@ -95,6 +95,8 @@ export type WorkspaceBrowserInjected = DirectoryPickingInjected & {
|
||||
open: (sessionId: SessionId) => void
|
||||
/** Rename a Session (explicit user title; resolves on host acceptance). */
|
||||
renameSession: (sessionId: SessionId, title: string) => Promise<void>
|
||||
/** Fork a Session at its last completed turn and open the child. */
|
||||
forkSession: (sessionId: SessionId) => void
|
||||
/** Rename a Host Workspace (rejects on name conflict; resolves on durability). */
|
||||
renameWorkspace: (workspaceId: WorkspaceId, title: string) => Promise<void>
|
||||
/** Delete only a Host Workspace registration; directory and Session logs remain. */
|
||||
|
||||
@@ -59,6 +59,13 @@ export function apply(ctx: ClientContext): void {
|
||||
const result = await session.rename(title)
|
||||
if (!result.ok) throw new Error(result.error.message)
|
||||
},
|
||||
forkSession: (sessionId) => {
|
||||
ctx.sessions.fork({ sessionId })
|
||||
.then((childId) => { ctx.sessions.open(childId) })
|
||||
.catch(() => {
|
||||
// Fork failure keeps the list untouched (composer-stop posture).
|
||||
})
|
||||
},
|
||||
renameWorkspace: async (workspaceId, title) => { await ctx.workspaces.rename(workspaceId, title) },
|
||||
deleteWorkspace: async (workspaceId) => { await ctx.workspaces.delete(workspaceId) },
|
||||
insertSessionBefore: async (workspaceId, sessionId, beforeSessionId) => {
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* Workspace browser tree row components (figma Cell set 14:3080): pure presentational —
|
||||
* all data and callbacks arrive via props. Hover swaps (folder->chevron,
|
||||
* time->ellipsis, action buttons) are CSS-only. Row ... menus are visual-only
|
||||
* except workspace Rename/Delete and session Rename; the session and workspace
|
||||
* hover cards are suppressed while a menu is open.
|
||||
* except workspace Rename/Delete and session Rename/Fork; the session and
|
||||
* workspace hover cards are suppressed while a menu is open.
|
||||
*/
|
||||
import { useState } from 'react'
|
||||
import clsx from 'clsx'
|
||||
@@ -184,7 +184,7 @@ function rowHalf(e: { clientY: number; currentTarget: HTMLElement }): 'before' |
|
||||
return e.clientY < rect.top + rect.height / 2 ? 'before' : 'after'
|
||||
}
|
||||
|
||||
export function SessionNodeItem({ node, depth, currentId, now, onOpen, onRename, onToggle, drag, flat = false }: {
|
||||
export function SessionNodeItem({ node, depth, currentId, now, onOpen, onRename, onFork, onToggle, drag, flat = false }: {
|
||||
node: SessionNode
|
||||
depth: number
|
||||
currentId: string | undefined
|
||||
@@ -192,6 +192,8 @@ export function SessionNodeItem({ node, depth, currentId, now, onOpen, onRename,
|
||||
onOpen: (id: SessionNode['id']) => void
|
||||
/** Open the browser-owned session rename dialog (row menu action). */
|
||||
onRename: (id: SessionNode['id'], currentTitle: string) => void
|
||||
/** Fork a session at its last completed turn (row menu action). */
|
||||
onFork: (id: SessionNode['id']) => void
|
||||
onToggle: (id: SessionNode['id']) => void
|
||||
/** Present only on draggable rows (workspace-group roots outside search). */
|
||||
drag?: RowDragProps | undefined
|
||||
@@ -259,9 +261,10 @@ export function SessionNodeItem({ node, depth, currentId, now, onOpen, onRename,
|
||||
open={menuOpen}
|
||||
onClose={() => { setMenuOpen(false) }}
|
||||
items={SESSION_MENU_ITEMS}
|
||||
onSelect={(id) => {
|
||||
setMenuOpen(false)
|
||||
if (id === 'rename') onRename(node.id, row.title) // fork/delete stay visual-only.
|
||||
onSelect={(id) => {
|
||||
setMenuOpen(false)
|
||||
if (id === 'rename') onRename(node.id, row.title)
|
||||
if (id === 'fork') onFork(node.id) // delete stays visual-only.
|
||||
}}
|
||||
portal
|
||||
closeOnPointerLeave
|
||||
@@ -295,6 +298,7 @@ export function SessionNodeItem({ node, depth, currentId, now, onOpen, onRename,
|
||||
now={now}
|
||||
onOpen={onOpen}
|
||||
onRename={onRename}
|
||||
onFork={onFork}
|
||||
onToggle={onToggle}
|
||||
/>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user