Skip to content

Commit 4695186

Browse files
authored
Merge pull request #12 from JasonLow8/fix/session-group-buckets
feat: projects view, question card UI, dark theme overhaul, and full feature parity
2 parents 9af0347 + c6f7d94 commit 4695186

14 files changed

Lines changed: 1541 additions & 446 deletions

web/src/App.tsx

Lines changed: 111 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@ import { App as CapApp } from "@capacitor/app"
33
import { useServerConfig } from "./hooks/useServerConfig"
44
import { useServerData } from "./hooks/useServerData"
55
import { useChat } from "./hooks/useChat"
6+
import ProjectsScreen from "./screens/ProjectsScreen"
67
import SessionsScreen from "./screens/SessionsScreen"
78
import ChatScreen from "./screens/ChatScreen"
89
import SettingsScreen from "./screens/SettingsScreen"
910
import HelpScreen from "./screens/HelpScreen"
11+
import type { Project } from "./types"
12+
13+
type Screen = "projects" | "sessions" | "chat" | "settings" | "help"
1014

1115
function App() {
1216
const sv = useServerConfig()
@@ -32,45 +36,50 @@ function App() {
3236
prefs: sv.prefs,
3337
})
3438

35-
const [tab, setTab] = useState<"sessions" | "settings">(() =>
36-
sv.hasConfiguredServer ? "sessions" : "settings",
39+
const [screen, setScreen] = useState<Screen>(() =>
40+
sv.hasConfiguredServer ? "projects" : "settings"
3741
)
38-
const [chatOpen, setChatOpen] = useState(false)
39-
const [helpOpen, setHelpOpen] = useState(false)
42+
const [selectedProject, setSelectedProject] = useState<Project | null>(null)
4043

4144
const connected = sv.connectedVersion !== ""
4245

4346
const isWorking = Boolean(
4447
sd.selectedSession && ["busy", "retry", "ask", "question", "permission"].includes(sd.selectedSession.status),
4548
)
4649

50+
function openProject(project: Project) {
51+
setSelectedProject(project)
52+
setScreen("sessions")
53+
}
54+
4755
function openSession(id: string, dir: string) {
4856
sd.setSelectedID(id)
4957
sd.loadSelected(id, dir).catch(() => undefined)
50-
setChatOpen(true)
58+
setScreen("chat")
59+
}
60+
61+
async function handleCreateSession(directory?: string) {
62+
await sd.createSession(directory)
63+
setScreen("chat")
5164
}
5265

5366
function handleSaveConfig() {
5467
sv.saveConfig()
5568
sd.setRuntimeError(null)
56-
setTab("sessions")
69+
setScreen("projects")
5770
}
5871

59-
async function handleCreateSession() {
60-
await sd.createSession()
61-
setChatOpen(true)
62-
}
63-
64-
// ── Android back button ──────────────────────────────────────────
72+
// Android back button
6573
useEffect(() => {
6674
let listener: { remove: () => void } | null = null
6775

6876
const onBack = () => {
69-
if (helpOpen) {
70-
setHelpOpen(false)
71-
} else if (chatOpen) {
72-
setChatOpen(false)
73-
setTab("sessions")
77+
if (screen === "help") {
78+
setScreen("settings")
79+
} else if (screen === "chat") {
80+
setScreen("sessions")
81+
} else if (screen === "sessions") {
82+
setScreen("projects")
7483
}
7584
// else: default system behaviour
7685
}
@@ -82,20 +91,11 @@ function App() {
8291
return () => {
8392
if (listener) listener.remove()
8493
}
85-
}, [helpOpen, chatOpen])
94+
}, [screen])
8695

87-
return (
88-
<div className="app-shell">
89-
{sd.toast && (
90-
<div
91-
className={`toast toast-${sd.toast.type} fade-in`}
92-
onClick={() => sd.setToast(null)}
93-
>
94-
{sd.toast.text}
95-
</div>
96-
)}
97-
98-
{chatOpen ? (
96+
const renderScreen = () => {
97+
if (screen === "chat") {
98+
return (
9999
<ChatScreen
100100
selectedSession={sd.selectedSession}
101101
renderedMessages={sd.renderedMessages}
@@ -106,10 +106,8 @@ function App() {
106106
setTodosExpanded={sd.setTodosExpanded}
107107
sessionInfo={sd.sessionInfo}
108108
runtimeError={sd.runtimeError}
109-
onBack={() => {
110-
setChatOpen(false)
111-
setTab("sessions")
112-
}}
109+
diff={sd.diff}
110+
onBack={() => setScreen("sessions")}
113111
isWorking={isWorking}
114112
abortSession={chat.abortSession}
115113
composer={chat.composer}
@@ -134,51 +132,91 @@ function App() {
134132
currentAgent={sd.currentAgent}
135133
primaryAgents={sd.primaryAgents}
136134
cycleAgent={chat.cycleAgent}
135+
questions={sd.questions}
137136
replyPermission={chat.replyPermission}
137+
replyQuestion={chat.replyQuestion}
138+
rejectQuestion={chat.rejectQuestion}
139+
forkSession={sd.forkSession}
140+
renameSession={sd.renameSession}
141+
onSessionForked={() => setScreen("sessions")}
138142
/>
139-
) : helpOpen ? (
143+
)
144+
}
145+
146+
if (screen === "help") {
147+
return (
140148
<HelpScreen
141-
onBack={() => setHelpOpen(false)}
149+
onBack={() => setScreen("settings")}
142150
commands={sd.commands}
143151
/>
144-
) : (
145-
<>
146-
{tab === "sessions" && (
147-
<SessionsScreen
148-
config={{ host: sv.config.host }}
149-
connected={connected}
150-
filteredSessions={sd.filteredSessions}
151-
selectedID={sd.selectedID}
152-
onOpenSession={openSession}
153-
query={sd.query}
154-
setQuery={sd.setQuery}
155-
createSession={handleCreateSession}
156-
serverDirectory={sd.serverDirectory}
157-
runtimeError={sd.runtimeError}
158-
refreshSessions={sd.refreshSessions}
159-
onOpenSettings={() => setTab("settings")}
160-
deleteSession={sd.deleteSession}
161-
/>
162-
)}
163-
{tab === "settings" && (
164-
<SettingsScreen
165-
theme={sv.theme}
166-
setTheme={sv.setTheme}
167-
draftConfig={sv.draftConfig}
168-
setDraftConfig={sv.setDraftConfig}
169-
connectedVersion={sv.connectedVersion}
170-
settingsNotice={sv.settingsNotice}
171-
testingConnection={sv.testingConnection}
172-
prefs={sv.prefs}
173-
setPrefs={sv.setPrefs}
174-
saveConfig={handleSaveConfig}
175-
testConnection={sv.testConnection}
176-
onOpenHelp={() => setHelpOpen(true)}
177-
onBack={() => setTab("sessions")}
178-
/>
179-
)}
180-
</>
152+
)
153+
}
154+
155+
if (screen === "settings") {
156+
return (
157+
<SettingsScreen
158+
theme={sv.theme}
159+
setTheme={sv.setTheme}
160+
draftConfig={sv.draftConfig}
161+
setDraftConfig={sv.setDraftConfig}
162+
connectedVersion={sv.connectedVersion}
163+
settingsNotice={sv.settingsNotice}
164+
testingConnection={sv.testingConnection}
165+
prefs={sv.prefs}
166+
setPrefs={sv.setPrefs}
167+
saveConfig={handleSaveConfig}
168+
testConnection={sv.testConnection}
169+
onOpenHelp={() => setScreen("help")}
170+
onBack={() => setScreen(sv.hasConfiguredServer ? "projects" : "settings")}
171+
mcpServers={sd.mcpServers}
172+
/>
173+
)
174+
}
175+
176+
if (screen === "sessions" && selectedProject) {
177+
return (
178+
<SessionsScreen
179+
project={selectedProject}
180+
sessions={sd.sessions}
181+
selectedID={sd.selectedID}
182+
onOpenSession={openSession}
183+
onBack={() => setScreen("projects")}
184+
query={sd.query}
185+
setQuery={sd.setQuery}
186+
createSession={handleCreateSession}
187+
runtimeError={sd.runtimeError}
188+
refreshSessions={sd.refreshSessions}
189+
deleteSession={sd.deleteSession}
190+
/>
191+
)
192+
}
193+
194+
// Default: projects
195+
return (
196+
<ProjectsScreen
197+
config={{ host: sv.config.host }}
198+
connected={connected}
199+
projects={sd.projects}
200+
sessions={sd.sessions}
201+
onOpenProject={openProject}
202+
onOpenSettings={() => setScreen("settings")}
203+
refreshSessions={sd.refreshSessions}
204+
runtimeError={sd.runtimeError}
205+
/>
206+
)
207+
}
208+
209+
return (
210+
<div className="app-shell">
211+
{sd.toast && (
212+
<div
213+
className={`toast toast-${sd.toast.type} fade-in`}
214+
onClick={() => sd.setToast(null)}
215+
>
216+
{sd.toast.text}
217+
</div>
181218
)}
219+
{renderScreen()}
182220
</div>
183221
)
184222
}

web/src/api.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@ import type {
44
CommandInfo,
55
DiffFile,
66
HealthResponse,
7+
McpServer,
78
MessageEnvelope,
89
PathInfo,
10+
Project,
911
ProviderResponse,
12+
QuestionRequest,
1013
ServerConfig,
1114
Session,
1215
SessionStatus,
@@ -194,10 +197,34 @@ export const api = {
194197
})
195198
},
196199

197-
replyQuestion(config: ServerConfig, requestID: string, directory: string, answers: string[]) {
198-
return request<boolean>(config, `/question/${requestID}/reply`, {
200+
listQuestions(config: ServerConfig) {
201+
return request<QuestionRequest[]>(config, "/question")
202+
},
203+
204+
replyQuestion(config: ServerConfig, requestID: string, directory: string, answers: string[][]) {
205+
const path = withDirectory(`/question/${requestID}/reply`, directory)
206+
return request<boolean>(config, path, {
199207
method: "POST",
200-
body: { directory, answers }
208+
body: { answers }
209+
})
210+
},
211+
212+
rejectQuestion(config: ServerConfig, requestID: string) {
213+
return request<boolean>(config, `/question/${requestID}/reject`, {
214+
method: "POST",
215+
body: {}
201216
})
217+
},
218+
219+
listProjects(config: ServerConfig) {
220+
return request<Project[]>(config, "/project")
221+
},
222+
223+
forkSession(config: ServerConfig, sessionID: string) {
224+
return request<Session>(config, `/session/${sessionID}/fork`, { method: "POST", body: {} })
225+
},
226+
227+
listMcp(config: ServerConfig) {
228+
return request<Record<string, McpServer>>(config, "/mcp")
202229
}
203230
}

web/src/components/SessionCard.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,19 @@ export default function SessionCard({ session, onClick, onLongPress, selected, s
9797
<div className="scard-path">{session.directory}</div>
9898
<div className="tags">
9999
{renderStatusTag(session.status, session.statusMessage)}
100+
{session.agent && (
101+
<span className="tag model">{session.agent}</span>
102+
)}
100103
</div>
101104
</div>
102-
{!selectionMode && <i className="ti ti-chevron-right scard-chevron" />}
105+
<div style={{ display: "flex", flexDirection: "column", alignItems: "flex-end", gap: 4, flexShrink: 0 }}>
106+
{!selectionMode && <i className="ti ti-chevron-right scard-chevron" />}
107+
{session.cost && session.cost >= 0.001 && (
108+
<span className="cost-badge">
109+
{session.cost < 0.01 ? `$${session.cost.toFixed(4)}` : `$${session.cost.toFixed(3)}`}
110+
</span>
111+
)}
112+
</div>
103113
</div>
104114
)
105115
}

web/src/hooks/useChat.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,23 @@ export function useChat(params: {
7878
}
7979
}
8080

81-
async function replyQuestion(requestID: string, text: string) {
81+
async function replyQuestion(requestID: string, directory: string, answers: string[][]) {
8282
if (!selectedSession) return
8383
try {
8484
setRuntimeError(null)
85-
await api.replyQuestion(config, requestID, selectedSession.directory, [text])
85+
await api.replyQuestion(config, requestID, directory, answers)
86+
await refreshSessions()
87+
await loadSelected(selectedSession.id, selectedSession.directory)
88+
} catch (err) {
89+
setRuntimeError((err as Error).message)
90+
}
91+
}
92+
93+
async function rejectQuestion(requestID: string) {
94+
if (!selectedSession) return
95+
try {
96+
setRuntimeError(null)
97+
await api.rejectQuestion(config, requestID)
8698
await refreshSessions()
8799
await loadSelected(selectedSession.id, selectedSession.directory)
88100
} catch (err) {
@@ -103,7 +115,7 @@ export function useChat(params: {
103115
// "permission" is handled by buttons only — user shouldn't be typing.
104116
const isQuestionState = (selectedSession.status === "question" || selectedSession.status === "ask") && selectedSession.requestID
105117
if (isQuestionState) {
106-
await replyQuestion(selectedSession.requestID!, text)
118+
await replyQuestion(selectedSession.requestID!, selectedSession.directory, [[text]])
107119
return
108120
}
109121

@@ -275,6 +287,7 @@ export function useChat(params: {
275287
abortSession,
276288
selectModel,
277289
replyPermission,
278-
replyQuestion
290+
replyQuestion,
291+
rejectQuestion
279292
}
280293
}

0 commit comments

Comments
 (0)