feat: restructure as pnpm monorepo with Tauri desktop shell

- Migrate to packages/web + packages/desktop workspace layout via git mv
- Add Tauri v2 desktop shell with @tauri-apps/plugin-http for CORS bypass
- Configure Turborepo with package-level dependsOn build graph
- Add semantic-release with exec plugin for GHA output and disabled PR comments
- Fix http:default capability scope to allow all HTTP/HTTPS origins
- Add Vite Tauri integration (clearScreen, TAURI_DEV_HOST, target, envPrefix)
- Add semantic-release.yml and release.yml GitHub Actions workflows
- Fix all Biome lint errors (noArrayIndexKey, noNonNullAssertion, button types)
This commit is contained in:
Offending Commit
2026-04-24 16:52:40 -05:00
parent 9a74182f97
commit 92c4dfd3dd
152 changed files with 14088 additions and 4774 deletions

View File

@@ -0,0 +1,200 @@
import { useChat } from "@/api/queries";
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/input";
import { SectionHeading } from "@/components/ui/typography";
import { Link, useParams } from "@tanstack/react-router";
import { AnimatePresence, motion } from "framer-motion";
import { Brain, Send } from "lucide-react";
import { useEffect, useRef, useState } from "react";
interface Message {
id: string;
role: "user" | "assistant";
content: string;
}
export function ChatPage() {
const { workspaceId, peerId } = useParams({ strict: false }) as {
workspaceId: string;
peerId: string;
};
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState("");
const bottomRef = useRef<HTMLDivElement>(null);
const chatMutation = useChat(workspaceId, peerId);
useEffect(() => {
if (messages.length > 0) {
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
}
}, [messages]);
async function handleSend() {
const trimmed = input.trim();
if (!trimmed || chatMutation.isPending) return;
setInput("");
setMessages((prev) => [...prev, { id: crypto.randomUUID(), role: "user", content: trimmed }]);
try {
const result = await chatMutation.mutateAsync(trimmed);
const responseText =
(result as { content?: string | null }).content ??
(typeof result === "string" ? result : JSON.stringify(result));
setMessages((prev) => [
...prev,
{ id: crypto.randomUUID(), role: "assistant", content: responseText },
]);
} catch (err) {
setMessages((prev) => [
...prev,
{
id: crypto.randomUUID(),
role: "assistant",
content: `Error: ${err instanceof Error ? err.message : "Unknown error"}`,
},
]);
}
}
function handleKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSend();
}
}
return (
<div className="flex flex-col h-screen" style={{ background: "var(--bg)" }}>
{/* Header */}
<div
className="shrink-0 px-6 py-4"
style={{ borderBottom: "1px solid var(--border)", background: "var(--bg-2)" }}
>
<div className="flex items-center gap-2 text-xs mb-1" style={{ color: "var(--text-3)" }}>
<Link
to="/workspaces/$workspaceId/peers/$peerId"
params={{ workspaceId, peerId } as never}
className="hover:underline font-mono"
>
{peerId}
</Link>
<span>/</span>
<span>Chat</span>
</div>
<div className="flex items-center gap-2">
<Brain className="w-4 h-4" style={{ color: "var(--accent)" }} strokeWidth={1.5} />
<SectionHeading as="h1" className="mb-0">
Memory-augmented chat
</SectionHeading>
</div>
<p className="text-xs mt-0.5" style={{ color: "var(--text-3)" }}>
Honcho responds using accumulated context for <span className="font-mono">{peerId}</span>
</p>
</div>
{/* Messages */}
<div className="flex-1 overflow-auto px-4 sm:px-6 py-4 space-y-4">
<AnimatePresence initial={false}>
{messages.length === 0 && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="flex items-center justify-center h-full"
>
<div className="text-center">
<div
className="w-14 h-14 rounded-2xl flex items-center justify-center mx-auto mb-4"
style={{
background: "var(--accent-dim)",
border: "1px solid var(--accent-border)",
}}
>
<Brain className="w-6 h-6" style={{ color: "var(--accent)" }} strokeWidth={1.5} />
</div>
<p className="text-sm font-medium" style={{ color: "var(--text-2)" }}>
Start a conversation
</p>
<p className="text-xs mt-1 max-w-xs" style={{ color: "var(--text-3)" }}>
Honcho will respond using accumulated memory context for this peer
</p>
</div>
</motion.div>
)}
{messages.map((msg) => (
<motion.div
key={msg.id}
initial={{ opacity: 0, y: 8, scale: 0.97 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
transition={{ type: "spring", stiffness: 300, damping: 25 }}
className={`flex ${msg.role === "user" ? "justify-end" : "justify-start"}`}
>
<div
className="max-w-[80%] sm:max-w-[70%] rounded-2xl px-4 py-3 text-sm"
style={
msg.role === "user"
? { background: "var(--accent)", color: "#fff" }
: {
background: "var(--bg-2)",
border: "1px solid var(--border)",
color: "var(--text-2)",
}
}
>
<p className="whitespace-pre-wrap leading-relaxed">{msg.content}</p>
</div>
</motion.div>
))}
</AnimatePresence>
{chatMutation.isPending && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="flex justify-start"
>
<div
className="rounded-2xl px-4 py-3 flex items-center gap-2"
style={{ background: "var(--bg-2)", border: "1px solid var(--border)" }}
>
<LoadingSpinner size="sm" />
<span className="text-xs" style={{ color: "var(--text-3)" }}>
Honcho is thinking...
</span>
</div>
</motion.div>
)}
<div ref={bottomRef} />
</div>
{/* Input */}
<div
className="shrink-0 px-4 sm:px-6 py-4"
style={{ borderTop: "1px solid var(--border)", background: "var(--bg-2)" }}
>
<div className="flex gap-3 max-w-3xl mx-auto">
<Textarea
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Message this peer... (Enter to send, Shift+Enter for newline)"
rows={2}
className="flex-1 resize-none"
/>
<Button
variant="primary"
onClick={handleSend}
disabled={!input.trim() || chatMutation.isPending}
className="self-end mb-0.5"
>
<Send className="w-4 h-4" strokeWidth={1.5} />
<span className="hidden sm:block">Send</span>
</Button>
</div>
</div>
</div>
);
}