repoburg 1.3.185 → 1.3.186
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent-pane/backend/package-lock.json +2359 -0
- package/agent-pane/backend/package.json +29 -0
- package/agent-pane/backend/src/codex/codex-provider.ts +260 -0
- package/agent-pane/backend/src/codex/item-mapper.ts +59 -0
- package/agent-pane/backend/src/db/index.ts +67 -0
- package/agent-pane/backend/src/db/schema.ts +65 -0
- package/agent-pane/backend/src/index.ts +55 -0
- package/agent-pane/backend/src/routes/sessions.ts +97 -0
- package/agent-pane/backend/src/routes/turns.ts +60 -0
- package/agent-pane/backend/src/ws/index.ts +43 -0
- package/agent-pane/backend/tsconfig.json +23 -0
- package/agent-pane/frontend/index.html +18 -0
- package/agent-pane/frontend/package-lock.json +1948 -0
- package/agent-pane/frontend/package.json +28 -0
- package/agent-pane/frontend/src/App.tsx +44 -0
- package/agent-pane/frontend/src/AppInitializer.tsx +46 -0
- package/agent-pane/frontend/src/api/client.ts +29 -0
- package/agent-pane/frontend/src/api/sessions.ts +31 -0
- package/agent-pane/frontend/src/api/websocket.ts +139 -0
- package/agent-pane/frontend/src/components/CommandInputBar.tsx +42 -0
- package/agent-pane/frontend/src/components/CommandPalette.tsx +196 -0
- package/agent-pane/frontend/src/components/DiffViewer.tsx +27 -0
- package/agent-pane/frontend/src/components/IdeWorkspace.tsx +25 -0
- package/agent-pane/frontend/src/components/NewSessionDialog.tsx +84 -0
- package/agent-pane/frontend/src/components/RenderArea.tsx +157 -0
- package/agent-pane/frontend/src/components/RightPanel.tsx +40 -0
- package/agent-pane/frontend/src/components/RuntimeIcon.tsx +16 -0
- package/agent-pane/frontend/src/components/SessionTabs.tsx +41 -0
- package/agent-pane/frontend/src/components/StatusBar.tsx +60 -0
- package/agent-pane/frontend/src/components/StatusDot.tsx +13 -0
- package/agent-pane/frontend/src/components/Toast.tsx +34 -0
- package/agent-pane/frontend/src/components/TurnContent.tsx +202 -0
- package/agent-pane/frontend/src/components/WorkspaceSidebar.tsx +60 -0
- package/agent-pane/frontend/src/components/panels/ChangesPanel.tsx +77 -0
- package/agent-pane/frontend/src/components/panels/LogsPanel.tsx +25 -0
- package/agent-pane/frontend/src/components/panels/MetricsPanel.tsx +69 -0
- package/agent-pane/frontend/src/components/panels/TurnsPanel.tsx +84 -0
- package/agent-pane/frontend/src/main.tsx +10 -0
- package/agent-pane/frontend/src/store/store.ts +492 -0
- package/agent-pane/frontend/src/styles/app.css +288 -0
- package/agent-pane/frontend/src/styles/tokens.css +62 -0
- package/agent-pane/frontend/src/types/index.ts +115 -0
- package/agent-pane/frontend/tsconfig.json +22 -0
- package/agent-pane/frontend/vite.config.ts +16 -0
- package/backend/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agent-pane-frontend",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "tsc --noEmit && vite build",
|
|
9
|
+
"preview": "vite preview",
|
|
10
|
+
"typecheck": "tsc --noEmit"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"dompurify": "^3.2.0",
|
|
14
|
+
"lucide-react": "^0.469.0",
|
|
15
|
+
"marked": "^15.0.0",
|
|
16
|
+
"react": "^18.3.1",
|
|
17
|
+
"react-dom": "^18.3.1",
|
|
18
|
+
"zustand": "^5.0.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/dompurify": "^3.0.5",
|
|
22
|
+
"@types/react": "^18.3.0",
|
|
23
|
+
"@types/react-dom": "^18.3.0",
|
|
24
|
+
"@vitejs/plugin-react": "^4.3.0",
|
|
25
|
+
"typescript": "^5.7.0",
|
|
26
|
+
"vite": "^6.0.0"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { useStore } from './store/store.ts';
|
|
3
|
+
import IdeWorkspace from './components/IdeWorkspace.tsx';
|
|
4
|
+
import NewSessionDialog from './components/NewSessionDialog.tsx';
|
|
5
|
+
import CommandPalette from './components/CommandPalette.tsx';
|
|
6
|
+
import Toast from './components/Toast.tsx';
|
|
7
|
+
import AppInitializer from './AppInitializer.tsx';
|
|
8
|
+
|
|
9
|
+
export default function App() {
|
|
10
|
+
const loading = useStore((s) => s.loading);
|
|
11
|
+
const error = useStore((s) => s.error);
|
|
12
|
+
const sessions = useStore((s) => s.sessions);
|
|
13
|
+
const setShowCommandPalette = useStore((s) => s.setShowCommandPalette);
|
|
14
|
+
|
|
15
|
+
// Cmd+K / Ctrl+K listener
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
function handleKeyDown(e: KeyboardEvent) {
|
|
18
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
19
|
+
e.preventDefault();
|
|
20
|
+
setShowCommandPalette(true);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
24
|
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
25
|
+
}, [setShowCommandPalette]);
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<>
|
|
29
|
+
<AppInitializer />
|
|
30
|
+
{loading && sessions.length === 0 ? (
|
|
31
|
+
<div className="empty-state">Loading sessions…</div>
|
|
32
|
+
) : error && sessions.length === 0 ? (
|
|
33
|
+
<div className="empty-state">Error: {error}</div>
|
|
34
|
+
) : (
|
|
35
|
+
<>
|
|
36
|
+
<IdeWorkspace />
|
|
37
|
+
<NewSessionDialog />
|
|
38
|
+
<CommandPalette />
|
|
39
|
+
<Toast />
|
|
40
|
+
</>
|
|
41
|
+
)}
|
|
42
|
+
</>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { useStore } from './store/store.ts';
|
|
3
|
+
import { connectWebSocket, disconnectWebSocket } from './api/websocket.ts';
|
|
4
|
+
|
|
5
|
+
export default function AppInitializer() {
|
|
6
|
+
const init = useStore((s) => s.init);
|
|
7
|
+
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
// Fetch initial sessions from REST API
|
|
10
|
+
init();
|
|
11
|
+
|
|
12
|
+
// Connect WebSocket for live streaming
|
|
13
|
+
connectWebSocket();
|
|
14
|
+
|
|
15
|
+
// visibilitychange — reconcile when tab returns to foreground
|
|
16
|
+
function handleVisibilityChange() {
|
|
17
|
+
if (document.visibilityState !== 'visible') return;
|
|
18
|
+
|
|
19
|
+
const state = useStore.getState();
|
|
20
|
+
const activeId = state.activeSessionId;
|
|
21
|
+
if (!activeId) return;
|
|
22
|
+
|
|
23
|
+
const detail = state.sessionDetails[activeId];
|
|
24
|
+
|
|
25
|
+
// If WS is not connected, reconnect
|
|
26
|
+
if (!state.wsConnected) {
|
|
27
|
+
connectWebSocket();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// If session was running, re-fetch to catch missed events
|
|
31
|
+
if (detail && detail.is_running === 1) {
|
|
32
|
+
state.refreshActiveSession();
|
|
33
|
+
state.addLogEntry('INFO', 'Tab returned — reconciling session state');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
document.addEventListener('visibilitychange', handleVisibilityChange);
|
|
38
|
+
|
|
39
|
+
return () => {
|
|
40
|
+
disconnectWebSocket();
|
|
41
|
+
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
|
42
|
+
};
|
|
43
|
+
}, [init]);
|
|
44
|
+
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const API_BASE = '/api';
|
|
2
|
+
|
|
3
|
+
export async function apiGet<T>(path: string): Promise<T> {
|
|
4
|
+
const res = await fetch(`${API_BASE}${path}`);
|
|
5
|
+
if (!res.ok) {
|
|
6
|
+
throw new Error(`GET ${path} failed: ${res.status} ${res.statusText}`);
|
|
7
|
+
}
|
|
8
|
+
return res.json() as Promise<T>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function apiPost<T>(path: string, body?: unknown): Promise<T> {
|
|
12
|
+
const res = await fetch(`${API_BASE}${path}`, {
|
|
13
|
+
method: 'POST',
|
|
14
|
+
headers: { 'Content-Type': 'application/json' },
|
|
15
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
16
|
+
});
|
|
17
|
+
if (!res.ok) {
|
|
18
|
+
throw new Error(`POST ${path} failed: ${res.status} ${res.statusText}`);
|
|
19
|
+
}
|
|
20
|
+
return res.json() as Promise<T>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function apiDelete<T>(path: string): Promise<T> {
|
|
24
|
+
const res = await fetch(`${API_BASE}${path}`, { method: 'DELETE' });
|
|
25
|
+
if (!res.ok) {
|
|
26
|
+
throw new Error(`DELETE ${path} failed: ${res.status} ${res.statusText}`);
|
|
27
|
+
}
|
|
28
|
+
return res.json() as Promise<T>;
|
|
29
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { apiGet, apiPost, apiDelete } from './client.ts';
|
|
2
|
+
import type { Session, SessionDetail, Turn } from '../types/index.ts';
|
|
3
|
+
|
|
4
|
+
export function fetchSessions(): Promise<Session[]> {
|
|
5
|
+
return apiGet<Session[]>('/sessions');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function fetchSessionDetail(id: string): Promise<SessionDetail> {
|
|
9
|
+
return apiGet<SessionDetail>(`/sessions/${id}`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function createSession(body: {
|
|
13
|
+
title?: string;
|
|
14
|
+
runtime_type?: string;
|
|
15
|
+
model?: string;
|
|
16
|
+
cwd?: string;
|
|
17
|
+
}): Promise<Session> {
|
|
18
|
+
return apiPost<Session>('/sessions', body);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function deleteSession(id: string): Promise<{ success: boolean }> {
|
|
22
|
+
return apiDelete<{ success: boolean }>(`/sessions/${id}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function submitPrompt(sessionId: string, userPrompt: string): Promise<Turn> {
|
|
26
|
+
return apiPost<Turn>(`/sessions/${sessionId}/turns`, { user_prompt: userPrompt });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function cancelTurn(sessionId: string): Promise<{ success: boolean }> {
|
|
30
|
+
return apiPost<{ success: boolean }>(`/sessions/${sessionId}/cancel`);
|
|
31
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { useStore } from '../store/store.ts';
|
|
2
|
+
import type { Item, TokenUsage } from '../types/index.ts';
|
|
3
|
+
|
|
4
|
+
type WsEventHandler = (data: unknown) => void;
|
|
5
|
+
|
|
6
|
+
let ws: WebSocket | null = null;
|
|
7
|
+
let reconnectTimer: number | null = null;
|
|
8
|
+
let reconnectDelay = 1000;
|
|
9
|
+
const MAX_RECONNECT_DELAY = 10000;
|
|
10
|
+
|
|
11
|
+
/** Build the WebSocket URL from the current page location. */
|
|
12
|
+
function buildWsUrl(): string {
|
|
13
|
+
const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
14
|
+
return `${proto}//${window.location.host}/ws`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Connect to the backend WebSocket server. Dispatches events to store actions. */
|
|
18
|
+
export function connectWebSocket(): void {
|
|
19
|
+
if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
ws = new WebSocket(buildWsUrl());
|
|
24
|
+
|
|
25
|
+
ws.onopen = () => {
|
|
26
|
+
const wasDisconnected = reconnectDelay > 1000;
|
|
27
|
+
reconnectDelay = 1000; // reset backoff
|
|
28
|
+
useStore.getState().setWsConnected(true);
|
|
29
|
+
useStore.getState().addLogEntry('INFO', 'WebSocket connected');
|
|
30
|
+
|
|
31
|
+
// On reconnect (not initial connect): reconcile active session
|
|
32
|
+
if (wasDisconnected) {
|
|
33
|
+
const state = useStore.getState();
|
|
34
|
+
if (state.activeSessionId) {
|
|
35
|
+
state.refreshActiveSession();
|
|
36
|
+
state.addToast('info', 'WebSocket reconnected — session state reconciled');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
ws.onclose = () => {
|
|
42
|
+
useStore.getState().setWsConnected(false);
|
|
43
|
+
useStore.getState().addLogEntry('WARN', 'WebSocket disconnected');
|
|
44
|
+
useStore.getState().addToast('warn', 'WebSocket disconnected — reconnecting…');
|
|
45
|
+
scheduleReconnect();
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
ws.onerror = () => {
|
|
49
|
+
// onclose will fire after onerror
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
ws.onmessage = (event: MessageEvent) => {
|
|
53
|
+
try {
|
|
54
|
+
const msg = JSON.parse(event.data as string);
|
|
55
|
+
const handler = HANDLERS[msg.event as string];
|
|
56
|
+
if (handler) {
|
|
57
|
+
handler(msg.data);
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
// Ignore malformed messages
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Disconnect the WebSocket (used on unmount). */
|
|
66
|
+
export function disconnectWebSocket(): void {
|
|
67
|
+
if (reconnectTimer) {
|
|
68
|
+
clearTimeout(reconnectTimer);
|
|
69
|
+
reconnectTimer = null;
|
|
70
|
+
}
|
|
71
|
+
if (ws) {
|
|
72
|
+
ws.onclose = null; // prevent reconnect on intentional close
|
|
73
|
+
ws.close();
|
|
74
|
+
ws = null;
|
|
75
|
+
}
|
|
76
|
+
useStore.getState().setWsConnected(false);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Schedule a reconnect with exponential backoff. */
|
|
80
|
+
function scheduleReconnect(): void {
|
|
81
|
+
if (reconnectTimer) return;
|
|
82
|
+
reconnectTimer = window.setTimeout(() => {
|
|
83
|
+
reconnectTimer = null;
|
|
84
|
+
connectWebSocket();
|
|
85
|
+
}, reconnectDelay);
|
|
86
|
+
reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_DELAY);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// --- Event handlers — dispatch to store actions ---
|
|
90
|
+
|
|
91
|
+
interface TurnEventPayload {
|
|
92
|
+
sessionId: string;
|
|
93
|
+
turnId: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
interface ItemEventPayload {
|
|
97
|
+
sessionId: string;
|
|
98
|
+
turnId: string;
|
|
99
|
+
item: Item;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
interface TurnCompletedPayload {
|
|
103
|
+
sessionId: string;
|
|
104
|
+
turnId: string;
|
|
105
|
+
usage: TokenUsage;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
interface TurnFailedPayload {
|
|
109
|
+
sessionId: string;
|
|
110
|
+
turnId: string;
|
|
111
|
+
error: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const HANDLERS: Record<string, WsEventHandler> = {
|
|
115
|
+
'turn:started': (data) => {
|
|
116
|
+
const p = data as TurnEventPayload;
|
|
117
|
+
useStore.getState().onTurnStarted(p.sessionId, p.turnId);
|
|
118
|
+
},
|
|
119
|
+
'item:started': (data) => {
|
|
120
|
+
const p = data as ItemEventPayload;
|
|
121
|
+
useStore.getState().onItemStarted(p.sessionId, p.turnId, p.item);
|
|
122
|
+
},
|
|
123
|
+
'item:updated': (data) => {
|
|
124
|
+
const p = data as ItemEventPayload;
|
|
125
|
+
useStore.getState().onItemUpdated(p.sessionId, p.turnId, p.item);
|
|
126
|
+
},
|
|
127
|
+
'item:completed': (data) => {
|
|
128
|
+
const p = data as ItemEventPayload;
|
|
129
|
+
useStore.getState().onItemCompleted(p.sessionId, p.turnId, p.item);
|
|
130
|
+
},
|
|
131
|
+
'turn:completed': (data) => {
|
|
132
|
+
const p = data as TurnCompletedPayload;
|
|
133
|
+
useStore.getState().onTurnCompleted(p.sessionId, p.turnId, p.usage);
|
|
134
|
+
},
|
|
135
|
+
'turn:failed': (data) => {
|
|
136
|
+
const p = data as TurnFailedPayload;
|
|
137
|
+
useStore.getState().onTurnFailed(p.sessionId, p.turnId, p.error);
|
|
138
|
+
},
|
|
139
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { CornerDownLeft, Square } from 'lucide-react';
|
|
3
|
+
import { useStore } from '../store/store.ts';
|
|
4
|
+
|
|
5
|
+
export default function CommandInputBar() {
|
|
6
|
+
const [value, setValue] = useState('');
|
|
7
|
+
const { submitPrompt, cancelTurn } = useStore();
|
|
8
|
+
const detail = useStore((s) => (s.activeSessionId ? s.sessionDetails[s.activeSessionId] : null));
|
|
9
|
+
const isRunning = detail?.is_running === 1;
|
|
10
|
+
|
|
11
|
+
function handleSend() {
|
|
12
|
+
const v = value.trim();
|
|
13
|
+
if (!v || isRunning) return;
|
|
14
|
+
setValue(''); // Clear input immediately for visual feedback
|
|
15
|
+
submitPrompt(v);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className="cmd-bar">
|
|
20
|
+
<input
|
|
21
|
+
className="cmd-input"
|
|
22
|
+
placeholder={isRunning ? 'Agent is running…' : 'Type a message to the agent…'}
|
|
23
|
+
autoComplete="off"
|
|
24
|
+
value={value}
|
|
25
|
+
disabled={isRunning}
|
|
26
|
+
onChange={(e) => setValue(e.target.value)}
|
|
27
|
+
onKeyDown={(e) => {
|
|
28
|
+
if (e.key === 'Enter' && !isRunning) handleSend();
|
|
29
|
+
}}
|
|
30
|
+
/>
|
|
31
|
+
{isRunning ? (
|
|
32
|
+
<button className="cmd-send" style={{ background: 'var(--red)' }} onClick={() => cancelTurn()}>
|
|
33
|
+
<Square size={14} /> Cancel
|
|
34
|
+
</button>
|
|
35
|
+
) : (
|
|
36
|
+
<button className="cmd-send" onClick={handleSend} disabled={!value.trim()}>
|
|
37
|
+
<CornerDownLeft size={14} /> Send
|
|
38
|
+
</button>
|
|
39
|
+
)}
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { useState, useEffect, useRef, useMemo } from 'react';
|
|
2
|
+
import { useStore } from '../store/store.ts';
|
|
3
|
+
import {
|
|
4
|
+
Plus,
|
|
5
|
+
Trash2,
|
|
6
|
+
Send,
|
|
7
|
+
Square,
|
|
8
|
+
List,
|
|
9
|
+
BarChart3,
|
|
10
|
+
FileCode,
|
|
11
|
+
ScrollText,
|
|
12
|
+
X,
|
|
13
|
+
CornerDownLeft,
|
|
14
|
+
} from 'lucide-react';
|
|
15
|
+
import type { RightTab } from '../types/index.ts';
|
|
16
|
+
|
|
17
|
+
interface Command {
|
|
18
|
+
id: string;
|
|
19
|
+
label: string;
|
|
20
|
+
hint: string;
|
|
21
|
+
icon: typeof Plus;
|
|
22
|
+
action: () => void;
|
|
23
|
+
available: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default function CommandPalette() {
|
|
27
|
+
const show = useStore((s) => s.showCommandPalette);
|
|
28
|
+
const setShow = useStore((s) => s.setShowCommandPalette);
|
|
29
|
+
const store = useStore();
|
|
30
|
+
|
|
31
|
+
const [query, setQuery] = useState('');
|
|
32
|
+
const [selectedIdx, setSelectedIdx] = useState(0);
|
|
33
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
34
|
+
|
|
35
|
+
// Build commands
|
|
36
|
+
const commands: Command[] = useMemo(() => {
|
|
37
|
+
const detail = store.activeSessionId ? store.sessionDetails[store.activeSessionId] : null;
|
|
38
|
+
const isRunning = detail?.is_running === 1;
|
|
39
|
+
|
|
40
|
+
return [
|
|
41
|
+
{
|
|
42
|
+
id: 'new-session',
|
|
43
|
+
label: 'New Session',
|
|
44
|
+
hint: 'Create a new Codex session',
|
|
45
|
+
icon: Plus,
|
|
46
|
+
action: () => store.setShowNewSessionDialog(true),
|
|
47
|
+
available: true,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: 'delete-session',
|
|
51
|
+
label: 'Delete Session',
|
|
52
|
+
hint: 'Delete the active session',
|
|
53
|
+
icon: Trash2,
|
|
54
|
+
action: () => { if (store.activeSessionId) store.deleteSession(store.activeSessionId); },
|
|
55
|
+
available: !!store.activeSessionId,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: 'send-message',
|
|
59
|
+
label: 'Send Message',
|
|
60
|
+
hint: 'Focus the message input',
|
|
61
|
+
icon: Send,
|
|
62
|
+
action: () => { const el = document.querySelector<HTMLInputElement>('.cmd-input'); el?.focus(); },
|
|
63
|
+
available: !!store.activeSessionId && !isRunning,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: 'cancel-turn',
|
|
67
|
+
label: 'Cancel Turn',
|
|
68
|
+
hint: 'Cancel the running turn',
|
|
69
|
+
icon: Square,
|
|
70
|
+
action: () => store.cancelTurn(),
|
|
71
|
+
available: isRunning,
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: 'tab-turns',
|
|
75
|
+
label: 'Switch to Turns Tab',
|
|
76
|
+
hint: 'View conversation timeline',
|
|
77
|
+
icon: List,
|
|
78
|
+
action: () => store.switchRightTab('turns' as RightTab),
|
|
79
|
+
available: !!store.activeSessionId,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: 'tab-metrics',
|
|
83
|
+
label: 'Switch to Metrics Tab',
|
|
84
|
+
hint: 'View token usage and session info',
|
|
85
|
+
icon: BarChart3,
|
|
86
|
+
action: () => store.switchRightTab('metrics' as RightTab),
|
|
87
|
+
available: !!store.activeSessionId,
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: 'tab-changes',
|
|
91
|
+
label: 'Switch to Changes Tab',
|
|
92
|
+
hint: 'View file changes',
|
|
93
|
+
icon: FileCode,
|
|
94
|
+
action: () => store.switchRightTab('changes' as RightTab),
|
|
95
|
+
available: !!store.activeSessionId,
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: 'tab-logs',
|
|
99
|
+
label: 'Switch to Logs Tab',
|
|
100
|
+
hint: 'View event log',
|
|
101
|
+
icon: ScrollText,
|
|
102
|
+
action: () => store.switchRightTab('logs' as RightTab),
|
|
103
|
+
available: !!store.activeSessionId,
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: 'close-tab',
|
|
107
|
+
label: 'Close Tab',
|
|
108
|
+
hint: 'Close the active session tab',
|
|
109
|
+
icon: X,
|
|
110
|
+
action: () => { if (store.activeSessionId) store.closeTab(store.activeSessionId); },
|
|
111
|
+
available: !!store.activeSessionId,
|
|
112
|
+
},
|
|
113
|
+
];
|
|
114
|
+
}, [store]);
|
|
115
|
+
|
|
116
|
+
// Filter commands
|
|
117
|
+
const filtered = useMemo(() => {
|
|
118
|
+
const q = query.toLowerCase().trim();
|
|
119
|
+
const available = commands.filter((c) => c.available);
|
|
120
|
+
if (!q) return available;
|
|
121
|
+
return available.filter((c) =>
|
|
122
|
+
c.label.toLowerCase().includes(q) || c.hint.toLowerCase().includes(q),
|
|
123
|
+
);
|
|
124
|
+
}, [commands, query]);
|
|
125
|
+
|
|
126
|
+
// Reset on open
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
if (show) {
|
|
129
|
+
setQuery('');
|
|
130
|
+
setSelectedIdx(0);
|
|
131
|
+
setTimeout(() => inputRef.current?.focus(), 50);
|
|
132
|
+
}
|
|
133
|
+
}, [show]);
|
|
134
|
+
|
|
135
|
+
// Keyboard navigation
|
|
136
|
+
function handleKeyDown(e: React.KeyboardEvent) {
|
|
137
|
+
if (e.key === 'ArrowDown') {
|
|
138
|
+
e.preventDefault();
|
|
139
|
+
setSelectedIdx((i) => Math.min(i + 1, filtered.length - 1));
|
|
140
|
+
} else if (e.key === 'ArrowUp') {
|
|
141
|
+
e.preventDefault();
|
|
142
|
+
setSelectedIdx((i) => Math.max(i - 1, 0));
|
|
143
|
+
} else if (e.key === 'Enter') {
|
|
144
|
+
e.preventDefault();
|
|
145
|
+
const cmd = filtered[selectedIdx];
|
|
146
|
+
if (cmd) {
|
|
147
|
+
cmd.action();
|
|
148
|
+
setShow(false);
|
|
149
|
+
}
|
|
150
|
+
} else if (e.key === 'Escape') {
|
|
151
|
+
setShow(false);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (!show) return null;
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<div className="palette-overlay" onClick={() => setShow(false)}>
|
|
159
|
+
<div className="palette" onClick={(e) => e.stopPropagation()}>
|
|
160
|
+
<div className="palette-search">
|
|
161
|
+
<input
|
|
162
|
+
ref={inputRef}
|
|
163
|
+
type="text"
|
|
164
|
+
placeholder="Type a command…"
|
|
165
|
+
value={query}
|
|
166
|
+
onChange={(e) => { setQuery(e.target.value); setSelectedIdx(0); }}
|
|
167
|
+
onKeyDown={handleKeyDown}
|
|
168
|
+
/>
|
|
169
|
+
<span className="palette-esc">ESC</span>
|
|
170
|
+
</div>
|
|
171
|
+
{filtered.length === 0 ? (
|
|
172
|
+
<div className="palette-empty">No matching commands</div>
|
|
173
|
+
) : (
|
|
174
|
+
<div className="palette-list">
|
|
175
|
+
{filtered.map((cmd, i) => {
|
|
176
|
+
const Icon = cmd.icon;
|
|
177
|
+
return (
|
|
178
|
+
<div
|
|
179
|
+
key={cmd.id}
|
|
180
|
+
className={`palette-item ${i === selectedIdx ? 'selected' : ''}`}
|
|
181
|
+
onClick={() => { cmd.action(); setShow(false); }}
|
|
182
|
+
onMouseEnter={() => setSelectedIdx(i)}
|
|
183
|
+
>
|
|
184
|
+
<span className="palette-item-icon"><Icon size={16} /></span>
|
|
185
|
+
<span className="palette-item-label">{cmd.label}</span>
|
|
186
|
+
<span className="palette-item-hint">{cmd.hint}</span>
|
|
187
|
+
{i === selectedIdx && <CornerDownLeft size={12} className="palette-item-enter" />}
|
|
188
|
+
</div>
|
|
189
|
+
);
|
|
190
|
+
})}
|
|
191
|
+
</div>
|
|
192
|
+
)}
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { DiffLine } from '../types/index.ts';
|
|
2
|
+
|
|
3
|
+
interface Props {
|
|
4
|
+
lines: DiffLine[];
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export default function DiffViewer({ lines }: Props) {
|
|
8
|
+
return (
|
|
9
|
+
<div className="diff-body">
|
|
10
|
+
{lines.map((line, i) => {
|
|
11
|
+
const cls =
|
|
12
|
+
line.type === 'add' ? 'diff-line-add' :
|
|
13
|
+
line.type === 'del' ? 'diff-line-del' : 'diff-line-ctx';
|
|
14
|
+
const sign = line.type === 'add' ? '+' : line.type === 'del' ? '−' : ' ';
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div key={i} className={`diff-line ${cls}`}>
|
|
18
|
+
<span className="diff-line-num">{line.oldLine ?? ''}</span>
|
|
19
|
+
<span className="diff-line-num">{line.newLine ?? ''}</span>
|
|
20
|
+
<span className="diff-sign">{sign}</span>
|
|
21
|
+
<span className="diff-line-content">{line.content}</span>
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
})}
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import WorkspaceSidebar from './WorkspaceSidebar.tsx';
|
|
2
|
+
import SessionTabs from './SessionTabs.tsx';
|
|
3
|
+
import RenderArea from './RenderArea.tsx';
|
|
4
|
+
import CommandInputBar from './CommandInputBar.tsx';
|
|
5
|
+
import RightPanel from './RightPanel.tsx';
|
|
6
|
+
import StatusBar from './StatusBar.tsx';
|
|
7
|
+
|
|
8
|
+
export default function IdeWorkspace() {
|
|
9
|
+
return (
|
|
10
|
+
<div className="ide-root">
|
|
11
|
+
<div className="ide-layout">
|
|
12
|
+
<WorkspaceSidebar />
|
|
13
|
+
<div className="ide-center">
|
|
14
|
+
<SessionTabs />
|
|
15
|
+
<div className="ide-render">
|
|
16
|
+
<RenderArea />
|
|
17
|
+
</div>
|
|
18
|
+
<CommandInputBar />
|
|
19
|
+
</div>
|
|
20
|
+
<RightPanel />
|
|
21
|
+
</div>
|
|
22
|
+
<StatusBar />
|
|
23
|
+
</div>
|
|
24
|
+
);
|
|
25
|
+
}
|