Skip to content

feat: chat reauth button, remove WorkspaceHome#244

Merged
charlesrhoward merged 3 commits intomainfrom
feat/chat-reauth-and-simplify
Mar 30, 2026
Merged

feat: chat reauth button, remove WorkspaceHome#244
charlesrhoward merged 3 commits intomainfrom
feat/chat-reauth-and-simplify

Conversation

@charlesrhoward
Copy link
Copy Markdown
Contributor

@charlesrhoward charlesrhoward commented Mar 30, 2026

Summary

  • Chat opens straight to the input — removed the WorkspaceHome screen with starter jobs, resume/fork, and automation installs
  • Auth errors in chat now show an inline "Re-authenticate Claude" button that opens a terminal tab and auto-runs claude for the OAuth flow
  • Bumps version to v1.3.2

Test plan

  • Open chat — should see empty chat with input ready, no starter UI
  • Simulate auth error (move ~/.claude/.credentials.json) — error should show green reauth button
  • Click reauth button — should open terminal tab with claude running
  • Non-auth errors should NOT show the reauth button

🤖 Generated with Claude Code


Note

Medium Risk
Adds a new re-authentication flow that programmatically opens a terminal and writes commands, which can affect runtime behavior and lifecycle cleanup. Also removes the WorkspaceHome empty-state UI and related run-resume/fork helpers, changing chat startup UX.

Overview
Chat now offers an inline recovery path for auth failures. Error bubbles detect likely authentication failures and, when provided, show a Re-authenticate Claude button that triggers opening a new terminal tab and auto-runs claude for OAuth.

The empty-chat WorkspaceHome screen and its resume/fork/automation entry points are removed. The chat panel always renders the message list + input, and related runHistory/workspace-home wiring is deleted.

Also bumps app version to 1.3.2 and adds a dependency override to require path-to-regexp >=8.4.0 (lockfile updated accordingly).

Written by Cursor Bugbot for commit 4f54e5c. This will update automatically on new commits. Configure here.

Chat now opens straight to the input instead of showing starter jobs,
resume/fork buttons, and automation installs. Auth errors show an inline
"Re-authenticate Claude" button that opens a terminal and auto-runs
`claude` for the OAuth flow.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel bot commented Mar 30, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agent-space-web Ready Ready Preview, Comment Mar 30, 2026 5:05pm

Request Review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c4e8703f4c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const AUTH_ERROR_PATTERNS = [
/not authenticated/i,
/authentication (?:required|failed|error)/i,
/unauthorized/i,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restrict reauth trigger to Claude auth failures

The new auth detector treats very generic strings like unauthorized and api key ... invalid as Claude-auth failures, so unrelated plugin/tool errors can now show a “Re-authenticate Claude” button and send users into the wrong flow. In this codebase, role: 'error' messages are also used for non-Claude command/plugin failures, so this broad pattern matching causes false positives and contradicts the intended “non-auth errors should NOT show the reauth button” behavior.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Auth error regex matches non-authentication token errors
    • I narrowed the invalid-token matcher to explicit authentication token phrases so non-auth token-limit/token-budget errors no longer trigger re-authentication UI.
Preview (574d5eeb98)
diff --git a/package.json b/package.json
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "agent-observer",
-  "version": "1.3.1",
+  "version": "1.3.2",
   "description": "Agent Office — 3D observability for AI agents",
   "main": "./out/main/index.js",
   "scripts": {

diff --git a/src/renderer/components/chat/ChatMessage.tsx b/src/renderer/components/chat/ChatMessage.tsx
--- a/src/renderer/components/chat/ChatMessage.tsx
+++ b/src/renderer/components/chat/ChatMessage.tsx
@@ -3,8 +3,27 @@
 
 interface Props {
   message: ChatMessageType
+  onReauthenticate?: () => void
 }
 
+const AUTH_ERROR_PATTERNS = [
+  /not authenticated/i,
+  /authentication (?:required|failed|error)/i,
+  /unauthorized/i,
+  /expired (?:token|session|credentials?)/i,
+  /oauth.*(?:error|failed|expired)/i,
+  /please (?:log in|login|sign in|authenticate)/i,
+  /api key.*(?:invalid|expired|missing|required)/i,
+  /invalid (?:api key|credentials?|access token|auth(?:entication)? token|bearer token)/i,
+  /session.*expired/i,
+  /login.*required/i,
+  /could not authenticate/i,
+] as const
+
+function isAuthError(content: string): boolean {
+  return AUTH_ERROR_PATTERNS.some((p) => p.test(content))
+}
+
 function formatTime(ts: number): string {
   const d = new Date(ts)
   return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
@@ -328,7 +347,7 @@
   return <>{elements}</>
 }
 
-export function ChatMessageBubble({ message }: Props) {
+export function ChatMessageBubble({ message, onReauthenticate }: Props) {
   // Tool calls and results get a task card
   if (message.role === 'tool' || (message.role === 'assistant' && message.toolName)) {
     return (
@@ -362,6 +381,7 @@
 
   // Error messages
   if (message.role === 'error') {
+    const showReauth = onReauthenticate && isAuthError(message.content)
     return (
       <div style={{ padding: '4px 0' }}>
         <div
@@ -375,6 +395,28 @@
           }}
         >
           {message.content}
+          {showReauth && (
+            <button
+              onClick={onReauthenticate}
+              style={{
+                marginTop: 8,
+                display: 'flex',
+                alignItems: 'center',
+                gap: 6,
+                padding: '6px 12px',
+                background: 'rgba(84, 140, 90, 0.15)',
+                border: '1px solid rgba(84, 140, 90, 0.4)',
+                borderRadius: 4,
+                color: '#548C5A',
+                fontSize: 12,
+                fontWeight: 600,
+                cursor: 'pointer',
+                fontFamily: 'inherit',
+              }}
+            >
+              Re-authenticate Claude
+            </button>
+          )}
         </div>
       </div>
     )

diff --git a/src/renderer/components/chat/ChatPanel.tsx b/src/renderer/components/chat/ChatPanel.tsx
--- a/src/renderer/components/chat/ChatPanel.tsx
+++ b/src/renderer/components/chat/ChatPanel.tsx
@@ -14,11 +14,8 @@
 import { logRendererEvent } from '../../lib/diagnostics'
 import { resolveClaudeProfile } from '../../lib/claudeProfile'
 import {
-  buildAutomationDraft,
-  buildForkPrompt,
   extractChangedFilesFromMessages,
   extractLastErrorFromMessages,
-  findLastResumableRun,
   findStarterJobTemplate,
 } from '../../lib/soloDevCockpit'
 import { useRunHistoryStore } from '../../store/runHistory'
@@ -42,10 +39,7 @@
   routeClaudeEvent,
   type SessionStatus,
 } from './claudeEventHandlers'
-import { WorkspaceHome } from './WorkspaceHome'
 import type {
-  BuiltInAutomationId,
-  RunRecord,
   StarterJobId,
 } from '../../../shared/run-history'
 
@@ -223,6 +217,9 @@
   const getNextDeskIndex = useAgentStore((s) => s.getNextDeskIndex)
   const addEvent = useAgentStore((s) => s.addEvent)
   const addToast = useAgentStore((s) => s.addToast)
+  const addTerminal = useAgentStore((s) => s.addTerminal)
+  const removeTerminal = useAgentStore((s) => s.removeTerminal)
+  const setActiveTerminal = useAgentStore((s) => s.setActiveTerminal)
   const updateChatSession = useAgentStore((s) => s.updateChatSession)
   const chatSession = useAgentStore(
     (s) => s.chatSessions.find((session) => session.id === chatSessionId) ?? null
@@ -234,7 +231,6 @@
 
   const workspaceRoot = useWorkspaceStore((s) => s.rootPath)
   const recentFolders = useWorkspaceStore((s) => s.recentFolders)
-  const openWorkspaceFolder = useWorkspaceStore((s) => s.openFolder)
   const scopes = useSettingsStore((s) => s.settings.scopes)
   const claudeProfilesConfig = useSettingsStore((s) => s.settings.claudeProfiles)
   const soundsEnabled = useSettingsStore((s) => s.settings.soundsEnabled)
@@ -247,12 +243,10 @@
   const addReward = useWorkspaceIntelligenceStore((s) => s.addReward)
   const rewards = useWorkspaceIntelligenceStore((s) => s.rewards)
   const latestContextForChat = useWorkspaceIntelligenceStore((s) => s.latestContextByChat[chatSessionId] ?? null)
-  const runs = useRunHistoryStore((s) => s.runs)
   const pendingStarterLaunch = useRunHistoryStore((s) => s.pendingStarterLaunch)
   const consumeStarterLaunch = useRunHistoryStore((s) => s.consumeStarterLaunch)
   const markStarterJobSuccess = useRunHistoryStore((s) => s.markStarterJobSuccess)
   const setLastResumableRunId = useRunHistoryStore((s) => s.setLastResumableRunId)
-  const lastSuccessfulStarterJobId = useRunHistoryStore((s) => s.lastSuccessfulStarterJobId)
   const [showRecentMenu, setShowRecentMenu] = useState(false)
   const recentMenuRef = useRef<HTMLDivElement>(null)
 
@@ -300,16 +294,6 @@
   const isDirectoryCustom = chatSession ? chatSession.directoryMode === 'custom' : false
   const hasStartedConversation = Boolean(chatSession?.agentId)
   const isRunActive = status === 'running' || Boolean(claudeSessionId)
-  const recentRuns = useMemo(() => {
-    const directory = workingDir ?? workspaceRoot ?? null
-    return runs.filter((run) => {
-      if (!directory) return true
-      return run.workspaceDirectory === directory
-    })
-  }, [runs, workingDir, workspaceRoot])
-  const lastResumableRun = useMemo(() => {
-    return findLastResumableRun(recentRuns, workingDir ?? workspaceRoot ?? null)
-  }, [recentRuns, workingDir, workspaceRoot])
   const activeClaudeProfile = useMemo(() => {
     return resolveClaudeProfile(claudeProfilesConfig, workingDir ?? null)
   }, [claudeProfilesConfig, workingDir])
@@ -1267,76 +1251,6 @@
     )
   }, [handleClaudePromptSend, resolveEffectiveWorkingDir])
 
-  const handleResumeRun = useCallback(async (run: RunRecord) => {
-    const effectiveWorkingDir = run.workspaceDirectory ?? await resolveEffectiveWorkingDir()
-    if (!effectiveWorkingDir) return
-    const conversationId = run.conversationId ?? chatSession?.claudeConversationId ?? createConversationId()
-    updateChatSession(chatSessionId, {
-      claudeConversationId: conversationId,
-      workingDirectory: effectiveWorkingDir,
-      directoryMode: effectiveWorkingDir === workspaceRoot ? 'workspace' : 'custom',
-    })
-    await handleClaudePromptSend(
-      'Resume the previous conversation from the current state. Restate progress, blockers, and the next best action, then continue the work if it is safe to do so.',
-      effectiveWorkingDir,
-      undefined,
-      undefined,
-      {
-        title: `Resume: ${run.title}`,
-        conversationIdOverride: conversationId,
-      }
-    )
-  }, [chatSession?.claudeConversationId, chatSessionId, handleClaudePromptSend, resolveEffectiveWorkingDir, updateChatSession, workspaceRoot])
-
-  const handleForkRun = useCallback(async (run: RunRecord) => {
-    const effectiveWorkingDir = run.workspaceDirectory ?? await resolveEffectiveWorkingDir()
-    if (!effectiveWorkingDir) return
-    const nextConversationId = createConversationId()
-    updateChatSession(chatSessionId, {
-      claudeConversationId: nextConversationId,
-      workingDirectory: effectiveWorkingDir,
-      directoryMode: effectiveWorkingDir === workspaceRoot ? 'workspace' : 'custom',
-    })
-    await handleClaudePromptSend(
-      buildForkPrompt(run),
-      effectiveWorkingDir,
-      undefined,
-      undefined,
-      {
-        title: `Fork: ${run.title}`,
-        forkedFromRunId: run.id,
-        conversationIdOverride: nextConversationId,
-      }
-    )
-  }, [chatSessionId, handleClaudePromptSend, resolveEffectiveWorkingDir, updateChatSession, workspaceRoot])
-
-  const handleInstallAutomation = useCallback(async (automationId: BuiltInAutomationId) => {
-    const effectiveWorkingDir = await resolveEffectiveWorkingDir()
-    if (!effectiveWorkingDir) return
-
-    try {
-      const draft = buildAutomationDraft(automationId, effectiveWorkingDir, yoloMode)
-      const existingTasks = await window.electronAPI.scheduler.list()
-      const existing = existingTasks.find((task) =>
-        task.name === draft.name && task.workingDirectory === effectiveWorkingDir
-      )
-      await window.electronAPI.scheduler.upsert({
-        ...draft,
-        id: existing?.id,
-      })
-      addToast({
-        type: 'success',
-        message: `${draft.name} installed for ${effectiveWorkingDir.split('/').pop() ?? effectiveWorkingDir}.`,
-      })
-    } catch (error) {
-      const message = error instanceof Error ? error.message : String(error)
-      addToast({
-        type: 'error',
-        message: `Failed to install automation: ${message}`,
-      })
-    }
-  }, [addToast, resolveEffectiveWorkingDir, yoloMode])
-
   useEffect(() => {
     if (!pendingStarterLaunch || messages.length > 0 || isRunActive) return
     consumeStarterLaunch(pendingStarterLaunch.jobId)
@@ -1365,6 +1279,42 @@
     runStartedAtRef.current = null
   }, [claudeSessionId, clearSubagentsForParent, finalizeRunReward, setActiveClaudeSession, updateAgent])
 
+  const handleReauthenticate = useCallback(async () => {
+    try {
+      const { id, cwd } = await window.electronAPI.terminal.create({ cols: 80, rows: 24 })
+      const { scopes } = useSettingsStore.getState().settings
+      const matched = matchScope(cwd, scopes)
+
+      addTerminal({
+        id,
+        label: 'Auth',
+        isClaudeRunning: false,
+        scopeId: matched?.id ?? null,
+        cwd,
+      })
+      setActiveTerminal(id)
+
+      const unsub = window.electronAPI.terminal.onExit((exitId) => {
+        if (exitId === id) {
+          removeAgent(id)
+          removeTerminal(id)
+          unsub()
+        }
+      })
+
+      // Auto-run claude after terminal initializes
+      setTimeout(() => {
+        window.electronAPI.terminal.write(id, 'claude\n')
+      }, 100)
+
+      window.dispatchEvent(new CustomEvent('agent:focusTerminal', { detail: { terminalId: id } }))
+    } catch (err) {
+      const msg = err instanceof Error ? err.message : String(err)
+      console.error(`[ChatPanel] Failed to create auth terminal: ${msg}`)
+      addToast({ type: 'error', message: 'Failed to open terminal for re-authentication' })
+    }
+  }, [addTerminal, removeTerminal, removeAgent, setActiveTerminal, addToast])
+
   const isRunning = isRunActive
 
   // ── Resizable input area ─────────────────────────────────────────
@@ -1687,35 +1637,17 @@
 
       {/* Messages */}
       <div style={{ flex: 1, overflow: 'auto', padding: '12px 16px', minHeight: 0 }}>
-        {messages.length === 0 ? (
-          <WorkspaceHome
-            workingDirectory={workingDir ?? null}
-            workspaceRoot={workspaceRoot ?? null}
-            recentFolders={recentFolders}
-            recentRuns={recentRuns}
-            lastResumableRun={lastResumableRun}
-            lastSuccessfulStarterJobId={lastSuccessfulStarterJobId}
-            onChooseFolder={() => { void handleChangeWorkingDir() }}
-            onOpenRecentFolder={(path) => {
-              openWorkspaceFolder(path)
-              handleSelectRecentDirectory(path)
-            }}
-            onLaunchStarterJob={(jobId) => { void handleLaunchStarterJob(jobId) }}
-            onResumeRun={(run) => { void handleResumeRun(run) }}
-            onForkRun={(run) => { void handleForkRun(run) }}
-            onInstallAutomation={(automationId) => { void handleInstallAutomation(automationId) }}
+        {messages.map((msg) => (
+          <ChatMessageBubble
+            key={msg.id}
+            message={msg}
+            onReauthenticate={msg.role === 'error' ? handleReauthenticate : undefined}
           />
-        ) : (
-          <>
-            {messages.map((msg) => (
-              <ChatMessageBubble key={msg.id} message={msg} />
-            ))}
-            {isRunning && messages[messages.length - 1]?.role !== 'thinking' && (
-              <TypingIndicator />
-            )}
-            <div ref={chatEndRef} />
-          </>
+        ))}
+        {isRunning && messages[messages.length - 1]?.role !== 'thinking' && (
+          <TypingIndicator />
         )}
+        <div ref={chatEndRef} />
       </div>
 
       {/* Draggable divider */}

Transitive dep via @modelcontextprotocol/sdk > express > router.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@charlesrhoward charlesrhoward merged commit f29e863 into main Mar 30, 2026
8 checks passed
@charlesrhoward charlesrhoward deleted the feat/chat-reauth-and-simplify branch March 30, 2026 17:06
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix is ON, but it could not run because the branch was deleted or merged before autofix could start.

// Auto-run claude after terminal initializes
setTimeout(() => {
window.electronAPI.terminal.write(id, 'claude\n')
}, 100)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Race condition writing command to new terminal

Low Severity

The setTimeout with a hardcoded 100ms delay to write 'claude\n' to the terminal races with shell initialization. On systems with heavy shell configs (e.g., large .zshrc/.bashrc) or under load, the shell may not be ready to accept input within 100ms, causing the command to be lost. The user would then see a blank terminal with no claude process running, silently defeating the re-authentication feature. Other terminal creation sites in the codebase don't auto-write commands, so there's no existing pattern to follow here. A more robust approach would be to listen for initial PTY output (the shell prompt) before sending the command.

Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants