feat: chat reauth button, remove WorkspaceHome#244
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
💡 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, |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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>
| // Auto-run claude after terminal initializes | ||
| setTimeout(() => { | ||
| window.electronAPI.terminal.write(id, 'claude\n') | ||
| }, 100) |
There was a problem hiding this comment.
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.


Summary
WorkspaceHomescreen with starter jobs, resume/fork, and automation installsclaudefor the OAuth flowTest plan
~/.claude/.credentials.json) — error should show green reauth buttonclauderunning🤖 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
WorkspaceHomeempty-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 Claudebutton that triggers opening a new terminal tab and auto-runsclaudefor OAuth.The empty-chat
WorkspaceHomescreen and its resume/fork/automation entry points are removed. The chat panel always renders the message list + input, and relatedrunHistory/workspace-home wiring is deleted.Also bumps app version to
1.3.2and adds a dependency override to requirepath-to-regexp >=8.4.0(lockfile updated accordingly).Written by Cursor Bugbot for commit 4f54e5c. This will update automatically on new commits. Configure here.