Skip to content

Commit 91c408c

Browse files
committed
feat(ui): implement functional pre-session configuration wiring
- Added accurate `low`, `medium`, `high` thinking levels instead of boolean flags. - Updated `chat.send` TRPC and gateway payload schemas to pass `model`, `thinkingLevel`, `workflow`, `skills`, and `attachments`. - Scaled the pre-session form UI to read local files and process them as `base64` payloads upon session start.
1 parent 015c42f commit 91c408c

File tree

3 files changed

+226
-28
lines changed

3 files changed

+226
-28
lines changed

src/app/sessions/page.tsx

Lines changed: 204 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,27 @@
11
"use client";
22

3-
import { useState } from "react";
3+
import { useState, useRef } from "react";
44
import { useRouter } from "next/navigation";
55
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
66
import { useTRPC } from "@/lib/trpc/react";
77
import { useGatewayHealth } from "@/hooks/use-gateway-health";
88
import { Button } from "@/components/ui/button";
9-
import { MessageSquarePlus } from "lucide-react";
9+
import { MessageSquarePlus, Paperclip, ChevronDown, Check, X } from "lucide-react";
1010
import { Textarea } from "@/components/ui/textarea";
1111

1212
export default function NewSessionPage() {
1313
const [agentId, setAgentId] = useState("");
1414
const [message, setMessage] = useState("");
15+
16+
// Real State Wiring for Pre-Session Configuration Form Updates
17+
const [model, setModel] = useState("");
18+
const [workflow, setWorkflow] = useState("");
19+
const [skill, setSkill] = useState("");
20+
const [thinkingLevel, setThinkingLevel] = useState("");
21+
const [attachments, setAttachments] = useState<File[]>([]);
22+
23+
const fileInputRef = useRef<HTMLInputElement>(null);
24+
1525
const router = useRouter();
1626
const trpc = useTRPC();
1727
const queryClient = useQueryClient();
@@ -27,52 +37,163 @@ export default function NewSessionPage() {
2737
onSuccess: (_data, variables) => {
2838
queryClient.invalidateQueries({ queryKey: trpc.sessions.list.queryKey() });
2939
setMessage("");
40+
setAttachments([]);
3041
router.push(`/sessions/${encodeURIComponent(variables.sessionKey)}`);
3142
},
3243
})
3344
);
3445

35-
const handleCreate = () => {
46+
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
47+
if (e.target.files) {
48+
setAttachments((prev) => [...prev, ...Array.from(e.target.files!)]);
49+
}
50+
};
51+
52+
const handleCreate = async () => {
3653
if (!message.trim() || !agentId) return;
3754
const sessionKey = `agent:${agentId}:${Date.now()}`;
3855
const idempotencyKey = crypto.randomUUID();
56+
57+
// Process files
58+
const filePayloads = await Promise.all(
59+
attachments.map(async (f) => {
60+
return new Promise<{ name: string; type: string; size: number; base64: string }>((resolve) => {
61+
const reader = new FileReader();
62+
reader.onload = (e) => {
63+
resolve({
64+
name: f.name,
65+
type: f.type,
66+
size: f.size,
67+
base64: e.target?.result as string, // Extracts Data-URI string format
68+
});
69+
};
70+
reader.readAsDataURL(f);
71+
});
72+
})
73+
);
74+
3975
sendMutation.mutate({
4076
sessionKey,
4177
message: message.trim(),
4278
idempotencyKey,
79+
model: model || undefined,
80+
thinkingLevel: thinkingLevel || undefined,
81+
workflow: workflow || undefined,
82+
skills: skill ? [skill] : undefined,
83+
attachments: filePayloads.length > 0 ? filePayloads : undefined,
4384
});
4485
};
4586

4687
return (
47-
<div className="flex h-full flex-col items-center justify-center p-6 bg-zinc-950">
48-
<div className="mx-auto flex w-full max-w-2xl flex-col items-center text-center">
49-
<div className="mb-6 flex h-16 w-16 items-center justify-center rounded-full bg-zinc-900 ring-1 ring-zinc-800">
88+
<div className="flex h-full flex-col items-center justify-center p-6 bg-zinc-950 overflow-y-auto">
89+
<div className="mx-auto flex w-full max-w-3xl flex-col items-center text-center my-auto">
90+
<div className="mb-6 flex h-16 w-16 items-center justify-center rounded-full bg-zinc-900 ring-1 ring-zinc-800 shadow-sm">
5091
<MessageSquarePlus className="h-8 w-8 text-zinc-400" />
5192
</div>
5293
<h1 className="mb-2 text-2xl font-semibold tracking-tight text-zinc-50">
5394
Start a New Session
5495
</h1>
5596
<p className="mb-8 max-w-md text-sm text-zinc-400">
56-
Select an agent and send your first message to begin a new conversation.
97+
Configure your agent and send your first message to begin a new conversation.
5798
</p>
5899

59-
<div className="w-full rounded-xl border border-zinc-800 bg-zinc-900/50 p-4 shadow-sm">
60-
<div className="mb-4">
61-
<select
62-
value={agentId}
63-
onChange={(e) => setAgentId(e.target.value)}
64-
className="w-full rounded-lg border border-zinc-700 bg-zinc-900 px-4 py-2.5 text-sm text-zinc-200 outline-none focus:border-zinc-500 focus:ring-1 focus:ring-zinc-500"
65-
>
66-
<option value="">Select an agent...</option>
67-
{agents.map((a) => (
68-
<option key={a.id} value={a.id}>
69-
{a.emoji ? `${a.emoji} ` : ""}{a.name ?? a.id}
70-
</option>
71-
))}
72-
</select>
100+
<div className="w-full rounded-2xl border border-zinc-800 bg-zinc-900/40 p-5 shadow-lg backdrop-blur-xl">
101+
102+
{/* Configuration Grid */}
103+
<div className="mb-5 grid grid-cols-2 gap-4">
104+
105+
{/* Agent Select */}
106+
<div className="flex flex-col gap-1.5 text-left">
107+
<label className="text-xs font-medium text-zinc-400 px-1">Agent</label>
108+
<div className="relative">
109+
<select
110+
value={agentId}
111+
onChange={(e) => setAgentId(e.target.value)}
112+
className="w-full appearance-none rounded-xl border border-zinc-700/80 bg-zinc-900/50 px-4 py-2.5 text-sm text-zinc-200 outline-none transition-all focus:border-indigo-500/50 focus:ring-1 focus:ring-indigo-500/50 hover:bg-zinc-800/50"
113+
disabled={sendMutation.isPending}
114+
>
115+
<option value="">Select an agent...</option>
116+
{agents.map((a) => (
117+
<option key={a.id} value={a.id}>
118+
{a.emoji ? `${a.emoji} ` : ""}{a.name ?? a.id}
119+
</option>
120+
))}
121+
</select>
122+
<ChevronDown className="absolute right-3 top-3 h-4 w-4 text-zinc-500 pointer-events-none" />
123+
</div>
124+
</div>
125+
126+
{/* Model Select */}
127+
<div className="flex flex-col gap-1.5 text-left">
128+
<label className="text-xs font-medium text-zinc-400 px-1">Model Override</label>
129+
<div className="relative">
130+
<select
131+
value={model}
132+
onChange={(e) => setModel(e.target.value)}
133+
className="w-full appearance-none rounded-xl border border-zinc-700/80 bg-zinc-900/50 px-4 py-2.5 text-sm text-zinc-200 outline-none transition-all focus:border-indigo-500/50 focus:ring-1 focus:ring-indigo-500/50 hover:bg-zinc-800/50"
134+
disabled={sendMutation.isPending}
135+
>
136+
<option value="">Agent Default</option>
137+
<option value="sonnet-4.6">Claude 4.6 Sonnet</option>
138+
<option value="opus-4.6">Claude 4.6 Opus</option>
139+
<option value="haiku-4.5">Claude 4.5 Haiku</option>
140+
<option value="gpt-5.2">GPT-5.2</option>
141+
<option value="gpt-5.3-codex">GPT-5.3 Codex</option>
142+
</select>
143+
<ChevronDown className="absolute right-3 top-3 h-4 w-4 text-zinc-500 pointer-events-none" />
144+
</div>
145+
</div>
146+
147+
{/* Workflow Select */}
148+
<div className="flex flex-col gap-1.5 text-left">
149+
<label className="text-xs font-medium text-zinc-400 px-1">Workflow Template</label>
150+
<div className="relative">
151+
<select
152+
value={workflow}
153+
onChange={(e) => setWorkflow(e.target.value)}
154+
className="w-full appearance-none rounded-xl border border-zinc-700/80 bg-zinc-900/50 px-4 py-2.5 text-sm text-zinc-200 outline-none transition-all focus:border-indigo-500/50 focus:ring-1 focus:ring-indigo-500/50 hover:bg-zinc-800/50"
155+
disabled={sendMutation.isPending}
156+
>
157+
<option value="">None applied</option>
158+
<option value="scaffold">Scaffold New Project (/scaffold)</option>
159+
<option value="pr-review">Pull Request Review (/pr-review)</option>
160+
<option value="atomic-commit">Deploy Atomic Commit (/atomic-commit)</option>
161+
</select>
162+
<ChevronDown className="absolute right-3 top-3 h-4 w-4 text-zinc-500 pointer-events-none" />
163+
</div>
164+
</div>
165+
166+
{/* Skills Map Select */}
167+
<div className="flex flex-col gap-1.5 text-left">
168+
<label className="text-xs font-medium text-zinc-400 px-1">Skill Profile</label>
169+
<div className="relative">
170+
<select
171+
value={skill}
172+
onChange={(e) => setSkill(e.target.value)}
173+
className="w-full appearance-none rounded-xl border border-zinc-700/80 bg-zinc-900/50 px-4 py-2.5 text-sm text-zinc-200 outline-none transition-all focus:border-indigo-500/50 focus:ring-1 focus:ring-indigo-500/50 hover:bg-zinc-800/50"
174+
disabled={sendMutation.isPending}
175+
>
176+
<option value="">Default toolkit</option>
177+
<option value="web-tavily">Deep Web Search (Tavily)</option>
178+
<option value="mcp-local">Local Filesystem (MCP)</option>
179+
<option value="all">Full Tool Belt</option>
180+
</select>
181+
<ChevronDown className="absolute right-3 top-3 h-4 w-4 text-zinc-500 pointer-events-none" />
182+
</div>
183+
</div>
184+
73185
</div>
74186

75-
<div className="relative">
187+
<input
188+
type="file"
189+
multiple
190+
className="hidden"
191+
ref={fileInputRef}
192+
onChange={handleFileChange}
193+
/>
194+
195+
{/* Main Input Area */}
196+
<div className="relative rounded-xl border border-zinc-700 bg-zinc-900/80 shadow-inner focus-within:border-zinc-500 focus-within:ring-1 focus-within:ring-zinc-500 transition-all flex flex-col">
76197
<Textarea
77198
value={message}
78199
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setMessage(e.target.value)}
@@ -83,20 +204,76 @@ export default function NewSessionPage() {
83204
}
84205
}}
85206
placeholder="What can I help you with?"
86-
className="min-h-[120px] resize-none border-zinc-700 bg-zinc-900 pb-12 pt-4 text-base placeholder:text-zinc-500 focus-visible:ring-1 focus-visible:ring-zinc-600 rounded-lg text-zinc-200"
207+
className="min-h-[140px] resize-none border-0 bg-transparent px-4 py-4 text-sm placeholder:text-zinc-500 focus-visible:ring-0 rounded-t-xl text-zinc-200"
208+
disabled={sendMutation.isPending}
87209
/>
88-
<div className="absolute bottom-3 right-3 flex items-center justify-end">
210+
211+
{attachments.length > 0 && (
212+
<div className="flex flex-wrap gap-2 px-3 pb-3">
213+
{attachments.map((file, i) => (
214+
<div key={i} className="flex items-center gap-1.5 bg-zinc-800/80 border border-zinc-700 rounded-md px-2 py-1 text-xs text-zinc-300">
215+
<Paperclip className="h-3 w-3 text-zinc-500" />
216+
<span className="truncate max-w-[150px]">{file.name}</span>
217+
<button
218+
onClick={() => setAttachments(a => a.filter((_, idx) => idx !== i))}
219+
className="ml-1 text-zinc-500 hover:text-red-400 transition-colors"
220+
aria-label="Remove attachment"
221+
>
222+
<X className="h-3 w-3" />
223+
</button>
224+
</div>
225+
))}
226+
</div>
227+
)}
228+
229+
{/* Input Action Bar */}
230+
<div className="flex items-center justify-between border-t border-zinc-800/60 bg-zinc-900/40 px-3 py-2 rounded-b-xl">
231+
<div className="flex items-center gap-2">
232+
233+
{/* File Attachment Button */}
234+
<button
235+
onClick={() => fileInputRef.current?.click()}
236+
className="flex h-8 items-center justify-center rounded-lg px-2.5 text-zinc-400 hover:bg-zinc-800 hover:text-zinc-200 transition-colors"
237+
aria-label="Attach file"
238+
title="Attach folders or files"
239+
disabled={sendMutation.isPending}
240+
>
241+
<Paperclip className="h-4 w-4" />
242+
</button>
243+
244+
<div className="h-4 w-px bg-zinc-800 mx-1"></div>
245+
246+
{/* Reasoning Controls */}
247+
<div className="flex bg-zinc-900/50 p-0.5 rounded-lg border border-zinc-800">
248+
{(["", "low", "medium", "high"] as const).map((level) => (
249+
<button
250+
key={level}
251+
onClick={() => setThinkingLevel(level)}
252+
disabled={sendMutation.isPending}
253+
className={`flex h-7 items-center px-3 rounded-md text-xs font-medium transition-all ${thinkingLevel === level
254+
? "bg-indigo-500/20 text-indigo-300 ring-1 ring-indigo-500/30"
255+
: "text-zinc-500 hover:text-zinc-300 hover:bg-zinc-800/50"
256+
}`}
257+
>
258+
{level === "" ? "Default" : level.charAt(0).toUpperCase() + level.slice(1)}
259+
</button>
260+
))}
261+
</div>
262+
</div>
263+
264+
{/* Submit Action */}
89265
<Button
90266
onClick={handleCreate}
91267
disabled={!message.trim() || !agentId || !canSend || sendMutation.isPending}
92-
className="h-8 shrink-0 rounded-md bg-zinc-100 px-4 text-xs font-medium text-zinc-900 transition-colors hover:bg-zinc-300 disabled:opacity-50"
268+
className="h-8 shrink-0 rounded-lg bg-zinc-100 px-5 text-xs font-semibold text-zinc-900 transition-all hover:bg-zinc-300 hover:scale-[1.02] active:scale-[0.98] disabled:opacity-50 disabled:hover:scale-100 shadow-[0_0_15px_rgba(255,255,255,0.1)]"
93269
>
94-
{sendMutation.isPending ? "Starting..." : "Start Session"}
270+
{sendMutation.isPending ? "Starting session..." : "Start Session"}
95271
</Button>
96272
</div>
97273
</div>
274+
98275
{!canSend && (
99-
<p className="mt-3 text-xs text-red-400/80 text-left">
276+
<p className="mt-3 text-xs text-red-400/80 text-left px-1">
100277
Gateway is disconnected or chat.send is unavailable.
101278
</p>
102279
)}

src/lib/gateway/client.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,12 +300,21 @@ export class GatewayClient extends EventEmitter {
300300
return this.request("config.get");
301301
}
302302

303-
// chat.inject does NOT exist. Use chat.send.
304303
async chatSend(params: {
305304
sessionKey: string;
306305
message: string;
307306
label?: string;
308307
idempotencyKey: string;
308+
model?: string;
309+
thinkingLevel?: string;
310+
workflow?: string;
311+
skills?: string[];
312+
attachments?: Array<{
313+
name: string;
314+
type: string;
315+
size: number;
316+
base64?: string;
317+
}>;
309318
}) {
310319
return this.request("chat.send", params);
311320
}

src/server/routers/sessions.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,18 @@ export const sessionsRouter = router({
110110
message: z.string(),
111111
label: z.string().optional(),
112112
idempotencyKey: z.string(),
113+
model: z.string().optional(),
114+
thinkingLevel: z.string().optional(),
115+
workflow: z.string().optional(),
116+
skills: z.array(z.string()).optional(),
117+
attachments: z.array(
118+
z.object({
119+
name: z.string(),
120+
type: z.string(),
121+
size: z.number(),
122+
base64: z.string().optional(),
123+
})
124+
).optional(),
113125
})
114126
)
115127
.mutation(async ({ ctx, input }) => {

0 commit comments

Comments
 (0)