authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-02-08 00:28:21-08:00
committergravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-02-08 00:28:21-08:00
logf0ac59306842c0392b2e11ac35ebf24763131de4
treef135ce1aa67c5945edd122e596e64c43d3eef563
parentfdb654de45d8bcac6bfee3d79199fb9bdcde844d
signature Commit is signed but in an unrecognized format.

Better input box and react compiler


5 files changed, 102 insertions(+), 52 deletions(-)

TODO.md+1
...@@ -3,6 +3,7 @@...@@ -3,6 +3,7 @@
3- [ ] Remove model selection from project creation3- [ ] Remove model selection from project creation
4- [ ] First message vanishes when thread is created?? Awful ux4- [ ] First message vanishes when thread is created?? Awful ux
5- [ ] Show more clearly when thinking is happening5- [ ] Show more clearly when thinking is happening
6- [ ] Enable react compiler
67
7## Bigger overhauls8## Bigger overhauls
89
apps/renderer/package.json+1
...@@ -21,6 +21,7 @@...@@ -21,6 +21,7 @@
21 "@types/react": "^19.0.0",21 "@types/react": "^19.0.0",
22 "@types/react-dom": "^19.0.0",22 "@types/react-dom": "^19.0.0",
23 "@vitejs/plugin-react": "^4.3.4",23 "@vitejs/plugin-react": "^4.3.4",
24 "babel-plugin-react-compiler": "^19.0.0-beta-e552027-20250112",
24 "tailwindcss": "^4.0.0",25 "tailwindcss": "^4.0.0",
25 "typescript": "^5.7.3",26 "typescript": "^5.7.3",
26 "vite": "^6.0.5"27 "vite": "^6.0.5"
apps/renderer/src/components/ChatView.tsx+79-51
...@@ -439,38 +439,46 @@ export default function ChatView() {...@@ -439,38 +439,46 @@ export default function ChatView() {
439 </div>439 </div>
440440
441 {/* Input bar */}441 {/* Input bar */}
442 <div className="border-t border-white/[0.08] px-5 py-3">442 <div className="px-5 pb-4 pt-2">
443 <form443 <form
444 onSubmit={onSend}444 onSubmit={onSend}
445 className="mx-auto flex max-w-3xl items-end gap-2"445 className="mx-auto max-w-3xl"
446 >446 >
447 <div className="flex-1 rounded-2xl border border-white/[0.1] bg-[#121214] px-3 py-3">447 <div className="group rounded-[20px] border border-white/[0.08] bg-[#141416] transition-colors duration-200 focus-within:border-white/[0.16]">
448 <textarea448 {/* Textarea area */}
449 ref={textareaRef}449 <div className="px-4 pt-4 pb-2">
450 className="w-full resize-none bg-transparent px-1 pb-1 font-mono text-sm text-[#e0e0e0] placeholder:text-[#a0a0a0]/30 focus:outline-none"450 <textarea
451 rows={1}451 ref={textareaRef}
452 value={prompt}452 className="w-full resize-none bg-transparent text-[14px] leading-relaxed text-[#e0e0e0] placeholder:text-[#a0a0a0]/35 focus:outline-none"
453 onChange={(e) => setPrompt(e.target.value)}453 rows={2}
454 onKeyDown={onKeyDown}454 value={prompt}
455 placeholder={455 onChange={(e) => setPrompt(e.target.value)}
456 phase === "disconnected"456 onKeyDown={onKeyDown}
457 ? "Type a message (session auto-connects)..."457 placeholder={
458 : "Type a message..."458 phase === "disconnected"
459 }459 ? "Ask for follow-up changes"
460 disabled={isSending || isConnecting}460 : "Ask anything..."
461 />461 }
462 <div className="mt-2 flex items-center justify-between gap-2">462 disabled={isSending || isConnecting}
463 <div className="flex items-center gap-2">463 />
464 </div>
465
466 {/* Bottom toolbar */}
467 <div className="flex items-center justify-between px-3 pb-3">
468 <div className="flex items-center gap-1">
469 {/* Model picker */}
464 <div className="relative" ref={modelMenuRef}>470 <div className="relative" ref={modelMenuRef}>
465 <button471 <button
466 type="button"472 type="button"
467 className="inline-flex items-center gap-2 rounded-full border border-white/[0.1] bg-white/[0.04] px-3 py-1.5 text-xs text-[#d8d8d8] transition-colors duration-150 hover:bg-white/[0.08]"473 className="inline-flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-[13px] text-[#a0a0a0]/70 transition-colors duration-150 hover:bg-white/[0.06] hover:text-[#d0d0d0]"
468 onClick={() => setIsModelMenuOpen((open) => !open)}474 onClick={() => setIsModelMenuOpen((open) => !open)}
469 >475 >
470 <span className="max-w-[180px] truncate font-mono">476 <span className="max-w-[180px] truncate">
471 {selectedModel}477 {selectedModel}
472 </span>478 </span>
473 <span className="text-[10px] text-[#a0a0a0]/70">▼</span>479 <svg width="10" height="10" viewBox="0 0 10 10" fill="none" className="opacity-50">
480 <path d="M2.5 4L5 6.5L7.5 4" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round" />
481 </svg>
474 </button>482 </button>
475 {isModelMenuOpen && (483 {isModelMenuOpen && (
476 <div className="absolute bottom-full left-0 z-20 mb-2 w-[320px] rounded-2xl border border-white/[0.1] bg-[#1b1b1d]/95 p-2 shadow-[0_16px_40px_rgba(0,0,0,0.55)] backdrop-blur">484 <div className="absolute bottom-full left-0 z-20 mb-2 w-[320px] rounded-2xl border border-white/[0.1] bg-[#1b1b1d]/95 p-2 shadow-[0_16px_40px_rgba(0,0,0,0.55)] backdrop-blur">
...@@ -506,14 +514,19 @@ export default function ChatView() {...@@ -506,14 +514,19 @@ export default function ChatView() {
506 </div>514 </div>
507 )}515 )}
508 </div>516 </div>
517
518 {/* Divider */}
519 <div className="mx-0.5 h-4 w-px bg-white/[0.08]" />
520
521 {/* Reasoning effort */}
509 <label522 <label
510 className="inline-flex items-center gap-2 rounded-full border border-white/[0.1] bg-white/[0.04] px-3 py-1.5 text-xs text-[#d8d8d8]"523 className="inline-flex cursor-pointer items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-[13px] text-[#a0a0a0]/70 transition-colors duration-150 hover:bg-white/[0.06] hover:text-[#d0d0d0]"
511 htmlFor="reasoning-effort"524 htmlFor="reasoning-effort"
512 >525 >
513 <span>Reasoning</span>526 <span>{selectedEffort.charAt(0).toUpperCase() + selectedEffort.slice(1)}</span>
514 <select527 <select
515 id="reasoning-effort"528 id="reasoning-effort"
516 className="bg-transparent font-mono text-xs text-[#d8d8d8] outline-none"529 className="absolute opacity-0 w-0 h-0"
517 value={selectedEffort}530 value={selectedEffort}
518 onChange={(event) => setSelectedEffort(event.target.value)}531 onChange={(event) => setSelectedEffort(event.target.value)}
519 >532 >
...@@ -524,36 +537,51 @@ export default function ChatView() {...@@ -524,36 +537,51 @@ export default function ChatView() {
524 </option>537 </option>
525 ))}538 ))}
526 </select>539 </select>
540 <svg width="10" height="10" viewBox="0 0 10 10" fill="none" className="opacity-50">
541 <path d="M2.5 4L5 6.5L7.5 4" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round" />
542 </svg>
527 </label>543 </label>
528 </div>544 </div>
529 {activeProject && (545
530 <span className="text-[10px] text-[#a0a0a0]/35">546 {/* Right side: send / stop button */}
531 {activeProject.name}547 <div className="flex items-center gap-2">
532 </span>548 {activeProject && (
533 )}549 <span className="text-[11px] text-[#a0a0a0]/25">
550 {activeProject.name}
551 </span>
552 )}
553 {phase === "running" ? (
554 <button
555 type="button"
556 className="flex h-8 w-8 items-center justify-center rounded-full bg-rose-500/90 text-white transition-all duration-150 hover:bg-rose-500 hover:scale-105"
557 onClick={() => void onInterrupt()}
558 aria-label="Stop generation"
559 >
560 <svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor">
561 <rect x="2" y="2" width="8" height="8" rx="1.5" />
562 </svg>
563 </button>
564 ) : (
565 <button
566 type="submit"
567 className="flex h-8 w-8 items-center justify-center rounded-full bg-white/90 text-[#0c0c0c] transition-all duration-150 hover:bg-white hover:scale-105 disabled:opacity-30 disabled:hover:scale-100"
568 disabled={isSending || isConnecting || !prompt.trim()}
569 aria-label={isConnecting ? "Connecting" : isSending ? "Sending" : "Send message"}
570 >
571 {isConnecting || isSending ? (
572 <svg width="14" height="14" viewBox="0 0 14 14" fill="none" className="animate-spin">
573 <circle cx="7" cy="7" r="5.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeDasharray="20 12" />
574 </svg>
575 ) : (
576 <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
577 <path d="M7 11.5V2.5M7 2.5L3 6.5M7 2.5L11 6.5" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" />
578 </svg>
579 )}
580 </button>
581 )}
582 </div>
534 </div>583 </div>
535 </div>584 </div>
536 {phase === "running" ? (
537 <button
538 type="button"
539 className="shrink-0 rounded-xl bg-rose-600/80 px-4 py-3 text-xs font-medium text-white transition-colors duration-150 hover:bg-rose-600"
540 onClick={() => void onInterrupt()}
541 >
542 Stop
543 </button>
544 ) : (
545 <button
546 type="submit"
547 className="shrink-0 rounded-xl bg-white px-4 py-3 text-xs font-medium text-[#0c0c0c] transition-colors duration-150 hover:bg-white/90 disabled:opacity-40"
548 disabled={isSending || isConnecting || !prompt.trim()}
549 >
550 {isConnecting
551 ? "Connecting..."
552 : isSending
553 ? "Sending..."
554 : "Send"}
555 </button>
556 )}
557 </form>585 </form>
558 </div>586 </div>
559 </div>587 </div>
apps/renderer/src/index.css+13
...@@ -63,3 +63,16 @@ input {...@@ -63,3 +63,16 @@ input {
63::-webkit-scrollbar-thumb:hover {63::-webkit-scrollbar-thumb:hover {
64 background: rgba(255, 255, 255, 0.18);64 background: rgba(255, 255, 255, 0.18);
65}65}
66
67/* Reasoning select — clickable label surface */
68label:has(> select#reasoning-effort) {
69 position: relative;
70}
71label:has(> select#reasoning-effort) select {
72 position: absolute;
73 inset: 0;
74 opacity: 0;
75 cursor: pointer;
76 width: 100%;
77 height: 100%;
78}
apps/renderer/vite.config.ts+8-1
...@@ -3,7 +3,14 @@ import react from "@vitejs/plugin-react";...@@ -3,7 +3,14 @@ import react from "@vitejs/plugin-react";
3import { defineConfig } from "vite";3import { defineConfig } from "vite";
44
5export default defineConfig({5export default defineConfig({
6 plugins: [react(), tailwindcss()],6 plugins: [
7 react({
8 babel: {
9 plugins: [["babel-plugin-react-compiler", { target: "19" }]],
10 },
11 }),
12 tailwindcss(),
13 ],
7 server: {14 server: {
8 port: Number(process.env.ELECTRON_RENDERER_PORT ?? 5173),15 port: Number(process.env.ELECTRON_RENDERER_PORT ?? 5173),
9 strictPort: true,16 strictPort: true,