mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
feat(p2): sidebar branch rows behind the session's expand control
Branch rows are hidden until the session row's expand control (the subagents' chevron) opens them, above the subagents; the control also appears for a session with two or more branches and no subagents, with the branch count beside the subagent count. Showing a subagent still expands the list; showing any other non-main conversation no longer does (right after a /tree switch the page shows the new branch before the row names it main).
This commit is contained in:
parent
6515a79772
commit
d8981c7ba0
5 changed files with 79 additions and 18 deletions
|
|
@ -115,7 +115,9 @@ node:
|
|||
- Web: "Edit" beside the owner's view of a user message (on hover; always shown on touch screens) and "Branch from
|
||||
here" under an answer, in the main conversation. Under the session in the sidebar, from two branches on, one row per
|
||||
branch (`Sessions` rows' `branches`: the stored labels, null for the session title, newest first in a stable order,
|
||||
the current one marked in place); choosing one switches the session to its end (`SessionTree.select`). The worker
|
||||
the current one marked in place), behind the session row's expand control, which also shows its subagents
|
||||
(collapsed by default, expanded while a subagent is shown); choosing one switches the session to its end
|
||||
(`SessionTree.select`). The worker
|
||||
reports `p2.main` (`treeSelf`: `main` and the branches with their labels) at start and whenever a commit changes it.
|
||||
- Search indexes each branch's own entries (after its fork point).
|
||||
|
||||
|
|
|
|||
|
|
@ -71,8 +71,8 @@ function ThemeButton() {
|
|||
}
|
||||
|
||||
/**
|
||||
* A session's `/tree` branches (docs/tree.md), from two on, newest first in a stable order: switching only moves the
|
||||
* "current" mark. Choosing another branch switches the session to it (`onBranch`, the owner's); the pencil beside a
|
||||
* A session's `/tree` branches (docs/tree.md), newest first in a stable order: switching only moves the "current" mark.
|
||||
* Shown from two branches on, behind the session row's expand control (`hidden` while collapsed). Choosing another branch switches the session to it (`onBranch`, the owner's); the pencil beside a
|
||||
* row renames it (`onRenameBranch`). A branch without a label (conversation 1's default) shows the session title.
|
||||
*/
|
||||
function BranchRows({
|
||||
|
|
@ -82,6 +82,7 @@ function BranchRows({
|
|||
onOpen,
|
||||
onBranch,
|
||||
onRenameBranch,
|
||||
hidden,
|
||||
}: {
|
||||
session: SessionSummary;
|
||||
conversationId: number;
|
||||
|
|
@ -89,10 +90,10 @@ function BranchRows({
|
|||
onOpen: (id: string, conversationId?: number) => void;
|
||||
onBranch: (id: string, conversationId: number) => void;
|
||||
onRenameBranch?: (id: string, conversationId: number) => void;
|
||||
hidden: boolean;
|
||||
}) {
|
||||
if (session.branches.length < 2) return null;
|
||||
return (
|
||||
<div className="branch-rows">
|
||||
<div className="branch-rows" id={`branches-${session.id}`} hidden={hidden}>
|
||||
{session.branches.map((branch) => {
|
||||
const current = branch.conversation === session.main;
|
||||
const label = branch.label ?? session.title;
|
||||
|
|
@ -151,11 +152,13 @@ function SessionRow({
|
|||
archived?: boolean;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
// A subagent shown expands its session's list.
|
||||
const [expanded, setExpanded] = useState(conversationId !== session.main);
|
||||
// One control expands a session's branch rows and its subagents; a subagent shown expands them. Not any conversation
|
||||
// but the main one: right after a `/tree` switch the page shows the new branch before the row names it main.
|
||||
const showsSubagent = session.subagents.some((agent) => agent.conversationId === conversationId);
|
||||
const [expanded, setExpanded] = useState(showsSubagent);
|
||||
useEffect(() => {
|
||||
if (conversationId !== session.main) setExpanded(true);
|
||||
}, [conversationId, session.main]);
|
||||
if (showsSubagent) setExpanded(true);
|
||||
}, [showsSubagent]);
|
||||
// One-shot agents are opened from their script's tool card; archived subagents wait behind their own row.
|
||||
const listed = (archived ? [] : session.subagents)
|
||||
.filter((agent) => agent.kind !== "agent")
|
||||
|
|
@ -164,6 +167,8 @@ function SessionRow({
|
|||
const stored = listed.filter((agent) => agent.archived);
|
||||
const [showStored, setShowStored] = useState(false);
|
||||
const storedOpen = showStored || stored.some((agent) => agent.conversationId === conversationId);
|
||||
const branches = onBranch !== undefined && !archived && session.branches.length >= 2;
|
||||
const nested = [branches && "branches", listed.length > 0 && "subagents"].filter(Boolean).join(" and ");
|
||||
const row = (agent: (typeof listed)[number]) => (
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -190,25 +195,33 @@ function SessionRow({
|
|||
);
|
||||
return (
|
||||
<div className="session-row">
|
||||
<div className={`session-header ${listed.length ? "has-subagents" : ""}`}>
|
||||
<div className={`session-header ${nested ? "has-subagents" : ""}`}>
|
||||
{children}
|
||||
{listed.length > 0 && (
|
||||
{nested && (
|
||||
<button
|
||||
type="button"
|
||||
className="subagent-toggle"
|
||||
aria-label={expanded ? "Collapse subagents" : "Expand subagents"}
|
||||
aria-label={`${expanded ? "Collapse" : "Expand"} ${nested}`}
|
||||
aria-expanded={expanded}
|
||||
aria-controls={`subagents-${session.id}`}
|
||||
aria-controls={[branches && `branches-${session.id}`, listed.length > 0 && `subagents-${session.id}`]
|
||||
.filter(Boolean)
|
||||
.join(" ")}
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
>
|
||||
<span>{agents.length}</span>
|
||||
{listed.length > 0 && <span>{agents.length}</span>}
|
||||
{branches && (
|
||||
<span className="toggle-branches" title={`${session.branches.length} branches`}>
|
||||
<BranchIcon size={11} />
|
||||
{session.branches.length}
|
||||
</span>
|
||||
)}
|
||||
<span className={`chev ${expanded ? "open" : ""}`}>
|
||||
<ChevronIcon size={12} />
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{onBranch && !archived && (
|
||||
{branches && (
|
||||
<BranchRows
|
||||
session={session}
|
||||
conversationId={conversationId}
|
||||
|
|
@ -216,6 +229,7 @@ function SessionRow({
|
|||
onOpen={onOpen}
|
||||
onBranch={onBranch}
|
||||
onRenameBranch={onRenameBranch}
|
||||
hidden={!expanded}
|
||||
/>
|
||||
)}
|
||||
{listed.length > 0 && (
|
||||
|
|
|
|||
|
|
@ -2845,6 +2845,12 @@ button.diff-number:hover {
|
|||
.subagent-toggle:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
/* The branch count beside the subagent count: the fork icon before it. */
|
||||
.toggle-branches {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
.subagent-row {
|
||||
min-height: var(--control-sm);
|
||||
padding-top: 2px;
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { Catalog, Sessions } from "../../plugins/sessions/contract.ts";
|
|||
import { Sharing } from "../../plugins/sharing/contract.ts";
|
||||
import { SessionStatus } from "../../plugins/status/contract.ts";
|
||||
import { Subagents } from "../../plugins/subagents/contract.ts";
|
||||
import { SessionTree } from "../../plugins/tree/contract.ts";
|
||||
import { Browser, browserTests } from "./browser.ts";
|
||||
import { type Fixture, holdIn, poll, startFixture, until } from "./helpers.ts";
|
||||
|
||||
|
|
@ -184,6 +185,29 @@ describe("drawer and new-session parity", { skip: !browserTests }, () => {
|
|||
const status = await holdIn(owner, SessionStatus, {}, "reader");
|
||||
await childChat.stop(BACKGROUND_CONTEXT);
|
||||
await until(status.statuses, (states) => !states?.[pinned.id]?.busy.includes(child.conversationId));
|
||||
// With `/tree` branches as well, the one control shows the branch rows, then the subagents, and hides both.
|
||||
await until(status.statuses, (states) => states?.[pinned.id]?.tasks === 0);
|
||||
const tree = await holdIn(owner, SessionTree, { session: pinned.id }, "owner");
|
||||
const first = (await tree.tree(BACKGROUND_CONTEXT)).nodes.find((node) => node.role === "user")!;
|
||||
await tree.navigate({ entry: first.entry }, BACKGROUND_CONTEXT);
|
||||
await tree.select({ conversation: 1 }, BACKGROUND_CONTEXT);
|
||||
tree.release();
|
||||
const toggle = `document.querySelector('.subagent-toggle[aria-controls="branches-${pinned.id} subagents-${pinned.id}"]')`;
|
||||
await browser.wait(`${toggle}?.getAttribute('aria-label')==='Collapse branches and subagents'`);
|
||||
assert.deepEqual(
|
||||
await browser.evaluate<string[]>(
|
||||
`[...document.getElementById('branches-${pinned.id}').parentElement.children].slice(1).map(e=>e.id+(e.hidden?':hidden':''))`,
|
||||
),
|
||||
[`branches-${pinned.id}`, `subagents-${pinned.id}`],
|
||||
);
|
||||
await browser.evaluate(`${toggle}.click()`);
|
||||
await browser.wait(
|
||||
`document.getElementById('branches-${pinned.id}').hidden && document.getElementById('subagents-${pinned.id}').hidden`,
|
||||
);
|
||||
assert.equal(
|
||||
await browser.evaluate<string>(`${toggle}.getAttribute('aria-label')`),
|
||||
"Expand branches and subagents",
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves rename, pin and archive after moving actions out of session rows", async () => {
|
||||
|
|
|
|||
|
|
@ -45,8 +45,9 @@ describe("branches in the browser", { skip: !browserTests }, () => {
|
|||
await browser.metrics(1280, 900);
|
||||
await browser.open(`http://127.0.0.1:${fixture.server.port}/s/${id}`);
|
||||
await browser.wait(`document.querySelector('.messages')?.textContent.includes('Echo: second')`);
|
||||
// One branch: no branch rows yet.
|
||||
// One branch: no branch rows and no expand control yet.
|
||||
assert.equal(await browser.evaluate<number>(`document.querySelectorAll('.branch-row').length`), 0);
|
||||
assert.equal(await browser.evaluate<number>(`document.querySelectorAll('.subagent-toggle').length`), 0);
|
||||
await browser.evaluate(
|
||||
`[...document.querySelectorAll('.user')].find(e=>e.textContent.includes('second')).querySelector('.edit-button').click()`,
|
||||
);
|
||||
|
|
@ -58,8 +59,16 @@ describe("branches in the browser", { skip: !browserTests }, () => {
|
|||
await browser.wait(
|
||||
`document.querySelector('.messages')?.textContent.includes('Echo: first') && !document.querySelector('.messages').textContent.includes('Echo: second')`,
|
||||
);
|
||||
// The fork has a row at once, newest first, labelled with the edited message; conversation 1 shows the title.
|
||||
// The fork has a row at once, newest first, labelled with the edited message; conversation 1 shows the title. The
|
||||
// rows wait collapsed behind the session's expand control (here without subagents), which shows them.
|
||||
await browser.wait(`document.querySelectorAll('.branch-row').length===2`);
|
||||
const branches = `document.getElementById('branches-${id}')`;
|
||||
const toggle = `document.querySelector('.subagent-toggle[aria-controls="branches-${id}"]')`;
|
||||
assert.equal(await browser.evaluate<boolean>(`${branches}.hidden`), true);
|
||||
assert.equal(await browser.evaluate<string>(`${toggle}.getAttribute('aria-label')`), "Expand branches");
|
||||
assert.equal(await browser.evaluate<string>(`${toggle}.textContent`), "2");
|
||||
await browser.evaluate(`${toggle}.click()`);
|
||||
await browser.wait(`!${branches}.hidden && ${toggle}.getAttribute('aria-expanded')==='true'`);
|
||||
assert.deepEqual(
|
||||
await browser.evaluate<string[]>(`[...document.querySelectorAll('.branch-row')].map(e=>e.textContent)`),
|
||||
["secondcurrent", "Branches"],
|
||||
|
|
@ -72,10 +81,16 @@ describe("branches in the browser", { skip: !browserTests }, () => {
|
|||
await browser.wait(`location.pathname==='/s/${id}'`);
|
||||
await browser.wait(`document.querySelector('.messages')?.textContent.includes('Echo: second')`);
|
||||
assert.equal((await row()).main, 1);
|
||||
// The rows keep their order: only the "current" mark moves.
|
||||
// The rows keep their order: only the "current" mark moves. They stay expanded across the switch.
|
||||
await browser.wait(
|
||||
`[...document.querySelectorAll('.branch-row')].map(e=>e.textContent).join('|')==='second|Branchescurrent'`,
|
||||
);
|
||||
assert.equal(await browser.evaluate<boolean>(`${branches}.hidden`), false);
|
||||
// Collapsing hides them again.
|
||||
await browser.evaluate(`${toggle}.click()`);
|
||||
await browser.wait(`${branches}.hidden && ${toggle}.getAttribute('aria-label')==='Expand branches'`);
|
||||
await browser.evaluate(`${toggle}.click()`);
|
||||
await browser.wait(`!${branches}.hidden`);
|
||||
});
|
||||
|
||||
it("renames a branch from its row; conversation 1 follows the session title until renamed", async () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue