From 807e3d18dec858c9694a595deaea4f9ac588db5f Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 11 Feb 2026 14:26:24 -0800 Subject: [PATCH] Add react-query, refactor BranchToolbar async state, and fix stale deps - 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 --- AGENTS.md | 7 + apps/desktop/turbo.jsonc | 12 +- apps/server/src/codexAppServerManager.test.ts | 8 +- apps/server/src/codexAppServerManager.ts | 44 +--- apps/server/src/git.ts | 12 +- apps/server/src/providerManager.test.ts | 5 +- apps/server/src/providerManager.ts | 11 +- apps/server/src/wsServer.ts | 5 +- apps/server/turbo.jsonc | 6 +- apps/web/index.html | 5 +- apps/web/package.json | 1 + apps/web/src/App.tsx | 11 +- apps/web/src/components/BranchToolbar.tsx | 201 ++++++++---------- apps/web/src/components/ChatView.tsx | 188 +++++----------- apps/web/src/components/DiffPanel.tsx | 4 +- apps/web/src/historyBootstrap.ts | 15 +- apps/web/src/index.css | 37 +--- apps/web/src/persistenceSchema.ts | 15 +- apps/web/src/session-logic.ts | 25 +-- apps/web/src/store.ts | 4 +- apps/web/src/wsNativeApi.ts | 6 +- apps/web/turbo.jsonc | 6 +- apps/web/vite.config.ts | 4 +- bun.lock | 5 + packages/contracts/src/git.ts | 8 +- packages/contracts/src/provider.ts | 27 +-- 26 files changed, 237 insertions(+), 435 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 7f5148fce0f2d5a09e6db4f11afc5d59ca8a0a5a..affea58d01f34d6826be7531dfb6f3d758725d79 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,11 +1,13 @@ # CLAUDE.md ## Project Snapshot + T3 Code is a minimal web GUI for using code agents like Codex and Claude Code (coming soon). This repository is a VERY EARLY WIP. Proposing sweeping changes that improve long-term maintainability is encouraged. ## Core Priorities + 1. Performance first. 2. Reliability first. 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 If a tradeoff is required, choose correctness and robustness over short-term convenience. ## Package Roles + - `apps/server`: Node.js WebSocket server. Wraps Codex app-server (JSON-RPC over stdio), serves the React web app, and manages provider sessions. - `apps/web`: React/Vite UI. Owns session UX, conversation/event rendering, and client-side state. Connects to the server via WebSocket. - `packages/contracts`: Shared Zod schemas and TypeScript contracts for provider events, WebSocket protocol, and model/session types. ## Codex App Server (Important) + 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. How we use it in this codebase: + - Session startup/resume and turn lifecycle are brokered in `apps/server/src/codexAppServerManager.ts`. - Provider dispatch and thread event logging are coordinated in `apps/server/src/providerManager.ts`. - WebSocket server routes NativeApi methods in `apps/server/src/wsServer.ts`. - Web app consumes provider event streams via WebSocket push on channel `providers.event`. Docs: + - Codex App Server docs: https://developers.openai.com/codex/sdk/#app-server ## Reference Repos + - Open-source Codex repo: https://github.com/openai/codex - Codex-Monitor (Tauri, feature-complete, strong reference implementation): https://github.com/Dimillian/CodexMonitor diff --git a/apps/desktop/turbo.jsonc b/apps/desktop/turbo.jsonc index 8994c7806266b3b8ca989f4f66d9020593509d6c..2ff803229eff9fc4d7ed1333287f910d106fd159 100644 --- a/apps/desktop/turbo.jsonc +++ b/apps/desktop/turbo.jsonc @@ -4,21 +4,21 @@ "tasks": { "build": { "dependsOn": ["^build"], - "outputs": ["dist-electron/**"] + "outputs": ["dist-electron/**"], }, "dev": { "dependsOn": ["t3#build"], - "persistent": true + "persistent": true, }, "start": { "dependsOn": ["build", "@t3tools/web#build", "t3#build"], "cache": false, - "persistent": true + "persistent": true, }, "smoke-test": { "dependsOn": ["build", "@t3tools/web#build", "t3#build"], "cache": false, - "outputs": [] - } - } + "outputs": [], + }, + }, } diff --git a/apps/server/src/codexAppServerManager.test.ts b/apps/server/src/codexAppServerManager.test.ts index b611e4d4de4d62389902dd6bd7450becc468749f..7c87e4442cc5ee3a6764804cb6871a92e94b6c1b 100644 --- a/apps/server/src/codexAppServerManager.test.ts +++ b/apps/server/src/codexAppServerManager.test.ts @@ -57,17 +57,13 @@ describe("normalizeCodexModelSlug", () => { describe("isRecoverableThreadResumeError", () => { it("matches not-found resume errors", () => { expect( - isRecoverableThreadResumeError( - new Error("thread/resume failed: thread not found"), - ), + isRecoverableThreadResumeError(new Error("thread/resume failed: thread not found")), ).toBe(true); }); it("ignores non-resume errors", () => { expect( - isRecoverableThreadResumeError( - new Error("thread/start failed: permission denied"), - ), + isRecoverableThreadResumeError(new Error("thread/start failed: permission denied")), ).toBe(false); }); diff --git a/apps/server/src/codexAppServerManager.ts b/apps/server/src/codexAppServerManager.ts index af1e67e36b6163659794ce35a1d74c9692de5ca5..f4ed54ab1514759638dae4fce79f665c2f8b511a 100644 --- a/apps/server/src/codexAppServerManager.ts +++ b/apps/server/src/codexAppServerManager.ts @@ -26,9 +26,7 @@ interface PendingRequest { interface PendingApprovalRequest { requestId: string; jsonRpcId: string | number; - method: - | "item/commandExecution/requestApproval" - | "item/fileChange/requestApproval"; + method: "item/commandExecution/requestApproval" | "item/fileChange/requestApproval"; requestKind: ProviderRequestKind; threadId?: string; turnId?: string; @@ -122,16 +120,12 @@ export function classifyCodexStderrLine(rawLine: string): { message: string } | } export function isRecoverableThreadResumeError(error: unknown): boolean { - const message = ( - error instanceof Error ? error.message : String(error) - ).toLowerCase(); + const message = (error instanceof Error ? error.message : String(error)).toLowerCase(); if (!message.includes("thread/resume")) { return false; } - return RECOVERABLE_THREAD_RESUME_ERROR_SNIPPETS.some((snippet) => - message.includes(snippet), - ); + return RECOVERABLE_THREAD_RESUME_ERROR_SNIPPETS.some((snippet) => message.includes(snippet)); } export interface CodexAppServerManagerEvents { @@ -209,14 +203,10 @@ export class CodexAppServerManager extends EventEmitter { +function runGit(args: string[], cwd: string, timeoutMs = 30_000): Promise { return new Promise((resolve, reject) => { const child = spawn("git", args, { cwd, @@ -127,11 +123,7 @@ export async function listGitBranches(input: GitListBranchesInput): Promise { method: string; threadId: string; }) => void; - threadLogStreams: Map< - string, - { writableEnded: boolean; destroyed: boolean } - >; + threadLogStreams: Map; }; internals.onCodexEvent({ id: "evt-1", diff --git a/apps/server/src/providerManager.ts b/apps/server/src/providerManager.ts index be17babc35b9e6e8c4da60fb2566d7b23c9d236b..52fb1b648475846cdc7448108419bab9a3c3418e 100644 --- a/apps/server/src/providerManager.ts +++ b/apps/server/src/providerManager.ts @@ -87,11 +87,7 @@ export class ProviderManager extends EventEmitter { throw new Error(`Unknown provider session: ${input.sessionId}`); } - await this.codex.respondToRequest( - input.sessionId, - input.requestId, - input.decision, - ); + await this.codex.respondToRequest(input.sessionId, input.requestId, input.decision); } stopSession(raw: ProviderStopSessionInput): void { @@ -151,10 +147,7 @@ export class ProviderManager extends EventEmitter { private resolveThreadId(event: ProviderEvent): string | undefined { const fromPayload = this.readThreadIdFromPayload(event.payload); - const threadId = - event.threadId ?? - fromPayload ?? - this.sessionThreadIds.get(event.sessionId); + const threadId = event.threadId ?? fromPayload ?? this.sessionThreadIds.get(event.sessionId); if (threadId) { this.sessionThreadIds.set(event.sessionId, threadId); diff --git a/apps/server/src/wsServer.ts b/apps/server/src/wsServer.ts index 97c35f1660ffec0608b31215023faad4cc36db0e..353535ae32311cdfcafc840f7253e66fb6a37ec1 100644 --- a/apps/server/src/wsServer.ts +++ b/apps/server/src/wsServer.ts @@ -369,7 +369,10 @@ export function createServer(options: ServerOptions) { const isServerNotRunningError = (error: unknown): boolean => { if (!(error instanceof Error)) return false; const maybeCode = (error as NodeJS.ErrnoException).code; - return maybeCode === "ERR_SERVER_NOT_RUNNING" || error.message.toLowerCase().includes("not running"); + return ( + maybeCode === "ERR_SERVER_NOT_RUNNING" || + error.message.toLowerCase().includes("not running") + ); }; const closeWebSocketServer = new Promise((resolve, reject) => { diff --git a/apps/server/turbo.jsonc b/apps/server/turbo.jsonc index c466a3a0251b38bf9a65ddcf7b5b9dad99adc544..34f8874f8e3c5edabd18f63ecada497c4c371ff0 100644 --- a/apps/server/turbo.jsonc +++ b/apps/server/turbo.jsonc @@ -5,7 +5,7 @@ "start": { "dependsOn": ["build"], "cache": false, - "persistent": true - } - } + "persistent": true, + }, + }, } diff --git a/apps/web/index.html b/apps/web/index.html index fe848c5287a8cb351c508d4b9696309f0b1f8c86..25aace82a32ba91765675ee14343205f69d2358e 100644 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -5,7 +5,10 @@ - + T3 Code diff --git a/apps/web/package.json b/apps/web/package.json index 2594309c1e3592c777b9170ecce176d6921c75a5..d995f4819b12ed0f15485481fdf66550dd0254c2 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "@t3tools/contracts": "workspace:*", + "@tanstack/react-query": "^5.90.0", "highlight.js": "^11.11.1", "lucide-react": "^0.563.0", "react": "^19.0.0", diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 969f55a04c41283c84963548080bf1ac84b1e446..4619f6c9330c157ba5fd1e136ec93bf65025f454 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -1,3 +1,4 @@ +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { useEffect, useMemo, useRef } from "react"; import ChatView from "./components/ChatView"; @@ -168,10 +169,14 @@ function Layout() { ); } +const queryClient = new QueryClient(); + export default function App() { return ( - - - + + + + + ); } diff --git a/apps/web/src/components/BranchToolbar.tsx b/apps/web/src/components/BranchToolbar.tsx index 1ba9c0cec183febe44fd3510a06a12320bb73fcf..aed876f2035041d0faf0a451d5d8615f93bc7c41 100644 --- a/apps/web/src/components/BranchToolbar.tsx +++ b/apps/web/src/components/BranchToolbar.tsx @@ -1,5 +1,6 @@ import type { GitBranch } from "@t3tools/contracts"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useEffect, useMemo, useRef, useState } from "react"; import { readNativeApi } from "../session-logic"; import { useStore } from "../store"; @@ -10,76 +11,85 @@ interface BranchToolbarProps { envLocked: boolean; } -export default function BranchToolbar({ - envMode, - onEnvModeChange, - envLocked, -}: BranchToolbarProps) { +export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: BranchToolbarProps) { const { state, dispatch } = useStore(); const api = useMemo(() => readNativeApi(), []); + const queryClient = useQueryClient(); - const [branches, setBranches] = useState([]); - const [isRepo, setIsRepo] = useState(false); - const [isLoadingBranches, setIsLoadingBranches] = useState(false); const [isBranchMenuOpen, setIsBranchMenuOpen] = useState(false); const [isCreatingBranch, setIsCreatingBranch] = useState(false); const [newBranchName, setNewBranchName] = useState(""); - const [isInitializingRepo, setIsInitializingRepo] = useState(false); const branchMenuRef = useRef(null); const activeThread = state.threads.find((thread) => thread.id === state.activeThreadId); const activeProject = state.projects.find((project) => project.id === activeThread?.projectId); - const branchCwd = activeThread?.worktreePath ?? activeProject?.cwd; - - const loadBranches = useCallback(async () => { - if (!api || !branchCwd) return; - setIsLoadingBranches(true); - try { - const result = await api.git.listBranches({ cwd: branchCwd }); - setIsRepo(result.isRepo); - setBranches(result.branches); - } catch (error) { - setIsRepo(false); - setBranches([]); - if (activeThread) { - dispatch({ - type: "SET_ERROR", - threadId: activeThread.id, - error: - error instanceof Error ? error.message : "Failed to load git branches.", - }); - } - } finally { - setIsLoadingBranches(false); - } - }, [activeThread, api, branchCwd, dispatch]); - - useEffect(() => { - void loadBranches(); - }, [loadBranches]); + const activeThreadId = activeThread?.id; + const activeThreadBranch = activeThread?.branch ?? null; + const activeWorktreePath = activeThread?.worktreePath ?? null; + const branchCwd = activeWorktreePath ?? activeProject?.cwd; + + // ── Queries ─────────────────────────────────────────────────────────── + + const branchesQuery = useQuery({ + queryKey: ["git", "branches", branchCwd], + queryFn: () => api!.git.listBranches({ cwd: branchCwd! }), + enabled: !!api && !!branchCwd, + }); + + const branches = branchesQuery.data?.branches ?? []; + const isRepo = branchesQuery.data?.isRepo ?? false; + + // ── Mutations ───────────────────────────────────────────────────────── + + const checkoutMutation = useMutation({ + mutationFn: (branch: string) => api!.git.checkout({ cwd: branchCwd!, branch }), + onSuccess: () => queryClient.invalidateQueries({ queryKey: ["git", "branches", branchCwd] }), + onError: (error) => + setThreadError(error instanceof Error ? error.message : "Failed to checkout branch."), + }); + + const createBranchMutation = useMutation({ + mutationFn: (branch: string) => + api!.git + .createBranch({ cwd: branchCwd!, branch }) + .then(() => api!.git.checkout({ cwd: branchCwd!, branch })), + onSuccess: () => queryClient.invalidateQueries({ queryKey: ["git", "branches", branchCwd] }), + onError: (error) => + setThreadError(error instanceof Error ? error.message : "Failed to create branch."), + }); + + const initMutation = useMutation({ + mutationFn: () => api!.git.init({ cwd: branchCwd! }), + onSuccess: () => { + setThreadError(null); + queryClient.invalidateQueries({ queryKey: ["git", "branches", branchCwd] }); + }, + onError: (error) => + setThreadError(error instanceof Error ? error.message : "Failed to initialize git repo."), + }); + + // ── Effects ─────────────────────────────────────────────────────────── // Keep thread branch synced to git current branch for local threads. + const queryBranches = branchesQuery.data?.branches; useEffect(() => { - if (!activeThread || activeThread.worktreePath) return; - const current = branches.find((branch) => branch.current); + if (!activeThreadId || activeWorktreePath) return; + const current = queryBranches?.find((branch) => branch.current); if (!current) return; - if (current.name === activeThread.branch) return; + if (current.name === activeThreadBranch) return; dispatch({ type: "SET_THREAD_BRANCH", - threadId: activeThread.id, + threadId: activeThreadId, branch: current.name, worktreePath: null, }); - }, [activeThread, branches, dispatch]); + }, [activeThreadId, activeWorktreePath, activeThreadBranch, queryBranches, dispatch]); useEffect(() => { if (!isBranchMenuOpen) return; const handleClickOutside = (event: MouseEvent) => { if (!branchMenuRef.current) return; - if ( - event.target instanceof Node && - !branchMenuRef.current.contains(event.target) - ) { + if (event.target instanceof Node && !branchMenuRef.current.contains(event.target)) { setIsBranchMenuOpen(false); } }; @@ -89,79 +99,50 @@ export default function BranchToolbar({ }; }, [isBranchMenuOpen]); + // ── Helpers ─────────────────────────────────────────────────────────── + const setThreadError = (error: string | null) => { - if (!activeThread) return; - dispatch({ - type: "SET_ERROR", - threadId: activeThread.id, - error, - }); + if (!activeThreadId) return; + dispatch({ type: "SET_ERROR", threadId: activeThreadId, error }); }; const setThreadBranch = (branch: string | null, worktreePath: string | null) => { - if (!activeThread) return; - dispatch({ - type: "SET_THREAD_BRANCH", - threadId: activeThread.id, - branch, - worktreePath, - }); + if (!activeThreadId) return; + dispatch({ type: "SET_THREAD_BRANCH", threadId: activeThreadId, branch, worktreePath }); }; - const initializeRepo = async () => { - if (!api || !branchCwd || !activeThread || isInitializingRepo) return; - setIsInitializingRepo(true); - try { - await api.git.init({ cwd: branchCwd }); - setThreadError(null); - await loadBranches(); - } catch (error) { - setThreadError(error instanceof Error ? error.message : "Failed to initialize git repo."); - } finally { - setIsInitializingRepo(false); - } - }; - - const selectBranch = async (branch: GitBranch) => { - if (!api || !activeThread || !branchCwd) return; + const selectBranch = (branch: GitBranch) => { + if (!api || !activeThreadId || !branchCwd) return; // For new worktree mode, selecting a branch picks the base branch. - if (envMode === "worktree" && !envLocked && !activeThread.worktreePath) { + if (envMode === "worktree" && !envLocked && !activeWorktreePath) { setThreadError(null); setThreadBranch(branch.name, null); setIsBranchMenuOpen(false); return; } - try { - await api.git.checkout({ - cwd: branchCwd, - branch: branch.name, - }); - setThreadError(null); - setThreadBranch(branch.name, activeThread.worktreePath); - setIsBranchMenuOpen(false); - await loadBranches(); - } catch (error) { - setThreadError(error instanceof Error ? error.message : "Failed to checkout branch."); - } + checkoutMutation.mutate(branch.name, { + onSuccess: () => { + setThreadError(null); + setThreadBranch(branch.name, activeWorktreePath); + setIsBranchMenuOpen(false); + }, + }); }; - const createBranch = async () => { + const createBranch = () => { const name = newBranchName.trim(); - if (!api || !activeThread || !branchCwd || !name) return; - try { - await api.git.createBranch({ cwd: branchCwd, branch: name }); - await api.git.checkout({ cwd: branchCwd, branch: name }); - setThreadError(null); - setThreadBranch(name, activeThread.worktreePath); - setNewBranchName(""); - setIsCreatingBranch(false); - setIsBranchMenuOpen(false); - await loadBranches(); - } catch (error) { - setThreadError(error instanceof Error ? error.message : "Failed to create branch."); - } + if (!api || !activeThreadId || !branchCwd || !name) return; + createBranchMutation.mutate(name, { + onSuccess: () => { + setThreadError(null); + setThreadBranch(name, activeWorktreePath); + setNewBranchName(""); + setIsCreatingBranch(false); + setIsBranchMenuOpen(false); + }, + }); }; if (!activeThread || !activeProject) return null; @@ -192,10 +173,10 @@ export default function BranchToolbar({ ) : (
@@ -203,7 +184,7 @@ export default function BranchToolbar({ type="button" 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" onClick={() => setIsBranchMenuOpen((open) => !open)} - disabled={isLoadingBranches} + disabled={branchesQuery.isLoading} > {activeThread.branch @@ -246,7 +227,7 @@ export default function BranchToolbar({ ? "bg-accent text-foreground" : "text-foreground/90 hover:bg-accent/50" }`} - onClick={() => void selectBranch(branch)} + onClick={() => selectBranch(branch)} > {branch.name} {(branch.current || branch.isDefault) && ( @@ -266,7 +247,7 @@ export default function BranchToolbar({ className="flex items-center gap-1 px-1" onSubmit={(event) => { event.preventDefault(); - void createBranch(); + createBranch(); }} > @@ -702,9 +660,7 @@ export default function ChatView() { {pendingApprovals.length > 0 && (
{pendingApprovals.map((approval) => { - const isResponding = respondingRequestIds.includes( - approval.requestId, - ); + const isResponding = respondingRequestIds.includes(approval.requestId); return (
- void onRespondToApproval(approval.requestId, "accept") - } + onClick={() => void onRespondToApproval(approval.requestId, "accept")} > Approve once @@ -738,12 +692,7 @@ export default function ChatView() { type="button" 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" disabled={isResponding} - onClick={() => - void onRespondToApproval( - approval.requestId, - "acceptForSession", - ) - } + onClick={() => void onRespondToApproval(approval.requestId, "acceptForSession")} > Always allow this session @@ -751,9 +700,7 @@ export default function ChatView() { type="button" 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" disabled={isResponding} - onClick={() => - void onRespondToApproval(approval.requestId, "decline") - } + onClick={() => void onRespondToApproval(approval.requestId, "decline")} > Decline @@ -761,9 +708,7 @@ export default function ChatView() { type="button" 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" disabled={isResponding} - onClick={() => - void onRespondToApproval(approval.requestId, "cancel") - } + onClick={() => void onRespondToApproval(approval.requestId, "cancel")} > Cancel turn @@ -778,15 +723,14 @@ export default function ChatView() {
{activeThread.messages.length === 0 && !isWorking ? (
-

Send a message to start the conversation.

+

+ Send a message to start the conversation. +

) : (
{timelineEntries.map((timelineEntry, index) => { - if ( - timelineEntry.kind === "work" && - timelineEntries[index - 1]?.kind === "work" - ) { + if (timelineEntry.kind === "work" && timelineEntries[index - 1]?.kind === "work") { return null; } @@ -807,16 +751,13 @@ export default function ChatView() { const groupId = timelineEntry.id; const isExpanded = expandedWorkGroups[groupId] ?? false; - const hasOverflow = - groupedEntries.length > MAX_VISIBLE_WORK_LOG_ENTRIES; + const hasOverflow = groupedEntries.length > MAX_VISIBLE_WORK_LOG_ENTRIES; const visibleEntries = hasOverflow && !isExpanded ? groupedEntries.slice(-MAX_VISIBLE_WORK_LOG_ENTRIES) : groupedEntries; const hiddenCount = groupedEntries.length - visibleEntries.length; - const onlyToolEntries = groupedEntries.every( - (entry) => entry.tone === "tool", - ); + const onlyToolEntries = groupedEntries.every((entry) => entry.tone === "tool"); const groupLabel = onlyToolEntries ? groupedEntries.length === 1 ? "Tool call" @@ -902,9 +843,7 @@ export default function ChatView() {
- {completionSummary - ? `Response • ${completionSummary}` - : "Response"} + {completionSummary ? `Response • ${completionSummary}` : "Response"}
@@ -913,24 +852,17 @@ export default function ChatView() {

{formatMessageMeta( timelineEntry.message.createdAt, timelineEntry.message.streaming - ? formatElapsed( - timelineEntry.message.createdAt, - nowIso, - ) + ? formatElapsed(timelineEntry.message.createdAt, nowIso) : formatElapsed( timelineEntry.message.createdAt, - assistantCompletionByItemId.get( - timelineEntry.message.id, - ), + assistantCompletionByItemId.get(timelineEntry.message.id), ), )}

@@ -1086,9 +1018,7 @@ export default function ChatView() { disabled={isSwitchingRuntimeMode} onClick={() => void handleRuntimeModeChange( - state.runtimeMode === "full-access" - ? "approval-required" - : "full-access", + state.runtimeMode === "full-access" ? "approval-required" : "full-access", ) } title={ @@ -1098,13 +1028,7 @@ export default function ChatView() { } > {state.runtimeMode === "full-access" ? ( - - {state.runtimeMode === "full-access" - ? "Full access" - : "Supervised"} - + {state.runtimeMode === "full-access" ? "Full access" : "Supervised"}
@@ -1226,11 +1140,7 @@ export default function ChatView() {
- +
); } diff --git a/apps/web/src/components/DiffPanel.tsx b/apps/web/src/components/DiffPanel.tsx index 6a546c848fab82d87987c852e64e556dc4442fb1..72e11aa4a5849ae96aee651f78f0c90b2ffb525f 100644 --- a/apps/web/src/components/DiffPanel.tsx +++ b/apps/web/src/components/DiffPanel.tsx @@ -7,7 +7,9 @@ export default function DiffPanel() { return (