workspace management checkpoint while agents are working on it
This commit is contained in:
parent
20f9e495a6
commit
4ec31764d5
52
gadget-code/frontend/src/components/FilesPanel.tsx
Normal file
52
gadget-code/frontend/src/components/FilesPanel.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { WorkspaceMode } from '../lib/types';
|
||||||
|
|
||||||
|
interface FilesPanelProps {
|
||||||
|
workspaceMode: WorkspaceMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FilesPanel({ workspaceMode }: FilesPanelProps) {
|
||||||
|
const isReadOnly = workspaceMode === WorkspaceMode.Agent;
|
||||||
|
const isReadWrite = workspaceMode === WorkspaceMode.User;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="border-t border-border-subtle">
|
||||||
|
<div className="flex items-center justify-between px-4 py-2 bg-bg-tertiary">
|
||||||
|
<h3 className="text-sm font-semibold text-text-secondary uppercase tracking-wider">
|
||||||
|
Files
|
||||||
|
</h3>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<span
|
||||||
|
className={`w-6 h-6 flex items-center justify-center font-mono font-bold text-xs rounded border ${
|
||||||
|
isReadOnly
|
||||||
|
? 'border-green-500 strobe text-green-500'
|
||||||
|
: 'border-border-default text-text-muted'
|
||||||
|
}`}
|
||||||
|
title="Read Only"
|
||||||
|
>
|
||||||
|
RO
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={`w-6 h-6 flex items-center justify-center font-mono font-bold text-xs rounded border ${
|
||||||
|
isReadWrite
|
||||||
|
? 'border-green-500 strobe text-green-500'
|
||||||
|
: 'border-border-default text-text-muted'
|
||||||
|
}`}
|
||||||
|
title="Read / Write"
|
||||||
|
>
|
||||||
|
RW
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 text-center text-text-muted text-sm">
|
||||||
|
<p>File browser coming soon</p>
|
||||||
|
<p className="text-xs mt-1">
|
||||||
|
{isReadOnly
|
||||||
|
? 'Files are read-only while Agent is working'
|
||||||
|
: isReadWrite
|
||||||
|
? 'Files are read/write enabled'
|
||||||
|
: 'Select User or Agent mode to access files'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
122
gadget-code/frontend/src/components/LogPanel.tsx
Normal file
122
gadget-code/frontend/src/components/LogPanel.tsx
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
import { useRef, useEffect } from 'react';
|
||||||
|
import { WorkspaceMode } from '../lib/types';
|
||||||
|
|
||||||
|
interface LogEntry {
|
||||||
|
id: string;
|
||||||
|
timestamp: Date;
|
||||||
|
level: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LogPanelProps {
|
||||||
|
logs: LogEntry[];
|
||||||
|
expanded: boolean;
|
||||||
|
workspaceMode: WorkspaceMode;
|
||||||
|
onToggleExpand: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LogPanel({ logs, expanded, workspaceMode, onToggleExpand }: LogPanelProps) {
|
||||||
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (scrollRef.current && !expanded) {
|
||||||
|
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||||
|
}
|
||||||
|
}, [logs, expanded]);
|
||||||
|
|
||||||
|
if (expanded) {
|
||||||
|
return (
|
||||||
|
<div className="absolute inset-0 z-10 bg-bg-primary flex flex-col border-t border-border-subtle">
|
||||||
|
<div className="flex items-center justify-between px-4 py-2 bg-bg-tertiary shrink-0 border-b border-border-subtle">
|
||||||
|
<h3 className="text-sm font-semibold text-text-secondary uppercase tracking-wider">
|
||||||
|
Log
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
onClick={onToggleExpand}
|
||||||
|
className="w-6 h-6 flex items-center justify-center text-text-muted hover:text-text-secondary transition-colors"
|
||||||
|
title="Collapse"
|
||||||
|
>
|
||||||
|
▾
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
ref={scrollRef}
|
||||||
|
className="flex-1 overflow-y-auto p-2 font-mono text-xs"
|
||||||
|
>
|
||||||
|
{logs.length === 0 ? (
|
||||||
|
<div className="text-text-muted">No log entries</div>
|
||||||
|
) : (
|
||||||
|
logs.map((entry) => (
|
||||||
|
<div key={entry.id} className="flex gap-2 py-0.5">
|
||||||
|
<span className="text-text-muted shrink-0">
|
||||||
|
{entry.timestamp.toLocaleTimeString()}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={`shrink-0 ${
|
||||||
|
entry.level === 'error'
|
||||||
|
? 'text-red-500'
|
||||||
|
: entry.level === 'warn'
|
||||||
|
? 'text-yellow-500'
|
||||||
|
: entry.level === 'info'
|
||||||
|
? 'text-blue-400'
|
||||||
|
: 'text-text-secondary'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
[{entry.level.toUpperCase()}]
|
||||||
|
</span>
|
||||||
|
<span className="text-text-primary">{entry.message}</span>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-48 border-t border-border-subtle bg-bg-secondary flex flex-col shrink-0">
|
||||||
|
<div className="flex items-center justify-between px-4 py-2 bg-bg-tertiary shrink-0">
|
||||||
|
<h3 className="text-sm font-semibold text-text-secondary uppercase tracking-wider">
|
||||||
|
Log
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
onClick={onToggleExpand}
|
||||||
|
className="w-6 h-6 flex items-center justify-center text-text-muted hover:text-text-secondary transition-colors"
|
||||||
|
title="Expand"
|
||||||
|
>
|
||||||
|
▴
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
ref={scrollRef}
|
||||||
|
className="flex-1 overflow-y-auto p-2 font-mono text-xs"
|
||||||
|
>
|
||||||
|
{logs.length === 0 ? (
|
||||||
|
<div className="text-text-muted">No log entries</div>
|
||||||
|
) : (
|
||||||
|
logs.map((entry) => (
|
||||||
|
<div key={entry.id} className="flex gap-2 py-0.5">
|
||||||
|
<span className="text-text-muted shrink-0">
|
||||||
|
{entry.timestamp.toLocaleTimeString()}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={`shrink-0 ${
|
||||||
|
entry.level === 'error'
|
||||||
|
? 'text-red-500'
|
||||||
|
: entry.level === 'warn'
|
||||||
|
? 'text-yellow-500'
|
||||||
|
: entry.level === 'info'
|
||||||
|
? 'text-blue-400'
|
||||||
|
: 'text-text-secondary'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
[{entry.level.toUpperCase()}]
|
||||||
|
</span>
|
||||||
|
<span className="text-text-primary">{entry.message}</span>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,84 @@
|
|||||||
|
import { WorkspaceMode } from '../lib/types';
|
||||||
|
|
||||||
|
interface WorkspaceModeIndicatorProps {
|
||||||
|
mode: WorkspaceMode;
|
||||||
|
onChange?: (mode: WorkspaceMode) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MODE_LABELS: Record<WorkspaceMode, string> = {
|
||||||
|
[WorkspaceMode.Idle]: 'I',
|
||||||
|
[WorkspaceMode.Syncing]: 'S',
|
||||||
|
[WorkspaceMode.User]: 'U',
|
||||||
|
[WorkspaceMode.Agent]: 'A',
|
||||||
|
};
|
||||||
|
|
||||||
|
const MODE_COLORS: Record<WorkspaceMode, string> = {
|
||||||
|
[WorkspaceMode.Idle]: 'text-gray-500 border-gray-500',
|
||||||
|
[WorkspaceMode.Syncing]: 'text-yellow-500 border-yellow-500',
|
||||||
|
[WorkspaceMode.User]: 'text-blue-400 border-blue-400',
|
||||||
|
[WorkspaceMode.Agent]: 'text-green-500 border-green-500',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function WorkspaceModeIndicator({
|
||||||
|
mode,
|
||||||
|
onChange,
|
||||||
|
disabled = false,
|
||||||
|
}: WorkspaceModeIndicatorProps) {
|
||||||
|
const modes = [
|
||||||
|
WorkspaceMode.Idle,
|
||||||
|
WorkspaceMode.Syncing,
|
||||||
|
WorkspaceMode.User,
|
||||||
|
WorkspaceMode.Agent,
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleClick = (m: WorkspaceMode) => {
|
||||||
|
if (disabled || m === WorkspaceMode.Syncing) return;
|
||||||
|
if (m === mode) return;
|
||||||
|
if (mode !== WorkspaceMode.Idle) return;
|
||||||
|
onChange?.(m);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{modes.map((m) => {
|
||||||
|
const isActive = m === mode;
|
||||||
|
const isClickable =
|
||||||
|
!disabled && m !== WorkspaceMode.Syncing && mode === WorkspaceMode.Idle;
|
||||||
|
const colorClass = MODE_COLORS[m];
|
||||||
|
const inactiveClass = isActive
|
||||||
|
? `border-2 ${colorClass} strobe shadow-[inset_0_1px_3px_rgba(0,0,0,0.5)]`
|
||||||
|
: `border border-border-default`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={m}
|
||||||
|
onClick={() => handleClick(m)}
|
||||||
|
disabled={!isClickable}
|
||||||
|
title={
|
||||||
|
m === WorkspaceMode.Idle
|
||||||
|
? 'Idle'
|
||||||
|
: m === WorkspaceMode.Syncing
|
||||||
|
? 'Syncing'
|
||||||
|
: m === WorkspaceMode.User
|
||||||
|
? 'User Mode'
|
||||||
|
: 'Agent Mode'
|
||||||
|
}
|
||||||
|
className={`
|
||||||
|
w-7 h-7 flex items-center justify-center
|
||||||
|
font-mono font-bold text-sm rounded
|
||||||
|
transition-all duration-150 select-none
|
||||||
|
${inactiveClass}
|
||||||
|
${isActive ? `active:text-${colorClass.split('-')[1]}` : 'text-text-muted'}
|
||||||
|
${isClickable ? 'hover:border-border-highlight hover:text-text-secondary cursor-pointer' : ''}
|
||||||
|
${!isClickable && !isActive ? 'cursor-default' : ''}
|
||||||
|
${isActive ? 'translate-y-0.5 shadow-inner' : 'translate-y-0'}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{MODE_LABELS[m]}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -74,4 +74,13 @@ input, textarea {
|
|||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: var(--color-border-highlight);
|
background: var(--color-border-highlight);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes strobe {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.4; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.strobe {
|
||||||
|
animation: strobe 1.2s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
@ -27,6 +27,13 @@ export interface ClientToServerEvents {
|
|||||||
chatSession: any,
|
chatSession: any,
|
||||||
cb: (success: boolean, chatSessionId: string) => void,
|
cb: (success: boolean, chatSessionId: string) => void,
|
||||||
) => void;
|
) => void;
|
||||||
|
requestWorkspaceMode: (
|
||||||
|
registration: any,
|
||||||
|
project: any,
|
||||||
|
chatSession: any,
|
||||||
|
mode: string,
|
||||||
|
cb: (success: boolean, mode: string) => void,
|
||||||
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SocketEvents {
|
export interface SocketEvents {
|
||||||
@ -66,6 +73,7 @@ export interface SocketEvents {
|
|||||||
message: string;
|
message: string;
|
||||||
role: "user" | "assistant" | "system";
|
role: "user" | "assistant" | "system";
|
||||||
}) => void;
|
}) => void;
|
||||||
|
workspaceModeChanged: (mode: string) => void;
|
||||||
connect: () => void;
|
connect: () => void;
|
||||||
disconnect: (reason: string) => void;
|
disconnect: (reason: string) => void;
|
||||||
error: (error: Error) => void;
|
error: (error: Error) => void;
|
||||||
@ -132,6 +140,10 @@ class SocketClient {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.socket.on("workspaceModeChanged", (mode: string) => {
|
||||||
|
this.emit("workspaceModeChanged", mode);
|
||||||
|
});
|
||||||
|
|
||||||
this._socket.on("connect", () => {
|
this._socket.on("connect", () => {
|
||||||
this.reconnectAttempts = 0;
|
this.reconnectAttempts = 0;
|
||||||
this.emit("connect");
|
this.emit("connect");
|
||||||
@ -218,6 +230,28 @@ class SocketClient {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
requestWorkspaceMode(
|
||||||
|
registration: any,
|
||||||
|
project: any,
|
||||||
|
chatSession: any,
|
||||||
|
mode: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (this._socket?.connected) {
|
||||||
|
this._socket.emit(
|
||||||
|
"requestWorkspaceMode",
|
||||||
|
registration,
|
||||||
|
project,
|
||||||
|
chatSession,
|
||||||
|
mode,
|
||||||
|
(success: boolean, _mode: string) => resolve(success),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const socketClient = new SocketClient();
|
export const socketClient = new SocketClient();
|
||||||
|
|||||||
6
gadget-code/frontend/src/lib/types.ts
Normal file
6
gadget-code/frontend/src/lib/types.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export enum WorkspaceMode {
|
||||||
|
Idle = "idle",
|
||||||
|
Syncing = "syncing",
|
||||||
|
User = "user",
|
||||||
|
Agent = "agent",
|
||||||
|
}
|
||||||
@ -2,6 +2,10 @@ import { useState, useEffect, useRef } from 'react';
|
|||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { socketClient } from '../lib/socket';
|
import { socketClient } from '../lib/socket';
|
||||||
import { chatSessionApi, projectApi, type ChatSession, type ChatTurn, type Project } from '../lib/api';
|
import { chatSessionApi, projectApi, type ChatSession, type ChatTurn, type Project } from '../lib/api';
|
||||||
|
import { WorkspaceMode } from '../lib/types';
|
||||||
|
import WorkspaceModeIndicator from '../components/WorkspaceModeIndicator';
|
||||||
|
import FilesPanel from '../components/FilesPanel';
|
||||||
|
import LogPanel from '../components/LogPanel';
|
||||||
|
|
||||||
interface ChatMessage {
|
interface ChatMessage {
|
||||||
id: string;
|
id: string;
|
||||||
@ -17,11 +21,18 @@ interface ChatMessage {
|
|||||||
timestamp: Date;
|
timestamp: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface LogEntry {
|
||||||
|
id: string;
|
||||||
|
timestamp: Date;
|
||||||
|
level: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default function ChatSessionView() {
|
export default function ChatSessionView() {
|
||||||
const { projectId, sessionId } = useParams<{ projectId: string; sessionId: string }>();
|
const { projectId, sessionId } = useParams<{ projectId: string; sessionId: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const socket = socketClient.socket;
|
const socket = socketClient.socket;
|
||||||
|
|
||||||
const [project, setProject] = useState<Project | null>(null);
|
const [project, setProject] = useState<Project | null>(null);
|
||||||
const [session, setSession] = useState<ChatSession | null>(null);
|
const [session, setSession] = useState<ChatSession | null>(null);
|
||||||
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||||||
@ -30,9 +41,14 @@ export default function ChatSessionView() {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [sessionLocked, setSessionLocked] = useState(true);
|
const [sessionLocked, setSessionLocked] = useState(true);
|
||||||
|
const [workspaceMode, setWorkspaceMode] = useState<WorkspaceMode>(WorkspaceMode.Idle);
|
||||||
|
const [logExpanded, setLogExpanded] = useState(false);
|
||||||
|
const [logs, setLogs] = useState<LogEntry[]>([]);
|
||||||
|
const [toast, setToast] = useState<string | null>(null);
|
||||||
|
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
const toastTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadSessionData();
|
loadSessionData();
|
||||||
@ -47,14 +63,20 @@ export default function ChatSessionView() {
|
|||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
}, [messages]);
|
}, [messages]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (toastTimerRef.current) {
|
||||||
|
clearTimeout(toastTimerRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const loadSessionData = async () => {
|
const loadSessionData = async () => {
|
||||||
try {
|
try {
|
||||||
// Load chat session first
|
|
||||||
if (sessionId) {
|
if (sessionId) {
|
||||||
const sessionData = await chatSessionApi.get(sessionId);
|
const sessionData = await chatSessionApi.get(sessionId);
|
||||||
setSession(sessionData);
|
setSession(sessionData);
|
||||||
|
|
||||||
// Load project using the project _id from the session
|
|
||||||
const projectRef = sessionData.project;
|
const projectRef = sessionData.project;
|
||||||
const projectObjectId = typeof projectRef === 'string' ? projectRef : projectRef?._id;
|
const projectObjectId = typeof projectRef === 'string' ? projectRef : projectRef?._id;
|
||||||
if (projectObjectId) {
|
if (projectObjectId) {
|
||||||
@ -62,7 +84,6 @@ export default function ChatSessionView() {
|
|||||||
setProject(projectData);
|
setProject(projectData);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load existing turns
|
|
||||||
const turns = await chatSessionApi.getTurns(sessionId);
|
const turns = await chatSessionApi.getTurns(sessionId);
|
||||||
const chatMessages: ChatMessage[] = turns.map((turn: ChatTurn) => ({
|
const chatMessages: ChatMessage[] = turns.map((turn: ChatTurn) => ({
|
||||||
id: turn._id,
|
id: turn._id,
|
||||||
@ -71,7 +92,6 @@ export default function ChatSessionView() {
|
|||||||
timestamp: new Date(turn.createdAt),
|
timestamp: new Date(turn.createdAt),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Add assistant responses
|
|
||||||
turns.forEach((turn: ChatTurn) => {
|
turns.forEach((turn: ChatTurn) => {
|
||||||
if (turn.thinking || turn.response || turn.toolCalls?.length) {
|
if (turn.thinking || turn.response || turn.toolCalls?.length) {
|
||||||
chatMessages.push({
|
chatMessages.push({
|
||||||
@ -86,8 +106,6 @@ export default function ChatSessionView() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
setMessages(chatMessages);
|
setMessages(chatMessages);
|
||||||
|
|
||||||
// Session is already locked by ProjectManager before navigation
|
|
||||||
setSessionLocked(true);
|
setSessionLocked(true);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -104,6 +122,8 @@ export default function ChatSessionView() {
|
|||||||
socket.on('response', handleResponse);
|
socket.on('response', handleResponse);
|
||||||
socket.on('toolCall', handleToolCall);
|
socket.on('toolCall', handleToolCall);
|
||||||
socket.on('workOrderComplete', handleWorkOrderComplete);
|
socket.on('workOrderComplete', handleWorkOrderComplete);
|
||||||
|
socket.on('workspaceModeChanged', handleWorkspaceModeChanged);
|
||||||
|
socket.on('log:entry', handleLogEntry);
|
||||||
};
|
};
|
||||||
|
|
||||||
const cleanupSocketListeners = () => {
|
const cleanupSocketListeners = () => {
|
||||||
@ -113,6 +133,8 @@ export default function ChatSessionView() {
|
|||||||
socket.off('response', handleResponse);
|
socket.off('response', handleResponse);
|
||||||
socket.off('toolCall', handleToolCall);
|
socket.off('toolCall', handleToolCall);
|
||||||
socket.off('workOrderComplete', handleWorkOrderComplete);
|
socket.off('workOrderComplete', handleWorkOrderComplete);
|
||||||
|
socket.off('workspaceModeChanged', handleWorkspaceModeChanged);
|
||||||
|
socket.off('log:entry', handleLogEntry);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleThinking = (content: string) => {
|
const handleThinking = (content: string) => {
|
||||||
@ -172,9 +194,51 @@ export default function ChatSessionView() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleWorkspaceModeChanged = (mode: string) => {
|
||||||
|
setWorkspaceMode(mode as WorkspaceMode);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogEntry = (data: { level: string; message: string; timestamp: number }) => {
|
||||||
|
setLogs(prev => [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
id: `log-${Date.now()}-${Math.random()}`,
|
||||||
|
timestamp: new Date(data.timestamp),
|
||||||
|
level: data.level,
|
||||||
|
message: data.message,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const showToast = (message: string) => {
|
||||||
|
setToast(message);
|
||||||
|
if (toastTimerRef.current) {
|
||||||
|
clearTimeout(toastTimerRef.current);
|
||||||
|
}
|
||||||
|
toastTimerRef.current = setTimeout(() => {
|
||||||
|
setToast(null);
|
||||||
|
}, 4000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleWorkspaceModeChange = async (mode: WorkspaceMode) => {
|
||||||
|
if (!session || !project) return;
|
||||||
|
|
||||||
|
const registration = session.drone as any;
|
||||||
|
const success = await socketClient.requestWorkspaceMode(
|
||||||
|
registration,
|
||||||
|
project,
|
||||||
|
session,
|
||||||
|
mode,
|
||||||
|
);
|
||||||
|
if (!success) {
|
||||||
|
showToast(`Cannot switch to ${mode} mode: workspace is not idle`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmitPrompt = async (e: React.FormEvent) => {
|
const handleSubmitPrompt = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!promptInput.trim() || isProcessing || !socket || !sessionLocked) return;
|
if (!promptInput.trim() || isProcessing || !socket || !sessionLocked) return;
|
||||||
|
if (workspaceMode !== WorkspaceMode.Agent) return;
|
||||||
|
|
||||||
const userMessage: ChatMessage = {
|
const userMessage: ChatMessage = {
|
||||||
id: `temp-${Date.now()}`,
|
id: `temp-${Date.now()}`,
|
||||||
@ -188,7 +252,6 @@ export default function ChatSessionView() {
|
|||||||
setIsProcessing(true);
|
setIsProcessing(true);
|
||||||
setError('');
|
setError('');
|
||||||
|
|
||||||
// Add assistant message placeholder
|
|
||||||
setMessages(prev => [...prev, {
|
setMessages(prev => [...prev, {
|
||||||
id: `response-${Date.now()}`,
|
id: `response-${Date.now()}`,
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
@ -196,7 +259,6 @@ export default function ChatSessionView() {
|
|||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
// Emit prompt to backend
|
|
||||||
socketClient.emitServer('submitPrompt', promptInput.trim());
|
socketClient.emitServer('submitPrompt', promptInput.trim());
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -204,6 +266,11 @@ export default function ChatSessionView() {
|
|||||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const promptDisabled = isProcessing || workspaceMode !== WorkspaceMode.Agent || !sessionLocked;
|
||||||
|
const promptPlaceholder = workspaceMode !== WorkspaceMode.Agent
|
||||||
|
? 'Select Agent mode to enter a prompt.'
|
||||||
|
: 'Enter your prompt... (Shift+Enter for new line)';
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 flex items-center justify-center bg-bg-primary">
|
<div className="flex-1 flex items-center justify-center bg-bg-primary">
|
||||||
@ -229,49 +296,121 @@ export default function ChatSessionView() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 flex bg-bg-primary overflow-hidden">
|
<div className="flex-1 flex bg-bg-primary overflow-hidden relative">
|
||||||
{/* Main Chat Area */}
|
{/* Toast notification */}
|
||||||
|
{toast && (
|
||||||
|
<div className="absolute top-4 left-1/2 -translate-x-1/2 z-50 px-4 py-2 bg-bg-elevated border border-border-default rounded shadow-lg text-sm text-text-primary">
|
||||||
|
{toast}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Content Area */}
|
||||||
<div className="flex-1 flex flex-col">
|
<div className="flex-1 flex flex-col">
|
||||||
{/* Messages */}
|
{/* Chat View (75%) */}
|
||||||
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
<div className="flex-1 flex flex-col overflow-hidden">
|
||||||
{messages.map((message) => (
|
{/* Messages */}
|
||||||
<ChatMessageBubble key={message.id} message={message} />
|
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
||||||
))}
|
{messages.map((message) => (
|
||||||
<div ref={messagesEndRef} />
|
<ChatMessageBubble key={message.id} message={message} />
|
||||||
|
))}
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Prompt Input */}
|
||||||
|
<div className="border-t border-border-subtle p-4 bg-bg-secondary shrink-0">
|
||||||
|
<form onSubmit={handleSubmitPrompt} className="flex gap-2">
|
||||||
|
<textarea
|
||||||
|
ref={inputRef}
|
||||||
|
value={promptInput}
|
||||||
|
onChange={(e) => setPromptInput(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSubmitPrompt(e);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder={promptPlaceholder}
|
||||||
|
className="flex-1 px-3 py-2 bg-bg-tertiary border border-border-default rounded text-text-primary focus:border-brand focus:outline-none resize-none disabled:opacity-50"
|
||||||
|
rows={3}
|
||||||
|
disabled={promptDisabled}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={promptDisabled || !promptInput.trim()}
|
||||||
|
className="px-6 py-2 bg-brand text-white rounded hover:bg-red-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{isProcessing ? 'Processing...' : 'Send'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Prompt Input */}
|
{/* Log Panel (25%) */}
|
||||||
<div className="border-t border-border-subtle p-4 bg-bg-secondary">
|
<LogPanel
|
||||||
<form onSubmit={handleSubmitPrompt} className="flex gap-2">
|
logs={logs}
|
||||||
<textarea
|
expanded={logExpanded}
|
||||||
ref={inputRef}
|
workspaceMode={workspaceMode}
|
||||||
value={promptInput}
|
onToggleExpand={() => setLogExpanded(!logExpanded)}
|
||||||
onChange={(e) => setPromptInput(e.target.value)}
|
/>
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
|
||||||
e.preventDefault();
|
|
||||||
handleSubmitPrompt(e);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
placeholder="Enter your prompt... (Shift+Enter for new line)"
|
|
||||||
className="flex-1 px-3 py-2 bg-bg-tertiary border border-border-default rounded text-text-primary focus:border-brand focus:outline-none resize-none"
|
|
||||||
rows={3}
|
|
||||||
disabled={isProcessing || !sessionLocked}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={isProcessing || !promptInput.trim() || !sessionLocked}
|
|
||||||
className="px-6 py-2 bg-brand text-white rounded hover:bg-red-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
>
|
|
||||||
{isProcessing ? 'Processing...' : 'Send'}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Session Sidebar */}
|
{/* Sidebar */}
|
||||||
<div className="w-80 border-l border-border-subtle bg-bg-secondary flex flex-col overflow-y-auto">
|
<div className="w-80 border-l border-border-subtle bg-bg-secondary flex flex-col overflow-y-auto">
|
||||||
<SessionSidebar session={session} project={project} sessionLocked={sessionLocked} />
|
{/* SESSION Panel */}
|
||||||
|
<div className="border-b border-border-subtle">
|
||||||
|
<div className="flex items-center justify-between px-4 py-2 bg-bg-tertiary">
|
||||||
|
<h3 className="text-sm font-semibold text-text-secondary uppercase tracking-wider">
|
||||||
|
Session
|
||||||
|
</h3>
|
||||||
|
<WorkspaceModeIndicator
|
||||||
|
mode={workspaceMode}
|
||||||
|
onChange={handleWorkspaceModeChange}
|
||||||
|
disabled={!sessionLocked}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 space-y-3">
|
||||||
|
<div className="flex items-center gap-1 text-green-500">
|
||||||
|
<span>✓</span>
|
||||||
|
<span className="text-xs">{sessionLocked ? 'Locked' : 'Unlocked'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-text-muted">Name</div>
|
||||||
|
<div className="text-text-primary">{session?.name}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-text-muted">Model</div>
|
||||||
|
<div className="text-text-primary">{session?.selectedModel}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* PROJECT Panel */}
|
||||||
|
<div className="border-b border-border-subtle">
|
||||||
|
<div className="flex items-center justify-between px-4 py-2 bg-bg-tertiary">
|
||||||
|
<h3 className="text-sm font-semibold text-text-secondary uppercase tracking-wider">
|
||||||
|
Project
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 space-y-2">
|
||||||
|
{project ? (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-text-muted">Name</div>
|
||||||
|
<div className="text-text-primary">{project.name}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-text-muted">Slug</div>
|
||||||
|
<div className="font-mono text-xs text-text-secondary">{project.slug}</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="text-text-muted text-sm">Loading...</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* FILES Panel */}
|
||||||
|
<FilesPanel workspaceMode={workspaceMode} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -336,97 +475,4 @@ function ChatMessageBubble({ message }: { message: ChatMessage }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SessionSidebar({ session, project, sessionLocked }: {
|
|
||||||
session: ChatSession | null;
|
|
||||||
project: Project | null;
|
|
||||||
sessionLocked: boolean;
|
|
||||||
}) {
|
|
||||||
if (!session) {
|
|
||||||
return (
|
|
||||||
<div className="p-4">
|
|
||||||
<p className="text-text-muted">No session loaded</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="p-4 space-y-4">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-sm font-semibold text-text-secondary uppercase tracking-wider mb-2">
|
|
||||||
Session
|
|
||||||
</h3>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div>
|
|
||||||
<div className="text-xs text-text-muted">Name</div>
|
|
||||||
<div className="text-text-primary">{session.name}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-xs text-text-muted">ID</div>
|
|
||||||
<div className="font-mono text-xs text-text-secondary truncate">
|
|
||||||
{session._id}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-xs text-text-muted">Mode</div>
|
|
||||||
<div className="text-text-primary capitalize">{session.mode}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-xs text-text-muted">Status</div>
|
|
||||||
<div className="flex items-center gap-1 text-green-500">
|
|
||||||
<span>✓</span>
|
|
||||||
<span>{sessionLocked ? 'Locked' : 'Unlocked'}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="border-t border-border-subtle pt-4">
|
|
||||||
<h3 className="text-sm font-semibold text-text-secondary uppercase tracking-wider mb-2">
|
|
||||||
Model
|
|
||||||
</h3>
|
|
||||||
<div className="text-text-primary">
|
|
||||||
{session.selectedModel}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-text-muted mt-1">
|
|
||||||
Provider: {typeof session.provider === 'string' ? 'Loaded' : session.provider?.name}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="border-t border-border-subtle pt-4">
|
|
||||||
<h3 className="text-sm font-semibold text-text-secondary uppercase tracking-wider mb-2">
|
|
||||||
Stats
|
|
||||||
</h3>
|
|
||||||
<div className="grid grid-cols-3 gap-2 text-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-xs text-text-muted">TC</div>
|
|
||||||
<div className="text-lg font-mono">{session.stats.toolCallCount}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-xs text-text-muted">FO</div>
|
|
||||||
<div className="text-lg font-mono">0</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-xs text-text-muted">SA</div>
|
|
||||||
<div className="text-lg font-mono">0</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="border-t border-border-subtle pt-4">
|
|
||||||
<h3 className="text-sm font-semibold text-text-secondary uppercase tracking-wider mb-2">
|
|
||||||
Project
|
|
||||||
</h3>
|
|
||||||
{project ? (
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="text-text-primary">{project.name}</div>
|
|
||||||
<div className="font-mono text-xs text-text-secondary">{project.slug}</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="text-text-muted text-sm">Loading...</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -15,6 +15,7 @@ import {
|
|||||||
ChatTurnStatus,
|
ChatTurnStatus,
|
||||||
GadgetId,
|
GadgetId,
|
||||||
ChatTurnDocument,
|
ChatTurnDocument,
|
||||||
|
WorkspaceMode,
|
||||||
} from "@gadget/api";
|
} from "@gadget/api";
|
||||||
|
|
||||||
import { ChatSessionService, SocketService } from "../services/index.ts";
|
import { ChatSessionService, SocketService } from "../services/index.ts";
|
||||||
@ -26,6 +27,7 @@ export class CodeSession extends SocketSession {
|
|||||||
protected chatSession: IChatSession | undefined;
|
protected chatSession: IChatSession | undefined;
|
||||||
protected selectedDrone: IDroneRegistration | undefined;
|
protected selectedDrone: IDroneRegistration | undefined;
|
||||||
protected currentTurnId: GadgetId | undefined;
|
protected currentTurnId: GadgetId | undefined;
|
||||||
|
protected workspaceMode: WorkspaceMode = WorkspaceMode.Idle;
|
||||||
|
|
||||||
constructor(socket: GadgetSocket, user: IUser) {
|
constructor(socket: GadgetSocket, user: IUser) {
|
||||||
super(socket, user);
|
super(socket, user);
|
||||||
@ -81,6 +83,7 @@ export class CodeSession extends SocketSession {
|
|||||||
this.project = project;
|
this.project = project;
|
||||||
SocketService.registerChatSession(chatSession._id, this);
|
SocketService.registerChatSession(chatSession._id, this);
|
||||||
droneSession.setChatSessionId(chatSession._id);
|
droneSession.setChatSessionId(chatSession._id);
|
||||||
|
droneSession.setCodeSession(this);
|
||||||
}
|
}
|
||||||
cb(success, chatSessionId);
|
cb(success, chatSessionId);
|
||||||
},
|
},
|
||||||
@ -142,4 +145,13 @@ export class CodeSession extends SocketSession {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by DroneSession when the drone emits a workspace mode change.
|
||||||
|
* Updates local state and forwards to the IDE socket.
|
||||||
|
*/
|
||||||
|
onWorkspaceModeChanged(mode: WorkspaceMode): void {
|
||||||
|
this.workspaceMode = mode;
|
||||||
|
this.socket.emit("workspaceModeChanged", mode);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import {
|
|||||||
IDroneRegistration,
|
IDroneRegistration,
|
||||||
ChatTurnStatus,
|
ChatTurnStatus,
|
||||||
GadgetId,
|
GadgetId,
|
||||||
|
WorkspaceMode,
|
||||||
} from "@gadget/api";
|
} from "@gadget/api";
|
||||||
import {
|
import {
|
||||||
GadgetSocket,
|
GadgetSocket,
|
||||||
@ -21,6 +22,8 @@ export class DroneSession extends SocketSession {
|
|||||||
registration: IDroneRegistration;
|
registration: IDroneRegistration;
|
||||||
chatSessionId: GadgetId | undefined;
|
chatSessionId: GadgetId | undefined;
|
||||||
currentTurnId: GadgetId | undefined;
|
currentTurnId: GadgetId | undefined;
|
||||||
|
workspaceMode: WorkspaceMode = WorkspaceMode.Idle;
|
||||||
|
codeSession: import("./code-session.js").CodeSession | undefined;
|
||||||
|
|
||||||
constructor(socket: GadgetSocket, registration: IDroneRegistration) {
|
constructor(socket: GadgetSocket, registration: IDroneRegistration) {
|
||||||
super(socket, registration.user as IUser);
|
super(socket, registration.user as IUser);
|
||||||
@ -39,6 +42,7 @@ export class DroneSession extends SocketSession {
|
|||||||
this.onRequestCrashRecovery.bind(this),
|
this.onRequestCrashRecovery.bind(this),
|
||||||
);
|
);
|
||||||
this.socket.on("requestTermination", this.onRequestTermination.bind(this));
|
this.socket.on("requestTermination", this.onRequestTermination.bind(this));
|
||||||
|
this.socket.on("workspaceModeChanged", this.onWorkspaceModeChanged.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -179,6 +183,25 @@ export class DroneSession extends SocketSession {
|
|||||||
this.currentTurnId = turnId;
|
this.currentTurnId = turnId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the associated code session for routing events back to the IDE.
|
||||||
|
*/
|
||||||
|
setCodeSession(codeSession: import("./code-session.js").CodeSession): void {
|
||||||
|
this.codeSession = codeSession;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the drone emits a workspace mode change.
|
||||||
|
*/
|
||||||
|
async onWorkspaceModeChanged(mode: WorkspaceMode): Promise<void> {
|
||||||
|
this.workspaceMode = mode;
|
||||||
|
this.log.info("workspace mode changed", { mode });
|
||||||
|
|
||||||
|
if (this.codeSession) {
|
||||||
|
this.codeSession.onWorkspaceModeChanged(mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the drone requests crash recovery for an incomplete work order.
|
* Called when the drone requests crash recovery for an incomplete work order.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -313,6 +313,7 @@ class GadgetDrone extends GadgetProcess {
|
|||||||
});
|
});
|
||||||
if (this.workspaceMode === WorkspaceMode.Idle) {
|
if (this.workspaceMode === WorkspaceMode.Idle) {
|
||||||
this.workspaceMode = mode;
|
this.workspaceMode = mode;
|
||||||
|
this.socket!.emit("workspaceModeChanged", this.workspaceMode);
|
||||||
return cb(true, this.workspaceMode);
|
return cb(true, this.workspaceMode);
|
||||||
}
|
}
|
||||||
return cb(false, this.workspaceMode);
|
return cb(false, this.workspaceMode);
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { IChatSession } from "../interfaces/chat-session.ts";
|
|||||||
import { IChatTurn } from "../interfaces/chat-turn.ts";
|
import { IChatTurn } from "../interfaces/chat-turn.ts";
|
||||||
import { IDroneRegistration } from "../interfaces/drone-registration.ts";
|
import { IDroneRegistration } from "../interfaces/drone-registration.ts";
|
||||||
import { IProject } from "../interfaces/project.ts";
|
import { IProject } from "../interfaces/project.ts";
|
||||||
|
import { WorkspaceMode } from "./ide.ts";
|
||||||
|
|
||||||
export type ProcessWorkOrderCallback = (
|
export type ProcessWorkOrderCallback = (
|
||||||
success: boolean,
|
success: boolean,
|
||||||
@ -49,3 +50,5 @@ export type CrashRecoveryResponseMessage = (data: {
|
|||||||
export type RequestTerminationMessage = (
|
export type RequestTerminationMessage = (
|
||||||
cb: (success: boolean) => void,
|
cb: (success: boolean) => void,
|
||||||
) => void;
|
) => void;
|
||||||
|
|
||||||
|
export type WorkspaceModeChangedMessage = (mode: WorkspaceMode) => void;
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import {
|
|||||||
RequestCrashRecoveryMessage,
|
RequestCrashRecoveryMessage,
|
||||||
CrashRecoveryResponseMessage,
|
CrashRecoveryResponseMessage,
|
||||||
RequestTerminationMessage,
|
RequestTerminationMessage,
|
||||||
|
WorkspaceModeChangedMessage,
|
||||||
} from "./drone.ts";
|
} from "./drone.ts";
|
||||||
import {
|
import {
|
||||||
RequestSessionLockMessage,
|
RequestSessionLockMessage,
|
||||||
@ -55,6 +56,7 @@ export interface ClientToServerEvents {
|
|||||||
workOrderComplete: WorkOrderCompleteMessage;
|
workOrderComplete: WorkOrderCompleteMessage;
|
||||||
requestCrashRecovery: RequestCrashRecoveryMessage;
|
requestCrashRecovery: RequestCrashRecoveryMessage;
|
||||||
requestTermination: RequestTerminationMessage;
|
requestTermination: RequestTerminationMessage;
|
||||||
|
workspaceModeChanged: WorkspaceModeChangedMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ServerToClientEvents {
|
export interface ServerToClientEvents {
|
||||||
@ -76,6 +78,7 @@ export interface ServerToClientEvents {
|
|||||||
response: ResponseMessage;
|
response: ResponseMessage;
|
||||||
toolCall: ToolCallMessage;
|
toolCall: ToolCallMessage;
|
||||||
workOrderComplete: WorkOrderCompleteMessage;
|
workOrderComplete: WorkOrderCompleteMessage;
|
||||||
|
workspaceModeChanged: WorkspaceModeChangedMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SocketData {
|
export interface SocketData {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user