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 branch = renamed?.branches.find((each) => each.conversation === renaming?.conversation);
const { setChoice } = useNewSessionState();
const { setChoice, setLocation } = useNewSessionState();
const onNew = useCallback(() => {
setChoice(undefined);
navigate({});
setTimeout(() => document.querySelector<HTMLTextAreaElement>(".composer textarea")?.focus(), 50);
}, [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 (
<>
<Drawer
@ -196,6 +204,7 @@ function Sidebar() {
onBranch={owner ? onBranch : undefined}
onRenameBranch={owner ? onRenameBranch : undefined}
onNew={onNew}
onNewIn={owner ? onNewIn : undefined}
onCollapse={toggleSidebar}
onSearch={search ? onSearch : undefined}
onRestore={onRestore}

View file

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

View file

@ -2830,6 +2830,25 @@ button.diff-number:hover {
letter-spacing: 0;
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 {
min-height: var(--control);
padding-top: var(--s1);

View file

@ -146,6 +146,16 @@ describe("drawer and new-session parity", { skip: !browserTests }, () => {
"Pinned parent",
"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");
await chat.send({ text: "subagent", requestId: "drawer-child" }, BACKGROUND_CONTEXT);
const agents = await holdIn(owner, Subagents, {}, "reader");