diff --git a/src/features/agents/Session.module.css b/src/features/agents/Session.module.css index 4cb8e2ae6..9fa61ba0e 100644 --- a/src/features/agents/Session.module.css +++ b/src/features/agents/Session.module.css @@ -560,41 +560,6 @@ min-width: 0; width: 280px; } -.changesCollapsed { - width: 28px; - align-items: center; - padding-top: 4px; -} -.changesCollapsed > * { - flex: none; -} -.changesRail { - flex: 1; - min-height: 0; - align-self: stretch; - display: flex; - flex-direction: column; - align-items: center; - gap: 6px; - padding: 4px 0 8px; - border: none; - background: transparent; - color: var(--fg-subtle); - font: inherit; - cursor: pointer; -} -.changesRail:hover { - color: var(--fg-strong); -} -.changesRailLabel { - writing-mode: vertical-rl; - font-size: var(--fs-small); - letter-spacing: 0.02em; -} -.changesRailCount { - font-size: var(--fs-small); - color: var(--fg-muted); -} .changesHeader { display: flex; align-items: center; @@ -844,3 +809,30 @@ overflow: hidden; text-overflow: ellipsis; } + +.changesToggle { + display: inline-flex; + align-items: center; + gap: 4px; + height: 22px; + padding: 0 6px; + border: 0; + border-radius: var(--radius-small); + background: transparent; + color: var(--fg-subtle); + font: inherit; + font-size: var(--fs-small); + cursor: pointer; +} +.changesToggle:hover:not(:disabled) { + background: var(--bg-hover); + color: var(--fg-strong); +} +.changesToggle[aria-pressed="true"] { + background: var(--bg-selected); + color: var(--fg-strong); +} +.changesToggle:disabled { + opacity: 0.4; + cursor: default; +} diff --git a/src/features/agents/SessionEditor.tsx b/src/features/agents/SessionEditor.tsx index eace379a5..e00d2502b 100644 --- a/src/features/agents/SessionEditor.tsx +++ b/src/features/agents/SessionEditor.tsx @@ -46,7 +46,6 @@ import { type SubagentEntry, } from "./model"; import { - archiveTask, cancelTask, loadEntries, markVisible, @@ -65,6 +64,7 @@ import { useAgents, } from "./store"; import { setFocusedSession, focusedSession, type FocusedSession } from "./sessionFocus"; +import { taskMenu } from "./taskActions"; import styles from "./Session.module.css"; export function SessionEditor({ input }: { input: EditorInput }) { @@ -73,6 +73,7 @@ export function SessionEditor({ input }: { input: EditorInput }) { const entries = useAgents((s) => s.entries.get(id)); const changes = useAgents((s) => s.changes.get(id)); const head = useLocalHead(task?.isolation === "local" ? task.status : null); + const [changesOpen, setChangesOpen] = useChangesOpen(id, changes ?? null); // The subagent conversation shown, with the subagents that lead to it (empty: the task's own). const [path, setPath] = useState([]); const view = path.at(-1) ?? null; @@ -116,7 +117,14 @@ export function SessionEditor({ input }: { input: EditorInput }) { }} >
-
+
setChangesOpen(!changesOpen)} + /> {view && } )}
- + {changesOpen && ( + setChangesOpen(false)} /> + )} ); } @@ -171,11 +181,32 @@ function useLocalHead(status: AgentTask["status"] | null): string | null { return local ? head : null; } -function Header({ task, entries, head }: { task: AgentTask; entries: AgentEntry[] | null; head: string | null }) { +function Header({ + task, + entries, + head, + changes, + changesOpen, + onToggleChanges, +}: { + task: AgentTask; + entries: AgentEntry[] | null; + head: string | null; + changes: AgentChange[] | null; + changesOpen: boolean; + onToggleChanges: () => void; +}) { + const count = changes?.length ?? 0; const busy = isBusy(task.status); const where = task.isolation === "worktree" ? branchLabel(task.branch) : null; return ( -
+
{ + e.preventDefault(); + showContextMenu(e, taskMenu([task])); + }} + > {task.title} @@ -205,18 +236,6 @@ function Header({ task, entries, head }: { task: AgentTask; entries: AgentEntry[ size="small" onClick={() => void executeAction("terminal.openAt", task.cwd)} /> - {!busy && task.status !== "archived" && task.capabilities?.fork && ( - - void forkTask(task.id, null).catch((e: unknown) => - notifications().notify({ level: "error", title: "Couldn't copy the task", body: String(e) }), - ) - } - /> - )} {task.status === "archived" && ( )} - {!busy && task.status !== "archived" && ( - void archiveTask(task.id)} /> - )} +
); } @@ -1216,58 +1243,53 @@ const KIND_COLOR: Partial> = { conflicted: "var(--vcs-conflict)", }; -const CHANGES_COLLAPSED_KEY = "openidea.agents.changesCollapsed.v1"; +const CHANGES_OPEN_KEY = "openidea.agents.changesOpen.v1"; -function loadCollapsed(): boolean { +function loadChangesOpen(): Record { try { - const raw = globalThis.localStorage?.getItem(CHANGES_COLLAPSED_KEY); - // Collapsed on first run: the chat is the focus, the diff opens on demand. - return raw === null ? true : (JSON.parse(raw) as boolean); + const raw = globalThis.localStorage?.getItem(CHANGES_OPEN_KEY); + const v: unknown = raw ? JSON.parse(raw) : {}; + return v && typeof v === "object" ? (v as Record) : {}; } catch { - return true; + return {}; } } -function saveCollapsed(value: boolean) { - try { - globalThis.localStorage?.setItem(CHANGES_COLLAPSED_KEY, JSON.stringify(value)); - } catch { - /* storage unavailable */ - } +/** + * Whether the Changes pane is shown for `task`: the user's choice once made, else + * only while the task has changes (so it opens when the agent first edits a file). + */ +function useChangesOpen(task: string, changes: AgentChange[] | null): [boolean, (open: boolean) => void] { + const [state, setState] = useState<{ task: string; open: boolean | undefined }>(() => ({ + task, + open: loadChangesOpen()[task], + })); + const chosen = state.task === task ? state.open : loadChangesOpen()[task]; + const set = (open: boolean) => { + setState({ task, open }); + try { + globalThis.localStorage?.setItem(CHANGES_OPEN_KEY, JSON.stringify({ ...loadChangesOpen(), [task]: open })); + } catch { + /* storage unavailable */ + } + }; + return [chosen ?? (changes?.length ?? 0) > 0, set]; } -function ChangesPane({ task, changes, head }: { task: AgentTask; changes: AgentChange[] | null; head: string | null }) { +function ChangesPane({ + task, + changes, + head, + onHide, +}: { + task: AgentTask; + changes: AgentChange[] | null; + head: string | null; + onHide: () => void; +}) { const [selected, setSelected] = useState>(() => new Set()); - const [collapsed, setCollapsed] = useState(loadCollapsed); const added = changes?.reduce((n, c) => n + c.added, 0) ?? 0; const removed = changes?.reduce((n, c) => n + c.removed, 0) ?? 0; - const toggleCollapsed = (next: boolean) => { - setCollapsed(next); - saveCollapsed(next); - }; - if (collapsed) { - return ( - - ); - } return (