Skip to content

Commit 177da94

Browse files
Test UserCopilot
andcommitted
fix: preserve kanban pre-session autonomy state
Persist provisional autonomous mode in panel hot state so optimistic kanban cards reflect the composer toggle before a real session exists. Also mark needs-review kanban items seen once that column is already surfacing them. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent dc6d969 commit 177da94

6 files changed

Lines changed: 59 additions & 5 deletions

File tree

packages/desktop/src/lib/acp/components/__tests__/agent-input-toolbar-structure.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ describe("agent input toolbar structure", () => {
2828
expect(agentInputSource).toContain("handleAutonomousToggle");
2929
expect(agentInputSource).toContain("sessionStore.setAutonomousEnabled");
3030
expect(agentInputSource).toContain("initialAutonomousEnabled");
31+
expect(agentInputSource).toContain("panelStore.setProvisionalAutonomousEnabled");
3132
});
3233

3334
it("renders the Autonomous toggle with a Phosphor robot icon that fills when enabled", () => {

packages/desktop/src/lib/acp/components/agent-input/agent-input-ui.svelte

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,13 @@ const autonomousSupportState = $derived.by(() =>
250250
})
251251
);
252252
253-
let provisionalAutonomousEnabled = $state(false);
253+
const provisionalAutonomousEnabled = $derived.by(() => {
254+
if (props.panelId) {
255+
return panelStore.getHotState(props.panelId).provisionalAutonomousEnabled;
256+
}
257+
258+
return false;
259+
});
254260
255261
const autonomousToggleActive = $derived(
256262
sessionHotState ? sessionHotState.autonomousEnabled : provisionalAutonomousEnabled
@@ -1388,7 +1394,9 @@ async function handleModeChange(modeId: string) {
13881394
agents: agentStore.agents,
13891395
}).supported
13901396
) {
1391-
provisionalAutonomousEnabled = false;
1397+
if (props.panelId) {
1398+
panelStore.setProvisionalAutonomousEnabled(props.panelId, false);
1399+
}
13921400
autonomousStatusMessage =
13931401
"Autonomous turned off because this mode is unsupported for the current agent.";
13941402
}
@@ -1400,8 +1408,12 @@ async function handleAutonomousToggle(): Promise<void> {
14001408
}
14011409
14021410
if (!props.sessionId) {
1403-
provisionalAutonomousEnabled = !provisionalAutonomousEnabled;
1404-
if (!provisionalAutonomousEnabled) {
1411+
if (!props.panelId) {
1412+
return;
1413+
}
1414+
const nextEnabled = !provisionalAutonomousEnabled;
1415+
panelStore.setProvisionalAutonomousEnabled(props.panelId, nextEnabled);
1416+
if (!nextEnabled) {
14051417
autonomousStatusMessage = "Future actions now require approval again.";
14061418
}
14071419
return;

packages/desktop/src/lib/acp/store/panel-store.svelte.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1072,6 +1072,10 @@ export class PanelStore {
10721072
return this.getHotState(panelId).messageDraft;
10731073
}
10741074

1075+
setProvisionalAutonomousEnabled(panelId: string, enabled: boolean): void {
1076+
this.updateHotState(panelId, { provisionalAutonomousEnabled: enabled });
1077+
}
1078+
10751079
setPendingComposerRestore(
10761080
panelId: string,
10771081
restore: NonNullable<PanelHotState["pendingComposerRestore"]>

packages/desktop/src/lib/acp/store/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,8 @@ export interface PanelHotState {
166166
readonly planSidebarExpanded: boolean;
167167
/** Draft message text in the input field */
168168
readonly messageDraft: string;
169+
/** Provisional autonomous state used before a real session exists. */
170+
readonly provisionalAutonomousEnabled: boolean;
169171
/** Pending full composer restore after a failed first-send handoff. */
170172
readonly pendingComposerRestore: ComposerRestoreSnapshot | null;
171173
/** Monotonic version used to remount the composer when restore data is queued. */
@@ -197,6 +199,7 @@ export const DEFAULT_PANEL_HOT_STATE: PanelHotState = {
197199
browserSidebarExpanded: false,
198200
browserSidebarUrl: null,
199201
messageDraft: "",
202+
provisionalAutonomousEnabled: false,
200203
pendingComposerRestore: null,
201204
composerRestoreVersion: 0,
202205
embeddedTerminalDrawerOpen: false,

packages/desktop/src/lib/components/main-app-view/components/content/kanban-view.svelte

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,18 @@ const threadBoardSources = $derived.by((): readonly ThreadBoardSource[] => {
350350
351351
const threadBoard = $derived.by(() => buildThreadBoard(threadBoardSources));
352352
353+
$effect(() => {
354+
for (const group of threadBoard) {
355+
if (group.status !== "needs_review") {
356+
continue;
357+
}
358+
359+
for (const item of group.items) {
360+
unseenStore.markSeen(item.panelId);
361+
}
362+
}
363+
});
364+
353365
function mapItemToCard(item: ThreadBoardItem): KanbanCardData {
354366
const isWorking = isActiveCompactActivityKind(item.state.activity.kind);
355367
const todoProgress = item.todoProgress
@@ -577,7 +589,7 @@ function buildOptimisticKanbanCards(): readonly OptimisticKanbanCard[] {
577589
title,
578590
agentIconSrc: getAgentIcon(panel.selectedAgentId, themeState.effectiveTheme),
579591
agentLabel: panel.selectedAgentId,
580-
isAutoMode: false,
592+
isAutoMode: hotState.provisionalAutonomousEnabled,
581593
projectName: project ? project.name : m.project_unknown(),
582594
projectColor: project ? project.color : Colors[COLOR_NAMES.PINK],
583595
activityText,

packages/desktop/src/lib/components/main-app-view/components/content/kanban-view.svelte.vitest.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,16 @@ describe("kanban empty-column contract", () => {
8181
expect(source).not.toContain("<PendingPermissionCard permission={permission} />");
8282
});
8383

84+
it("mirrors provisional autonomous state onto optimistic kanban cards", () => {
85+
expect(existsSync(kanbanViewPath)).toBe(true);
86+
if (!existsSync(kanbanViewPath)) return;
87+
88+
const source = readFileSync(kanbanViewPath, "utf8");
89+
90+
expect(source).toContain("const hotState = panelStore.getHotState(panel.id);");
91+
expect(source).toContain("isAutoMode: hotState.provisionalAutonomousEnabled");
92+
});
93+
8494
it("omits the kanban footer wrapper when there is no footer content", () => {
8595
expect(existsSync(kanbanViewPath)).toBe(true);
8696
if (!existsSync(kanbanViewPath)) return;
@@ -217,6 +227,18 @@ describe("kanban empty-column contract", () => {
217227
expect(source).toContain("hasUnseenCompletion,");
218228
});
219229

230+
it("marks needs-review items seen when kanban is already surfacing them", () => {
231+
expect(existsSync(kanbanViewPath)).toBe(true);
232+
if (!existsSync(kanbanViewPath)) return;
233+
234+
const source = readFileSync(kanbanViewPath, "utf8");
235+
236+
expect(source).toContain("const unseenStore = getUnseenStore();");
237+
expect(source).toContain("$effect(() => {");
238+
expect(source).toContain('if (group.status !== "needs_review") {');
239+
expect(source).toContain("unseenStore.markSeen(item.panelId);");
240+
});
241+
220242
it("maps live kanban tool rows to a simple verb plus optional file chip", () => {
221243
expect(existsSync(kanbanViewPath)).toBe(true);
222244
if (!existsSync(kanbanViewPath)) return;

0 commit comments

Comments
 (0)