mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-19 11:03:48 +00:00
Merge origin/main into share-chat-ui-components
This commit is contained in:
commit
b609f1ab2b
306 changed files with 44551 additions and 1001 deletions
|
|
@ -8,7 +8,7 @@
|
|||
<!-- Preconnect to font providers -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
|
|
@ -51,7 +51,8 @@
|
|||
|
||||
body {
|
||||
color: var(--text);
|
||||
font-family: 'Open Sans', system-ui, sans-serif;
|
||||
font-family: 'Manrope', system-ui, sans-serif;
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
|
@ -1601,6 +1602,118 @@
|
|||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Permission prompt */
|
||||
.permission-prompt {
|
||||
margin: 8px 0;
|
||||
padding: 12px 14px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.permission-prompt.resolved {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.permission-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.permission-icon {
|
||||
color: var(--warning, #f59e0b);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.permission-prompt.resolved .permission-icon {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.permission-title {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.permission-description {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 8px;
|
||||
padding-left: 22px;
|
||||
}
|
||||
|
||||
.permission-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
padding-left: 22px;
|
||||
}
|
||||
|
||||
.permission-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border);
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
transition: background var(--transition), color var(--transition), border-color var(--transition);
|
||||
}
|
||||
|
||||
.permission-btn:hover:not(:disabled) {
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.permission-btn.allow:hover:not(:disabled) {
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
border-color: rgba(34, 197, 94, 0.3);
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.permission-btn.reject:hover:not(:disabled) {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border-color: rgba(239, 68, 68, 0.3);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.permission-btn:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.permission-btn.selected {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.permission-btn.selected.allow {
|
||||
background: rgba(34, 197, 94, 0.15);
|
||||
border-color: rgba(34, 197, 94, 0.4);
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.permission-btn.selected.reject {
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
border-color: rgba(239, 68, 68, 0.4);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.permission-btn.dimmed {
|
||||
opacity: 0.35;
|
||||
}
|
||||
|
||||
.permission-auto-resolved {
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.status-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import {
|
|||
type AgentInfo,
|
||||
type SessionEvent,
|
||||
type Session,
|
||||
type SessionPermissionRequest,
|
||||
type PermissionReply,
|
||||
InMemorySessionPersistDriver,
|
||||
type SessionPersistDriver,
|
||||
} from "sandbox-agent";
|
||||
|
|
@ -295,6 +297,11 @@ export default function App() {
|
|||
const clientRef = useRef<SandboxAgent | null>(null);
|
||||
const activeSessionRef = useRef<Session | null>(null);
|
||||
const eventUnsubRef = useRef<(() => void) | null>(null);
|
||||
const permissionUnsubRef = useRef<(() => void) | null>(null);
|
||||
const pendingPermissionsRef = useRef<Map<string, SessionPermissionRequest>>(new Map());
|
||||
const permissionToolCallToIdRef = useRef<Map<string, string>>(new Map());
|
||||
const [pendingPermissionIds, setPendingPermissionIds] = useState<Set<string>>(new Set());
|
||||
const [resolvedPermissions, setResolvedPermissions] = useState<Map<string, string>>(new Map());
|
||||
const sessionEventsCacheRef = useRef<Map<string, SessionEvent[]>>(new Map());
|
||||
const selectedSessionIdRef = useRef(sessionId);
|
||||
const resumeInFlightSessionIdRef = useRef<string | null>(null);
|
||||
|
|
@ -538,8 +545,45 @@ export default function App() {
|
|||
});
|
||||
});
|
||||
eventUnsubRef.current = unsub;
|
||||
|
||||
// Subscribe to permission requests
|
||||
if (permissionUnsubRef.current) {
|
||||
permissionUnsubRef.current();
|
||||
permissionUnsubRef.current = null;
|
||||
}
|
||||
const permUnsub = session.onPermissionRequest((request: SessionPermissionRequest) => {
|
||||
if (!isCurrentSubscription()) return;
|
||||
pendingPermissionsRef.current.set(request.id, request);
|
||||
if (request.toolCall?.toolCallId) {
|
||||
permissionToolCallToIdRef.current.set(request.toolCall.toolCallId, request.id);
|
||||
}
|
||||
setPendingPermissionIds((prev) => new Set([...prev, request.id]));
|
||||
});
|
||||
permissionUnsubRef.current = permUnsub;
|
||||
}, [getClient]);
|
||||
|
||||
const handlePermissionReply = useCallback(async (permissionId: string, reply: PermissionReply) => {
|
||||
const session = activeSessionRef.current;
|
||||
if (!session) return;
|
||||
try {
|
||||
await session.respondPermission(permissionId, reply);
|
||||
const request = pendingPermissionsRef.current.get(permissionId);
|
||||
const selectedOption = request?.options.find((o) =>
|
||||
reply === "always" ? o.kind === "allow_always" :
|
||||
reply === "once" ? o.kind === "allow_once" :
|
||||
o.kind === "reject_once" || o.kind === "reject_always"
|
||||
);
|
||||
setResolvedPermissions((prev) => new Map([...prev, [permissionId, selectedOption?.optionId ?? reply]]));
|
||||
setPendingPermissionIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(permissionId);
|
||||
return next;
|
||||
});
|
||||
} catch (error) {
|
||||
pushErrorToast(error, "Failed to respond to permission request");
|
||||
}
|
||||
}, [pushErrorToast]);
|
||||
|
||||
const connectToDaemon = async (reportError: boolean, overrideEndpoint?: string) => {
|
||||
setConnecting(true);
|
||||
if (reportError) {
|
||||
|
|
@ -551,6 +595,10 @@ export default function App() {
|
|||
eventUnsubRef.current();
|
||||
eventUnsubRef.current = null;
|
||||
}
|
||||
if (permissionUnsubRef.current) {
|
||||
permissionUnsubRef.current();
|
||||
permissionUnsubRef.current = null;
|
||||
}
|
||||
subscriptionGenerationRef.current += 1;
|
||||
activeSessionRef.current = null;
|
||||
if (clientRef.current) {
|
||||
|
|
@ -603,6 +651,10 @@ export default function App() {
|
|||
eventUnsubRef.current();
|
||||
eventUnsubRef.current = null;
|
||||
}
|
||||
if (permissionUnsubRef.current) {
|
||||
permissionUnsubRef.current();
|
||||
permissionUnsubRef.current = null;
|
||||
}
|
||||
subscriptionGenerationRef.current += 1;
|
||||
activeSessionRef.current = null;
|
||||
if (clientRef.current) {
|
||||
|
|
@ -818,7 +870,7 @@ export default function App() {
|
|||
// Apply mode if selected
|
||||
if (!skipPostCreateConfig && config.agentMode) {
|
||||
try {
|
||||
await session.send("session/set_mode", { modeId: config.agentMode });
|
||||
await session.rawSend("session/set_mode", { modeId: config.agentMode });
|
||||
} catch {
|
||||
// Mode application is best-effort
|
||||
}
|
||||
|
|
@ -834,7 +886,7 @@ export default function App() {
|
|||
(opt) => opt.category === "model" && opt.type === "select" && typeof opt.id === "string"
|
||||
);
|
||||
if (modelOption && config.model !== modelOption.currentValue) {
|
||||
await session.send("session/set_config_option", {
|
||||
await session.rawSend("session/set_config_option", {
|
||||
optionId: modelOption.id,
|
||||
value: config.model,
|
||||
});
|
||||
|
|
@ -880,6 +932,10 @@ export default function App() {
|
|||
eventUnsubRef.current();
|
||||
eventUnsubRef.current = null;
|
||||
}
|
||||
if (permissionUnsubRef.current) {
|
||||
permissionUnsubRef.current();
|
||||
permissionUnsubRef.current = null;
|
||||
}
|
||||
activeSessionRef.current = null;
|
||||
await fetchSessions();
|
||||
} catch (error) {
|
||||
|
|
@ -1165,6 +1221,43 @@ export default function App() {
|
|||
continue;
|
||||
}
|
||||
|
||||
if (event.sender === "agent" && method === "session/request_permission") {
|
||||
const params = payload.params as {
|
||||
options?: Array<{ optionId: string; name: string; kind: string }>;
|
||||
toolCall?: { title?: string; toolCallId?: string; description?: string };
|
||||
} | undefined;
|
||||
const toolCallId = params?.toolCall?.toolCallId;
|
||||
const sdkPermissionId = toolCallId
|
||||
? permissionToolCallToIdRef.current.get(toolCallId)
|
||||
: undefined;
|
||||
const permissionId = sdkPermissionId
|
||||
?? (typeof payload.id === "number" || typeof payload.id === "string"
|
||||
? String(payload.id)
|
||||
: event.id);
|
||||
const options = (params?.options ?? []).map((o) => ({
|
||||
optionId: o.optionId,
|
||||
name: o.name,
|
||||
kind: o.kind,
|
||||
}));
|
||||
const title = params?.toolCall?.title ?? params?.toolCall?.toolCallId ?? "Permission request";
|
||||
const resolved = resolvedPermissions.get(permissionId);
|
||||
entries.push({
|
||||
id: event.id,
|
||||
eventId: event.id,
|
||||
kind: "permission",
|
||||
time,
|
||||
permission: {
|
||||
permissionId,
|
||||
title,
|
||||
description: params?.toolCall?.description,
|
||||
options,
|
||||
resolved: resolved != null || sdkPermissionId == null,
|
||||
selectedOptionId: resolved,
|
||||
},
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (event.sender === "agent" && method === "_sandboxagent/agent/unparsed") {
|
||||
const params = payload.params as { error?: string; location?: string } | undefined;
|
||||
entries.push({
|
||||
|
|
@ -1194,7 +1287,7 @@ export default function App() {
|
|||
}
|
||||
|
||||
return entries;
|
||||
}, [events]);
|
||||
}, [events, resolvedPermissions]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
|
|
@ -1202,6 +1295,10 @@ export default function App() {
|
|||
eventUnsubRef.current();
|
||||
eventUnsubRef.current = null;
|
||||
}
|
||||
if (permissionUnsubRef.current) {
|
||||
permissionUnsubRef.current();
|
||||
permissionUnsubRef.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
|
@ -1684,6 +1781,7 @@ export default function App() {
|
|||
isThinking={isThinking}
|
||||
agentId={agentId}
|
||||
tokenUsage={tokenUsage}
|
||||
onPermissionReply={handlePermissionReply}
|
||||
/>
|
||||
|
||||
<DebugPanel
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ const ChatPanel = ({
|
|||
isThinking,
|
||||
agentId,
|
||||
tokenUsage,
|
||||
onPermissionReply,
|
||||
}: {
|
||||
sessionId: string;
|
||||
transcriptEntries: TranscriptEntry[];
|
||||
|
|
@ -86,6 +87,7 @@ const ChatPanel = ({
|
|||
isThinking?: boolean;
|
||||
agentId?: string;
|
||||
tokenUsage?: { used: number; size: number; cost?: number } | null;
|
||||
onPermissionReply?: (permissionId: string, reply: "once" | "always" | "reject") => void;
|
||||
}) => {
|
||||
const [showAgentMenu, setShowAgentMenu] = useState(false);
|
||||
const [copiedSessionId, setCopiedSessionId] = useState(false);
|
||||
|
|
@ -265,6 +267,7 @@ const ChatPanel = ({
|
|||
onKeyDown={onKeyDown}
|
||||
placeholder={sessionEnded ? "Session ended" : "Send a message..."}
|
||||
disabled={sessionEnded}
|
||||
onPermissionReply={onPermissionReply}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,9 +3,10 @@ import {
|
|||
type AgentConversationClassNames,
|
||||
type AgentTranscriptClassNames,
|
||||
type ChatComposerClassNames,
|
||||
type PermissionReply,
|
||||
type TranscriptEntry,
|
||||
} from "@sandbox-agent/react";
|
||||
import { AlertTriangle, Brain, ChevronDown, ChevronRight, ExternalLink, Info, PlayCircle, Send, Wrench } from "lucide-react";
|
||||
import { AlertTriangle, Brain, Check, ChevronDown, ChevronRight, ExternalLink, Info, PlayCircle, Send, Shield, Wrench, X } from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
import MarkdownText from "./MarkdownText";
|
||||
|
||||
|
|
@ -48,6 +49,14 @@ const transcriptClassNames: Partial<AgentTranscriptClassNames> = {
|
|||
toolSectionTitle: "tool-section-title",
|
||||
toolCode: "tool-code",
|
||||
toolCodeMuted: "muted",
|
||||
permissionPrompt: "permission-prompt",
|
||||
permissionHeader: "permission-header",
|
||||
permissionIcon: "permission-icon",
|
||||
permissionTitle: "permission-title",
|
||||
permissionDescription: "permission-description",
|
||||
permissionActions: "permission-actions",
|
||||
permissionButton: "permission-btn",
|
||||
permissionAutoResolved: "permission-auto-resolved",
|
||||
thinkingRow: "thinking-row",
|
||||
thinkingIndicator: "thinking-indicator",
|
||||
};
|
||||
|
|
@ -86,6 +95,7 @@ export interface InspectorConversationProps {
|
|||
onKeyDown: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void;
|
||||
placeholder: string;
|
||||
disabled: boolean;
|
||||
onPermissionReply?: (permissionId: string, reply: PermissionReply) => void;
|
||||
}
|
||||
|
||||
const InspectorConversation = ({
|
||||
|
|
@ -103,6 +113,7 @@ const InspectorConversation = ({
|
|||
onKeyDown,
|
||||
placeholder,
|
||||
disabled,
|
||||
onPermissionReply,
|
||||
}: InspectorConversationProps) => {
|
||||
return (
|
||||
<AgentConversation
|
||||
|
|
@ -132,6 +143,14 @@ const InspectorConversation = ({
|
|||
renderToolGroupIcon: () => <PlayCircle size={14} />,
|
||||
renderChevron: (expanded) => (expanded ? <ChevronDown size={12} /> : <ChevronRight size={12} />),
|
||||
renderEventLinkContent: () => <ExternalLink size={10} />,
|
||||
onPermissionReply,
|
||||
renderPermissionIcon: () => <Shield size={14} />,
|
||||
renderPermissionOptionContent: ({ option, label, selected }) => (
|
||||
<>
|
||||
{selected ? (option.kind.startsWith("allow") ? <Check size={12} /> : <X size={12} />) : null}
|
||||
{label}
|
||||
</>
|
||||
),
|
||||
renderThinkingState: ({ agentId: activeAgentId }) => (
|
||||
<div className="thinking-row">
|
||||
<div className="thinking-avatar">
|
||||
|
|
|
|||
|
|
@ -46,13 +46,9 @@ const structuredData = {
|
|||
<!-- Preconnect to font providers -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link rel="preconnect" href="https://api.fontshare.com" crossorigin />
|
||||
|
||||
<!-- Satoshi for headings (from Fontshare) -->
|
||||
<link href="https://api.fontshare.com/v2/css?f[]=satoshi@700,900&display=swap" rel="stylesheet" />
|
||||
|
||||
<!-- Open Sans + JetBrains Mono (from Google Fonts) -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Open+Sans:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||||
<!-- Manrope + JetBrains Mono (from Google Fonts) -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Manrope:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
|
||||
|
||||
<title>{title}</title>
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,8 @@
|
|||
|
||||
body {
|
||||
@apply bg-black text-white antialiased;
|
||||
font-family: 'Open Sans', system-ui, sans-serif;
|
||||
font-family: 'Manrope', system-ui, sans-serif;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Text selection - matches rivet.dev */
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ export default {
|
|||
'code-comment': '#737373',
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Open Sans', 'system-ui', 'sans-serif'],
|
||||
heading: ['Satoshi', 'Open Sans', 'system-ui', 'sans-serif'],
|
||||
sans: ['Manrope', 'system-ui', 'sans-serif'],
|
||||
heading: ['Manrope', 'system-ui', 'sans-serif'],
|
||||
mono: ['JetBrains Mono', 'monospace'],
|
||||
},
|
||||
animation: {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue