Skip to content

Commit dc93641

Browse files
authored
fix: Persist new task draft across navigation (#1436)
## Problem Task input draft in command center cells is lost when navigating away because the creating state uses local useState that resets on unmount. ## Changes 1. Move isCreating state to persisted creatingCells in command center store 2. Give each cell a unique draft session ID (cc-cell-{index}) to avoid collisions 3. Accept optional sessionId prop on TaskInput for cell-specific drafts 4. Clean up creating state and drafts on task assignment, cancel and layout resize ## How did you test this? Manually
1 parent b6832ad commit dc93641

File tree

3 files changed

+78
-19
lines changed

3 files changed

+78
-19
lines changed

apps/code/src/renderer/features/command-center/components/CommandCenterPanel.tsx

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1+
import { useDraftStore } from "@features/message-editor/stores/draftStore";
12
import { TaskInput } from "@features/task-detail/components/TaskInput";
23
import { ArrowsOut, Plus, X } from "@phosphor-icons/react";
34
import { Flex, Text } from "@radix-ui/themes";
45
import type { Task } from "@shared/types";
56
import { useNavigationStore } from "@stores/navigationStore";
6-
import { useCallback, useState } from "react";
7+
import { useCallback, useEffect, useRef, useState } from "react";
78
import type { CommandCenterCellData } from "../hooks/useCommandCenterData";
8-
import { useCommandCenterStore } from "../stores/commandCenterStore";
9+
import {
10+
getCellSessionId,
11+
useCommandCenterStore,
12+
} from "../stores/commandCenterStore";
913
import { CommandCenterSessionView } from "./CommandCenterSessionView";
1014
import { StatusBadge } from "./StatusBadge";
1115
import { TaskSelector } from "./TaskSelector";
@@ -17,16 +21,37 @@ interface CommandCenterPanelProps {
1721

1822
function EmptyCell({ cellIndex }: { cellIndex: number }) {
1923
const [selectorOpen, setSelectorOpen] = useState(false);
20-
const [isCreating, setIsCreating] = useState(false);
24+
const isCreating = useCommandCenterStore((s) =>
25+
s.creatingCells.includes(cellIndex),
26+
);
2127
const assignTask = useCommandCenterStore((s) => s.assignTask);
28+
const startCreating = useCommandCenterStore((s) => s.startCreating);
29+
const stopCreating = useCommandCenterStore((s) => s.stopCreating);
30+
const clearDraft = useDraftStore((s) => s.actions.setDraft);
31+
32+
const sessionId = getCellSessionId(cellIndex);
2233

2334
const handleTaskCreated = useCallback(
2435
(task: Task) => {
2536
assignTask(cellIndex, task.id);
37+
clearDraft(sessionId, null);
2638
},
27-
[assignTask, cellIndex],
39+
[assignTask, cellIndex, clearDraft, sessionId],
2840
);
2941

42+
const handleCancel = useCallback(() => {
43+
stopCreating(cellIndex);
44+
clearDraft(sessionId, null);
45+
}, [stopCreating, cellIndex, clearDraft, sessionId]);
46+
47+
const wasCreatingRef = useRef(false);
48+
useEffect(() => {
49+
if (wasCreatingRef.current && !isCreating) {
50+
clearDraft(sessionId, null);
51+
}
52+
wasCreatingRef.current = isCreating;
53+
}, [isCreating, clearDraft, sessionId]);
54+
3055
if (isCreating) {
3156
return (
3257
<Flex direction="column" height="100%">
@@ -46,15 +71,15 @@ function EmptyCell({ cellIndex }: { cellIndex: number }) {
4671
</Text>
4772
<button
4873
type="button"
49-
onClick={() => setIsCreating(false)}
74+
onClick={handleCancel}
5075
className="flex h-5 w-5 items-center justify-center rounded text-gray-10 transition-colors hover:bg-gray-4 hover:text-gray-12"
5176
title="Cancel"
5277
>
5378
<X size={12} />
5479
</button>
5580
</Flex>
5681
<Flex direction="column" className="min-h-0 flex-1">
57-
<TaskInput onTaskCreated={handleTaskCreated} />
82+
<TaskInput sessionId={sessionId} onTaskCreated={handleTaskCreated} />
5883
</Flex>
5984
</Flex>
6085
);
@@ -67,7 +92,7 @@ function EmptyCell({ cellIndex }: { cellIndex: number }) {
6792
cellIndex={cellIndex}
6893
open={selectorOpen}
6994
onOpenChange={setSelectorOpen}
70-
onNewTask={() => setIsCreating(true)}
95+
onNewTask={() => startCreating(cellIndex)}
7196
>
7297
<button
7398
type="button"

apps/code/src/renderer/features/command-center/stores/commandCenterStore.ts

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ interface CommandCenterStoreState {
2424
cells: (string | null)[];
2525
activeTaskId: string | null;
2626
zoom: number;
27+
creatingCells: number[];
2728
}
2829

2930
interface CommandCenterStoreActions {
@@ -36,6 +37,8 @@ interface CommandCenterStoreActions {
3637
setZoom: (zoom: number) => void;
3738
zoomIn: () => void;
3839
zoomOut: () => void;
40+
startCreating: (cellIndex: number) => void;
41+
stopCreating: (cellIndex: number) => void;
3942
}
4043

4144
type CommandCenterStore = CommandCenterStoreState & CommandCenterStoreActions;
@@ -57,24 +60,33 @@ function clampZoom(value: number): number {
5760
return Math.round(Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, value)) * 10) / 10;
5861
}
5962

63+
export function getCellSessionId(cellIndex: number): string {
64+
return `cc-cell-${cellIndex}`;
65+
}
66+
6067
export const useCommandCenterStore = create<CommandCenterStore>()(
6168
persist(
6269
(set) => ({
6370
layout: "2x2",
6471
cells: [null, null, null, null],
6572
activeTaskId: null,
6673
zoom: 1,
74+
creatingCells: [],
6775

6876
setLayout: (preset) =>
69-
set((state) => ({
70-
activeTaskId: resizeCells(state.cells, getCellCount(preset)).includes(
71-
state.activeTaskId,
72-
)
73-
? state.activeTaskId
74-
: null,
75-
layout: preset,
76-
cells: resizeCells(state.cells, getCellCount(preset)),
77-
})),
77+
set((state) => {
78+
const newCount = getCellCount(preset);
79+
return {
80+
activeTaskId: resizeCells(state.cells, newCount).includes(
81+
state.activeTaskId,
82+
)
83+
? state.activeTaskId
84+
: null,
85+
layout: preset,
86+
cells: resizeCells(state.cells, newCount),
87+
creatingCells: state.creatingCells.filter((i) => i < newCount),
88+
};
89+
}),
7890

7991
setActiveTask: (taskId) => set({ activeTaskId: taskId }),
8092

@@ -87,7 +99,11 @@ export const useCommandCenterStore = create<CommandCenterStore>()(
8799
cells[existingIndex] = null;
88100
}
89101
cells[cellIndex] = taskId;
90-
return { cells, activeTaskId: taskId };
102+
return {
103+
cells,
104+
activeTaskId: taskId,
105+
creatingCells: state.creatingCells.filter((i) => i !== cellIndex),
106+
};
91107
}),
92108

93109
removeTask: (cellIndex) =>
@@ -121,13 +137,26 @@ export const useCommandCenterStore = create<CommandCenterStore>()(
121137
set((state) => ({
122138
activeTaskId: null,
123139
cells: state.cells.map(() => null),
140+
creatingCells: [],
124141
})),
125142

126143
setZoom: (zoom) => set({ zoom: clampZoom(zoom) }),
127144
zoomIn: () =>
128145
set((state) => ({ zoom: clampZoom(state.zoom + ZOOM_STEP) })),
129146
zoomOut: () =>
130147
set((state) => ({ zoom: clampZoom(state.zoom - ZOOM_STEP) })),
148+
149+
startCreating: (cellIndex) =>
150+
set((state) => ({
151+
creatingCells: state.creatingCells.includes(cellIndex)
152+
? state.creatingCells
153+
: [...state.creatingCells, cellIndex],
154+
})),
155+
156+
stopCreating: (cellIndex) =>
157+
set((state) => ({
158+
creatingCells: state.creatingCells.filter((i) => i !== cellIndex),
159+
})),
131160
}),
132161
{
133162
name: "command-center-storage",
@@ -137,6 +166,7 @@ export const useCommandCenterStore = create<CommandCenterStore>()(
137166
cells: state.cells,
138167
activeTaskId: state.activeTaskId,
139168
zoom: state.zoom,
169+
creatingCells: state.creatingCells,
140170
}),
141171
},
142172
),

apps/code/src/renderer/features/task-detail/components/TaskInput.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,14 @@ import { type WorkspaceMode, WorkspaceModeSelect } from "./WorkspaceModeSelect";
4040
const DOT_FILL = "var(--gray-6)";
4141

4242
interface TaskInputProps {
43+
sessionId?: string;
4344
onTaskCreated?: (task: import("@shared/types").Task) => void;
4445
}
4546

46-
export function TaskInput({ onTaskCreated }: TaskInputProps = {}) {
47+
export function TaskInput({
48+
sessionId = "task-input",
49+
onTaskCreated,
50+
}: TaskInputProps = {}) {
4751
const { cloudRegion } = useAuthStore();
4852
const trpcReact = useTRPC();
4953
const { view } = useNavigationStore();
@@ -441,7 +445,7 @@ export function TaskInput({ onTaskCreated }: TaskInputProps = {}) {
441445

442446
<TaskInputEditor
443447
ref={editorRef}
444-
sessionId="task-input"
448+
sessionId={sessionId}
445449
repoPath={selectedDirectory}
446450
isCreatingTask={isCreatingTask}
447451
canSubmit={canSubmit}

0 commit comments

Comments
 (0)