feat(p2): add a + button to sidebar folders to start a new session there

This commit is contained in:
Armin Ronacher 2026-11-05 14:42:28 +01:00
parent 55a5c69145
commit 5a31a6d5f4
4 changed files with 97 additions and 39 deletions

View file

@ -178,12 +178,20 @@ function Sidebar() {
); );
const renamed = renaming && list?.find((each) => each.id === renaming.session); const renamed = renaming && list?.find((each) => each.id === renaming.session);
const branch = renamed?.branches.find((each) => each.conversation === renaming?.conversation); const branch = renamed?.branches.find((each) => each.conversation === renaming?.conversation);
const { setChoice } = useNewSessionState(); const { setChoice, setLocation } = useNewSessionState();
const onNew = useCallback(() => { const onNew = useCallback(() => {
setChoice(undefined); setChoice(undefined);
navigate({}); navigate({});
setTimeout(() => document.querySelector<HTMLTextAreaElement>(".composer textarea")?.focus(), 50); setTimeout(() => document.querySelector<HTMLTextAreaElement>(".composer textarea")?.focus(), 50);
}, [setChoice]); }, [setChoice]);
// A folder's "+": the new session starts in that folder's location.
const onNewIn = useCallback(
(env: string | null, cwd: string) => {
setLocation({ env, cwd });
onNew();
},
[setLocation, onNew],
);
return ( return (
<> <>
<Drawer <Drawer
@ -196,6 +204,7 @@ function Sidebar() {
onBranch={owner ? onBranch : undefined} onBranch={owner ? onBranch : undefined}
onRenameBranch={owner ? onRenameBranch : undefined} onRenameBranch={owner ? onRenameBranch : undefined}
onNew={onNew} onNew={onNew}
onNewIn={owner ? onNewIn : undefined}
onCollapse={toggleSidebar} onCollapse={toggleSidebar}
onSearch={search ? onSearch : undefined} onSearch={search ? onSearch : undefined}
onRestore={onRestore} onRestore={onRestore}

View file

@ -274,6 +274,7 @@ export const Drawer = memo(function Drawer({
onBranch, onBranch,
onRenameBranch, onRenameBranch,
onNew, onNew,
onNewIn,
onCollapse, onCollapse,
onProviders, onProviders,
onEnvs, onEnvs,
@ -294,6 +295,8 @@ export const Drawer = memo(function Drawer({
/** Asks for a new label of one of a session's `/tree` branches (the owner's). */ /** Asks for a new label of one of a session's `/tree` branches (the owner's). */
onRenameBranch?: (id: string, conversationId: number) => void; onRenameBranch?: (id: string, conversationId: number) => void;
onNew: () => void; onNew: () => void;
/** Starts a new session in a sidebar folder's location; absent: folders have no "+". */
onNewIn?: (env: string | null, cwd: string) => void;
onCollapse: () => void; onCollapse: () => void;
onProviders: () => void; onProviders: () => void;
onEnvs: () => void; onEnvs: () => void;
@ -412,45 +415,62 @@ export const Drawer = memo(function Drawer({
/> />
</label> </label>
<div className="session-scroll"> <div className="session-scroll">
{ordered.map(([location, list]) => ( {ordered.map(([location, list]) => {
<section key={location} className="group"> // A group holds at least one session; its first names the folder's location.
<h3 title={list[0] ? `${list[0].env ? `${list[0].env}:` : ""}${list[0].cwd}` : location}> const first = list[0];
<FolderIcon /> {location} return (
</h3> <section key={location} className="group">
{list.map((session) => ( <h3 title={first ? `${first.env ? `${first.env}:` : ""}${first.cwd}` : location}>
<SessionRow <FolderIcon />
key={session.id} <span className="group-label">{location}</span>
session={session} {onNewIn && !archived && first && (
archived={archived} <button
conversationId={session.id === activeId ? conversationId : session.main} type="button"
onOpen={onOpen} className="group-new"
onBranch={onBranch} aria-label={`New session in ${location}`}
onRenameBranch={onRenameBranch} title={`New session in ${location}`}
active={session.id === activeId} disabled={!connected}
> onClick={() => onNewIn(first.env ?? null, first.cwd)}
<button >
type="button" <PlusIcon size={14} />
className={`session-main ${session.id === activeId && conversationId === session.main ? "active" : ""}`} </button>
onClick={() => (archived ? restore(session.id) : onOpen(session.id))} )}
</h3>
{list.map((session) => (
<SessionRow
key={session.id}
session={session}
archived={archived}
conversationId={session.id === activeId ? conversationId : session.main}
onOpen={onOpen}
onBranch={onBranch}
onRenameBranch={onRenameBranch}
active={session.id === activeId}
> >
<span className="session-title"> <button
{session.pinned && <PinIcon size={12} />} type="button"
{session.title} className={`session-main ${session.id === activeId && conversationId === session.main ? "active" : ""}`}
</span> onClick={() => (archived ? restore(session.id) : onOpen(session.id))}
<span className="session-meta"> >
{session.worker.error && ( <span className="session-title">
<span className="error session-worker-error" title={session.worker.error}> {session.pinned && <PinIcon size={12} />}
Worker failed {session.title}
</span> </span>
)} <span className="session-meta">
<SessionDot worker={session.worker} /> {session.worker.error && (
{archived ? "Tap to restore" : ago(session.updatedAt)} <span className="error session-worker-error" title={session.worker.error}>
</span> Worker failed
</button> </span>
</SessionRow> )}
))} <SessionDot worker={session.worker} />
</section> {archived ? "Tap to restore" : ago(session.updatedAt)}
))} </span>
</button>
</SessionRow>
))}
</section>
);
})}
{results && results.messages.length > 0 && ( {results && results.messages.length > 0 && (
<section className="group"> <section className="group">
<h3>Messages</h3> <h3>Messages</h3>

View file

@ -2830,6 +2830,25 @@ button.diff-number:hover {
letter-spacing: 0; letter-spacing: 0;
white-space: nowrap; white-space: nowrap;
} }
.group-label {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
.group-new {
display: inline-flex;
flex: none;
align-items: center;
justify-content: center;
width: var(--control-sm);
height: var(--control-sm);
margin: calc(-1 * var(--s1)) 0;
color: var(--faint);
}
.group-new:hover:not(:disabled) {
color: var(--text);
}
.session-main { .session-main {
min-height: var(--control); min-height: var(--control);
padding-top: var(--s1); padding-top: var(--s1);

View file

@ -146,6 +146,16 @@ describe("drawer and new-session parity", { skip: !browserTests }, () => {
"Pinned parent", "Pinned parent",
"Recent sibling", "Recent sibling",
]); ]);
// A folder's "+" opens a new session in that folder.
const label = await browser.evaluate<string>(
`document.querySelector('.session-scroll .group h3 .group-label').textContent`,
);
await browser.click(".session-scroll .group h3 .group-new");
await browser.wait(
`location.pathname==='/' && document.querySelector('.cwd-pick span')?.textContent===${JSON.stringify(label)}`,
);
await browser.open(`http://127.0.0.1:${fixture.server.port}/s/${pinned.id}`);
await browser.wait(`[...document.querySelectorAll('.session-title')].some(e=>e.textContent==='Pinned parent')`);
const chat = await holdIn(owner, Chat, { session: pinned.id, conversation: 1 }, "participant"); const chat = await holdIn(owner, Chat, { session: pinned.id, conversation: 1 }, "participant");
await chat.send({ text: "subagent", requestId: "drawer-child" }, BACKGROUND_CONTEXT); await chat.send({ text: "subagent", requestId: "drawer-child" }, BACKGROUND_CONTEXT);
const agents = await holdIn(owner, Subagents, {}, "reader"); const agents = await holdIn(owner, Subagents, {}, "reader");