-
- What should the agent do?
-
+ {!docked && (
+
+ What should the agent do?
+
+ )}
({ value: path, label: basename(path), detail: path }))}
onChange={(path) => {
@@ -139,12 +145,15 @@ export function HomeEditor() {
}}
/>
) : (
- {members.length ? basename(root) : project?.name}
+
+ {members.length ? basename(root) : project?.name}
+
)}
{isGit && (
- {probe && (
+ {probe && (!docked || probe.error) && (
{probe.error
? txTemplate(`${provider?.name ?? probe.provider} didn't start: ${probe.error}`)
diff --git a/src/features/agents/Session.module.css b/src/features/agents/Session.module.css
index 5b104f185..a28570ef3 100644
--- a/src/features/agents/Session.module.css
+++ b/src/features/agents/Session.module.css
@@ -16,6 +16,20 @@
border-right: 1px solid var(--border);
}
+.conversation[hidden] {
+ display: none;
+}
+.docked {
+ grid-template-columns: minmax(0, 1fr);
+ overflow: hidden;
+}
+.docked .conversation {
+ border-right: 0;
+}
+.docked .changes {
+ width: auto;
+}
+
/* The conversation hugs its content; spare space stays below the composer
instead of inside the chat. */
.streamArea,
@@ -568,6 +582,7 @@
width: 280px;
}
.changesHeader {
+ min-width: 0;
display: flex;
align-items: center;
gap: 6px;
@@ -577,11 +592,17 @@
border-bottom: 1px solid var(--border);
}
.changesTitle {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
font-weight: 600;
color: var(--fg-strong);
}
.changesSummary {
flex: none;
+ overflow: hidden;
+ text-overflow: ellipsis;
padding: 4px 10px;
color: var(--fg-subtle);
white-space: pre;
@@ -608,8 +629,11 @@
gap: 4px;
}
.changeName {
- flex: none;
+ flex: 0 1 auto;
+ min-width: 0;
white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
.changeDir {
flex: 1 1 0;
diff --git a/src/features/agents/SessionEditor.tsx b/src/features/agents/SessionEditor.tsx
index 00d2ae510..deddc4c4c 100644
--- a/src/features/agents/SessionEditor.tsx
+++ b/src/features/agents/SessionEditor.tsx
@@ -68,15 +68,13 @@ import { setFocusedSession, focusedSession, type FocusedSession } from "./sessio
import { taskMenu } from "./taskActions";
import styles from "./Session.module.css";
-export function SessionEditor({ input }: { input: EditorInput }) {
+export function SessionEditor({ input, docked = false }: { input: EditorInput; docked?: boolean }) {
const id = (input.data as { task: string }).task;
- return ;
+ return ;
}
-function TaskSession({ id }: { id: string }) {
+function TaskSession({ id, docked }: { id: string; docked: boolean }) {
const task = useAgents((s) => s.tasks.get(id));
- // The tool window bar above already shows the title of a docked conversation.
- const docked = useAgents((s) => s.docked === id);
const entries = useAgents((s) => s.entries.get(id));
const changes = useAgents((s) => s.changes.get(id));
const project = useProject((s) => s.project);
@@ -84,7 +82,7 @@ function TaskSession({ id }: { id: string }) {
task?.isolation === "local" ? task.status : null,
project?.workspace ? (task?.projectRoot ?? project.root) : null,
);
- const [changesOpen, setChangesOpen] = useChangesOpen(id, changes ?? null);
+ const [changesOpen, setChangesOpen] = useChangesOpen(id, changes ?? null, docked);
// 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;
@@ -96,8 +94,10 @@ function TaskSession({ id }: { id: string }) {
useEffect(() => {
void loadEntries(id).catch(() => undefined);
void refreshChanges(id);
- return markVisible(id);
}, [id]);
+ useEffect(() => {
+ if (!docked || !changesOpen) return markVisible(id);
+ }, [id, docked, changesOpen]);
useEffect(() => {
if (view) void loadEntries(id, view).catch(() => undefined);
}, [id, view]);
@@ -127,7 +127,7 @@ function TaskSession({ id }: { id: string }) {
}
}}
>
-
+
{changesOpen && (
- setChangesOpen(false)} />
+ setChangesOpen(false)}
+ />
)}
);
@@ -270,6 +276,7 @@ function Header({
className={styles.changesToggle}
aria-pressed={changesOpen}
disabled={count === 0 && !changesOpen}
+ aria-label={changesOpen ? tx("Hide changes") : tx("Show changes")}
title={changesOpen ? tx("Hide changes") : tx("Show changes")}
onClick={onToggleChanges}
>
@@ -1292,16 +1299,22 @@ function loadChangesOpen(): Record
{
/**
* 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).
+ * only while the task has changes. Docked sessions stay in the conversation until
+ * the user explicitly opens changes; navigation there does not change editor preferences.
*/
-function useChangesOpen(task: string, changes: AgentChange[] | null): [boolean, (open: boolean) => void] {
+function useChangesOpen(
+ task: string,
+ changes: AgentChange[] | null,
+ docked: boolean,
+): [boolean, (open: boolean) => void] {
const [state, setState] = useState<{ task: string; open: boolean | undefined }>(() => ({
task,
- open: loadChangesOpen()[task],
+ open: docked ? false : loadChangesOpen()[task],
}));
- const chosen = state.task === task ? state.open : loadChangesOpen()[task];
+ const chosen = state.task === task ? state.open : docked ? false : loadChangesOpen()[task];
const set = (open: boolean) => {
setState({ task, open });
+ if (docked) return;
try {
globalThis.localStorage?.setItem(CHANGES_OPEN_KEY, JSON.stringify({ ...loadChangesOpen(), [task]: open }));
} catch {
@@ -1315,11 +1328,13 @@ function ChangesPane({
task,
changes,
head,
+ docked,
onHide,
}: {
task: AgentTask;
changes: AgentChange[] | null;
head: string | null;
+ docked: boolean;
onHide: () => void;
}) {
const [selected, setSelected] = useState>(() => new Set());
@@ -1328,6 +1343,7 @@ function ChangesPane({
return (