Skip to content

Commit 344f257

Browse files
committed
feat(api,ui): refactor session init with working directory and model selection via project picking
- Introduce optional working_directory and model to start-session API and Tauri command to initialize backend sessions. - Update frontend to call new API, expose selectedModel and startNewConversation in context, and add input bar with MentionInput and send controls to active conversations. - Enable starting a new discussion from project detail, initializing a session with project path. This enhances project-aware conversations and preserves compatibility by falling back to CLI availability check when no directory is provided.
1 parent 7ce00b0 commit 344f257

File tree

5 files changed

+221
-126
lines changed

5 files changed

+221
-126
lines changed

crates/server/src/main.rs

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,8 @@ struct AppState {
198198
#[derive(Serialize, Deserialize)]
199199
struct StartSessionRequest {
200200
session_id: String,
201+
working_directory: Option<String>,
202+
model: Option<String>,
201203
}
202204

203205
#[derive(Serialize, Deserialize)]
@@ -360,18 +362,28 @@ async fn check_cli_installed(state: &State<AppState>) -> Result<Json<bool>, Stat
360362

361363
#[post("/start-session", data = "<request>")]
362364
async fn start_session(request: Json<StartSessionRequest>, state: &State<AppState>) -> Status {
363-
let _request = request; // Use the parameter to avoid warning
364-
// For compatibility with existing frontend, just check if CLI is installed
365+
let req = request.into_inner();
365366
let backend = state.backend.lock().await;
366-
match backend.check_cli_installed().await {
367-
Ok(available) => {
368-
if available {
369-
Status::Ok
370-
} else {
371-
Status::ServiceUnavailable
367+
368+
// If working_directory is provided, initialize a session with that directory
369+
if let Some(working_directory) = req.working_directory {
370+
let model = req.model.unwrap_or_else(|| "gemini-2.0-flash-exp".to_string());
371+
match backend.initialize_session(req.session_id, working_directory, model).await {
372+
Ok(_) => Status::Ok,
373+
Err(_) => Status::InternalServerError,
374+
}
375+
} else {
376+
// For compatibility with existing frontend, just check if CLI is installed
377+
match backend.check_cli_installed().await {
378+
Ok(available) => {
379+
if available {
380+
Status::Ok
381+
} else {
382+
Status::ServiceUnavailable
383+
}
372384
}
385+
Err(_) => Status::InternalServerError,
373386
}
374-
Err(_) => Status::InternalServerError,
375387
}
376388
}
377389

crates/tauri-app/src/lib.rs

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,25 @@ async fn check_cli_installed(state: State<'_, AppState>) -> Result<bool, String>
5656
}
5757

5858
#[tauri::command]
59-
async fn start_session(_session_id: String, state: State<'_, AppState>) -> Result<(), String> {
60-
// For compatibility with existing frontend, just check if CLI is installed
61-
let available = state.backend.check_cli_installed().await.map_err(|e| e.to_string())?;
62-
if available {
63-
Ok(())
59+
async fn start_session(
60+
session_id: String,
61+
working_directory: Option<String>,
62+
model: Option<String>,
63+
state: State<'_, AppState>
64+
) -> Result<(), String> {
65+
// If working_directory is provided, initialize a session with that directory
66+
if let Some(working_directory) = working_directory {
67+
let model = model.unwrap_or_else(|| "gemini-2.0-flash-exp".to_string());
68+
state.backend.initialize_session(session_id, working_directory, model).await
69+
.map_err(|e| e.to_string())
6470
} else {
65-
Err("Gemini CLI not available".to_string())
71+
// For compatibility with existing frontend, just check if CLI is installed
72+
let available = state.backend.check_cli_installed().await.map_err(|e| e.to_string())?;
73+
if available {
74+
Ok(())
75+
} else {
76+
Err("Gemini CLI not available".to_string())
77+
}
6678
}
6779
}
6880

frontend/src/App.tsx

Lines changed: 142 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
import { MessageContent } from "./components/MessageContent";
1616
import { ThinkingBlock } from "./components/ThinkingBlock";
1717
import { ConversationList } from "./components/ConversationList";
18+
import { MentionInput } from "./components/MentionInput";
1819
import { GeminiLogo } from "./components/GeminiLogo";
1920
import { PiebaldLogo } from "./components/PiebaldLogo";
2021
import { type ToolCall, type ToolCallResult } from "./utils/toolCallParser";
@@ -26,6 +27,8 @@ import {
2627
AlertTriangle,
2728
UserRound,
2829
FolderKanban,
30+
Send,
31+
ImagePlus,
2932
} from "lucide-react";
3033
import "./index.css";
3134
import { ToolCallDisplay } from "./components/ToolCallDisplay";
@@ -168,6 +171,10 @@ export const api = {
168171
return webApi.list_projects(args) as Promise<T>;
169172
case "get_project_discussions":
170173
return webApi.get_project_discussions(args) as Promise<T>;
174+
case "list_enriched_projects":
175+
return webApi.list_projects_enriched() as Promise<T>;
176+
case "start_session":
177+
return webApi.start_session(args.sessionId, args.workingDirectory, args.model) as Promise<T>;
171178
default:
172179
throw new Error(`Unknown command: ${command}`);
173180
}
@@ -289,6 +296,8 @@ interface ConversationContextType {
289296
_mentions: unknown[]
290297
) => void;
291298
handleSendMessage: (e: React.FormEvent) => Promise<void>;
299+
selectedModel: string;
300+
startNewConversation: (title: string, workingDirectory?: string) => Promise<string>;
292301
}
293302

294303
const ConversationContext = createContext<ConversationContextType | undefined>(undefined);
@@ -762,6 +771,36 @@ function RootLayout() {
762771
}
763772
};
764773

774+
const startNewConversation = async (title: string, workingDirectory?: string): Promise<string> => {
775+
const convId = Date.now().toString();
776+
777+
// Create conversation in UI
778+
const newConversation: Conversation = {
779+
id: convId,
780+
title,
781+
messages: [],
782+
lastUpdated: new Date(),
783+
isStreaming: false,
784+
};
785+
786+
setConversations((prev) => [newConversation, ...prev]);
787+
setActiveConversation(convId);
788+
789+
// Initialize session with working directory if provided
790+
if (workingDirectory) {
791+
await api.invoke("start_session", {
792+
sessionId: convId,
793+
workingDirectory,
794+
model: selectedModel
795+
});
796+
}
797+
798+
// Set up event listeners
799+
await setupEventListenerForConversation(convId);
800+
801+
return convId;
802+
};
803+
765804

766805

767806
const handleModelChange = (model: string) => {
@@ -915,9 +954,112 @@ function RootLayout() {
915954
cliIOLogs,
916955
handleInputChange,
917956
handleSendMessage,
957+
selectedModel,
958+
startNewConversation,
918959
}}>
919960
<Outlet />
920961
</ConversationContext.Provider>
962+
963+
{/* Message Input Bar - Only show when there's an active conversation */}
964+
{activeConversation && (
965+
<div className="sticky bottom-0 bg-white dark:bg-neutral-900 flex items-center border-t border-gray-200 dark:border-neutral-700">
966+
<div className="px-6 py-2 w-full">
967+
<div className="mx-auto">
968+
<form
969+
className="flex gap-3 items-end"
970+
onSubmit={handleSendMessage}
971+
>
972+
<div className="flex-1 relative">
973+
<MentionInput
974+
value={input}
975+
onChange={handleInputChange}
976+
placeholder={
977+
isCliInstalled === false
978+
? "Gemini CLI not found"
979+
: "Type @ to mention files..."
980+
}
981+
disabled={isCliInstalled === false}
982+
className="h-9 w-full"
983+
onKeyDown={(e) => {
984+
if (e.key === "Enter" && !e.shiftKey) {
985+
e.preventDefault();
986+
handleSendMessage(e);
987+
}
988+
}}
989+
/>
990+
</div>
991+
<Button
992+
type="submit"
993+
disabled={
994+
isCliInstalled === false ||
995+
!input.trim()
996+
}
997+
size="icon"
998+
>
999+
<Send />
1000+
</Button>
1001+
<Dialog>
1002+
<DialogTrigger asChild>
1003+
<Button
1004+
type="button"
1005+
size="icon"
1006+
variant="outline"
1007+
title="View CLI Input/Output"
1008+
>
1009+
<Info className="h-4 w-4" />
1010+
</Button>
1011+
</DialogTrigger>
1012+
<DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto">
1013+
<DialogHeader>
1014+
<DialogTitle>CLI Input/Output Logs</DialogTitle>
1015+
</DialogHeader>
1016+
<div className="space-y-2 max-h-96 overflow-y-auto">
1017+
{cliIOLogs.map((log, index) => (
1018+
<div key={index} className="border rounded p-2">
1019+
<div className="flex items-center gap-2 mb-1">
1020+
<span
1021+
className={`text-xs font-mono px-2 py-1 rounded ${
1022+
log.type === "input"
1023+
? "bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200"
1024+
: "bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200"
1025+
}`}
1026+
>
1027+
{log.type === "input" ? "IN" : "OUT"}
1028+
</span>
1029+
<span className="text-xs text-muted-foreground">
1030+
{log.timestamp.toLocaleTimeString()}
1031+
</span>
1032+
<span className="text-xs text-muted-foreground font-mono">
1033+
{log.conversationId}
1034+
</span>
1035+
</div>
1036+
<pre className="text-xs whitespace-pre-wrap break-all font-mono bg-white dark:bg-gray-900 p-2 rounded border">
1037+
{log.data}
1038+
</pre>
1039+
</div>
1040+
))}
1041+
{cliIOLogs.length === 0 && (
1042+
<div className="text-center text-muted-foreground py-8">
1043+
No CLI I/O logs available yet. Start a conversation
1044+
to see the raw communication.
1045+
</div>
1046+
)}
1047+
</div>
1048+
</DialogContent>
1049+
</Dialog>
1050+
<Button
1051+
type="button"
1052+
disabled={true}
1053+
size="icon"
1054+
variant="outline"
1055+
>
1056+
<ImagePlus />
1057+
</Button>
1058+
</form>
1059+
</div>
1060+
</div>
1061+
</div>
1062+
)}
9211063
</div>
9221064
</div>
9231065

@@ -1284,112 +1426,6 @@ function HomeDashboard() {
12841426
</div>
12851427
</div>
12861428
)}
1287-
1288-
{/* <div className="sticky bottom-0 bg-white dark:bg-neutral-900 flex items-center border-t border-gray-200 dark:border-neutral-700">
1289-
<div className="px-6 py-2 w-full">
1290-
<div className="mx-auto">
1291-
<form
1292-
className="flex gap-3 items-end"
1293-
onSubmit={handleSendMessage}
1294-
>
1295-
<div className="flex-1 relative">
1296-
<MentionInput
1297-
value={input}
1298-
onChange={handleInputChange}
1299-
placeholder={
1300-
isCliInstalled === false
1301-
? "Gemini CLI not found"
1302-
: "Type @ to mention files..."
1303-
}
1304-
disabled={isCliInstalled === false}
1305-
className="h-9 w-full"
1306-
onKeyDown={(e) => {
1307-
if (e.key === "Enter" && !e.shiftKey) {
1308-
e.preventDefault();
1309-
handleSendMessage(e);
1310-
}
1311-
}}
1312-
/>
1313-
</div>
1314-
<Button
1315-
type="submit"
1316-
disabled={
1317-
isCliInstalled === false ||
1318-
!input.trim()
1319-
}
1320-
size="icon"
1321-
>
1322-
<Send />
1323-
</Button>
1324-
<Dialog>
1325-
<DialogTrigger asChild>
1326-
<Button
1327-
type="button"
1328-
size="icon"
1329-
variant="outline"
1330-
title="View CLI Input/Output"
1331-
>
1332-
<Info />
1333-
</Button>
1334-
</DialogTrigger>
1335-
<DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto">
1336-
<DialogHeader>
1337-
<DialogTitle>CLI Input/Output Log</DialogTitle>
1338-
</DialogHeader>
1339-
<div className="space-y-2 max-h-[60vh] overflow-y-auto">
1340-
{cliIOLogs
1341-
.map((log, index) => (
1342-
<div
1343-
key={index}
1344-
className={`p-3 rounded-lg border ${
1345-
log.type === "input"
1346-
? "bg-blue-50 dark:bg-blue-950/30 border-blue-200 dark:border-blue-800"
1347-
: "bg-green-50 dark:bg-green-950/30 border-green-200 dark:border-green-800"
1348-
}`}
1349-
>
1350-
<div className="flex items-center gap-2 mb-2">
1351-
<span
1352-
className={`text-xs font-mono px-2 py-1 rounded ${
1353-
log.type === "input"
1354-
? "bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200"
1355-
: "bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200"
1356-
}`}
1357-
>
1358-
{log.type === "input" ? "IN" : "OUT"}
1359-
</span>
1360-
<span className="text-xs text-muted-foreground">
1361-
{log.timestamp.toLocaleTimeString()}
1362-
</span>
1363-
<span className="text-xs text-muted-foreground font-mono">
1364-
{log.conversationId}
1365-
</span>
1366-
</div>
1367-
<pre className="text-xs whitespace-pre-wrap break-all font-mono bg-white dark:bg-gray-900 p-2 rounded border">
1368-
{log.data}
1369-
</pre>
1370-
</div>
1371-
))}
1372-
{cliIOLogs.length === 0 && (
1373-
<div className="text-center text-muted-foreground py-8">
1374-
No CLI I/O logs available yet. Start a conversation
1375-
to see the raw communication.
1376-
</div>
1377-
)}
1378-
</div>
1379-
</DialogContent>
1380-
</Dialog>
1381-
<Button
1382-
type="button"
1383-
disabled={true}
1384-
size="icon"
1385-
variant="outline"
1386-
>
1387-
<ImagePlus />
1388-
</Button>
1389-
</form>
1390-
</div>
1391-
</div>
1392-
</div> */}
13931429
</>
13941430
);
13951431
}

0 commit comments

Comments
 (0)