Skip to content

Commit 3743dfd

Browse files
guzusclaude
andcommitted
fix(frontend): persist completed tasks and reinforce types
- Add centralized types file with SessionId, DraftSession, Message types - Add type guards: isDraftSessionId, isGeneralSession, isTaskSession - Add session renaming for draft sessions (double-click or pencil icon) - Fix messages disappearing after task completion - Add getAllTasks() to include completed tasks in API response - Add "Claude is working..." indicator during task execution - Sort tasks by start time descending Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 1ae0201 commit 3743dfd

10 files changed

Lines changed: 224 additions & 55 deletions

File tree

frontend/src/app/page.tsx

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ import { FileViewer } from "@/components/chat/file-viewer";
88
import { SettingsView } from "@/components/chat/settings-view";
99
import { ChatHeader } from "@/components/chat/chat-header";
1010
import { WelcomeSection } from "@/components/chat/welcome-section";
11-
import { MessageItem, type Message } from "@/components/chat/message-item";
11+
import { MessageItem } from "@/components/chat/message-item";
1212
import { TypingIndicator } from "@/components/chat/typing-indicator";
1313
import { ChatInput } from "@/components/chat/chat-input";
14+
import { type Message, isDraftSessionId, isGeneralSession } from "@/lib/types";
1415

1516
export default function HomePage() {
1617
const [messages, setMessages] = useState<Message[]>([]);
@@ -26,10 +27,13 @@ export default function HomePage() {
2627
try {
2728
const data = await api.getTasks();
2829
setTasks(data);
29-
// Clear pending message if it now exists in tasks
30+
// Clear pending message only if task exists AND has output (response received)
3031
setPendingMessage((prev) => {
31-
if (prev && data.some((t) => t.prompt === prev.content)) {
32-
return null;
32+
if (prev) {
33+
const matchingTask = data.find((t) => t.prompt === prev.content);
34+
if (matchingTask && matchingTask.output) {
35+
return null;
36+
}
3337
}
3438
return prev;
3539
});
@@ -46,17 +50,14 @@ export default function HomePage() {
4650

4751
// Update messages based on active session
4852
useEffect(() => {
49-
// Check if this is a draft session
50-
const isDraftSession = typeof activeSession === "string" && activeSession.startsWith("draft-");
51-
52-
if (isDraftSession) {
53+
if (isDraftSessionId(activeSession)) {
5354
// Draft session - only show pending message if exists
5455
if (pendingMessage) {
5556
setMessages([pendingMessage]);
5657
} else {
5758
setMessages([]);
5859
}
59-
} else if (activeSession === "general" || !activeSession) {
60+
} else if (isGeneralSession(activeSession)) {
6061
// Show all messages for general session
6162
const taskMessages: Message[] = tasks.flatMap((task) => {
6263
const msgs: Message[] = [
@@ -118,9 +119,11 @@ export default function HomePage() {
118119
}
119120

120121
setMessages(sessionMessages);
121-
} else {
122-
setMessages([]);
122+
} else if (pendingMessage) {
123+
// Task not found yet but we have a pending message - show it
124+
setMessages([pendingMessage]);
123125
}
126+
// If no task and no pending message, keep previous messages (don't clear)
124127
}
125128
}, [activeSession, tasks, pendingMessage]);
126129

@@ -142,8 +145,7 @@ export default function HomePage() {
142145
type: "text",
143146
};
144147

145-
// Check if we're in a draft session
146-
const isDraftSession = typeof activeSession === "string" && activeSession.startsWith("draft-");
148+
const wasDraftSession = isDraftSessionId(activeSession);
147149

148150
setPendingMessage(userMessage);
149151
setInput("");
@@ -154,8 +156,8 @@ export default function HomePage() {
154156
const newTask = await api.createTask(input, workingDir, 1);
155157

156158
// If this was a draft session, remove it and switch to the new task
157-
if (isDraftSession) {
158-
removeDraftSession(activeSession);
159+
if (wasDraftSession) {
160+
removeDraftSession(activeSession as `draft-${string}`);
159161
// Switch to the new task's session
160162
if (newTask?.id) {
161163
setActiveSession(newTask.id);
@@ -183,10 +185,9 @@ export default function HomePage() {
183185
const runningTask = tasks.find((t) => t.status === "running");
184186

185187
// Get current session name
186-
const isDraftSession = typeof activeSession === "string" && activeSession.startsWith("draft-");
187-
const currentSessionName = isDraftSession
188+
const currentSessionName = isDraftSessionId(activeSession)
188189
? "New Session"
189-
: activeSession === "general" || !activeSession
190+
: isGeneralSession(activeSession)
190191
? "General"
191192
: tasks.find((t) => t.id === activeSession)?.prompt.slice(0, 30) || "Session";
192193

@@ -238,8 +239,18 @@ export default function HomePage() {
238239
))}
239240
</div>
240241

242+
{/* Running Indicator - show when task is running */}
243+
{runningTask && (
244+
<div className="px-6 py-2 mx-4">
245+
<div className="flex items-center gap-2 text-sm text-muted-foreground">
246+
<div className="w-2 h-2 bg-amber-500 rounded-full animate-pulse" />
247+
<span>Claude is working...</span>
248+
</div>
249+
</div>
250+
)}
251+
241252
{/* Typing Indicator */}
242-
{isTyping && <TypingIndicator />}
253+
{isTyping && !runningTask && <TypingIndicator />}
243254
</div>
244255
</ScrollArea>
245256

frontend/src/components/chat/chat-layout.tsx

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,24 @@ import { TooltipProvider } from "@/components/ui/tooltip";
55
import { ServerBar } from "./server-bar";
66
import { ChatSidebar } from "./chat-sidebar";
77
import { api, type Repository } from "@/lib/api";
8+
import { type DraftSession, type SessionId, isDraftSessionId } from "@/lib/types";
89

9-
export interface DraftSession {
10-
id: string;
11-
name: string;
12-
createdAt: string;
13-
}
10+
export type { DraftSession };
1411

1512
interface ChatContextType {
1613
activeWorkspace: string;
1714
setActiveWorkspace: (id: string) => void;
18-
activeSession: string;
19-
setActiveSession: (id: string) => void;
15+
activeSession: SessionId;
16+
setActiveSession: (id: SessionId) => void;
2017
selectedFile: string | null;
2118
setSelectedFile: (path: string | null) => void;
2219
currentRepository: Repository | null;
2320
showSettings: boolean;
2421
setShowSettings: (show: boolean) => void;
2522
draftSessions: DraftSession[];
26-
createNewSession: () => string;
27-
removeDraftSession: (id: string) => void;
23+
createNewSession: () => DraftSession["id"];
24+
removeDraftSession: (id: DraftSession["id"]) => void;
25+
renameDraftSession: (id: DraftSession["id"], name: string) => void;
2826
}
2927

3028
const ChatContext = createContext<ChatContextType | null>(null);
@@ -43,15 +41,15 @@ interface ChatLayoutProps {
4341

4442
export function ChatLayout({ children }: ChatLayoutProps) {
4543
const [activeWorkspace, setActiveWorkspace] = useState<string>("");
46-
const [activeSession, setActiveSession] = useState<string>("general");
44+
const [activeSession, setActiveSession] = useState<SessionId>("general");
4745
const [selectedFile, setSelectedFile] = useState<string | null>(null);
4846
const [currentRepository, setCurrentRepository] = useState<Repository | null>(null);
4947
const [showSettings, setShowSettings] = useState(false);
5048
const [draftSessions, setDraftSessions] = useState<DraftSession[]>([]);
5149

5250
// Create a new draft session
53-
const createNewSession = useCallback(() => {
54-
const id = `draft-${Date.now()}`;
51+
const createNewSession = useCallback((): DraftSession["id"] => {
52+
const id = `draft-${Date.now()}` as const;
5553
const newSession: DraftSession = {
5654
id,
5755
name: "New Session",
@@ -65,10 +63,17 @@ export function ChatLayout({ children }: ChatLayoutProps) {
6563
}, []);
6664

6765
// Remove a draft session (when it becomes a real task)
68-
const removeDraftSession = useCallback((id: string) => {
66+
const removeDraftSession = useCallback((id: DraftSession["id"]) => {
6967
setDraftSessions((prev) => prev.filter((s) => s.id !== id));
7068
}, []);
7169

70+
// Rename a draft session
71+
const renameDraftSession = useCallback((id: DraftSession["id"], name: string) => {
72+
setDraftSessions((prev) =>
73+
prev.map((s) => (s.id === id ? { ...s, name } : s))
74+
);
75+
}, []);
76+
7277
// Fetch repository details when workspace changes
7378
useEffect(() => {
7479
if (!activeWorkspace) {
@@ -104,6 +109,7 @@ export function ChatLayout({ children }: ChatLayoutProps) {
104109
draftSessions,
105110
createNewSession,
106111
removeDraftSession,
112+
renameDraftSession,
107113
}}
108114
>
109115
<TooltipProvider>
@@ -126,6 +132,11 @@ export function ChatLayout({ children }: ChatLayoutProps) {
126132
setSelectedFile(null);
127133
setShowSettings(false);
128134
}}
135+
onSessionRename={(sessionId, name) => {
136+
if (isDraftSessionId(sessionId)) {
137+
renameDraftSession(sessionId, name);
138+
}
139+
}}
129140
onFileSelect={setSelectedFile}
130141
onNewSession={() => {
131142
createNewSession();

frontend/src/components/chat/chat-sidebar.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ interface ChatSidebarProps {
1616
activeSession?: string;
1717
draftSessions?: DraftSession[];
1818
onSessionSelect?: (sessionId: string) => void;
19+
onSessionRename?: (sessionId: string, name: string) => void;
1920
onFileSelect?: (filePath: string) => void;
2021
onNewSession?: () => void;
2122
onShowSettings?: () => void;
@@ -34,6 +35,7 @@ export function ChatSidebar({
3435
activeSession,
3536
draftSessions = [],
3637
onSessionSelect,
38+
onSessionRename,
3739
onFileSelect,
3840
onNewSession,
3941
onShowSettings,
@@ -187,6 +189,7 @@ export function ChatSidebar({
187189
sessions={sessions}
188190
activeSession={activeSession}
189191
onSessionSelect={onSessionSelect}
192+
onSessionRename={onSessionRename}
190193
onNewSession={onNewSession}
191194
/>
192195
) : (

frontend/src/components/chat/message-item.tsx

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,10 @@
11
"use client";
22

3-
import { cn } from "@/lib/utils";
4-
import { formatDate } from "@/lib/utils";
3+
import { cn, formatDate } from "@/lib/utils";
54
import { Bot, User, Terminal, FileCode, CheckCircle2 } from "lucide-react";
5+
import { type Message } from "@/lib/types";
66

7-
export interface Message {
8-
id: string;
9-
author: {
10-
name: string;
11-
avatar?: string;
12-
isBot?: boolean;
13-
};
14-
content: string;
15-
timestamp: string;
16-
type?: "text" | "code" | "action";
17-
actionType?: "command" | "file_change" | "tool";
18-
}
7+
export type { Message };
198

209
interface MessageItemProps {
2110
message: Message;

frontend/src/components/chat/sidebar/chat-tab.tsx

Lines changed: 83 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
"use client";
22

3+
import { useState, useRef, useEffect } from "react";
34
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
45
import { cn } from "@/lib/utils";
5-
import { Plus, MessageCircle, Play, CheckCircle2, Circle } from "lucide-react";
6+
import { Plus, MessageCircle, Play, CheckCircle2, Circle, Pencil } from "lucide-react";
7+
import { isDraftSessionId } from "@/lib/types";
68

79
export interface Session {
810
id: string;
@@ -15,10 +17,42 @@ interface ChatTabProps {
1517
sessions: Session[];
1618
activeSession?: string;
1719
onSessionSelect?: (sessionId: string) => void;
20+
onSessionRename?: (sessionId: string, name: string) => void;
1821
onNewSession?: () => void;
1922
}
2023

21-
export function ChatTab({ sessions, activeSession, onSessionSelect, onNewSession }: ChatTabProps) {
24+
export function ChatTab({ sessions, activeSession, onSessionSelect, onSessionRename, onNewSession }: ChatTabProps) {
25+
const [editingSessionId, setEditingSessionId] = useState<string | null>(null);
26+
const [editingName, setEditingName] = useState("");
27+
const inputRef = useRef<HTMLInputElement>(null);
28+
29+
// Focus input when editing starts
30+
useEffect(() => {
31+
if (editingSessionId && inputRef.current) {
32+
inputRef.current.focus();
33+
inputRef.current.select();
34+
}
35+
}, [editingSessionId]);
36+
37+
const startEditing = (session: Session) => {
38+
if (isDraftSessionId(session.id)) {
39+
setEditingSessionId(session.id);
40+
setEditingName(session.name);
41+
}
42+
};
43+
44+
const finishEditing = () => {
45+
if (editingSessionId && editingName.trim()) {
46+
onSessionRename?.(editingSessionId, editingName.trim());
47+
}
48+
setEditingSessionId(null);
49+
setEditingName("");
50+
};
51+
52+
const cancelEditing = () => {
53+
setEditingSessionId(null);
54+
setEditingName("");
55+
};
2256
const getStatusIcon = (status: Session["status"]) => {
2357
switch (status) {
2458
case "running":
@@ -64,6 +98,9 @@ export function ChatTab({ sessions, activeSession, onSessionSelect, onNewSession
6498
) : (
6599
sessions.map((session) => {
66100
const isActive = activeSession === session.id || (!activeSession && session.id === "general");
101+
const isEditing = editingSessionId === session.id;
102+
const isRenamable = isDraftSessionId(session.id);
103+
67104
return (
68105
<div
69106
key={session.id}
@@ -73,11 +110,52 @@ export function ChatTab({ sessions, activeSession, onSessionSelect, onNewSession
73110
? "bg-primary/10 text-primary"
74111
: "text-muted-foreground hover:bg-secondary hover:text-foreground"
75112
)}
76-
onClick={() => onSessionSelect?.(session.id)}
113+
onClick={() => !isEditing && onSessionSelect?.(session.id)}
77114
>
78115
{getStatusIcon(session.status)}
79-
<MessageCircle className="w-3.5 h-3.5 opacity-60" />
80-
<span className="flex-1 text-sm truncate">{session.name}</span>
116+
<MessageCircle className="w-3.5 h-3.5 opacity-60 flex-shrink-0" />
117+
{isEditing ? (
118+
<input
119+
ref={inputRef}
120+
type="text"
121+
value={editingName}
122+
onChange={(e) => setEditingName(e.target.value)}
123+
onBlur={finishEditing}
124+
onKeyDown={(e) => {
125+
if (e.key === "Enter") {
126+
finishEditing();
127+
} else if (e.key === "Escape") {
128+
cancelEditing();
129+
}
130+
}}
131+
className="flex-1 text-sm bg-background border border-border rounded px-1 py-0.5 outline-none focus:ring-1 focus:ring-primary"
132+
onClick={(e) => e.stopPropagation()}
133+
/>
134+
) : (
135+
<>
136+
<span
137+
className="flex-1 text-sm truncate"
138+
onDoubleClick={(e) => {
139+
e.stopPropagation();
140+
startEditing(session);
141+
}}
142+
>
143+
{session.name}
144+
</span>
145+
{isRenamable && (
146+
<button
147+
type="button"
148+
onClick={(e) => {
149+
e.stopPropagation();
150+
startEditing(session);
151+
}}
152+
className="opacity-0 group-hover:opacity-100 w-5 h-5 flex items-center justify-center rounded hover:bg-secondary/80 text-muted-foreground hover:text-foreground transition-opacity"
153+
>
154+
<Pencil className="w-3 h-3" />
155+
</button>
156+
)}
157+
</>
158+
)}
81159
</div>
82160
);
83161
})

0 commit comments

Comments
 (0)