diff --git a/backend/src/routes/sessions.ts b/backend/src/routes/sessions.ts index 9872263..b8d8961 100644 --- a/backend/src/routes/sessions.ts +++ b/backend/src/routes/sessions.ts @@ -93,7 +93,7 @@ export async function sessionRoutes(fastify: FastifyInstance) { const modeRow = db .prepare("SELECT value FROM settings WHERE key = 'session_mode'") .get() as { value: string } | undefined - const mode = modeRow?.value === 'terminal' ? 'terminal' : 'chat' + const mode = modeRow?.value === 'chat' ? 'chat' : 'terminal' const id = randomUUID() const now = Date.now() @@ -101,7 +101,7 @@ export async function sessionRoutes(fastify: FastifyInstance) { 'INSERT INTO sessions (id, workdir, name, mode, started_at, last_used) VALUES (?, ?, ?, ?, ?, ?)', ).run(id, workdir, name?.trim() || null, mode, now, now) - return reply.status(201).send({ sessionId: id }) + return reply.status(201).send({ sessionId: id, mode }) }) fastify.post<{ Params: { id: string } }>('/api/sessions/:id/stop', async (req, reply) => { diff --git a/backend/src/ws/session.ts b/backend/src/ws/session.ts index 515c6de..f4ea777 100644 --- a/backend/src/ws/session.ts +++ b/backend/src/ws/session.ts @@ -448,7 +448,15 @@ export async function sessionWsRoutes(fastify: FastifyInstance) { const { id } = req.params const row = db.prepare('SELECT * FROM sessions WHERE id = ?').get(id) as - | { workdir: string; ended_at: number | null; claude_session_id: string | null; total_tokens: number; working_time_ms: number } + | { + workdir: string; ended_at: number | null; claude_session_id: string | null + total_tokens: number; working_time_ms: number; model: string | null + cost_usd: number | null; api_duration_ms: number | null + lines_added: number | null; lines_removed: number | null + context_input_tokens: number | null; context_output_tokens: number | null + context_window_size: number | null; context_pct: number | null + effort_level: string | null; thinking_enabled: number | null + } | undefined if (!row) { @@ -475,6 +483,26 @@ export async function sessionWsRoutes(fastify: FastifyInstance) { gitBranch: getGitBranch(row.workdir), })) + // Replay last statusline data so reconnecting clients see up-to-date header stats + // (cost, API duration, effort, etc.) without waiting for the next statusline push. + socket.send(JSON.stringify({ + type: 'statusline', + statuslineData: { + model: row.model ?? null, + costUsd: row.cost_usd ?? null, + apiDurationMs: row.api_duration_ms ?? null, + linesAdded: row.lines_added ?? null, + linesRemoved: row.lines_removed ?? null, + contextInputTokens: row.context_input_tokens ?? null, + contextOutputTokens: row.context_output_tokens ?? null, + contextWindowSize: row.context_window_size ?? null, + contextPct: row.context_pct ?? null, + effortLevel: row.effort_level ?? null, + thinkingEnabled: row.thinking_enabled !== null ? row.thinking_enabled === 1 : null, + rateLimits: null, + } satisfies StatuslinePayload, + })) + // Clear ended_at so resumed sessions show as active; stamp last_used db.prepare('UPDATE sessions SET last_used = ?, ended_at = NULL WHERE id = ?').run(Date.now(), id) diff --git a/backend/src/ws/terminal.ts b/backend/src/ws/terminal.ts index 0ab71e0..1db7966 100644 --- a/backend/src/ws/terminal.ts +++ b/backend/src/ws/terminal.ts @@ -287,8 +287,21 @@ export async function terminalWsRoutes(fastify: FastifyInstance) { (socket, req) => { const { id } = req.params - const row = db.prepare('SELECT workdir, ended_at, claude_session_id, context_input_tokens, context_output_tokens FROM sessions WHERE id = ?').get(id) as - | { workdir: string; ended_at: number | null; claude_session_id: string | null; context_input_tokens: number | null; context_output_tokens: number | null } + const row = db.prepare( + `SELECT workdir, ended_at, claude_session_id, + model, cost_usd, api_duration_ms, lines_added, lines_removed, + context_input_tokens, context_output_tokens, context_window_size, context_pct, + effort_level, thinking_enabled + FROM sessions WHERE id = ?`, + ).get(id) as + | { + workdir: string; ended_at: number | null; claude_session_id: string | null + model: string | null; cost_usd: number | null; api_duration_ms: number | null + lines_added: number | null; lines_removed: number | null + context_input_tokens: number | null; context_output_tokens: number | null + context_window_size: number | null; context_pct: number | null + effort_level: string | null; thinking_enabled: number | null + } | undefined if (!row) { @@ -319,6 +332,26 @@ export async function terminalWsRoutes(fastify: FastifyInstance) { contextOutputTokens: row.context_output_tokens ?? 0, })) + // Replay last statusline data so reconnecting clients see up-to-date header stats + // (cost, API duration, effort, etc.) without waiting for the next statusline push. + socket.send(JSON.stringify({ + type: 'statusline', + statuslineData: { + model: row.model ?? null, + costUsd: row.cost_usd ?? null, + apiDurationMs: row.api_duration_ms ?? null, + linesAdded: row.lines_added ?? null, + linesRemoved: row.lines_removed ?? null, + contextInputTokens: row.context_input_tokens ?? null, + contextOutputTokens: row.context_output_tokens ?? null, + contextWindowSize: row.context_window_size ?? null, + contextPct: row.context_pct ?? null, + effortLevel: row.effort_level ?? null, + thinkingEnabled: row.thinking_enabled !== null ? row.thinking_enabled === 1 : null, + rateLimits: null, + } satisfies StatuslinePayload, + })) + socket.on('message', (raw: Buffer | string) => { try { const msg = JSON.parse(raw.toString()) as { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 236f72d..dbce276 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,5 +1,6 @@ import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom' import HomeView from './views/HomeView' +import NewSessionView from './views/NewSessionView' import SessionRoute from './views/SessionRoute' import SettingsView from './views/SettingsView' import SessionsTableView from './views/SessionsTableView' @@ -14,7 +15,7 @@ export default function App() { } /> } /> - } /> + } /> } /> } /> } /> diff --git a/frontend/src/components/NewSessionModal.tsx b/frontend/src/components/NewSessionModal.tsx index 6dd6ad3..4f7cfeb 100644 --- a/frontend/src/components/NewSessionModal.tsx +++ b/frontend/src/components/NewSessionModal.tsx @@ -2,10 +2,11 @@ import { useState, useEffect } from 'react' import { FolderOpen } from 'lucide-react' interface NewSessionModalProps { - onStart: (sessionId: string, workdir: string, name: string | null) => void + onStart: (sessionId: string, workdir: string, name: string | null, mode: 'chat' | 'terminal') => void + onCancel?: () => void } -export default function NewSessionModal({ onStart }: NewSessionModalProps) { +export default function NewSessionModal({ onStart, onCancel }: NewSessionModalProps) { const [name, setName] = useState('') const [workdir, setWorkdir] = useState('') const [loading, setLoading] = useState(false) @@ -73,8 +74,8 @@ export default function NewSessionModal({ onStart }: NewSessionModalProps) { body: JSON.stringify(body), }) if (!res.ok) throw new Error(await res.text()) - const { sessionId } = (await res.json()) as { sessionId: string } - onStart(sessionId, workdir.trim(), name.trim() || null) + const { sessionId, mode } = (await res.json()) as { sessionId: string; mode: 'chat' | 'terminal' } + onStart(sessionId, workdir.trim(), name.trim() || null, mode) } catch (err) { setError(err instanceof Error ? err.message : 'Failed to create session') setLoading(false) @@ -142,13 +143,24 @@ export default function NewSessionModal({ onStart }: NewSessionModalProps) { {error &&

{error}

} - +
+ {onCancel && ( + + )} + +
diff --git a/frontend/src/components/SessionHeader.tsx b/frontend/src/components/SessionHeader.tsx index a1def35..cd94a08 100644 --- a/frontend/src/components/SessionHeader.tsx +++ b/frontend/src/components/SessionHeader.tsx @@ -5,7 +5,6 @@ import { useSession } from '../context/SessionContext' import { formatModelName, formatTokens, formatDuration, formatCost } from '../utils/format' interface SessionHeaderProps { - onNewSession: () => void onStopSession: () => void onSessionsList: () => void totalTokens: number @@ -41,7 +40,6 @@ function StatChip({ label, value, valueClass = 'text-text-secondary' }: StatChip } export default function SessionHeader({ - onNewSession, onStopSession, onSessionsList, totalTokens, @@ -123,7 +121,6 @@ export default function SessionHeader({
{ e.preventDefault(); onNewSession() }} className="flex items-center gap-1.5 text-text-muted hover:text-accent text-xs bg-bg-elevated border border-border-subtle hover:border-accent px-2 py-1 rounded transition-colors" > diff --git a/frontend/src/components/SessionList.tsx b/frontend/src/components/SessionList.tsx index 3c0a11f..9bfd974 100644 --- a/frontend/src/components/SessionList.tsx +++ b/frontend/src/components/SessionList.tsx @@ -7,7 +7,6 @@ interface SessionListProps { sessions: Session[] onStop: (sessionId: string) => void onDelete: (sessionId: string) => void - onNewSession: () => void } @@ -19,7 +18,6 @@ export default function SessionList({ sessions, onStop, onDelete, - onNewSession, }: SessionListProps) { async function handleDelete(sessionId: string) { if (!window.confirm('Delete this session? This cannot be undone.')) return @@ -38,7 +36,6 @@ export default function SessionList({ Sessions { e.preventDefault(); onNewSession() }} className="flex items-center gap-1.5 px-3 py-1.5 text-xs bg-accent text-black hover:bg-accent-hover rounded transition-colors" > @@ -53,7 +50,6 @@ export default function SessionList({

No sessions yet

{ e.preventDefault(); onNewSession() }} className="flex items-center gap-2 px-4 py-2 text-sm bg-accent text-black hover:bg-accent-hover rounded transition-colors" > diff --git a/frontend/src/views/HomeView.tsx b/frontend/src/views/HomeView.tsx index 9ab4973..4ba7f07 100644 --- a/frontend/src/views/HomeView.tsx +++ b/frontend/src/views/HomeView.tsx @@ -1,5 +1,5 @@ import { useState, useRef, useCallback, useEffect } from 'react' -import { useLocation, useNavigate } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { ChevronDown, ChevronUp } from 'lucide-react' import { useSession } from '../context/SessionContext' import { useWebSocket } from '../hooks/useWebSocket' @@ -10,20 +10,17 @@ import SessionHeader from '../components/SessionHeader' import MessageList from '../components/MessageList' import ChatInput from '../components/ChatInput' import TerminalDrawer, { type TerminalDrawerHandle } from '../components/TerminalDrawer' -import NewSessionModal from '../components/NewSessionModal' import UsageChart from '../components/UsageChart' import PermissionDialog from '../components/PermissionDialog' import TerminalSession from '../components/TerminalSession' export default function HomeView() { const { state, dispatch } = useSession() - const location = useLocation() const navigate = useNavigate() const terminalRef = useRef(null) - const [showModal, setShowModal] = useState(location.state?.openModal === true) const [chartOpen, setChartOpen] = useState(false) - const { account, usage, sessions, activeSessions, defaultSessionMode, loading, refresh } = useHomeData() + const { account, usage, sessions, activeSessions, loading, refresh } = useHomeData() // Local sessions state for optimistic deletion const [localSessions, setLocalSessions] = useState(null) const displaySessions = localSessions ?? sessions @@ -55,22 +52,6 @@ export default function HomeView() { }, [send, state.mode]) - function handleSessionStart(sessionId: string, workdir: string, name: string | null) { - dispatch({ type: 'SESSION_CREATED', sessionId, workdir, mode: defaultSessionMode, ...(name ? { name } : {}) }) - if (account?.model) dispatch({ type: 'MODEL_SET', model: account.model }) - setShowModal(false) - refresh() - navigate(`/session/${sessionId}`) - } - - function handleNewSession() { - if (state.sessionId) { - fetch(`/api/sessions/${state.sessionId}/stop`, { method: 'POST' }).catch(() => {}) - } - dispatch({ type: 'SESSION_CLEARED' }) - navigate('/new') - } - function handleSessionsList() { dispatch({ type: 'SESSION_CLEARED' }) navigate('/') @@ -175,13 +156,11 @@ export default function HomeView() { /> )} @@ -199,13 +178,11 @@ export default function HomeView() { /> )} @@ -215,14 +192,11 @@ export default function HomeView() { disabled={state.wsState === 'disconnected' || state.wsState === 'error'} />
- ) : showModal ? ( - ) : ( setShowModal(true)} /> ) )} diff --git a/frontend/src/views/NewSessionView.tsx b/frontend/src/views/NewSessionView.tsx new file mode 100644 index 0000000..539b5b1 --- /dev/null +++ b/frontend/src/views/NewSessionView.tsx @@ -0,0 +1,31 @@ +import { useEffect, useState } from 'react' +import { useNavigate } from 'react-router-dom' +import { useSession } from '../context/SessionContext' +import NewSessionModal from '../components/NewSessionModal' +import type { AccountInfo } from '../hooks/useAccount' + +export default function NewSessionView() { + const { dispatch } = useSession() + const navigate = useNavigate() + const [account, setAccount] = useState(null) + + useEffect(() => { + fetch('/api/account') + .then((r) => (r.ok ? (r.json() as Promise) : null)) + .then((data) => { if (data) setAccount(data) }) + .catch(() => {}) + }, []) + + function handleStart(sessionId: string, workdir: string, name: string | null, mode: 'chat' | 'terminal') { + dispatch({ type: 'SESSION_CREATED', sessionId, workdir, mode, ...(name ? { name } : {}) }) + if (account?.model) dispatch({ type: 'MODEL_SET', model: account.model }) + navigate(`/session/${sessionId}`, { replace: true }) + } + + return ( + navigate(-1)} + /> + ) +} diff --git a/frontend/src/views/SessionRoute.tsx b/frontend/src/views/SessionRoute.tsx index 5ef6ad6..5d74276 100644 --- a/frontend/src/views/SessionRoute.tsx +++ b/frontend/src/views/SessionRoute.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react' -import { useParams, Navigate, useLocation, useNavigate } from 'react-router-dom' +import { useParams, Navigate, useLocation } from 'react-router-dom' import { useSession } from '../context/SessionContext' import HomeView from './HomeView' import type { Session } from '../hooks/useHomeData' @@ -8,7 +8,6 @@ export default function SessionRoute() { const { sessionId } = useParams<{ sessionId: string }>() const { state, dispatch } = useSession() const location = useLocation() - const navigate = useNavigate() // Skip loading state if the session is already in context (e.g. right after SESSION_CREATED). const [loading, setLoading] = useState(state.sessionId !== sessionId) const [error, setError] = useState(false) @@ -32,17 +31,20 @@ export default function SessionRoute() { } // Slow path: new tab or direct URL — fetch session + account from API. + let cancelled = false Promise.all([ fetch(`/api/sessions/${sessionId}`).then((r) => (r.ok ? r.json() as Promise : Promise.reject())), fetch('/api/account').then((r) => (r.ok ? r.json() : null)).catch(() => null), ]) .then(([session, account]) => { + if (cancelled) return dispatch({ type: 'RESUME_SESSION', id: session.id, workdir: session.workdir, mode: session.mode, ...(session.name ? { name: session.name } : {}) }) if (account?.model) dispatch({ type: 'MODEL_SET', model: account.model }) setLoading(false) }) - .catch(() => setError(true)) - }, [sessionId]) // eslint-disable-line react-hooks/exhaustive-deps + .catch(() => { if (!cancelled) setError(true) }) + return () => { cancelled = true } + }, [sessionId, state.sessionId]) // eslint-disable-line react-hooks/exhaustive-deps // Keep URL and context in sync: clear session state whenever this route unmounts. // This handles browser Back/Forward navigation, which bypasses the explicit navigate() @@ -52,12 +54,6 @@ export default function SessionRoute() { return () => { dispatch({ type: 'SESSION_CLEARED' }) } }, [dispatch]) - // Safety net: if session is cleared while mounted (e.g. an explicit navigate call already - // fired — this effect just cleans up any edge case where it didn't). - useEffect(() => { - if (!loading && !state.sessionId) navigate('/', { replace: true }) - }, [loading, state.sessionId, navigate]) - function fetchAndSetModel() { fetch('/api/account') .then((r) => (r.ok ? r.json() : null)) @@ -67,7 +63,7 @@ export default function SessionRoute() { if (error) return - if (loading) { + if (loading || !state.sessionId) { return (
Loading session…