Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions apps/desktop/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -619,10 +619,9 @@ export function Sidebar({
: sessions.filter(
(session) => !sessionArchived(session, sessionMeta[session.id]),
);
// Empty sessions are durable sidebar rows now. Their message count, not
// their title, controls New Task reuse, so a manual rename never changes
// the empty-slot behavior.
return candidates;
// New-task drafts stay out of history until they carry input: sessions
// with only a default title (including legacy empty drafts) never render.
return candidates.filter((session) => !isDefaultSessionTitle(session.title));
}, [sessions, showArchived, sessionMeta]);

const compareSessions = useCallback((a: SessionSummary, b: SessionSummary) => {
Expand Down
40 changes: 18 additions & 22 deletions apps/desktop/src/stores/slices/session-slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
sessionNeedsModelPin,
} from "../../lib/session-model";
import {
clearSessionPanes,
retainSessionPane,
} from "../../lib/session-panes";
import {
Expand Down Expand Up @@ -476,28 +477,23 @@ export function createSessionSlice({
if (!runtime.navigationIntentIsCurrent(intent)) return;
}

const latest = runtime.latestSessionInScope(
get().sessions,
requestedProjectPath,
get().sessionMeta,
);
if (
latest &&
sessionIsReusableEmpty(latest, {
running: get().runningSessions[latest.id] === true,
liveMessageCount: runtime.liveMessageCountForSession(latest.id, get()),
submitted: runtime.submittedComposerDrafts.has(latest.id),
})
) {
if (get().activeSessionId === latest.id && get().page === "chat") return;
await get().selectSession(latest.id, { navigationIntent: intent });
return;
}

await persistSessionAndSelect({
intent,
projectPath: requestedProjectPath,
draftConfiguration: null,
// New task starts as an unpersisted draft (ADR 0084): no session is created
// and no sidebar history row appears until the first message materializes it.
// The draft stays within its requested project scope, if any.
set((s) => {
const stack = s.navStack.slice(0, s.navIndex + 1);
const nextStack = [...stack, { page: "chat" as const }].slice(-50);
return {
...switchWorkPanelSession(s, undefined),
...clearSessionPanes(),
activeSessionId: undefined,
draftConfiguration: null,
messages: [],
page: "chat" as const,
navStack: nextStack,
navIndex: nextStack.length - 1,
isRunning: false,
};
});
})();
runtime.pendingNewSessionRequests.set(scopeKey, request);
Expand Down
7 changes: 3 additions & 4 deletions apps/desktop/test/app-store-sidebar.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,12 @@ test("project new-session creation uses one store-owned navigation transaction",
);
});

test("durable empty sessions render and title heuristics do not filter them", () => {
assert.doesNotMatch(
test("new-task drafts stay out of sidebar history until the first message", () => {
assert.match(
sidebarSource,
/candidates\.filter\(\(session\) => !isDefaultSessionTitle\(session\.title\)\)/,
);
assert.match(storeSource, /latestSessionInScope/);
assert.match(storeSource, /sessionIsReusableEmpty/);
assert.match(storeSource, /New task starts as an unpersisted draft/);
assert.match(storeSource, /pendingNewSessionRequests/);
assert.doesNotMatch(sidebarSource, /keptEmptyScopes/);
});
Expand Down
16 changes: 4 additions & 12 deletions apps/desktop/test/composer-send-state.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -189,26 +189,18 @@ test("editing a queued prompt needs an empty composer and restores its draft", (
assert.doesNotMatch(queueSlice, /text: item\.content/);
});

test("new task persists or reuses an empty session and keeps the run flag scoped", () => {
test("new task opens an unpersisted draft and keeps the run flag scoped", () => {
const newSession = sessionSlice.match(
/newSession: async [\s\S]*?\n forkSession: async/,
)?.[0] ?? "";
assert.ok(newSession.length > 0, "newSession implementation not found");
assert.match(newSession, /latestSessionInScope/);
assert.match(newSession, /sessionIsReusableEmpty/);
assert.match(newSession, /persistSessionAndSelect/);
assert.match(newSession, /New task starts as an unpersisted draft/);
assert.match(newSession, /activeSessionId: undefined/);
assert.match(newSession, /pendingNewSessionRequests/);
assert.doesNotMatch(newSession, /refreshSessions/);
// A newly selected empty session uses its own run state, so a turn still
// streaming in the previous session cannot leave it stuck on the stop
// button.
assert.match(
sessionCoordination,
/async function persistSessionAndSelect[\s\S]*?\n return sessionId;\n }\n/,
);
assert.match(
sessionCoordination,
/isRunning: current\.runningSessions\[summary\.id\] \?\? false/,
/async function materializeDraftSession[\s\S]*?\n return persistSessionAndSelect\(\{ intent \}\);\n \}\n/,
);
});

Expand Down
7 changes: 3 additions & 4 deletions apps/desktop/test/session-create.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,13 @@ test("empty sessions have a whole, already-known transcript window", () => {
});
});

test("new task reuses renderer-empty sessions without a blocking list refresh", () => {
test("new task opens unpersisted draft without a blocking list refresh", () => {
const newSession =
sessionSlice.match(/newSession: async [\s\S]*?\n forkSession: async/)?.[0] ?? "";
assert.ok(newSession.length > 0, "newSession implementation not found");
assert.match(newSession, /sessionIsReusableEmpty/);
assert.match(newSession, /liveMessageCountForSession/);
assert.match(newSession, /New task starts as an unpersisted draft/);
assert.match(newSession, /activeSessionId: undefined/);
assert.doesNotMatch(newSession, /refreshSessions/);
assert.match(newSession, /persistSessionAndSelect/);
assert.match(newSession, /pendingNewSessionRequests/);
});

Expand Down
2 changes: 1 addition & 1 deletion docs/adr/0084-deferred-new-task-session-creation.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

- **Status:** Accepted
- **Date:** 2026-08-14
- **Related:** D220 · D088 · D093 · E2E-011b · E2E-011d
- **Related:** D220 · D088 · D093 · E2E-011b · E2E-011d · #746

## Context

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ADR 0113: Persist the New Task empty slot immediately and deduplicate it by message count

- **Status:** Accepted
- **Status:** Superseded by reinstatement of ADR 0084 (#746)
- **Date:** 2026-08-21
- **Supersedes:** ADR 0084 / D220
- **Related:** D088 · D093 · D305 · E2E-011b · E2E-011d · E2E-011e · E2E-011g
Expand Down
Loading