From 6712e58c6c469d13d065c2728036d581f7a734bb Mon Sep 17 00:00:00 2001 From: katipally Date: Wed, 15 Jul 2026 04:41:22 -0700 Subject: [PATCH 01/35] added agents --- apps/desktop/main.cjs | 5 + apps/web/public/agents/claude.svg | 1 + apps/web/public/agents/codex.svg | 1 + apps/web/public/agents/cursor.svg | 1 + apps/web/src/app/api/agents/route.ts | 41 +++ apps/web/src/app/api/settings/route.ts | 6 +- apps/web/src/app/globals.css | 10 +- apps/web/src/app/page.tsx | 18 +- apps/web/src/components/live/AgentBar.tsx | 95 +++++++ .../web/src/components/live/AgentControls.tsx | 118 ++++++++ apps/web/src/components/live/AgentIcon.tsx | 18 ++ apps/web/src/components/live/LiveDock.tsx | 42 +-- apps/web/src/components/live/LiveStage.tsx | 67 +---- apps/web/src/components/live/Lobby.tsx | 196 +++++++++++++ apps/web/src/components/live/TopBar.tsx | 6 +- .../src/components/live/TranscriptPanel.tsx | 2 +- .../components/settings/AgentsSettings.tsx | 117 ++++++++ .../components/settings/PipelineSettings.tsx | 104 +++++++ .../src/components/settings/SearchSelect.tsx | 2 +- .../src/components/settings/SettingsModal.tsx | 27 +- apps/web/src/lib/api.ts | 11 +- apps/web/src/lib/live/liveClient.ts | 17 +- apps/web/src/lib/live/liveStore.ts | 10 + apps/web/src/lib/live/models.ts | 35 ++- apps/web/src/lib/live/models.worker.ts | 26 +- apps/web/src/lib/live/pipelineConfig.test.ts | 34 +++ apps/web/src/lib/live/pipelineConfig.ts | 106 +++++++ apps/web/src/lib/live/useLiveSession.ts | 141 +++++++++- apps/web/src/lib/live/voiceEngine.ts | 30 +- apps/web/src/lib/uiStore.ts | 4 + packages/harness/src/anthropic.ts | 2 +- packages/shared/src/live-events.ts | 27 ++ packages/shared/src/sse-events.ts | 4 +- pnpm-lock.yaml | 11 + services/agent/package.json | 1 + services/agent/src/agents/acp-agent.ts | 259 ++++++++++++++++++ services/agent/src/agents/index.ts | 51 ++++ services/agent/src/agents/supervisor.test.ts | 41 +++ services/agent/src/agents/supervisor.ts | 124 +++++++++ services/agent/src/agents/types.ts | 42 +++ services/agent/src/live/session.ts | 82 +++++- services/agent/src/live/turn-runner.ts | 8 +- services/agent/src/server.ts | 6 + services/agent/src/tools.ts | 16 +- services/agent/tsconfig.json | 3 +- 45 files changed, 1806 insertions(+), 162 deletions(-) create mode 100644 apps/web/public/agents/claude.svg create mode 100644 apps/web/public/agents/codex.svg create mode 100644 apps/web/public/agents/cursor.svg create mode 100644 apps/web/src/app/api/agents/route.ts create mode 100644 apps/web/src/components/live/AgentBar.tsx create mode 100644 apps/web/src/components/live/AgentControls.tsx create mode 100644 apps/web/src/components/live/AgentIcon.tsx create mode 100644 apps/web/src/components/live/Lobby.tsx create mode 100644 apps/web/src/components/settings/AgentsSettings.tsx create mode 100644 apps/web/src/components/settings/PipelineSettings.tsx create mode 100644 apps/web/src/lib/live/pipelineConfig.test.ts create mode 100644 apps/web/src/lib/live/pipelineConfig.ts create mode 100644 services/agent/src/agents/acp-agent.ts create mode 100644 services/agent/src/agents/index.ts create mode 100644 services/agent/src/agents/supervisor.test.ts create mode 100644 services/agent/src/agents/supervisor.ts create mode 100644 services/agent/src/agents/types.ts diff --git a/apps/desktop/main.cjs b/apps/desktop/main.cjs index def2b8b..e45f038 100644 --- a/apps/desktop/main.cjs +++ b/apps/desktop/main.cjs @@ -279,6 +279,11 @@ function wireBridgeIpc() { try { if (op === "clipboard_read") { const t = clipboard.readText(); return t ? `The clipboard contains: ${t}` : "The clipboard is empty."; } if (op === "clipboard_write") { clipboard.writeText(String(arg ?? "")); return "Copied it to the clipboard."; } + if (op === "pick_folder") { + const opts = { title: "Choose a project folder", properties: ["openDirectory", "createDirectory"] }; + const r = await (mainWin ? dialog.showOpenDialog(mainWin, opts) : dialog.showOpenDialog(opts)); + return r.canceled ? "" : (r.filePaths[0] ?? ""); + } if (op === "open_url") { let u = String(arg ?? "").trim(); if (!/^https?:\/\//i.test(u)) u = `https://${u}`; diff --git a/apps/web/public/agents/claude.svg b/apps/web/public/agents/claude.svg new file mode 100644 index 0000000..e29f328 --- /dev/null +++ b/apps/web/public/agents/claude.svg @@ -0,0 +1 @@ +Claude \ No newline at end of file diff --git a/apps/web/public/agents/codex.svg b/apps/web/public/agents/codex.svg new file mode 100644 index 0000000..78caf4f --- /dev/null +++ b/apps/web/public/agents/codex.svg @@ -0,0 +1 @@ +OpenAI \ No newline at end of file diff --git a/apps/web/public/agents/cursor.svg b/apps/web/public/agents/cursor.svg new file mode 100644 index 0000000..a5b2ee3 --- /dev/null +++ b/apps/web/public/agents/cursor.svg @@ -0,0 +1 @@ +Cursor \ No newline at end of file diff --git a/apps/web/src/app/api/agents/route.ts b/apps/web/src/app/api/agents/route.ts new file mode 100644 index 0000000..0851576 --- /dev/null +++ b/apps/web/src/app/api/agents/route.ts @@ -0,0 +1,41 @@ +import { NextResponse } from "next/server"; +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; +import { homedir } from "node:os"; + +export const runtime = "nodejs"; +export const dynamic = "force-dynamic"; + +// Transparency for the Agents panel: is each agent's CLI installed, and where does +// it keep its sessions. The agents run on THIS machine with the user's own login; +// this just reports what's set up. Session dirs are the tools' own stores (resume +// by reopening the OpenLive conversation, which replays via ACP session/load). +const AGENTS = [ + { id: "claude-code", label: "Claude Code", bins: ["claude"], sessions: "~/.claude" }, + { id: "codex", label: "Codex", bins: ["codex"], sessions: "~/.codex/sessions" }, + { id: "cursor", label: "Cursor", bins: ["cursor-agent", "agent"], sessions: "~/.cursor" }, +]; + +// GUI-launched apps get a skeletal PATH on macOS; widen it like the agent driver +// does so an installed CLI in homebrew/npm still resolves. +function widenedPath(): string { + const extra = ["/usr/local/bin", "/opt/homebrew/bin", `${homedir()}/.local/bin`, `${homedir()}/bin`, `${homedir()}/.npm-global/bin`]; + const cur = (process.env.PATH ?? "").split(":"); + return [...cur, ...extra.filter((p) => !cur.includes(p))].join(":"); +} + +async function present(bin: string): Promise { + const finder = process.platform === "win32" ? "where" : "which"; + try { await promisify(execFile)(finder, [bin], { env: { ...process.env, PATH: widenedPath() }, timeout: 3000 }); return true; } + catch { return false; } +} + +export async function GET() { + const home = homedir(); + const rows = await Promise.all(AGENTS.map(async (a) => ({ + id: a.id, label: a.label, + installed: (await Promise.all(a.bins.map(present))).some(Boolean), + sessions: a.sessions, home, + }))); + return NextResponse.json(rows); +} diff --git a/apps/web/src/app/api/settings/route.ts b/apps/web/src/app/api/settings/route.ts index 9aae189..339c862 100644 --- a/apps/web/src/app/api/settings/route.ts +++ b/apps/web/src/app/api/settings/route.ts @@ -7,7 +7,9 @@ export const dynamic = "force-dynamic"; // Provider + model are chosen live in Settings; nothing hardcoded. Live effort // defaults to "auto" (lowest the model supports → smoothest voice). const DEFAULTS = { liveEffort: "auto" }; -const KEYS = ["liveModel", "liveProviderId", "liveEffort", "visionProviderId", "visionModel"]; +const KEYS = ["liveModel", "liveProviderId", "liveEffort", "visionProviderId", "visionModel", "agentCwd"]; +// Per-agent config keys (posture:, acpCommand:) are also writable. +const PREFIXES = ["posture:", "acpCommand:"]; export function GET() { return NextResponse.json({ ...DEFAULTS, ...getAllSettings() }); @@ -16,7 +18,7 @@ export function GET() { export async function PUT(req: Request) { const body = (await req.json()) as Record; for (const [k, v] of Object.entries(body)) { - if (KEYS.includes(k) && typeof v === "string") setSetting(k, v); + if (typeof v === "string" && (KEYS.includes(k) || PREFIXES.some((p) => k.startsWith(p)))) setSetting(k, v); } return NextResponse.json({ ...DEFAULTS, ...getAllSettings() }); } diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css index 58f080e..34b3e07 100644 --- a/apps/web/src/app/globals.css +++ b/apps/web/src/app/globals.css @@ -1,6 +1,6 @@ @import "tailwindcss"; -/* ─── Takt design tokens ─────────────────────────────────────────────────── +/* ─── OpenLive design tokens ─────────────────────────────────────────────── Token *names* are mapped to CSS custom properties via @theme inline, so the Tailwind utilities (bg-card, text-accent, …) resolve to var() at runtime. The actual values live in :root (warm-paper light) and .dark (near-black), @@ -129,18 +129,18 @@ body::before { .desktop .app-drag :is(button, a, input, select, textarea, label, [role="button"], [data-interactive]) { -webkit-app-region: no-drag; } /* ─── Scrollbars ──────────────────────────────────────────────────────────── */ -.takt-scroll { +.openlive-scroll { scrollbar-width: thin; scrollbar-color: var(--scrollbar-thumb) transparent; } -.takt-scroll::-webkit-scrollbar { width: 9px; height: 9px; } -.takt-scroll::-webkit-scrollbar-thumb { +.openlive-scroll::-webkit-scrollbar { width: 9px; height: 9px; } +.openlive-scroll::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb); border-radius: 999px; border: 2px solid transparent; background-clip: content-box; } -.takt-scroll::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-thumb-hover); background-clip: content-box; } +.openlive-scroll::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-thumb-hover); background-clip: content-box; } /* ─── Interaction animations ───────────────────────────────────────────────── */ @keyframes streaming-fade { diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index bf921de..988a93e 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -7,8 +7,11 @@ import { api } from "@/lib/api"; import { useUi } from "@/lib/uiStore"; import { LiveDock } from "@/components/live/LiveDock"; import { SettingsModal } from "@/components/settings/SettingsModal"; +import { AgentSelect } from "@/components/live/AgentControls"; import { OpenLiveMark } from "@/components/OpenLiveMark"; import { useAppVersion } from "@/lib/useAppVersion"; +import { setConversationBind } from "@/lib/live/useLiveSession"; +import { useLiveStore } from "@/lib/live/liveStore"; import { loadModels, modelsCached, modelsReady } from "@/lib/live/models"; function relTime(iso: string): string { @@ -41,7 +44,7 @@ function ResumeMenu({ onPick }: { onPick: (id: string) => void }) { {open && (
-
+
{chats.length === 0 &&

No saved conversations yet.

} {chats.map((c) => (
+ {/* Choose what a new conversation talks to — the built-in assistant or a + coding agent (Claude Code / Codex / Cursor). Carried into "New". */} +
+ Talk to +