| author | |
| committer | |
| log | 807e3d18dec858c9694a595deaea4f9ac588db5f |
| tree | c926654e81418cae6b15529bbce4d882ce031d01 |
| parent | 198cc58377fd87ff39b8a37464dd8cb1a77beb0b |
- Add @tanstack/react-query and wire up QueryClientProvider in App.tsx
- Refactor BranchToolbar to use useQuery for branch listing and
useMutation for checkout, create branch, and git init, replacing
manual useState/useCallback/useEffect async patterns
- Fix branch sync effect and ChatView envMode effect to use stable
primitive deps instead of full object references, preventing
unnecessary re-runs on unrelated store dispatches
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>26 files changed, 235 insertions(+), 433 deletions(-)
AGENTS.md+7| ... | ... | @@ -1,11 +1,13 @@ |
| 1 | 1 | # CLAUDE.md |
| 2 | 2 | |
| 3 | 3 | ## Project Snapshot |
| 4 | ||
| 4 | 5 | T3 Code is a minimal web GUI for using code agents like Codex and Claude Code (coming soon). |
| 5 | 6 | |
| 6 | 7 | This repository is a VERY EARLY WIP. Proposing sweeping changes that improve long-term maintainability is encouraged. |
| 7 | 8 | |
| 8 | 9 | ## Core Priorities |
| 10 | ||
| 9 | 11 | 1. Performance first. |
| 10 | 12 | 2. Reliability first. |
| 11 | 13 | 3. Keep behavior predictable under load and during failures (session restarts, reconnects, partial streams). |
| ... | ... | @@ -13,23 +15,28 @@ This repository is a VERY EARLY WIP. Proposing sweeping changes that improve lon |
| 13 | 15 | If a tradeoff is required, choose correctness and robustness over short-term convenience. |
| 14 | 16 | |
| 15 | 17 | ## Package Roles |
| 18 | ||
| 16 | 19 | - `apps/server`: Node.js WebSocket server. Wraps Codex app-server (JSON-RPC over stdio), serves the React web app, and manages provider sessions. |
| 17 | 20 | - `apps/web`: React/Vite UI. Owns session UX, conversation/event rendering, and client-side state. Connects to the server via WebSocket. |
| 18 | 21 | - `packages/contracts`: Shared Zod schemas and TypeScript contracts for provider events, WebSocket protocol, and model/session types. |
| 19 | 22 | |
| 20 | 23 | ## Codex App Server (Important) |
| 24 | ||
| 21 | 25 | T3 Code is currently Codex-first. The server starts `codex app-server` (JSON-RPC over stdio) per provider session, then streams structured events to the browser through WebSocket push messages. |
| 22 | 26 | |
| 23 | 27 | How we use it in this codebase: |
| 28 | ||
| 24 | 29 | - Session startup/resume and turn lifecycle are brokered in `apps/server/src/codexAppServerManager.ts`. |
| 25 | 30 | - Provider dispatch and thread event logging are coordinated in `apps/server/src/providerManager.ts`. |
| 26 | 31 | - WebSocket server routes NativeApi methods in `apps/server/src/wsServer.ts`. |
| 27 | 32 | - Web app consumes provider event streams via WebSocket push on channel `providers.event`. |
| 28 | 33 | |
| 29 | 34 | Docs: |
| 35 | ||
| 30 | 36 | - Codex App Server docs: https://developers.openai.com/codex/sdk/#app-server |
| 31 | 37 | |
| 32 | 38 | ## Reference Repos |
| 39 | ||
| 33 | 40 | - Open-source Codex repo: https://github.com/openai/codex |
| 34 | 41 | - Codex-Monitor (Tauri, feature-complete, strong reference implementation): https://github.com/Dimillian/CodexMonitor |
| 35 | 42 |
apps/desktop/turbo.jsonc+6-6| ... | ... | @@ -4,21 +4,21 @@ |
| 4 | 4 | "tasks": { |
| 5 | 5 | "build": { |
| 6 | 6 | "dependsOn": ["^build"], |
| 7 | "outputs": ["dist-electron/**"] | |
| 7 | "outputs": ["dist-electron/**"], | |
| 8 | 8 | }, |
| 9 | 9 | "dev": { |
| 10 | 10 | "dependsOn": ["t3#build"], |
| 11 | "persistent": true | |
| 11 | "persistent": true, | |
| 12 | 12 | }, |
| 13 | 13 | "start": { |
| 14 | 14 | "dependsOn": ["build", "@t3tools/web#build", "t3#build"], |
| 15 | 15 | "cache": false, |
| 16 | "persistent": true | |
| 16 | "persistent": true, | |
| 17 | 17 | }, |
| 18 | 18 | "smoke-test": { |
| 19 | 19 | "dependsOn": ["build", "@t3tools/web#build", "t3#build"], |
| 20 | 20 | "cache": false, |
| 21 | "outputs": [] | |
| 22 | } | |
| 23 | } | |
| 21 | "outputs": [], | |
| 22 | }, | |
| 23 | }, | |
| 24 | 24 | } |
apps/server/src/codexAppServerManager.test.ts+2-6| ... | ... | @@ -57,17 +57,13 @@ describe("normalizeCodexModelSlug", () => { |
| 57 | 57 | describe("isRecoverableThreadResumeError", () => { |
| 58 | 58 | it("matches not-found resume errors", () => { |
| 59 | 59 | expect( |
| 60 | isRecoverableThreadResumeError( | |
| 61 | new Error("thread/resume failed: thread not found"), | |
| 62 | ), | |
| 60 | isRecoverableThreadResumeError(new Error("thread/resume failed: thread not found")), | |
| 63 | 61 | ).toBe(true); |
| 64 | 62 | }); |
| 65 | 63 | |
| 66 | 64 | it("ignores non-resume errors", () => { |
| 67 | 65 | expect( |
| 68 | isRecoverableThreadResumeError( | |
| 69 | new Error("thread/start failed: permission denied"), | |
| 70 | ), | |
| 66 | isRecoverableThreadResumeError(new Error("thread/start failed: permission denied")), | |
| 71 | 67 | ).toBe(false); |
| 72 | 68 | }); |
| 73 | 69 |
apps/server/src/codexAppServerManager.ts+11-33| ... | ... | @@ -26,9 +26,7 @@ interface PendingRequest { |
| 26 | 26 | interface PendingApprovalRequest { |
| 27 | 27 | requestId: string; |
| 28 | 28 | jsonRpcId: string | number; |
| 29 | method: | |
| 30 | | "item/commandExecution/requestApproval" | |
| 31 | | "item/fileChange/requestApproval"; | |
| 29 | method: "item/commandExecution/requestApproval" | "item/fileChange/requestApproval"; | |
| 32 | 30 | requestKind: ProviderRequestKind; |
| 33 | 31 | threadId?: string; |
| 34 | 32 | turnId?: string; |
| ... | ... | @@ -122,16 +120,12 @@ export function classifyCodexStderrLine(rawLine: string): { message: string } | |
| 122 | 120 | } |
| 123 | 121 | |
| 124 | 122 | export function isRecoverableThreadResumeError(error: unknown): boolean { |
| 125 | const message = ( | |
| 126 | error instanceof Error ? error.message : String(error) | |
| 127 | ).toLowerCase(); | |
| 123 | const message = (error instanceof Error ? error.message : String(error)).toLowerCase(); | |
| 128 | 124 | if (!message.includes("thread/resume")) { |
| 129 | 125 | return false; |
| 130 | 126 | } |
| 131 | 127 | |
| 132 | return RECOVERABLE_THREAD_RESUME_ERROR_SNIPPETS.some((snippet) => | |
| 133 | message.includes(snippet), | |
| 134 | ); | |
| 128 | return RECOVERABLE_THREAD_RESUME_ERROR_SNIPPETS.some((snippet) => message.includes(snippet)); | |
| 135 | 129 | } |
| 136 | 130 | |
| 137 | 131 | export interface CodexAppServerManagerEvents { |
| ... | ... | @@ -209,14 +203,10 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve |
| 209 | 203 | if (input.resumeThreadId) { |
| 210 | 204 | try { |
| 211 | 205 | threadOpenMethod = "thread/resume"; |
| 212 | threadOpenResponse = await this.sendRequest( | |
| 213 | context, | |
| 214 | "thread/resume", | |
| 215 | { | |
| 216 | ...sessionOverrides, | |
| 217 | threadId: input.resumeThreadId, | |
| 218 | }, | |
| 219 | ); | |
| 206 | threadOpenResponse = await this.sendRequest(context, "thread/resume", { | |
| 207 | ...sessionOverrides, | |
| 208 | threadId: input.resumeThreadId, | |
| 209 | }); | |
| 220 | 210 | } catch (error) { |
| 221 | 211 | if (!isRecoverableThreadResumeError(error)) { |
| 222 | 212 | throw error; |
| ... | ... | @@ -228,19 +218,11 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve |
| 228 | 218 | "session/threadResumeFallback", |
| 229 | 219 | `Could not resume thread ${input.resumeThreadId}; started a new thread instead.`, |
| 230 | 220 | ); |
| 231 | threadOpenResponse = await this.sendRequest( | |
| 232 | context, | |
| 233 | "thread/start", | |
| 234 | threadStartParams, | |
| 235 | ); | |
| 221 | threadOpenResponse = await this.sendRequest(context, "thread/start", threadStartParams); | |
| 236 | 222 | } |
| 237 | 223 | } else { |
| 238 | 224 | threadOpenMethod = "thread/start"; |
| 239 | threadOpenResponse = await this.sendRequest( | |
| 240 | context, | |
| 241 | "thread/start", | |
| 242 | threadStartParams, | |
| 243 | ); | |
| 225 | threadOpenResponse = await this.sendRequest(context, "thread/start", threadStartParams); | |
| 244 | 226 | } |
| 245 | 227 | |
| 246 | 228 | const threadOpenRecord = this.readObject(threadOpenResponse); |
| ... | ... | @@ -248,9 +230,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve |
| 248 | 230 | this.readString(this.readObject(threadOpenRecord, "thread"), "id") ?? |
| 249 | 231 | this.readString(threadOpenRecord, "threadId"); |
| 250 | 232 | if (!threadId) { |
| 251 | throw new Error( | |
| 252 | `${threadOpenMethod} response did not include a thread id.`, | |
| 253 | ); | |
| 233 | throw new Error(`${threadOpenMethod} response did not include a thread id.`); | |
| 254 | 234 | } |
| 255 | 235 | |
| 256 | 236 | this.updateSession(context, { |
| ... | ... | @@ -732,9 +712,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve |
| 732 | 712 | }; |
| 733 | 713 | } |
| 734 | 714 | |
| 735 | private requestKindForMethod( | |
| 736 | method: string, | |
| 737 | ): ProviderRequestKind | undefined { | |
| 715 | private requestKindForMethod(method: string): ProviderRequestKind | undefined { | |
| 738 | 716 | if (method === "item/commandExecution/requestApproval") { |
| 739 | 717 | return "command"; |
| 740 | 718 | } |
apps/server/src/git.ts+2-10| ... | ... | @@ -16,11 +16,7 @@ import type { |
| 16 | 16 | } from "@t3tools/contracts"; |
| 17 | 17 | |
| 18 | 18 | /** Spawn git directly with an argv array — no shell, no quoting needed. */ |
| 19 | function runGit( | |
| 20 | args: string[], | |
| 21 | cwd: string, | |
| 22 | timeoutMs = 30_000, | |
| 23 | ): Promise<TerminalCommandResult> { | |
| 19 | function runGit(args: string[], cwd: string, timeoutMs = 30_000): Promise<TerminalCommandResult> { | |
| 24 | 20 | return new Promise((resolve, reject) => { |
| 25 | 21 | const child = spawn("git", args, { |
| 26 | 22 | cwd, |
| ... | ... | @@ -127,11 +123,7 @@ export async function listGitBranches(input: GitListBranchesInput): Promise<GitL |
| 127 | 123 | } |
| 128 | 124 | |
| 129 | 125 | // Resolve the real default branch from the remote |
| 130 | const defaultRef = await runGit( | |
| 131 | ["symbolic-ref", "refs/remotes/origin/HEAD"], | |
| 132 | input.cwd, | |
| 133 | 5_000, | |
| 134 | ); | |
| 126 | const defaultRef = await runGit(["symbolic-ref", "refs/remotes/origin/HEAD"], input.cwd, 5_000); | |
| 135 | 127 | const defaultBranch = |
| 136 | 128 | defaultRef.code === 0 ? defaultRef.stdout.trim().replace(/^refs\/remotes\/origin\//, "") : null; |
| 137 | 129 |
apps/server/src/providerManager.test.ts+1-4| ... | ... | @@ -16,10 +16,7 @@ describe("ProviderManager", () => { |
| 16 | 16 | method: string; |
| 17 | 17 | threadId: string; |
| 18 | 18 | }) => void; |
| 19 | threadLogStreams: Map< | |
| 20 | string, | |
| 21 | { writableEnded: boolean; destroyed: boolean } | |
| 22 | >; | |
| 19 | threadLogStreams: Map<string, { writableEnded: boolean; destroyed: boolean }>; | |
| 23 | 20 | }; |
| 24 | 21 | internals.onCodexEvent({ |
| 25 | 22 | id: "evt-1", |
apps/server/src/providerManager.ts+2-9| ... | ... | @@ -87,11 +87,7 @@ export class ProviderManager extends EventEmitter<ProviderManagerEvents> { |
| 87 | 87 | throw new Error(`Unknown provider session: ${input.sessionId}`); |
| 88 | 88 | } |
| 89 | 89 | |
| 90 | await this.codex.respondToRequest( | |
| 91 | input.sessionId, | |
| 92 | input.requestId, | |
| 93 | input.decision, | |
| 94 | ); | |
| 90 | await this.codex.respondToRequest(input.sessionId, input.requestId, input.decision); | |
| 95 | 91 | } |
| 96 | 92 | |
| 97 | 93 | stopSession(raw: ProviderStopSessionInput): void { |
| ... | ... | @@ -151,10 +147,7 @@ export class ProviderManager extends EventEmitter<ProviderManagerEvents> { |
| 151 | 147 | |
| 152 | 148 | private resolveThreadId(event: ProviderEvent): string | undefined { |
| 153 | 149 | const fromPayload = this.readThreadIdFromPayload(event.payload); |
| 154 | const threadId = | |
| 155 | event.threadId ?? | |
| 156 | fromPayload ?? | |
| 157 | this.sessionThreadIds.get(event.sessionId); | |
| 150 | const threadId = event.threadId ?? fromPayload ?? this.sessionThreadIds.get(event.sessionId); | |
| 158 | 151 | |
| 159 | 152 | if (threadId) { |
| 160 | 153 | this.sessionThreadIds.set(event.sessionId, threadId); |
apps/server/src/wsServer.ts+4-1| ... | ... | @@ -369,7 +369,10 @@ export function createServer(options: ServerOptions) { |
| 369 | 369 | const isServerNotRunningError = (error: unknown): boolean => { |
| 370 | 370 | if (!(error instanceof Error)) return false; |
| 371 | 371 | const maybeCode = (error as NodeJS.ErrnoException).code; |
| 372 | return maybeCode === "ERR_SERVER_NOT_RUNNING" || error.message.toLowerCase().includes("not running"); | |
| 372 | return ( | |
| 373 | maybeCode === "ERR_SERVER_NOT_RUNNING" || | |
| 374 | error.message.toLowerCase().includes("not running") | |
| 375 | ); | |
| 373 | 376 | }; |
| 374 | 377 | |
| 375 | 378 | const closeWebSocketServer = new Promise<void>((resolve, reject) => { |
apps/server/turbo.jsonc+3-3| ... | ... | @@ -5,7 +5,7 @@ |
| 5 | 5 | "start": { |
| 6 | 6 | "dependsOn": ["build"], |
| 7 | 7 | "cache": false, |
| 8 | "persistent": true | |
| 9 | } | |
| 10 | } | |
| 8 | "persistent": true, | |
| 9 | }, | |
| 10 | }, | |
| 11 | 11 | } |
apps/web/index.html+4-1| ... | ... | @@ -5,7 +5,10 @@ |
| 5 | 5 | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
| 6 | 6 | <link rel="preconnect" href="https://fonts.googleapis.com" /> |
| 7 | 7 | <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> |
| 8 | <link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300..800;1,9..40,300..800&display=swap" rel="stylesheet" /> | |
| 8 | <link | |
| 9 | href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300..800;1,9..40,300..800&display=swap" | |
| 10 | rel="stylesheet" | |
| 11 | /> | |
| 9 | 12 | <title>T3 Code</title> |
| 10 | 13 | </head> |
| 11 | 14 | <body> |
apps/web/package.json+1| ... | ... | @@ -12,6 +12,7 @@ |
| 12 | 12 | }, |
| 13 | 13 | "dependencies": { |
| 14 | 14 | "@t3tools/contracts": "workspace:*", |
| 15 | "@tanstack/react-query": "^5.90.0", | |
| 15 | 16 | "highlight.js": "^11.11.1", |
| 16 | 17 | "lucide-react": "^0.563.0", |
| 17 | 18 | "react": "^19.0.0", |
apps/web/src/App.tsx+8-3| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; | |
| 1 | 2 | import { useEffect, useMemo, useRef } from "react"; |
| 2 | 3 | |
| 3 | 4 | import ChatView from "./components/ChatView"; |
| ... | ... | @@ -168,10 +169,14 @@ function Layout() { |
| 168 | 169 | ); |
| 169 | 170 | } |
| 170 | 171 | |
| 172 | const queryClient = new QueryClient(); | |
| 173 | ||
| 171 | 174 | export default function App() { |
| 172 | 175 | return ( |
| 173 | <StoreProvider> | |
| 174 | <Layout /> | |
| 175 | </StoreProvider> | |
| 176 | <QueryClientProvider client={queryClient}> | |
| 177 | <StoreProvider> | |
| 178 | <Layout /> | |
| 179 | </StoreProvider> | |
| 180 | </QueryClientProvider> | |
| 176 | 181 | ); |
| 177 | 182 | } |
apps/web/src/components/BranchToolbar.tsx+89-108| ... | ... | @@ -1,5 +1,6 @@ |
| 1 | 1 | import type { GitBranch } from "@t3tools/contracts"; |
| 2 | import { useCallback, useEffect, useMemo, useRef, useState } from "react"; | |
| 2 | import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; | |
| 3 | import { useEffect, useMemo, useRef, useState } from "react"; | |
| 3 | 4 | |
| 4 | 5 | import { readNativeApi } from "../session-logic"; |
| 5 | 6 | import { useStore } from "../store"; |
| ... | ... | @@ -10,76 +11,85 @@ interface BranchToolbarProps { |
| 10 | 11 | envLocked: boolean; |
| 11 | 12 | } |
| 12 | 13 | |
| 13 | export default function BranchToolbar({ | |
| 14 | envMode, | |
| 15 | onEnvModeChange, | |
| 16 | envLocked, | |
| 17 | }: BranchToolbarProps) { | |
| 14 | export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: BranchToolbarProps) { | |
| 18 | 15 | const { state, dispatch } = useStore(); |
| 19 | 16 | const api = useMemo(() => readNativeApi(), []); |
| 17 | const queryClient = useQueryClient(); | |
| 20 | 18 | |
| 21 | const [branches, setBranches] = useState<GitBranch[]>([]); | |
| 22 | const [isRepo, setIsRepo] = useState(false); | |
| 23 | const [isLoadingBranches, setIsLoadingBranches] = useState(false); | |
| 24 | 19 | const [isBranchMenuOpen, setIsBranchMenuOpen] = useState(false); |
| 25 | 20 | const [isCreatingBranch, setIsCreatingBranch] = useState(false); |
| 26 | 21 | const [newBranchName, setNewBranchName] = useState(""); |
| 27 | const [isInitializingRepo, setIsInitializingRepo] = useState(false); | |
| 28 | 22 | const branchMenuRef = useRef<HTMLDivElement>(null); |
| 29 | 23 | |
| 30 | 24 | const activeThread = state.threads.find((thread) => thread.id === state.activeThreadId); |
| 31 | 25 | const activeProject = state.projects.find((project) => project.id === activeThread?.projectId); |
| 32 | const branchCwd = activeThread?.worktreePath ?? activeProject?.cwd; | |
| 26 | const activeThreadId = activeThread?.id; | |
| 27 | const activeThreadBranch = activeThread?.branch ?? null; | |
| 28 | const activeWorktreePath = activeThread?.worktreePath ?? null; | |
| 29 | const branchCwd = activeWorktreePath ?? activeProject?.cwd; | |
| 33 | 30 | |
| 34 | const loadBranches = useCallback(async () => { | |
| 35 | if (!api || !branchCwd) return; | |
| 36 | setIsLoadingBranches(true); | |
| 37 | try { | |
| 38 | const result = await api.git.listBranches({ cwd: branchCwd }); | |
| 39 | setIsRepo(result.isRepo); | |
| 40 | setBranches(result.branches); | |
| 41 | } catch (error) { | |
| 42 | setIsRepo(false); | |
| 43 | setBranches([]); | |
| 44 | if (activeThread) { | |
| 45 | dispatch({ | |
| 46 | type: "SET_ERROR", | |
| 47 | threadId: activeThread.id, | |
| 48 | error: | |
| 49 | error instanceof Error ? error.message : "Failed to load git branches.", | |
| 50 | }); | |
| 51 | } | |
| 52 | } finally { | |
| 53 | setIsLoadingBranches(false); | |
| 54 | } | |
| 55 | }, [activeThread, api, branchCwd, dispatch]); | |
| 31 | // ── Queries ─────────────────────────────────────────────────────────── | |
| 56 | 32 | |
| 57 | useEffect(() => { | |
| 58 | void loadBranches(); | |
| 59 | }, [loadBranches]); | |
| 33 | const branchesQuery = useQuery({ | |
| 34 | queryKey: ["git", "branches", branchCwd], | |
| 35 | queryFn: () => api!.git.listBranches({ cwd: branchCwd! }), | |
| 36 | enabled: !!api && !!branchCwd, | |
| 37 | }); | |
| 38 | ||
| 39 | const branches = branchesQuery.data?.branches ?? []; | |
| 40 | const isRepo = branchesQuery.data?.isRepo ?? false; | |
| 41 | ||
| 42 | // ── Mutations ───────────────────────────────────────────────────────── | |
| 43 | ||
| 44 | const checkoutMutation = useMutation({ | |
| 45 | mutationFn: (branch: string) => api!.git.checkout({ cwd: branchCwd!, branch }), | |
| 46 | onSuccess: () => queryClient.invalidateQueries({ queryKey: ["git", "branches", branchCwd] }), | |
| 47 | onError: (error) => | |
| 48 | setThreadError(error instanceof Error ? error.message : "Failed to checkout branch."), | |
| 49 | }); | |
| 50 | ||
| 51 | const createBranchMutation = useMutation({ | |
| 52 | mutationFn: (branch: string) => | |
| 53 | api!.git | |
| 54 | .createBranch({ cwd: branchCwd!, branch }) | |
| 55 | .then(() => api!.git.checkout({ cwd: branchCwd!, branch })), | |
| 56 | onSuccess: () => queryClient.invalidateQueries({ queryKey: ["git", "branches", branchCwd] }), | |
| 57 | onError: (error) => | |
| 58 | setThreadError(error instanceof Error ? error.message : "Failed to create branch."), | |
| 59 | }); | |
| 60 | ||
| 61 | const initMutation = useMutation({ | |
| 62 | mutationFn: () => api!.git.init({ cwd: branchCwd! }), | |
| 63 | onSuccess: () => { | |
| 64 | setThreadError(null); | |
| 65 | queryClient.invalidateQueries({ queryKey: ["git", "branches", branchCwd] }); | |
| 66 | }, | |
| 67 | onError: (error) => | |
| 68 | setThreadError(error instanceof Error ? error.message : "Failed to initialize git repo."), | |
| 69 | }); | |
| 70 | ||
| 71 | // ── Effects ─────────────────────────────────────────────────────────── | |
| 60 | 72 | |
| 61 | 73 | // Keep thread branch synced to git current branch for local threads. |
| 74 | const queryBranches = branchesQuery.data?.branches; | |
| 62 | 75 | useEffect(() => { |
| 63 | if (!activeThread || activeThread.worktreePath) return; | |
| 64 | const current = branches.find((branch) => branch.current); | |
| 76 | if (!activeThreadId || activeWorktreePath) return; | |
| 77 | const current = queryBranches?.find((branch) => branch.current); | |
| 65 | 78 | if (!current) return; |
| 66 | if (current.name === activeThread.branch) return; | |
| 79 | if (current.name === activeThreadBranch) return; | |
| 67 | 80 | dispatch({ |
| 68 | 81 | type: "SET_THREAD_BRANCH", |
| 69 | threadId: activeThread.id, | |
| 82 | threadId: activeThreadId, | |
| 70 | 83 | branch: current.name, |
| 71 | 84 | worktreePath: null, |
| 72 | 85 | }); |
| 73 | }, [activeThread, branches, dispatch]); | |
| 86 | }, [activeThreadId, activeWorktreePath, activeThreadBranch, queryBranches, dispatch]); | |
| 74 | 87 | |
| 75 | 88 | useEffect(() => { |
| 76 | 89 | if (!isBranchMenuOpen) return; |
| 77 | 90 | const handleClickOutside = (event: MouseEvent) => { |
| 78 | 91 | if (!branchMenuRef.current) return; |
| 79 | if ( | |
| 80 | event.target instanceof Node && | |
| 81 | !branchMenuRef.current.contains(event.target) | |
| 82 | ) { | |
| 92 | if (event.target instanceof Node && !branchMenuRef.current.contains(event.target)) { | |
| 83 | 93 | setIsBranchMenuOpen(false); |
| 84 | 94 | } |
| 85 | 95 | }; |
| ... | ... | @@ -89,79 +99,50 @@ export default function BranchToolbar({ |
| 89 | 99 | }; |
| 90 | 100 | }, [isBranchMenuOpen]); |
| 91 | 101 | |
| 102 | // ── Helpers ─────────────────────────────────────────────────────────── | |
| 103 | ||
| 92 | 104 | const setThreadError = (error: string | null) => { |
| 93 | if (!activeThread) return; | |
| 94 | dispatch({ | |
| 95 | type: "SET_ERROR", | |
| 96 | threadId: activeThread.id, | |
| 97 | error, | |
| 98 | }); | |
| 105 | if (!activeThreadId) return; | |
| 106 | dispatch({ type: "SET_ERROR", threadId: activeThreadId, error }); | |
| 99 | 107 | }; |
| 100 | 108 | |
| 101 | 109 | const setThreadBranch = (branch: string | null, worktreePath: string | null) => { |
| 102 | if (!activeThread) return; | |
| 103 | dispatch({ | |
| 104 | type: "SET_THREAD_BRANCH", | |
| 105 | threadId: activeThread.id, | |
| 106 | branch, | |
| 107 | worktreePath, | |
| 108 | }); | |
| 109 | }; | |
| 110 | ||
| 111 | const initializeRepo = async () => { | |
| 112 | if (!api || !branchCwd || !activeThread || isInitializingRepo) return; | |
| 113 | setIsInitializingRepo(true); | |
| 114 | try { | |
| 115 | await api.git.init({ cwd: branchCwd }); | |
| 116 | setThreadError(null); | |
| 117 | await loadBranches(); | |
| 118 | } catch (error) { | |
| 119 | setThreadError(error instanceof Error ? error.message : "Failed to initialize git repo."); | |
| 120 | } finally { | |
| 121 | setIsInitializingRepo(false); | |
| 122 | } | |
| 110 | if (!activeThreadId) return; | |
| 111 | dispatch({ type: "SET_THREAD_BRANCH", threadId: activeThreadId, branch, worktreePath }); | |
| 123 | 112 | }; |
| 124 | 113 | |
| 125 | const selectBranch = async (branch: GitBranch) => { | |
| 126 | if (!api || !activeThread || !branchCwd) return; | |
| 114 | const selectBranch = (branch: GitBranch) => { | |
| 115 | if (!api || !activeThreadId || !branchCwd) return; | |
| 127 | 116 | |
| 128 | 117 | // For new worktree mode, selecting a branch picks the base branch. |
| 129 | if (envMode === "worktree" && !envLocked && !activeThread.worktreePath) { | |
| 118 | if (envMode === "worktree" && !envLocked && !activeWorktreePath) { | |
| 130 | 119 | setThreadError(null); |
| 131 | 120 | setThreadBranch(branch.name, null); |
| 132 | 121 | setIsBranchMenuOpen(false); |
| 133 | 122 | return; |
| 134 | 123 | } |
| 135 | 124 | |
| 136 | try { | |
| 137 | await api.git.checkout({ | |
| 138 | cwd: branchCwd, | |
| 139 | branch: branch.name, | |
| 140 | }); | |
| 141 | setThreadError(null); | |
| 142 | setThreadBranch(branch.name, activeThread.worktreePath); | |
| 143 | setIsBranchMenuOpen(false); | |
| 144 | await loadBranches(); | |
| 145 | } catch (error) { | |
| 146 | setThreadError(error instanceof Error ? error.message : "Failed to checkout branch."); | |
| 147 | } | |
| 125 | checkoutMutation.mutate(branch.name, { | |
| 126 | onSuccess: () => { | |
| 127 | setThreadError(null); | |
| 128 | setThreadBranch(branch.name, activeWorktreePath); | |
| 129 | setIsBranchMenuOpen(false); | |
| 130 | }, | |
| 131 | }); | |
| 148 | 132 | }; |
| 149 | 133 | |
| 150 | const createBranch = async () => { | |
| 134 | const createBranch = () => { | |
| 151 | 135 | const name = newBranchName.trim(); |
| 152 | if (!api || !activeThread || !branchCwd || !name) return; | |
| 153 | try { | |
| 154 | await api.git.createBranch({ cwd: branchCwd, branch: name }); | |
| 155 | await api.git.checkout({ cwd: branchCwd, branch: name }); | |
| 156 | setThreadError(null); | |
| 157 | setThreadBranch(name, activeThread.worktreePath); | |
| 158 | setNewBranchName(""); | |
| 159 | setIsCreatingBranch(false); | |
| 160 | setIsBranchMenuOpen(false); | |
| 161 | await loadBranches(); | |
| 162 | } catch (error) { | |
| 163 | setThreadError(error instanceof Error ? error.message : "Failed to create branch."); | |
| 164 | } | |
| 136 | if (!api || !activeThreadId || !branchCwd || !name) return; | |
| 137 | createBranchMutation.mutate(name, { | |
| 138 | onSuccess: () => { | |
| 139 | setThreadError(null); | |
| 140 | setThreadBranch(name, activeWorktreePath); | |
| 141 | setNewBranchName(""); | |
| 142 | setIsCreatingBranch(false); | |
| 143 | setIsBranchMenuOpen(false); | |
| 144 | }, | |
| 145 | }); | |
| 165 | 146 | }; |
| 166 | 147 | |
| 167 | 148 | if (!activeThread || !activeProject) return null; |
| ... | ... | @@ -192,10 +173,10 @@ export default function BranchToolbar({ |
| 192 | 173 | <button |
| 193 | 174 | type="button" |
| 194 | 175 | className="inline-flex items-center gap-1.5 rounded-lg border border-border px-2 py-1 text-[12px] text-muted-foreground/70 transition-colors duration-150 hover:bg-accent hover:text-foreground/80 disabled:cursor-not-allowed disabled:opacity-50" |
| 195 | disabled={!branchCwd || isInitializingRepo} | |
| 196 | onClick={() => void initializeRepo()} | |
| 176 | disabled={!branchCwd || initMutation.isPending} | |
| 177 | onClick={() => initMutation.mutate()} | |
| 197 | 178 | > |
| 198 | {isInitializingRepo ? "Initializing…" : "Initialize git"} | |
| 179 | {initMutation.isPending ? "Initializing\u2026" : "Initialize git"} | |
| 199 | 180 | </button> |
| 200 | 181 | ) : ( |
| 201 | 182 | <div className="relative" ref={branchMenuRef}> |
| ... | ... | @@ -203,7 +184,7 @@ export default function BranchToolbar({ |
| 203 | 184 | type="button" |
| 204 | 185 | className="inline-flex items-center gap-1.5 rounded-lg px-2 py-1 text-[12px] text-muted-foreground/60 transition-colors duration-150 hover:bg-accent/50 hover:text-muted-foreground/80" |
| 205 | 186 | onClick={() => setIsBranchMenuOpen((open) => !open)} |
| 206 | disabled={isLoadingBranches} | |
| 187 | disabled={branchesQuery.isLoading} | |
| 207 | 188 | > |
| 208 | 189 | <span className="max-w-[240px] truncate font-mono"> |
| 209 | 190 | {activeThread.branch |
| ... | ... | @@ -246,7 +227,7 @@ export default function BranchToolbar({ |
| 246 | 227 | ? "bg-accent text-foreground" |
| 247 | 228 | : "text-foreground/90 hover:bg-accent/50" |
| 248 | 229 | }`} |
| 249 | onClick={() => void selectBranch(branch)} | |
| 230 | onClick={() => selectBranch(branch)} | |
| 250 | 231 | > |
| 251 | 232 | <span className="truncate">{branch.name}</span> |
| 252 | 233 | {(branch.current || branch.isDefault) && ( |
| ... | ... | @@ -266,7 +247,7 @@ export default function BranchToolbar({ |
| 266 | 247 | className="flex items-center gap-1 px-1" |
| 267 | 248 | onSubmit={(event) => { |
| 268 | 249 | event.preventDefault(); |
| 269 | void createBranch(); | |
| 250 | createBranch(); | |
| 270 | 251 | }} |
| 271 | 252 | > |
| 272 | 253 | <input |
| ... | ... | @@ -286,7 +267,7 @@ export default function BranchToolbar({ |
| 286 | 267 | /> |
| 287 | 268 | <button |
| 288 | 269 | type="submit" |
| 289 | disabled={!newBranchName.trim()} | |
| 270 | disabled={!newBranchName.trim() || createBranchMutation.isPending} | |
| 290 | 271 | className="rounded-lg px-2 py-1.5 text-xs text-muted-foreground/70 transition-colors hover:bg-accent hover:text-foreground/80 disabled:opacity-30" |
| 291 | 272 | > |
| 292 | 273 | Create |
apps/web/src/components/ChatView.tsx+49-139| ... | ... | @@ -92,23 +92,17 @@ function approvalDetail(event: ProviderEvent): string | undefined { |
| 92 | 92 | return asString(payload?.reason); |
| 93 | 93 | } |
| 94 | 94 | |
| 95 | function derivePendingApprovals( | |
| 96 | events: ProviderEvent[], | |
| 97 | ): PendingApprovalCard[] { | |
| 95 | function derivePendingApprovals(events: ProviderEvent[]): PendingApprovalCard[] { | |
| 98 | 96 | const pending = new Map<string, PendingApprovalCard>(); |
| 99 | 97 | const ordered = [...events].toReversed(); |
| 100 | 98 | |
| 101 | 99 | for (const event of ordered) { |
| 102 | if ( | |
| 103 | event.method === "session/closed" || | |
| 104 | event.method === "session/exited" | |
| 105 | ) { | |
| 100 | if (event.method === "session/closed" || event.method === "session/exited") { | |
| 106 | 101 | pending.clear(); |
| 107 | 102 | continue; |
| 108 | 103 | } |
| 109 | 104 | |
| 110 | const requestId = | |
| 111 | event.requestId ?? asString(asRecord(event.payload)?.requestId); | |
| 105 | const requestId = event.requestId ?? asString(asRecord(event.payload)?.requestId); | |
| 112 | 106 | if (!requestId) continue; |
| 113 | 107 | |
| 114 | 108 | if ( |
| ... | ... | @@ -143,20 +137,13 @@ export default function ChatView() { |
| 143 | 137 | const [isEditorMenuOpen, setIsEditorMenuOpen] = useState(false); |
| 144 | 138 | const [lastEditor, setLastEditor] = useState<EditorId>(() => { |
| 145 | 139 | const stored = localStorage.getItem(LAST_EDITOR_KEY); |
| 146 | return EDITORS.some((e) => e.id === stored) | |
| 147 | ? (stored as EditorId) | |
| 148 | : EDITORS[0].id; | |
| 140 | return EDITORS.some((e) => e.id === stored) ? (stored as EditorId) : EDITORS[0].id; | |
| 149 | 141 | }); |
| 150 | const [selectedEffort, setSelectedEffort] = | |
| 151 | useState<string>(DEFAULT_REASONING); | |
| 142 | const [selectedEffort, setSelectedEffort] = useState<string>(DEFAULT_REASONING); | |
| 152 | 143 | const [envMode, setEnvMode] = useState<"local" | "worktree">("local"); |
| 153 | 144 | const [isSwitchingRuntimeMode, setIsSwitchingRuntimeMode] = useState(false); |
| 154 | const [respondingRequestIds, setRespondingRequestIds] = useState<string[]>( | |
| 155 | [], | |
| 156 | ); | |
| 157 | const [expandedWorkGroups, setExpandedWorkGroups] = useState< | |
| 158 | Record<string, boolean> | |
| 159 | >({}); | |
| 145 | const [respondingRequestIds, setRespondingRequestIds] = useState<string[]>([]); | |
| 146 | const [expandedWorkGroups, setExpandedWorkGroups] = useState<Record<string, boolean>>({}); | |
| 160 | 147 | const [nowTick, setNowTick] = useState(() => Date.now()); |
| 161 | 148 | const messagesEndRef = useRef<HTMLDivElement>(null); |
| 162 | 149 | const textareaRef = useRef<HTMLTextAreaElement>(null); |
| ... | ... | @@ -177,11 +164,7 @@ export default function ChatView() { |
| 177 | 164 | [activeThread?.events], |
| 178 | 165 | ); |
| 179 | 166 | const latestTurnWorkEntries = useMemo( |
| 180 | () => | |
| 181 | deriveWorkLogEntries( | |
| 182 | activeThread?.events ?? [], | |
| 183 | activeThread?.latestTurnId, | |
| 184 | ), | |
| 167 | () => deriveWorkLogEntries(activeThread?.events ?? [], activeThread?.latestTurnId), | |
| 185 | 168 | [activeThread?.events, activeThread?.latestTurnId], |
| 186 | 169 | ); |
| 187 | 170 | const pendingApprovals = useMemo( |
| ... | ... | @@ -269,22 +252,18 @@ export default function ChatView() { |
| 269 | 252 | } as const); |
| 270 | 253 | const envLocked = Boolean( |
| 271 | 254 | activeThread && |
| 272 | (activeThread.messages.length > 0 || | |
| 273 | (activeThread.session !== null && activeThread.session.status !== "closed")), | |
| 255 | (activeThread.messages.length > 0 || | |
| 256 | (activeThread.session !== null && activeThread.session.status !== "closed")), | |
| 274 | 257 | ); |
| 275 | 258 | |
| 276 | const handleRuntimeModeChange = async ( | |
| 277 | mode: "approval-required" | "full-access", | |
| 278 | ) => { | |
| 259 | const handleRuntimeModeChange = async (mode: "approval-required" | "full-access") => { | |
| 279 | 260 | if (mode === state.runtimeMode) return; |
| 280 | 261 | dispatch({ type: "SET_RUNTIME_MODE", mode }); |
| 281 | 262 | if (!api) return; |
| 282 | 263 | |
| 283 | 264 | const sessionIds = state.threads |
| 284 | 265 | .map((t) => t.session) |
| 285 | .filter( | |
| 286 | (s): s is NonNullable<typeof s> => s !== null && s.status !== "closed", | |
| 287 | ) | |
| 266 | .filter((s): s is NonNullable<typeof s> => s !== null && s.status !== "closed") | |
| 288 | 267 | .map((s) => s.sessionId); |
| 289 | 268 | |
| 290 | 269 | if (sessionIds.length === 0) return; |
| ... | ... | @@ -292,9 +271,7 @@ export default function ChatView() { |
| 292 | 271 | setIsSwitchingRuntimeMode(true); |
| 293 | 272 | try { |
| 294 | 273 | await Promise.all( |
| 295 | sessionIds.map((id) => | |
| 296 | api.providers.stopSession({ sessionId: id }).catch(() => undefined), | |
| 297 | ), | |
| 274 | sessionIds.map((id) => api.providers.stopSession({ sessionId: id }).catch(() => undefined)), | |
| 298 | 275 | ); |
| 299 | 276 | } finally { |
| 300 | 277 | setIsSwitchingRuntimeMode(false); |
| ... | ... | @@ -316,10 +293,11 @@ export default function ChatView() { |
| 316 | 293 | setExpandedWorkGroups({}); |
| 317 | 294 | }, [activeThread?.id]); |
| 318 | 295 | |
| 296 | const activeWorktreePath = activeThread?.worktreePath; | |
| 319 | 297 | useEffect(() => { |
| 320 | if (!activeThread) return; | |
| 321 | setEnvMode(activeThread.worktreePath ? "worktree" : "local"); | |
| 322 | }, [activeThread?.id, activeThread?.worktreePath]); | |
| 298 | if (!activeThread?.id) return; | |
| 299 | setEnvMode(activeWorktreePath ? "worktree" : "local"); | |
| 300 | }, [activeThread?.id, activeWorktreePath]); | |
| 323 | 301 | |
| 324 | 302 | // Auto-resize textarea |
| 325 | 303 | useEffect(() => { |
| ... | ... | @@ -360,10 +338,7 @@ export default function ChatView() { |
| 360 | 338 | |
| 361 | 339 | const handleClickOutside = (event: MouseEvent) => { |
| 362 | 340 | if (!editorMenuRef.current) return; |
| 363 | if ( | |
| 364 | event.target instanceof Node && | |
| 365 | !editorMenuRef.current.contains(event.target) | |
| 366 | ) { | |
| 341 | if (event.target instanceof Node && !editorMenuRef.current.contains(event.target)) { | |
| 367 | 342 | setIsEditorMenuOpen(false); |
| 368 | 343 | } |
| 369 | 344 | }; |
| ... | ... | @@ -398,9 +373,7 @@ export default function ChatView() { |
| 398 | 373 | setIsEditorMenuOpen(false); |
| 399 | 374 | }; |
| 400 | 375 | |
| 401 | const ensureSession = async ( | |
| 402 | cwdOverride?: string, | |
| 403 | ): Promise<EnsuredSessionInfo | null> => { | |
| 376 | const ensureSession = async (cwdOverride?: string): Promise<EnsuredSessionInfo | null> => { | |
| 404 | 377 | if (!api || !activeThread || !activeProject) return null; |
| 405 | 378 | if (activeThread.session && activeThread.session.status !== "closed") { |
| 406 | 379 | const sessionThreadId = activeThread.session.threadId ?? null; |
| ... | ... | @@ -527,14 +500,9 @@ export default function ChatView() { |
| 527 | 500 | try { |
| 528 | 501 | const shouldBootstrap = |
| 529 | 502 | previousMessages.length > 0 && |
| 530 | (sessionInfo.continuityState === "new" || | |
| 531 | sessionInfo.continuityState === "fallback_new"); | |
| 503 | (sessionInfo.continuityState === "new" || sessionInfo.continuityState === "fallback_new"); | |
| 532 | 504 | const input = shouldBootstrap |
| 533 | ? buildBootstrapInput( | |
| 534 | previousMessages, | |
| 535 | trimmed, | |
| 536 | PROVIDER_SEND_TURN_MAX_INPUT_CHARS, | |
| 537 | ).text | |
| 505 | ? buildBootstrapInput(previousMessages, trimmed, PROVIDER_SEND_TURN_MAX_INPUT_CHARS).text | |
| 538 | 506 | : trimmed; |
| 539 | 507 | await api.providers.sendTurn({ |
| 540 | 508 | sessionId: sessionInfo.sessionId, |
| ... | ... | @@ -561,10 +529,7 @@ export default function ChatView() { |
| 561 | 529 | }); |
| 562 | 530 | }; |
| 563 | 531 | |
| 564 | const onRespondToApproval = async ( | |
| 565 | requestId: string, | |
| 566 | decision: ProviderApprovalDecision, | |
| 567 | ) => { | |
| 532 | const onRespondToApproval = async (requestId: string, decision: ProviderApprovalDecision) => { | |
| 568 | 533 | if (!api || !activeThread?.session) return; |
| 569 | 534 | |
| 570 | 535 | setRespondingRequestIds((existing) => |
| ... | ... | @@ -580,15 +545,10 @@ export default function ChatView() { |
| 580 | 545 | dispatch({ |
| 581 | 546 | type: "SET_ERROR", |
| 582 | 547 | threadId: activeThread.id, |
| 583 | error: | |
| 584 | err instanceof Error | |
| 585 | ? err.message | |
| 586 | : "Failed to submit approval decision.", | |
| 548 | error: err instanceof Error ? err.message : "Failed to submit approval decision.", | |
| 587 | 549 | }); |
| 588 | 550 | } finally { |
| 589 | setRespondingRequestIds((existing) => | |
| 590 | existing.filter((id) => id !== requestId), | |
| 591 | ); | |
| 551 | setRespondingRequestIds((existing) => existing.filter((id) => id !== requestId)); | |
| 592 | 552 | } |
| 593 | 553 | }; |
| 594 | 554 | |
| ... | ... | @@ -626,11 +586,11 @@ export default function ChatView() { |
| 626 | 586 | return ( |
| 627 | 587 | <div className="flex flex-1 flex-col bg-background"> |
| 628 | 588 | {/* Top bar */} |
| 629 | <header className={`flex items-center justify-between border-b border-border px-5 pb-3 ${isElectron ? "drag-region pt-[28px]" : "pt-3"}`}> | |
| 589 | <header | |
| 590 | className={`flex items-center justify-between border-b border-border px-5 pb-3 ${isElectron ? "drag-region pt-[28px]" : "pt-3"}`} | |
| 591 | > | |
| 630 | 592 | <div className="flex items-center gap-3"> |
| 631 | <h2 className="text-sm font-medium text-foreground"> | |
| 632 | {activeThread.title} | |
| 633 | </h2> | |
| 593 | <h2 className="text-sm font-medium text-foreground">{activeThread.title}</h2> | |
| 634 | 594 | {activeProject && ( |
| 635 | 595 | <span className="rounded-full bg-accent px-2 py-0.5 text-[10px] text-muted-foreground/50"> |
| 636 | 596 | {activeProject.name} |
| ... | ... | @@ -666,9 +626,7 @@ export default function ChatView() { |
| 666 | 626 | {editorLabel(editor)} |
| 667 | 627 | {editor.id === lastEditor && ( |
| 668 | 628 | <kbd className="ml-auto text-[9px] text-muted-foreground/40"> |
| 669 | {navigator.platform.includes("Mac") | |
| 670 | ? "\u2318O" | |
| 671 | : "Ctrl+O"} | |
| 629 | {navigator.platform.includes("Mac") ? "\u2318O" : "Ctrl+O"} | |
| 672 | 630 | </kbd> |
| 673 | 631 | )} |
| 674 | 632 | </button> |
| ... | ... | @@ -702,9 +660,7 @@ export default function ChatView() { |
| 702 | 660 | {pendingApprovals.length > 0 && ( |
| 703 | 661 | <div className="mx-4 mt-3 space-y-2"> |
| 704 | 662 | {pendingApprovals.map((approval) => { |
| 705 | const isResponding = respondingRequestIds.includes( | |
| 706 | approval.requestId, | |
| 707 | ); | |
| 663 | const isResponding = respondingRequestIds.includes(approval.requestId); | |
| 708 | 664 | return ( |
| 709 | 665 | <div |
| 710 | 666 | key={approval.requestId} |
| ... | ... | @@ -728,9 +684,7 @@ export default function ChatView() { |
| 728 | 684 | type="button" |
| 729 | 685 | className="rounded-md border border-border bg-accent px-2 py-1 text-[11px] text-foreground transition-colors duration-150 hover:bg-accent/80 disabled:cursor-not-allowed disabled:opacity-50" |
| 730 | 686 | disabled={isResponding} |
| 731 | onClick={() => | |
| 732 | void onRespondToApproval(approval.requestId, "accept") | |
| 733 | } | |
| 687 | onClick={() => void onRespondToApproval(approval.requestId, "accept")} | |
| 734 | 688 | > |
| 735 | 689 | Approve once |
| 736 | 690 | </button> |
| ... | ... | @@ -738,12 +692,7 @@ export default function ChatView() { |
| 738 | 692 | type="button" |
| 739 | 693 | className="rounded-md border border-sky-300/30 bg-sky-500/[0.15] px-2 py-1 text-[11px] text-sky-100 transition-colors duration-150 hover:bg-sky-500/[0.22] disabled:cursor-not-allowed disabled:opacity-50" |
| 740 | 694 | disabled={isResponding} |
| 741 | onClick={() => | |
| 742 | void onRespondToApproval( | |
| 743 | approval.requestId, | |
| 744 | "acceptForSession", | |
| 745 | ) | |
| 746 | } | |
| 695 | onClick={() => void onRespondToApproval(approval.requestId, "acceptForSession")} | |
| 747 | 696 | > |
| 748 | 697 | Always allow this session |
| 749 | 698 | </button> |
| ... | ... | @@ -751,9 +700,7 @@ export default function ChatView() { |
| 751 | 700 | type="button" |
| 752 | 701 | className="rounded-md border border-border px-2 py-1 text-[11px] text-foreground/90 transition-colors duration-150 hover:bg-accent disabled:cursor-not-allowed disabled:opacity-50" |
| 753 | 702 | disabled={isResponding} |
| 754 | onClick={() => | |
| 755 | void onRespondToApproval(approval.requestId, "decline") | |
| 756 | } | |
| 703 | onClick={() => void onRespondToApproval(approval.requestId, "decline")} | |
| 757 | 704 | > |
| 758 | 705 | Decline |
| 759 | 706 | </button> |
| ... | ... | @@ -761,9 +708,7 @@ export default function ChatView() { |
| 761 | 708 | type="button" |
| 762 | 709 | className="rounded-md border border-rose-300/30 bg-rose-500/[0.12] px-2 py-1 text-[11px] text-rose-100 transition-colors duration-150 hover:bg-rose-500/[0.2] disabled:cursor-not-allowed disabled:opacity-50" |
| 763 | 710 | disabled={isResponding} |
| 764 | onClick={() => | |
| 765 | void onRespondToApproval(approval.requestId, "cancel") | |
| 766 | } | |
| 711 | onClick={() => void onRespondToApproval(approval.requestId, "cancel")} | |
| 767 | 712 | > |
| 768 | 713 | Cancel turn |
| 769 | 714 | </button> |
| ... | ... | @@ -778,15 +723,14 @@ export default function ChatView() { |
| 778 | 723 | <div className="flex-1 overflow-y-auto px-5 py-4"> |
| 779 | 724 | {activeThread.messages.length === 0 && !isWorking ? ( |
| 780 | 725 | <div className="flex h-full items-center justify-center"> |
| 781 | <p className="text-sm text-muted-foreground/30">Send a message to start the conversation.</p> | |
| 726 | <p className="text-sm text-muted-foreground/30"> | |
| 727 | Send a message to start the conversation. | |
| 728 | </p> | |
| 782 | 729 | </div> |
| 783 | 730 | ) : ( |
| 784 | 731 | <div className="mx-auto max-w-3xl space-y-4"> |
| 785 | 732 | {timelineEntries.map((timelineEntry, index) => { |
| 786 | if ( | |
| 787 | timelineEntry.kind === "work" && | |
| 788 | timelineEntries[index - 1]?.kind === "work" | |
| 789 | ) { | |
| 733 | if (timelineEntry.kind === "work" && timelineEntries[index - 1]?.kind === "work") { | |
| 790 | 734 | return null; |
| 791 | 735 | } |
| 792 | 736 | |
| ... | ... | @@ -807,16 +751,13 @@ export default function ChatView() { |
| 807 | 751 | |
| 808 | 752 | const groupId = timelineEntry.id; |
| 809 | 753 | const isExpanded = expandedWorkGroups[groupId] ?? false; |
| 810 | const hasOverflow = | |
| 811 | groupedEntries.length > MAX_VISIBLE_WORK_LOG_ENTRIES; | |
| 754 | const hasOverflow = groupedEntries.length > MAX_VISIBLE_WORK_LOG_ENTRIES; | |
| 812 | 755 | const visibleEntries = |
| 813 | 756 | hasOverflow && !isExpanded |
| 814 | 757 | ? groupedEntries.slice(-MAX_VISIBLE_WORK_LOG_ENTRIES) |
| 815 | 758 | : groupedEntries; |
| 816 | 759 | const hiddenCount = groupedEntries.length - visibleEntries.length; |
| 817 | const onlyToolEntries = groupedEntries.every( | |
| 818 | (entry) => entry.tone === "tool", | |
| 819 | ); | |
| 760 | const onlyToolEntries = groupedEntries.every((entry) => entry.tone === "tool"); | |
| 820 | 761 | const groupLabel = onlyToolEntries |
| 821 | 762 | ? groupedEntries.length === 1 |
| 822 | 763 | ? "Tool call" |
| ... | ... | @@ -902,9 +843,7 @@ export default function ChatView() { |
| 902 | 843 | <div className="my-3 flex items-center gap-3"> |
| 903 | 844 | <span className="h-px flex-1 bg-border" /> |
| 904 | 845 | <span className="rounded-full border border-border bg-background px-2.5 py-1 text-[10px] uppercase tracking-[0.14em] text-muted-foreground/80"> |
| 905 | {completionSummary | |
| 906 | ? `Response • ${completionSummary}` | |
| 907 | : "Response"} | |
| 846 | {completionSummary ? `Response • ${completionSummary}` : "Response"} | |
| 908 | 847 | </span> |
| 909 | 848 | <span className="h-px flex-1 bg-border" /> |
| 910 | 849 | </div> |
| ... | ... | @@ -913,24 +852,17 @@ export default function ChatView() { |
| 913 | 852 | <ChatMarkdown |
| 914 | 853 | text={ |
| 915 | 854 | timelineEntry.message.text || |
| 916 | (timelineEntry.message.streaming | |
| 917 | ? "" | |
| 918 | : "(empty response)") | |
| 855 | (timelineEntry.message.streaming ? "" : "(empty response)") | |
| 919 | 856 | } |
| 920 | 857 | /> |
| 921 | 858 | <p className="mt-1.5 text-[10px] text-muted-foreground/30"> |
| 922 | 859 | {formatMessageMeta( |
| 923 | 860 | timelineEntry.message.createdAt, |
| 924 | 861 | timelineEntry.message.streaming |
| 925 | ? formatElapsed( | |
| 926 | timelineEntry.message.createdAt, | |
| 927 | nowIso, | |
| 928 | ) | |
| 862 | ? formatElapsed(timelineEntry.message.createdAt, nowIso) | |
| 929 | 863 | : formatElapsed( |
| 930 | 864 | timelineEntry.message.createdAt, |
| 931 | assistantCompletionByItemId.get( | |
| 932 | timelineEntry.message.id, | |
| 933 | ), | |
| 865 | assistantCompletionByItemId.get(timelineEntry.message.id), | |
| 934 | 866 | ), |
| 935 | 867 | )} |
| 936 | 868 | </p> |
| ... | ... | @@ -1086,9 +1018,7 @@ export default function ChatView() { |
| 1086 | 1018 | disabled={isSwitchingRuntimeMode} |
| 1087 | 1019 | onClick={() => |
| 1088 | 1020 | void handleRuntimeModeChange( |
| 1089 | state.runtimeMode === "full-access" | |
| 1090 | ? "approval-required" | |
| 1091 | : "full-access", | |
| 1021 | state.runtimeMode === "full-access" ? "approval-required" : "full-access", | |
| 1092 | 1022 | ) |
| 1093 | 1023 | } |
| 1094 | 1024 | title={ |
| ... | ... | @@ -1098,13 +1028,7 @@ export default function ChatView() { |
| 1098 | 1028 | } |
| 1099 | 1029 | > |
| 1100 | 1030 | {state.runtimeMode === "full-access" ? ( |
| 1101 | <svg | |
| 1102 | width="14" | |
| 1103 | height="14" | |
| 1104 | viewBox="0 0 14 14" | |
| 1105 | fill="none" | |
| 1106 | aria-hidden="true" | |
| 1107 | > | |
| 1031 | <svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true"> | |
| 1108 | 1032 | <rect |
| 1109 | 1033 | x="2" |
| 1110 | 1034 | y="5.5" |
| ... | ... | @@ -1122,13 +1046,7 @@ export default function ChatView() { |
| 1122 | 1046 | /> |
| 1123 | 1047 | </svg> |
| 1124 | 1048 | ) : ( |
| 1125 | <svg | |
| 1126 | width="14" | |
| 1127 | height="14" | |
| 1128 | viewBox="0 0 14 14" | |
| 1129 | fill="none" | |
| 1130 | aria-hidden="true" | |
| 1131 | > | |
| 1049 | <svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true"> | |
| 1132 | 1050 | <rect |
| 1133 | 1051 | x="2" |
| 1134 | 1052 | y="5.5" |
| ... | ... | @@ -1146,11 +1064,7 @@ export default function ChatView() { |
| 1146 | 1064 | /> |
| 1147 | 1065 | </svg> |
| 1148 | 1066 | )} |
| 1149 | <span> | |
| 1150 | {state.runtimeMode === "full-access" | |
| 1151 | ? "Full access" | |
| 1152 | : "Supervised"} | |
| 1153 | </span> | |
| 1067 | <span>{state.runtimeMode === "full-access" ? "Full access" : "Supervised"}</span> | |
| 1154 | 1068 | </button> |
| 1155 | 1069 | </div> |
| 1156 | 1070 | |
| ... | ... | @@ -1226,11 +1140,7 @@ export default function ChatView() { |
| 1226 | 1140 | </form> |
| 1227 | 1141 | </div> |
| 1228 | 1142 | |
| 1229 | <BranchToolbar | |
| 1230 | envMode={envMode} | |
| 1231 | onEnvModeChange={setEnvMode} | |
| 1232 | envLocked={envLocked} | |
| 1233 | /> | |
| 1143 | <BranchToolbar envMode={envMode} onEnvModeChange={setEnvMode} envLocked={envLocked} /> | |
| 1234 | 1144 | </div> |
| 1235 | 1145 | ); |
| 1236 | 1146 | } |
apps/web/src/components/DiffPanel.tsx+3-1| ... | ... | @@ -7,7 +7,9 @@ export default function DiffPanel() { |
| 7 | 7 | return ( |
| 8 | 8 | <aside className="flex h-full w-[400px] shrink-0 flex-col border-l border-border bg-card"> |
| 9 | 9 | {/* Header */} |
| 10 | <div className={`flex items-center justify-between border-b border-border px-4 pb-3 ${isElectron ? "drag-region pt-[28px]" : "pt-3"}`}> | |
| 10 | <div | |
| 11 | className={`flex items-center justify-between border-b border-border px-4 pb-3 ${isElectron ? "drag-region pt-[28px]" : "pt-3"}`} | |
| 12 | > | |
| 11 | 13 | <h3 className="text-xs font-medium text-foreground">Uncommitted changes</h3> |
| 12 | 14 | <button |
| 13 | 15 | type="button" |
apps/web/src/historyBootstrap.ts+4-11| ... | ... | @@ -36,13 +36,8 @@ export function buildBootstrapInput( |
| 36 | 36 | latestPrompt: string, |
| 37 | 37 | maxChars: number, |
| 38 | 38 | ): BootstrapInputResult { |
| 39 | const budget = Number.isFinite(maxChars) | |
| 40 | ? Math.max(1, Math.floor(maxChars)) | |
| 41 | : 1; | |
| 42 | const promptOnly = | |
| 43 | latestPrompt.length <= budget | |
| 44 | ? latestPrompt | |
| 45 | : latestPrompt.slice(0, budget); | |
| 39 | const budget = Number.isFinite(maxChars) ? Math.max(1, Math.floor(maxChars)) : 1; | |
| 40 | const promptOnly = latestPrompt.length <= budget ? latestPrompt : latestPrompt.slice(0, budget); | |
| 46 | 41 | |
| 47 | 42 | if (previousMessages.length === 0) { |
| 48 | 43 | return { |
| ... | ... | @@ -87,8 +82,7 @@ export function buildBootstrapInput( |
| 87 | 82 | |
| 88 | 83 | let includedChronological = [...includedNewestFirst].reverse(); |
| 89 | 84 | while (true) { |
| 90 | const omittedCount = | |
| 91 | newestFirstBlocks.length - includedChronological.length; | |
| 85 | const omittedCount = newestFirstBlocks.length - includedChronological.length; | |
| 92 | 86 | const transcriptBody = |
| 93 | 87 | omittedCount > 0 |
| 94 | 88 | ? includedChronological.length > 0 |
| ... | ... | @@ -101,8 +95,7 @@ export function buildBootstrapInput( |
| 101 | 95 | text: finalized, |
| 102 | 96 | includedCount: includedChronological.length, |
| 103 | 97 | omittedCount, |
| 104 | truncated: | |
| 105 | omittedCount > 0 || latestPrompt.length !== promptOnly.length, | |
| 98 | truncated: omittedCount > 0 || latestPrompt.length !== promptOnly.length, | |
| 106 | 99 | }; |
| 107 | 100 | } |
| 108 | 101 |
apps/web/src/index.css+11-26| ... | ... | @@ -88,11 +88,7 @@ |
| 88 | 88 | --secondary: --alpha(var(--color-black) / 4%); |
| 89 | 89 | --secondary-foreground: var(--color-neutral-800); |
| 90 | 90 | --muted: --alpha(var(--color-black) / 4%); |
| 91 | --muted-foreground: color-mix( | |
| 92 | in srgb, | |
| 93 | var(--color-neutral-500) 90%, | |
| 94 | var(--color-black) | |
| 95 | ); | |
| 91 | --muted-foreground: color-mix(in srgb, var(--color-neutral-500) 90%, var(--color-black)); | |
| 96 | 92 | --accent: --alpha(var(--color-black) / 5%); |
| 97 | 93 | --accent-foreground: var(--color-neutral-800); |
| 98 | 94 | --destructive: var(--color-red-500); |
| ... | ... | @@ -122,11 +118,7 @@ |
| 122 | 118 | |
| 123 | 119 | @variant dark { |
| 124 | 120 | color-scheme: dark; |
| 125 | --background: color-mix( | |
| 126 | in srgb, | |
| 127 | var(--color-neutral-950) 95%, | |
| 128 | var(--color-white) | |
| 129 | ); | |
| 121 | --background: color-mix(in srgb, var(--color-neutral-950) 95%, var(--color-white)); | |
| 130 | 122 | --foreground: var(--color-neutral-100); |
| 131 | 123 | --card: color-mix(in srgb, var(--background) 98%, var(--color-white)); |
| 132 | 124 | --card-foreground: var(--color-neutral-100); |
| ... | ... | @@ -137,18 +129,10 @@ |
| 137 | 129 | --secondary: --alpha(var(--color-white) / 4%); |
| 138 | 130 | --secondary-foreground: var(--color-neutral-100); |
| 139 | 131 | --muted: --alpha(var(--color-white) / 4%); |
| 140 | --muted-foreground: color-mix( | |
| 141 | in srgb, | |
| 142 | var(--color-neutral-500) 90%, | |
| 143 | var(--color-white) | |
| 144 | ); | |
| 132 | --muted-foreground: color-mix(in srgb, var(--color-neutral-500) 90%, var(--color-white)); | |
| 145 | 133 | --accent: --alpha(var(--color-white) / 4%); |
| 146 | 134 | --accent-foreground: var(--color-neutral-100); |
| 147 | --destructive: color-mix( | |
| 148 | in srgb, | |
| 149 | var(--color-red-500) 90%, | |
| 150 | var(--color-white) | |
| 151 | ); | |
| 135 | --destructive: color-mix(in srgb, var(--color-red-500) 90%, var(--color-white)); | |
| 152 | 136 | --border: --alpha(var(--color-white) / 6%); |
| 153 | 137 | --input: --alpha(var(--color-white) / 8%); |
| 154 | 138 | --ring: oklch(0.588 0.217 264); |
| ... | ... | @@ -177,7 +161,12 @@ |
| 177 | 161 | |
| 178 | 162 | body { |
| 179 | 163 | font-family: |
| 180 | "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; | |
| 164 | "DM Sans", | |
| 165 | -apple-system, | |
| 166 | BlinkMacSystemFont, | |
| 167 | "Segoe UI", | |
| 168 | system-ui, | |
| 169 | sans-serif; | |
| 181 | 170 | margin: 0; |
| 182 | 171 | padding: 0; |
| 183 | 172 | min-height: 100vh; |
| ... | ... | @@ -287,11 +276,7 @@ label:has(> select#reasoning-effort) select { |
| 287 | 276 | .chat-markdown a { |
| 288 | 277 | color: var(--info-foreground); |
| 289 | 278 | text-decoration: underline; |
| 290 | text-decoration-color: color-mix( | |
| 291 | in srgb, | |
| 292 | var(--info-foreground) 40%, | |
| 293 | transparent | |
| 294 | ); | |
| 279 | text-decoration-color: color-mix(in srgb, var(--info-foreground) 40%, transparent); | |
| 295 | 280 | } |
| 296 | 281 | |
| 297 | 282 | .chat-markdown a:hover { |
apps/web/src/persistenceSchema.ts+3-12| ... | ... | @@ -1,12 +1,7 @@ |
| 1 | 1 | import { z } from "zod"; |
| 2 | 2 | |
| 3 | 3 | import { DEFAULT_MODEL, resolveModelSlug } from "./model-logic"; |
| 4 | import { | |
| 5 | DEFAULT_RUNTIME_MODE, | |
| 6 | type Project, | |
| 7 | type RuntimeMode, | |
| 8 | type Thread, | |
| 9 | } from "./types"; | |
| 4 | import { DEFAULT_RUNTIME_MODE, type Project, type RuntimeMode, type Thread } from "./types"; | |
| 10 | 5 | |
| 11 | 6 | const LEGACY_DEFAULT_MODEL = "gpt-5.2-codex"; |
| 12 | 7 | |
| ... | ... | @@ -150,13 +145,9 @@ export function hydratePersistedState( |
| 150 | 145 | return { |
| 151 | 146 | projects, |
| 152 | 147 | threads, |
| 153 | activeThreadId: hasActiveThread | |
| 154 | ? parsedState.data.activeThreadId | |
| 155 | : (threads[0]?.id ?? null), | |
| 148 | activeThreadId: hasActiveThread ? parsedState.data.activeThreadId : (threads[0]?.id ?? null), | |
| 156 | 149 | runtimeMode: |
| 157 | "runtimeMode" in parsedState.data | |
| 158 | ? parsedState.data.runtimeMode | |
| 159 | : DEFAULT_RUNTIME_MODE, | |
| 150 | "runtimeMode" in parsedState.data ? parsedState.data.runtimeMode : DEFAULT_RUNTIME_MODE, | |
| 160 | 151 | }; |
| 161 | 152 | } |
| 162 | 153 |
apps/web/src/session-logic.ts+5-20| ... | ... | @@ -109,11 +109,7 @@ function normalizeItemType(raw: string | undefined): string { |
| 109 | 109 | } |
| 110 | 110 | |
| 111 | 111 | function shouldDropItemType(type: string): boolean { |
| 112 | if ( | |
| 113 | type.includes("preamble") || | |
| 114 | type.includes("reasoning") || | |
| 115 | type.includes("thought") | |
| 116 | ) { | |
| 112 | if (type.includes("preamble") || type.includes("reasoning") || type.includes("thought")) { | |
| 117 | 113 | return true; |
| 118 | 114 | } |
| 119 | 115 | |
| ... | ... | @@ -121,11 +117,7 @@ function shouldDropItemType(type: string): boolean { |
| 121 | 117 | } |
| 122 | 118 | |
| 123 | 119 | function shouldShowItemLifecycle(type: string): boolean { |
| 124 | return ( | |
| 125 | type.includes("tool") || | |
| 126 | type.includes("command") || | |
| 127 | type.includes("file change") | |
| 128 | ); | |
| 120 | return type.includes("tool") || type.includes("command") || type.includes("file change"); | |
| 129 | 121 | } |
| 130 | 122 | |
| 131 | 123 | function shouldDropMethod(method: string): boolean { |
| ... | ... | @@ -182,9 +174,7 @@ function extractDetail( |
| 182 | 174 | return undefined; |
| 183 | 175 | } |
| 184 | 176 | |
| 185 | function lifecycleCandidateFromItemEvent( | |
| 186 | event: ProviderEvent, | |
| 187 | ): ItemLifecycleCandidate | null { | |
| 177 | function lifecycleCandidateFromItemEvent(event: ProviderEvent): ItemLifecycleCandidate | null { | |
| 188 | 178 | const payload = asObject(event.payload); |
| 189 | 179 | const item = asObject(payload?.item); |
| 190 | 180 | const normalizedType = normalizeItemType(asString(item?.type)); |
| ... | ... | @@ -360,10 +350,7 @@ export function deriveWorkLogEntries( |
| 360 | 350 | ) { |
| 361 | 351 | continue; |
| 362 | 352 | } |
| 363 | if ( | |
| 364 | lifecycleCandidate.label === "Tool call" && | |
| 365 | !lifecycleCandidate.detail | |
| 366 | ) { | |
| 353 | if (lifecycleCandidate.label === "Tool call" && !lifecycleCandidate.detail) { | |
| 367 | 354 | continue; |
| 368 | 355 | } |
| 369 | 356 | |
| ... | ... | @@ -371,9 +358,7 @@ export function deriveWorkLogEntries( |
| 371 | 358 | id: lifecycleCandidate.id, |
| 372 | 359 | createdAt: lifecycleCandidate.createdAt, |
| 373 | 360 | label: lifecycleCandidate.label, |
| 374 | ...(lifecycleCandidate.detail | |
| 375 | ? { detail: lifecycleCandidate.detail } | |
| 376 | : {}), | |
| 361 | ...(lifecycleCandidate.detail ? { detail: lifecycleCandidate.detail } : {}), | |
| 377 | 362 | tone: lifecycleCandidate.tone, |
| 378 | 363 | }); |
| 379 | 364 | continue; |
apps/web/src/store.ts+1-3| ... | ... | @@ -395,9 +395,7 @@ export function reducer(state: AppState, action: Action): AppState { |
| 395 | 395 | case "DELETE_THREAD": { |
| 396 | 396 | const threads = state.threads.filter((t) => t.id !== action.threadId); |
| 397 | 397 | const activeThreadId = |
| 398 | state.activeThreadId === action.threadId | |
| 399 | ? (threads[0]?.id ?? null) | |
| 400 | : state.activeThreadId; | |
| 398 | state.activeThreadId === action.threadId ? (threads[0]?.id ?? null) : state.activeThreadId; | |
| 401 | 399 | return { ...state, threads, activeThreadId }; |
| 402 | 400 | } |
| 403 | 401 |
apps/web/src/wsNativeApi.ts+2-4| ... | ... | @@ -98,10 +98,8 @@ export function createWsNativeApi(): NativeApi { |
| 98 | 98 | }, |
| 99 | 99 | git: { |
| 100 | 100 | listBranches: (input) => transport.request(WS_METHODS.gitListBranches, input), |
| 101 | createWorktree: (input) => | |
| 102 | transport.request(WS_METHODS.gitCreateWorktree, input), | |
| 103 | removeWorktree: (input) => | |
| 104 | transport.request(WS_METHODS.gitRemoveWorktree, input), | |
| 101 | createWorktree: (input) => transport.request(WS_METHODS.gitCreateWorktree, input), | |
| 102 | removeWorktree: (input) => transport.request(WS_METHODS.gitRemoveWorktree, input), | |
| 105 | 103 | createBranch: (input) => transport.request(WS_METHODS.gitCreateBranch, input), |
| 106 | 104 | checkout: (input) => transport.request(WS_METHODS.gitCheckout, input), |
| 107 | 105 | init: (input) => transport.request(WS_METHODS.gitInit, input), |
apps/web/turbo.jsonc+3-3| ... | ... | @@ -3,7 +3,7 @@ |
| 3 | 3 | "extends": ["//"], |
| 4 | 4 | "tasks": { |
| 5 | 5 | "dev": { |
| 6 | "persistent": true | |
| 7 | } | |
| 8 | } | |
| 6 | "persistent": true, | |
| 7 | }, | |
| 8 | }, | |
| 9 | 9 | } |
apps/web/vite.config.ts+1-3| ... | ... | @@ -13,9 +13,7 @@ export default defineConfig({ |
| 13 | 13 | ], |
| 14 | 14 | define: { |
| 15 | 15 | // In dev mode, tell the web app where the WebSocket server lives |
| 16 | "import.meta.env.VITE_WS_URL": JSON.stringify( | |
| 17 | process.env.VITE_WS_URL ?? "", | |
| 18 | ), | |
| 16 | "import.meta.env.VITE_WS_URL": JSON.stringify(process.env.VITE_WS_URL ?? ""), | |
| 19 | 17 | }, |
| 20 | 18 | server: { |
| 21 | 19 | port: Number(process.env.PORT ?? 5173), |
bun.lock+5| ... | ... | @@ -51,6 +51,7 @@ |
| 51 | 51 | "version": "0.0.0", |
| 52 | 52 | "dependencies": { |
| 53 | 53 | "@t3tools/contracts": "workspace:*", |
| 54 | "@tanstack/react-query": "^5.90.0", | |
| 54 | 55 | "highlight.js": "^11.11.1", |
| 55 | 56 | "lucide-react": "^0.563.0", |
| 56 | 57 | "react": "^19.0.0", |
| ... | ... | @@ -364,6 +365,10 @@ |
| 364 | 365 | |
| 365 | 366 | "@tailwindcss/vite": ["@tailwindcss/vite@4.1.18", "", { "dependencies": { "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "tailwindcss": "4.1.18" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA=="], |
| 366 | 367 | |
| 368 | "@tanstack/query-core": ["@tanstack/query-core@5.90.20", "", {}, "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg=="], | |
| 369 | ||
| 370 | "@tanstack/react-query": ["@tanstack/react-query@5.90.21", "", { "dependencies": { "@tanstack/query-core": "5.90.20" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg=="], | |
| 371 | ||
| 367 | 372 | "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], |
| 368 | 373 | |
| 369 | 374 | "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], |
packages/contracts/src/git.ts+2-6| ... | ... | @@ -51,13 +51,9 @@ export const gitWorktreeSchema = z.object({ |
| 51 | 51 | |
| 52 | 52 | export type GitListBranchesInput = z.infer<typeof gitListBranchesInputSchema>; |
| 53 | 53 | export type GitBranch = z.infer<typeof gitBranchSchema>; |
| 54 | export type GitCreateWorktreeInput = z.infer< | |
| 55 | typeof gitCreateWorktreeInputSchema | |
| 56 | >; | |
| 54 | export type GitCreateWorktreeInput = z.infer<typeof gitCreateWorktreeInputSchema>; | |
| 57 | 55 | export type GitWorktree = z.infer<typeof gitWorktreeSchema>; |
| 58 | export type GitRemoveWorktreeInput = z.infer< | |
| 59 | typeof gitRemoveWorktreeInputSchema | |
| 60 | >; | |
| 56 | export type GitRemoveWorktreeInput = z.infer<typeof gitRemoveWorktreeInputSchema>; | |
| 61 | 57 | export type GitCreateBranchInput = z.infer<typeof gitCreateBranchInputSchema>; |
| 62 | 58 | export type GitCheckoutInput = z.infer<typeof gitCheckoutInputSchema>; |
| 63 | 59 | export type GitInitInput = z.infer<typeof gitInitInputSchema>; |
packages/contracts/src/provider.ts+6-21| ... | ... | @@ -83,12 +83,7 @@ export const providerRespondToRequestInputSchema = z.object({ |
| 83 | 83 | decision: providerApprovalDecisionSchema, |
| 84 | 84 | }); |
| 85 | 85 | |
| 86 | export const providerEventKindSchema = z.enum([ | |
| 87 | "session", | |
| 88 | "notification", | |
| 89 | "request", | |
| 90 | "error", | |
| 91 | ]); | |
| 86 | export const providerEventKindSchema = z.enum(["session", "notification", "request", "error"]); | |
| 92 | 87 | |
| 93 | 88 | export const providerEventSchema = z.object({ |
| 94 | 89 | id: z.string().min(1), |
| ... | ... | @@ -111,24 +106,14 @@ export type ProviderKind = z.infer<typeof providerKindSchema>; |
| 111 | 106 | export type ProviderApprovalPolicy = z.infer<typeof providerApprovalPolicySchema>; |
| 112 | 107 | export type ProviderSandboxMode = z.infer<typeof providerSandboxModeSchema>; |
| 113 | 108 | export type ProviderRequestKind = z.infer<typeof providerRequestKindSchema>; |
| 114 | export type ProviderApprovalDecision = z.infer< | |
| 115 | typeof providerApprovalDecisionSchema | |
| 116 | >; | |
| 109 | export type ProviderApprovalDecision = z.infer<typeof providerApprovalDecisionSchema>; | |
| 117 | 110 | export type ProviderSessionStatus = z.infer<typeof providerSessionStatusSchema>; |
| 118 | 111 | export type ProviderSession = z.infer<typeof providerSessionSchema>; |
| 119 | 112 | export type ProviderSessionStartInput = z.input<typeof providerSessionStartInputSchema>; |
| 120 | 113 | export type ProviderSendTurnInput = z.input<typeof providerSendTurnInputSchema>; |
| 121 | export type ProviderTurnStartResult = z.infer< | |
| 122 | typeof providerTurnStartResultSchema | |
| 123 | >; | |
| 124 | export type ProviderInterruptTurnInput = z.input< | |
| 125 | typeof providerInterruptTurnInputSchema | |
| 126 | >; | |
| 127 | export type ProviderStopSessionInput = z.input< | |
| 128 | typeof providerStopSessionInputSchema | |
| 129 | >; | |
| 130 | export type ProviderRespondToRequestInput = z.input< | |
| 131 | typeof providerRespondToRequestInputSchema | |
| 132 | >; | |
| 114 | export type ProviderTurnStartResult = z.infer<typeof providerTurnStartResultSchema>; | |
| 115 | export type ProviderInterruptTurnInput = z.input<typeof providerInterruptTurnInputSchema>; | |
| 116 | export type ProviderStopSessionInput = z.input<typeof providerStopSessionInputSchema>; | |
| 117 | export type ProviderRespondToRequestInput = z.input<typeof providerRespondToRequestInputSchema>; | |
| 133 | 118 | export type ProviderEventKind = z.infer<typeof providerEventKindSchema>; |
| 134 | 119 | export type ProviderEvent = z.infer<typeof providerEventSchema>; |