Workspace Terminal tab: xterm front end + short-lived token route
- New Terminal tab in the workspace shell, backed by xterm.js, that connects to the worker's PTY WebSocket using a short-lived token minted by /api/agent-jobs/:id/terminal-token (owner-auth'd, never exposes the worker secret to the browser) - Site-matched xterm theme (light/dark, live theme switching), Victor Mono, binary stdin + JSON resize protocol, reconnect, graceful 'not configured' state when NEXT_PUBLIC_SPOON_AGENT_WORKER_WS_URL is unset - env: NEXT_PUBLIC_SPOON_AGENT_WORKER_WS_URL (client), SPOON_AGENT_TERMINAL_SECRET
This commit is contained in:
@@ -3,7 +3,13 @@
|
||||
import type { CSSProperties, PointerEvent as ReactPointerEvent } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useMutation, useQuery } from 'convex/react';
|
||||
import { FileCode, GitCompare, Loader2, MessagesSquare } from 'lucide-react';
|
||||
import {
|
||||
FileCode,
|
||||
GitCompare,
|
||||
Loader2,
|
||||
MessagesSquare,
|
||||
SquareTerminal,
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import type { Id } from '@spoon/backend/convex/_generated/dataModel.js';
|
||||
@@ -34,6 +40,9 @@ import { FileTabs } from './file-tabs';
|
||||
import { FileTree } from './file-tree';
|
||||
import { JobStatusBar } from './job-status-bar';
|
||||
import { WorkspaceActions } from './workspace-actions';
|
||||
import { WorkspaceTerminal } from './workspace-terminal';
|
||||
|
||||
type WorkspaceTab = 'editor' | 'diff' | 'thread' | 'terminal';
|
||||
|
||||
type OpenFileState = {
|
||||
path: string;
|
||||
@@ -81,9 +90,8 @@ export const AgentWorkspaceShell = ({ jobId }: { jobId: Id<'agentJobs'> }) => {
|
||||
const [focusedDiffPath, setFocusedDiffPath] = useState<string>();
|
||||
const [workspaceError, setWorkspaceError] = useState<string>();
|
||||
const [agentTurnActive, setAgentTurnActive] = useState(false);
|
||||
const [activeWorkspaceTab, setActiveWorkspaceTab] = useState<
|
||||
'editor' | 'diff' | 'thread'
|
||||
>('editor');
|
||||
const [activeWorkspaceTab, setActiveWorkspaceTab] =
|
||||
useState<WorkspaceTab>('editor');
|
||||
const [pendingOverwrite, setPendingOverwrite] = useState<PendingOverwrite>();
|
||||
const [pendingClosePath, setPendingClosePath] = useState<string>();
|
||||
|
||||
@@ -575,7 +583,7 @@ export const AgentWorkspaceShell = ({ jobId }: { jobId: Id<'agentJobs'> }) => {
|
||||
<Tabs
|
||||
value={activeWorkspaceTab}
|
||||
onValueChange={(value) =>
|
||||
setActiveWorkspaceTab(value as 'editor' | 'diff' | 'thread')
|
||||
setActiveWorkspaceTab(value as WorkspaceTab)
|
||||
}
|
||||
className='flex min-h-0 flex-1 flex-col'
|
||||
>
|
||||
@@ -594,6 +602,13 @@ export const AgentWorkspaceShell = ({ jobId }: { jobId: Id<'agentJobs'> }) => {
|
||||
<GitCompare className='size-4' />
|
||||
Diff viewer
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value='terminal'
|
||||
className='data-active:bg-background data-active:text-foreground data-active:shadow-sm'
|
||||
>
|
||||
<SquareTerminal className='size-4' />
|
||||
Terminal
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value='thread'
|
||||
className='data-active:bg-background data-active:text-foreground data-active:shadow-sm 2xl:hidden'
|
||||
@@ -642,6 +657,15 @@ export const AgentWorkspaceShell = ({ jobId }: { jobId: Id<'agentJobs'> }) => {
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent
|
||||
value='terminal'
|
||||
className='m-0 min-h-0 flex-1 overflow-hidden'
|
||||
>
|
||||
<WorkspaceTerminal
|
||||
jobId={jobId}
|
||||
active={activeWorkspaceTab === 'terminal' && workspaceReady}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent value='diff' className='m-0 min-h-0 flex-1'>
|
||||
<DiffViewer
|
||||
diff={diff}
|
||||
|
||||
Reference in New Issue
Block a user