mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-23 08:45:50 +00:00
style(B1-3): 格式化 components/messages,permissions,mcp,sandbox,shell (104 files)
纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,60 +1,35 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React from 'react';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { Byline } from '../design-system/Byline.js';
|
||||
import React from 'react'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { Byline } from '../design-system/Byline.js'
|
||||
|
||||
type Props = {
|
||||
serverToolsCount: number;
|
||||
serverPromptsCount: number;
|
||||
serverResourcesCount: number;
|
||||
};
|
||||
export function CapabilitiesSection(t0) {
|
||||
const $ = _c(9);
|
||||
const {
|
||||
serverToolsCount: number
|
||||
serverPromptsCount: number
|
||||
serverResourcesCount: number
|
||||
}
|
||||
|
||||
export function CapabilitiesSection({
|
||||
serverToolsCount,
|
||||
serverPromptsCount,
|
||||
serverResourcesCount
|
||||
} = t0;
|
||||
let capabilities;
|
||||
if ($[0] !== serverPromptsCount || $[1] !== serverResourcesCount || $[2] !== serverToolsCount) {
|
||||
capabilities = [];
|
||||
serverResourcesCount,
|
||||
}: Props): React.ReactNode {
|
||||
const capabilities = []
|
||||
if (serverToolsCount > 0) {
|
||||
capabilities.push("tools");
|
||||
capabilities.push('tools')
|
||||
}
|
||||
if (serverResourcesCount > 0) {
|
||||
capabilities.push("resources");
|
||||
capabilities.push('resources')
|
||||
}
|
||||
if (serverPromptsCount > 0) {
|
||||
capabilities.push("prompts");
|
||||
capabilities.push('prompts')
|
||||
}
|
||||
$[0] = serverPromptsCount;
|
||||
$[1] = serverResourcesCount;
|
||||
$[2] = serverToolsCount;
|
||||
$[3] = capabilities;
|
||||
} else {
|
||||
capabilities = $[3];
|
||||
}
|
||||
let t1;
|
||||
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <Text bold={true}>Capabilities: </Text>;
|
||||
$[4] = t1;
|
||||
} else {
|
||||
t1 = $[4];
|
||||
}
|
||||
let t2;
|
||||
if ($[5] !== capabilities) {
|
||||
t2 = capabilities.length > 0 ? <Byline>{capabilities}</Byline> : "none";
|
||||
$[5] = capabilities;
|
||||
$[6] = t2;
|
||||
} else {
|
||||
t2 = $[6];
|
||||
}
|
||||
let t3;
|
||||
if ($[7] !== t2) {
|
||||
t3 = <Box>{t1}<Text color="text">{t2}</Text></Box>;
|
||||
$[7] = t2;
|
||||
$[8] = t3;
|
||||
} else {
|
||||
t3 = $[8];
|
||||
}
|
||||
return t3;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Text bold>Capabilities: </Text>
|
||||
<Text color="text">
|
||||
{capabilities.length > 0 ? <Byline>{capabilities}</Byline> : 'none'}
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,24 +1,29 @@
|
||||
import figures from 'figures';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import type { CommandResultDisplay } from '../../commands.js';
|
||||
import { Box, color, Link, Text, useTheme } from '../../ink.js';
|
||||
import { useKeybinding } from '../../keybindings/useKeybinding.js';
|
||||
import { AuthenticationCancelledError, performMCPOAuthFlow } from '../../services/mcp/auth.js';
|
||||
import { capitalize } from '../../utils/stringUtils.js';
|
||||
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js';
|
||||
import { Select } from '../CustomSelect/index.js';
|
||||
import { Byline } from '../design-system/Byline.js';
|
||||
import { Dialog } from '../design-system/Dialog.js';
|
||||
import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js';
|
||||
import { Spinner } from '../Spinner.js';
|
||||
import type { AgentMcpServerInfo } from './types.js';
|
||||
import figures from 'figures'
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import type { CommandResultDisplay } from '../../commands.js'
|
||||
import { Box, color, Link, Text, useTheme } from '../../ink.js'
|
||||
import { useKeybinding } from '../../keybindings/useKeybinding.js'
|
||||
import {
|
||||
AuthenticationCancelledError,
|
||||
performMCPOAuthFlow,
|
||||
} from '../../services/mcp/auth.js'
|
||||
import { capitalize } from '../../utils/stringUtils.js'
|
||||
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'
|
||||
import { Select } from '../CustomSelect/index.js'
|
||||
import { Byline } from '../design-system/Byline.js'
|
||||
import { Dialog } from '../design-system/Dialog.js'
|
||||
import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'
|
||||
import { Spinner } from '../Spinner.js'
|
||||
import type { AgentMcpServerInfo } from './types.js'
|
||||
|
||||
type Props = {
|
||||
agentServer: AgentMcpServerInfo;
|
||||
onCancel: () => void;
|
||||
onComplete?: (result?: string, options?: {
|
||||
display?: CommandResultDisplay;
|
||||
}) => void;
|
||||
};
|
||||
agentServer: AgentMcpServerInfo
|
||||
onCancel: () => void
|
||||
onComplete?: (
|
||||
result?: string,
|
||||
options?: { display?: CommandResultDisplay },
|
||||
) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Menu for agent-specific MCP servers.
|
||||
@@ -28,113 +33,165 @@ type Props = {
|
||||
export function MCPAgentServerMenu({
|
||||
agentServer,
|
||||
onCancel,
|
||||
onComplete
|
||||
onComplete,
|
||||
}: Props): React.ReactNode {
|
||||
const [theme] = useTheme();
|
||||
const [isAuthenticating, setIsAuthenticating] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [authorizationUrl, setAuthorizationUrl] = useState<string | null>(null);
|
||||
const authAbortControllerRef = useRef<AbortController | null>(null);
|
||||
const [theme] = useTheme()
|
||||
const [isAuthenticating, setIsAuthenticating] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [authorizationUrl, setAuthorizationUrl] = useState<string | null>(null)
|
||||
const authAbortControllerRef = useRef<AbortController | null>(null)
|
||||
|
||||
// Abort OAuth flow on unmount so the callback server is closed even if a
|
||||
// parent component's Esc handler navigates away before ours fires.
|
||||
useEffect(() => () => authAbortControllerRef.current?.abort(), []);
|
||||
useEffect(() => () => authAbortControllerRef.current?.abort(), [])
|
||||
|
||||
// Handle ESC to cancel authentication flow
|
||||
const handleEscCancel = useCallback(() => {
|
||||
if (isAuthenticating) {
|
||||
authAbortControllerRef.current?.abort();
|
||||
authAbortControllerRef.current = null;
|
||||
setIsAuthenticating(false);
|
||||
setAuthorizationUrl(null);
|
||||
authAbortControllerRef.current?.abort()
|
||||
authAbortControllerRef.current = null
|
||||
setIsAuthenticating(false)
|
||||
setAuthorizationUrl(null)
|
||||
}
|
||||
}, [isAuthenticating]);
|
||||
}, [isAuthenticating])
|
||||
|
||||
useKeybinding('confirm:no', handleEscCancel, {
|
||||
context: 'Confirmation',
|
||||
isActive: isAuthenticating
|
||||
});
|
||||
isActive: isAuthenticating,
|
||||
})
|
||||
|
||||
const handleAuthenticate = useCallback(async () => {
|
||||
if (!agentServer.needsAuth || !agentServer.url) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
setIsAuthenticating(true);
|
||||
setError(null);
|
||||
const controller = new AbortController();
|
||||
authAbortControllerRef.current = controller;
|
||||
|
||||
setIsAuthenticating(true)
|
||||
setError(null)
|
||||
|
||||
const controller = new AbortController()
|
||||
authAbortControllerRef.current = controller
|
||||
|
||||
try {
|
||||
// Create a temporary config for OAuth
|
||||
const tempConfig = {
|
||||
type: agentServer.transport as 'http' | 'sse',
|
||||
url: agentServer.url
|
||||
};
|
||||
await performMCPOAuthFlow(agentServer.name, tempConfig, setAuthorizationUrl, controller.signal);
|
||||
onComplete?.(`Authentication successful for ${agentServer.name}. The server will connect when the agent runs.`);
|
||||
url: agentServer.url,
|
||||
}
|
||||
|
||||
await performMCPOAuthFlow(
|
||||
agentServer.name,
|
||||
tempConfig,
|
||||
setAuthorizationUrl,
|
||||
controller.signal,
|
||||
)
|
||||
|
||||
onComplete?.(
|
||||
`Authentication successful for ${agentServer.name}. The server will connect when the agent runs.`,
|
||||
)
|
||||
} catch (err) {
|
||||
// Don't show error if it was a cancellation
|
||||
if (err instanceof Error && !(err instanceof AuthenticationCancelledError)) {
|
||||
setError(err.message);
|
||||
if (
|
||||
err instanceof Error &&
|
||||
!(err instanceof AuthenticationCancelledError)
|
||||
) {
|
||||
setError(err.message)
|
||||
}
|
||||
} finally {
|
||||
setIsAuthenticating(false);
|
||||
authAbortControllerRef.current = null;
|
||||
setIsAuthenticating(false)
|
||||
authAbortControllerRef.current = null
|
||||
}
|
||||
}, [agentServer, onComplete]);
|
||||
const capitalizedServerName = capitalize(String(agentServer.name));
|
||||
}, [agentServer, onComplete])
|
||||
|
||||
const capitalizedServerName = capitalize(String(agentServer.name))
|
||||
|
||||
if (isAuthenticating) {
|
||||
return <Box flexDirection="column" gap={1} padding={1}>
|
||||
return (
|
||||
<Box flexDirection="column" gap={1} padding={1}>
|
||||
<Text color="claude">Authenticating with {agentServer.name}…</Text>
|
||||
<Box>
|
||||
<Spinner />
|
||||
<Text> A browser window will open for authentication</Text>
|
||||
</Box>
|
||||
{authorizationUrl && <Box flexDirection="column">
|
||||
{authorizationUrl && (
|
||||
<Box flexDirection="column">
|
||||
<Text dimColor>
|
||||
If your browser doesn't open automatically, copy this URL
|
||||
manually:
|
||||
</Text>
|
||||
<Link url={authorizationUrl} />
|
||||
</Box>}
|
||||
</Box>
|
||||
)}
|
||||
<Box marginLeft={3}>
|
||||
<Text dimColor>
|
||||
Return here after authenticating in your browser.{' '}
|
||||
<ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="go back" />
|
||||
<ConfigurableShortcutHint
|
||||
action="confirm:no"
|
||||
context="Confirmation"
|
||||
fallback="Esc"
|
||||
description="go back"
|
||||
/>
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>;
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
const menuOptions = [];
|
||||
|
||||
const menuOptions = []
|
||||
|
||||
// Only show authenticate option for HTTP/SSE servers
|
||||
if (agentServer.needsAuth) {
|
||||
menuOptions.push({
|
||||
label: agentServer.isAuthenticated ? 'Re-authenticate' : 'Authenticate',
|
||||
value: 'auth'
|
||||
});
|
||||
value: 'auth',
|
||||
})
|
||||
}
|
||||
|
||||
menuOptions.push({
|
||||
label: 'Back',
|
||||
value: 'back'
|
||||
});
|
||||
return <Dialog title={`${capitalizedServerName} MCP Server`} subtitle="agent-only" onCancel={onCancel} inputGuide={exitState => exitState.pending ? <Text>Press {exitState.keyName} again to exit</Text> : <Byline>
|
||||
value: 'back',
|
||||
})
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
title={`${capitalizedServerName} MCP Server`}
|
||||
subtitle="agent-only"
|
||||
onCancel={onCancel}
|
||||
inputGuide={exitState =>
|
||||
exitState.pending ? (
|
||||
<Text>Press {exitState.keyName} again to exit</Text>
|
||||
) : (
|
||||
<Byline>
|
||||
<KeyboardShortcutHint shortcut="↑↓" action="navigate" />
|
||||
<KeyboardShortcutHint shortcut="Enter" action="confirm" />
|
||||
<ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="go back" />
|
||||
</Byline>}>
|
||||
<ConfigurableShortcutHint
|
||||
action="confirm:no"
|
||||
context="Confirmation"
|
||||
fallback="Esc"
|
||||
description="go back"
|
||||
/>
|
||||
</Byline>
|
||||
)
|
||||
}
|
||||
>
|
||||
<Box flexDirection="column" gap={0}>
|
||||
<Box>
|
||||
<Text bold>Type: </Text>
|
||||
<Text dimColor>{agentServer.transport}</Text>
|
||||
</Box>
|
||||
|
||||
{agentServer.url && <Box>
|
||||
{agentServer.url && (
|
||||
<Box>
|
||||
<Text bold>URL: </Text>
|
||||
<Text dimColor>{agentServer.url}</Text>
|
||||
</Box>}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{agentServer.command && <Box>
|
||||
{agentServer.command && (
|
||||
<Box>
|
||||
<Text bold>Command: </Text>
|
||||
<Text dimColor>{agentServer.command}</Text>
|
||||
</Box>}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box>
|
||||
<Text bold>Used by: </Text>
|
||||
@@ -149,34 +206,47 @@ export function MCPAgentServerMenu({
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{agentServer.needsAuth && <Box>
|
||||
{agentServer.needsAuth && (
|
||||
<Box>
|
||||
<Text bold>Auth: </Text>
|
||||
{agentServer.isAuthenticated ? <Text>{color('success', theme)(figures.tick)} authenticated</Text> : <Text>
|
||||
{agentServer.isAuthenticated ? (
|
||||
<Text>{color('success', theme)(figures.tick)} authenticated</Text>
|
||||
) : (
|
||||
<Text>
|
||||
{color('warning', theme)(figures.triangleUpOutline)} may need
|
||||
authentication
|
||||
</Text>}
|
||||
</Box>}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text dimColor>This server connects only when running the agent.</Text>
|
||||
</Box>
|
||||
|
||||
{error && <Box>
|
||||
{error && (
|
||||
<Box>
|
||||
<Text color="error">Error: {error}</Text>
|
||||
</Box>}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box>
|
||||
<Select options={menuOptions} onChange={async value => {
|
||||
<Select
|
||||
options={menuOptions}
|
||||
onChange={async value => {
|
||||
switch (value) {
|
||||
case 'auth':
|
||||
await handleAuthenticate();
|
||||
break;
|
||||
await handleAuthenticate()
|
||||
break
|
||||
case 'back':
|
||||
onCancel();
|
||||
break;
|
||||
onCancel()
|
||||
break
|
||||
}
|
||||
}} onCancel={onCancel} />
|
||||
}}
|
||||
onCancel={onCancel}
|
||||
/>
|
||||
</Box>
|
||||
</Dialog>;
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,503 +1,361 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import figures from 'figures';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import type { CommandResultDisplay } from '../../commands.js';
|
||||
import { Box, color, Link, Text, useTheme } from '../../ink.js';
|
||||
import { useKeybindings } from '../../keybindings/useKeybinding.js';
|
||||
import type { ConfigScope } from '../../services/mcp/types.js';
|
||||
import { describeMcpConfigFilePath } from '../../services/mcp/utils.js';
|
||||
import { isDebugMode } from '../../utils/debug.js';
|
||||
import { plural } from '../../utils/stringUtils.js';
|
||||
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js';
|
||||
import { Byline } from '../design-system/Byline.js';
|
||||
import { Dialog } from '../design-system/Dialog.js';
|
||||
import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js';
|
||||
import { McpParsingWarnings } from './McpParsingWarnings.js';
|
||||
import type { AgentMcpServerInfo, ServerInfo } from './types.js';
|
||||
import figures from 'figures'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import type { CommandResultDisplay } from '../../commands.js'
|
||||
import { Box, color, Link, Text, useTheme } from '../../ink.js'
|
||||
import { useKeybindings } from '../../keybindings/useKeybinding.js'
|
||||
import type { ConfigScope } from '../../services/mcp/types.js'
|
||||
import { describeMcpConfigFilePath } from '../../services/mcp/utils.js'
|
||||
import { isDebugMode } from '../../utils/debug.js'
|
||||
import { plural } from '../../utils/stringUtils.js'
|
||||
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'
|
||||
import { Byline } from '../design-system/Byline.js'
|
||||
import { Dialog } from '../design-system/Dialog.js'
|
||||
import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'
|
||||
import { McpParsingWarnings } from './McpParsingWarnings.js'
|
||||
import type { AgentMcpServerInfo, ServerInfo } from './types.js'
|
||||
|
||||
type Props = {
|
||||
servers: ServerInfo[];
|
||||
agentServers?: AgentMcpServerInfo[];
|
||||
onSelectServer: (server: ServerInfo) => void;
|
||||
onSelectAgentServer?: (agentServer: AgentMcpServerInfo) => void;
|
||||
onComplete: (result?: string, options?: {
|
||||
display?: CommandResultDisplay;
|
||||
}) => void;
|
||||
defaultTab?: string;
|
||||
};
|
||||
type SelectableItem = {
|
||||
type: 'server';
|
||||
server: ServerInfo;
|
||||
} | {
|
||||
type: 'agent-server';
|
||||
agentServer: AgentMcpServerInfo;
|
||||
};
|
||||
servers: ServerInfo[]
|
||||
agentServers?: AgentMcpServerInfo[]
|
||||
onSelectServer: (server: ServerInfo) => void
|
||||
onSelectAgentServer?: (agentServer: AgentMcpServerInfo) => void
|
||||
onComplete: (
|
||||
result?: string,
|
||||
options?: { display?: CommandResultDisplay },
|
||||
) => void
|
||||
defaultTab?: string
|
||||
}
|
||||
|
||||
type SelectableItem =
|
||||
| { type: 'server'; server: ServerInfo }
|
||||
| { type: 'agent-server'; agentServer: AgentMcpServerInfo }
|
||||
|
||||
// Define scope order for display (constant, outside component)
|
||||
// 'dynamic' (built-in) is rendered separately at the end
|
||||
const SCOPE_ORDER: ConfigScope[] = ['project', 'local', 'user', 'enterprise'];
|
||||
const SCOPE_ORDER: ConfigScope[] = ['project', 'local', 'user', 'enterprise']
|
||||
|
||||
// Get scope heading parts (label is bold, path is grey)
|
||||
function getScopeHeading(scope: ConfigScope): {
|
||||
label: string;
|
||||
path?: string;
|
||||
} {
|
||||
function getScopeHeading(scope: ConfigScope): { label: string; path?: string } {
|
||||
switch (scope) {
|
||||
case 'project':
|
||||
return {
|
||||
label: 'Project MCPs',
|
||||
path: describeMcpConfigFilePath(scope)
|
||||
};
|
||||
return { label: 'Project MCPs', path: describeMcpConfigFilePath(scope) }
|
||||
case 'user':
|
||||
return {
|
||||
label: 'User MCPs',
|
||||
path: describeMcpConfigFilePath(scope)
|
||||
};
|
||||
return { label: 'User MCPs', path: describeMcpConfigFilePath(scope) }
|
||||
case 'local':
|
||||
return {
|
||||
label: 'Local MCPs',
|
||||
path: describeMcpConfigFilePath(scope)
|
||||
};
|
||||
return { label: 'Local MCPs', path: describeMcpConfigFilePath(scope) }
|
||||
case 'enterprise':
|
||||
return {
|
||||
label: 'Enterprise MCPs'
|
||||
};
|
||||
return { label: 'Enterprise MCPs' }
|
||||
case 'dynamic':
|
||||
return {
|
||||
label: 'Built-in MCPs',
|
||||
path: 'always available'
|
||||
};
|
||||
return { label: 'Built-in MCPs', path: 'always available' }
|
||||
default:
|
||||
return {
|
||||
label: scope
|
||||
};
|
||||
return { label: scope }
|
||||
}
|
||||
}
|
||||
|
||||
// Group servers by scope
|
||||
function groupServersByScope(serverList: ServerInfo[]): Map<ConfigScope, ServerInfo[]> {
|
||||
const groups = new Map<ConfigScope, ServerInfo[]>();
|
||||
function groupServersByScope(
|
||||
serverList: ServerInfo[],
|
||||
): Map<ConfigScope, ServerInfo[]> {
|
||||
const groups = new Map<ConfigScope, ServerInfo[]>()
|
||||
for (const server of serverList) {
|
||||
const scope = server.scope;
|
||||
const scope = server.scope
|
||||
if (!groups.has(scope)) {
|
||||
groups.set(scope, []);
|
||||
groups.set(scope, [])
|
||||
}
|
||||
groups.get(scope)!.push(server);
|
||||
groups.get(scope)!.push(server)
|
||||
}
|
||||
// Sort servers within each group alphabetically
|
||||
for (const [, groupServers] of groups) {
|
||||
groupServers.sort((a, b) => a.name.localeCompare(b.name));
|
||||
groupServers.sort((a, b) => a.name.localeCompare(b.name))
|
||||
}
|
||||
return groups;
|
||||
return groups
|
||||
}
|
||||
export function MCPListPanel(t0) {
|
||||
const $ = _c(78);
|
||||
const {
|
||||
|
||||
export function MCPListPanel({
|
||||
servers,
|
||||
agentServers: t1,
|
||||
agentServers = [],
|
||||
onSelectServer,
|
||||
onSelectAgentServer,
|
||||
onComplete
|
||||
} = t0;
|
||||
let t2;
|
||||
if ($[0] !== t1) {
|
||||
t2 = t1 === undefined ? [] : t1;
|
||||
$[0] = t1;
|
||||
$[1] = t2;
|
||||
} else {
|
||||
t2 = $[1];
|
||||
}
|
||||
const agentServers = t2;
|
||||
const [theme] = useTheme();
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
let t3;
|
||||
if ($[2] !== servers) {
|
||||
const regularServers = servers.filter(_temp);
|
||||
t3 = groupServersByScope(regularServers);
|
||||
$[2] = servers;
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t3 = $[3];
|
||||
}
|
||||
const serversByScope = t3;
|
||||
let t4;
|
||||
if ($[4] !== servers) {
|
||||
t4 = servers.filter(_temp2).sort(_temp3);
|
||||
$[4] = servers;
|
||||
$[5] = t4;
|
||||
} else {
|
||||
t4 = $[5];
|
||||
}
|
||||
const claudeAiServers = t4;
|
||||
let t5;
|
||||
if ($[6] !== serversByScope) {
|
||||
t5 = (serversByScope.get("dynamic") ?? []).sort(_temp4);
|
||||
$[6] = serversByScope;
|
||||
$[7] = t5;
|
||||
} else {
|
||||
t5 = $[7];
|
||||
}
|
||||
const dynamicServers = t5;
|
||||
let t6;
|
||||
if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t6 = getScopeHeading("dynamic");
|
||||
$[8] = t6;
|
||||
} else {
|
||||
t6 = $[8];
|
||||
}
|
||||
const dynamicHeading = t6;
|
||||
let items;
|
||||
if ($[9] !== agentServers || $[10] !== claudeAiServers || $[11] !== dynamicServers || $[12] !== serversByScope) {
|
||||
items = [];
|
||||
onComplete,
|
||||
}: Props): React.ReactNode {
|
||||
const [theme] = useTheme()
|
||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||
|
||||
// Non-claudeai servers grouped by scope
|
||||
const serversByScope = React.useMemo(() => {
|
||||
const regularServers = servers.filter(
|
||||
s => s.client.config.type !== 'claudeai-proxy',
|
||||
)
|
||||
return groupServersByScope(regularServers)
|
||||
}, [servers])
|
||||
|
||||
const claudeAiServers = React.useMemo(
|
||||
() =>
|
||||
servers
|
||||
.filter(s => s.client.config.type === 'claudeai-proxy')
|
||||
.sort((a, b) => a.name.localeCompare(b.name)),
|
||||
[servers],
|
||||
)
|
||||
|
||||
// Built-in (dynamic) servers - rendered last
|
||||
const dynamicServers = React.useMemo(
|
||||
() =>
|
||||
(serversByScope.get('dynamic') ?? []).sort((a, b) =>
|
||||
a.name.localeCompare(b.name),
|
||||
),
|
||||
[serversByScope],
|
||||
)
|
||||
|
||||
// Pre-compute dynamic heading for render
|
||||
const dynamicHeading = getScopeHeading('dynamic')
|
||||
|
||||
// Build flat list of selectable items in display order
|
||||
const selectableItems = React.useMemo(() => {
|
||||
const items: SelectableItem[] = []
|
||||
for (const scope of SCOPE_ORDER) {
|
||||
const scopeServers = serversByScope.get(scope) ?? [];
|
||||
const scopeServers = serversByScope.get(scope) ?? []
|
||||
for (const server of scopeServers) {
|
||||
items.push({
|
||||
type: "server",
|
||||
server
|
||||
});
|
||||
items.push({ type: 'server', server })
|
||||
}
|
||||
}
|
||||
for (const server_0 of claudeAiServers) {
|
||||
items.push({
|
||||
type: "server",
|
||||
server: server_0
|
||||
});
|
||||
for (const server of claudeAiServers) {
|
||||
items.push({ type: 'server', server })
|
||||
}
|
||||
for (const agentServer of agentServers) {
|
||||
items.push({
|
||||
type: "agent-server",
|
||||
agentServer
|
||||
});
|
||||
items.push({ type: 'agent-server', agentServer })
|
||||
}
|
||||
for (const server_1 of dynamicServers) {
|
||||
items.push({
|
||||
type: "server",
|
||||
server: server_1
|
||||
});
|
||||
// Dynamic (built-in) servers come last
|
||||
for (const server of dynamicServers) {
|
||||
items.push({ type: 'server', server })
|
||||
}
|
||||
$[9] = agentServers;
|
||||
$[10] = claudeAiServers;
|
||||
$[11] = dynamicServers;
|
||||
$[12] = serversByScope;
|
||||
$[13] = items;
|
||||
} else {
|
||||
items = $[13];
|
||||
return items
|
||||
}, [serversByScope, claudeAiServers, agentServers, dynamicServers])
|
||||
|
||||
const handleCancel = useCallback((): void => {
|
||||
onComplete('MCP dialog dismissed', {
|
||||
display: 'system',
|
||||
})
|
||||
}, [onComplete])
|
||||
|
||||
const handleSelect = useCallback((): void => {
|
||||
const item = selectableItems[selectedIndex]
|
||||
if (!item) return
|
||||
if (item.type === 'server') {
|
||||
onSelectServer(item.server)
|
||||
} else if (item.type === 'agent-server' && onSelectAgentServer) {
|
||||
onSelectAgentServer(item.agentServer)
|
||||
}
|
||||
const selectableItems = items;
|
||||
let t7;
|
||||
if ($[14] !== onComplete) {
|
||||
t7 = () => {
|
||||
onComplete("MCP dialog dismissed", {
|
||||
display: "system"
|
||||
});
|
||||
};
|
||||
$[14] = onComplete;
|
||||
$[15] = t7;
|
||||
} else {
|
||||
t7 = $[15];
|
||||
}, [selectableItems, selectedIndex, onSelectServer, onSelectAgentServer])
|
||||
|
||||
// Use configurable keybindings for navigation and selection
|
||||
useKeybindings(
|
||||
{
|
||||
'confirm:previous': () =>
|
||||
setSelectedIndex(prev =>
|
||||
prev === 0 ? selectableItems.length - 1 : prev - 1,
|
||||
),
|
||||
'confirm:next': () =>
|
||||
setSelectedIndex(prev =>
|
||||
prev === selectableItems.length - 1 ? 0 : prev + 1,
|
||||
),
|
||||
'confirm:yes': handleSelect,
|
||||
'confirm:no': handleCancel,
|
||||
},
|
||||
{ context: 'Confirmation' },
|
||||
)
|
||||
|
||||
// Build index lookup for each server
|
||||
const getServerIndex = (server: ServerInfo): number => {
|
||||
return selectableItems.findIndex(
|
||||
item => item.type === 'server' && item.server === server,
|
||||
)
|
||||
}
|
||||
const handleCancel = t7;
|
||||
let t8;
|
||||
if ($[16] !== onSelectAgentServer || $[17] !== onSelectServer || $[18] !== selectableItems || $[19] !== selectedIndex) {
|
||||
t8 = () => {
|
||||
const item = selectableItems[selectedIndex];
|
||||
if (!item) {
|
||||
return;
|
||||
|
||||
const getAgentServerIndex = (agentServer: AgentMcpServerInfo): number => {
|
||||
return selectableItems.findIndex(
|
||||
item => item.type === 'agent-server' && item.agentServer === agentServer,
|
||||
)
|
||||
}
|
||||
if (item.type === "server") {
|
||||
onSelectServer(item.server);
|
||||
} else {
|
||||
if (item.type === "agent-server" && onSelectAgentServer) {
|
||||
onSelectAgentServer(item.agentServer);
|
||||
}
|
||||
}
|
||||
};
|
||||
$[16] = onSelectAgentServer;
|
||||
$[17] = onSelectServer;
|
||||
$[18] = selectableItems;
|
||||
$[19] = selectedIndex;
|
||||
$[20] = t8;
|
||||
} else {
|
||||
t8 = $[20];
|
||||
}
|
||||
const handleSelect = t8;
|
||||
let t10;
|
||||
let t9;
|
||||
if ($[21] !== selectableItems) {
|
||||
t9 = () => setSelectedIndex(prev => prev === 0 ? selectableItems.length - 1 : prev - 1);
|
||||
t10 = () => setSelectedIndex(prev_0 => prev_0 === selectableItems.length - 1 ? 0 : prev_0 + 1);
|
||||
$[21] = selectableItems;
|
||||
$[22] = t10;
|
||||
$[23] = t9;
|
||||
} else {
|
||||
t10 = $[22];
|
||||
t9 = $[23];
|
||||
}
|
||||
let t11;
|
||||
if ($[24] !== handleCancel || $[25] !== handleSelect || $[26] !== t10 || $[27] !== t9) {
|
||||
t11 = {
|
||||
"confirm:previous": t9,
|
||||
"confirm:next": t10,
|
||||
"confirm:yes": handleSelect,
|
||||
"confirm:no": handleCancel
|
||||
};
|
||||
$[24] = handleCancel;
|
||||
$[25] = handleSelect;
|
||||
$[26] = t10;
|
||||
$[27] = t9;
|
||||
$[28] = t11;
|
||||
} else {
|
||||
t11 = $[28];
|
||||
}
|
||||
let t12;
|
||||
if ($[29] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t12 = {
|
||||
context: "Confirmation"
|
||||
};
|
||||
$[29] = t12;
|
||||
} else {
|
||||
t12 = $[29];
|
||||
}
|
||||
useKeybindings(t11, t12);
|
||||
let t13;
|
||||
if ($[30] !== selectableItems) {
|
||||
t13 = server_2 => selectableItems.findIndex(item_0 => item_0.type === "server" && item_0.server === server_2);
|
||||
$[30] = selectableItems;
|
||||
$[31] = t13;
|
||||
} else {
|
||||
t13 = $[31];
|
||||
}
|
||||
const getServerIndex = t13;
|
||||
let t14;
|
||||
if ($[32] !== selectableItems) {
|
||||
t14 = agentServer_0 => selectableItems.findIndex(item_1 => item_1.type === "agent-server" && item_1.agentServer === agentServer_0);
|
||||
$[32] = selectableItems;
|
||||
$[33] = t14;
|
||||
} else {
|
||||
t14 = $[33];
|
||||
}
|
||||
const getAgentServerIndex = t14;
|
||||
let t15;
|
||||
if ($[34] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t15 = isDebugMode();
|
||||
$[34] = t15;
|
||||
} else {
|
||||
t15 = $[34];
|
||||
}
|
||||
const debugMode = t15;
|
||||
let t16;
|
||||
if ($[35] !== servers) {
|
||||
t16 = servers.some(_temp5);
|
||||
$[35] = servers;
|
||||
$[36] = t16;
|
||||
} else {
|
||||
t16 = $[36];
|
||||
}
|
||||
const hasFailedClients = t16;
|
||||
|
||||
const debugMode = isDebugMode()
|
||||
const hasFailedClients = servers.some(s => s.client.type === 'failed')
|
||||
|
||||
if (servers.length === 0 && agentServers.length === 0) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
let t17;
|
||||
if ($[37] !== getServerIndex || $[38] !== selectedIndex || $[39] !== theme) {
|
||||
t17 = server_3 => {
|
||||
const index = getServerIndex(server_3);
|
||||
const isSelected = selectedIndex === index;
|
||||
let statusIcon;
|
||||
let statusText;
|
||||
if (server_3.client.type === "disabled") {
|
||||
statusIcon = color("inactive", theme)(figures.radioOff);
|
||||
statusText = "disabled";
|
||||
} else {
|
||||
if (server_3.client.type === "connected") {
|
||||
statusIcon = color("success", theme)(figures.tick);
|
||||
statusText = "connected";
|
||||
} else {
|
||||
if (server_3.client.type === "pending") {
|
||||
statusIcon = color("inactive", theme)(figures.radioOff);
|
||||
const {
|
||||
reconnectAttempt,
|
||||
maxReconnectAttempts
|
||||
} = server_3.client;
|
||||
|
||||
const renderServerItem = (server: ServerInfo): React.ReactNode => {
|
||||
const index = getServerIndex(server)
|
||||
const isSelected = selectedIndex === index
|
||||
let statusIcon = ''
|
||||
let statusText = ''
|
||||
|
||||
if (server.client.type === 'disabled') {
|
||||
statusIcon = color('inactive', theme)(figures.radioOff)
|
||||
statusText = 'disabled'
|
||||
} else if (server.client.type === 'connected') {
|
||||
statusIcon = color('success', theme)(figures.tick)
|
||||
statusText = 'connected'
|
||||
} else if (server.client.type === 'pending') {
|
||||
statusIcon = color('inactive', theme)(figures.radioOff)
|
||||
const { reconnectAttempt, maxReconnectAttempts } = server.client
|
||||
if (reconnectAttempt && maxReconnectAttempts) {
|
||||
statusText = `reconnecting (${reconnectAttempt}/${maxReconnectAttempts})…`;
|
||||
statusText = `reconnecting (${reconnectAttempt}/${maxReconnectAttempts})…`
|
||||
} else {
|
||||
statusText = "connecting\u2026";
|
||||
statusText = 'connecting…'
|
||||
}
|
||||
} else if (server.client.type === 'needs-auth') {
|
||||
statusIcon = color('warning', theme)(figures.triangleUpOutline)
|
||||
statusText = 'needs authentication'
|
||||
} else {
|
||||
if (server_3.client.type === "needs-auth") {
|
||||
statusIcon = color("warning", theme)(figures.triangleUpOutline);
|
||||
statusText = "needs authentication";
|
||||
} else {
|
||||
statusIcon = color("error", theme)(figures.cross);
|
||||
statusText = "failed";
|
||||
statusIcon = color('error', theme)(figures.cross)
|
||||
statusText = 'failed'
|
||||
}
|
||||
|
||||
return (
|
||||
<Box key={`${server.name}-${index}`}>
|
||||
<Text color={isSelected ? 'suggestion' : undefined}>
|
||||
{isSelected ? `${figures.pointer} ` : ' '}
|
||||
</Text>
|
||||
<Text color={isSelected ? 'suggestion' : undefined}>{server.name}</Text>
|
||||
<Text dimColor={!isSelected}> · {statusIcon} </Text>
|
||||
<Text dimColor={!isSelected}>{statusText}</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const renderAgentServerItem = (
|
||||
agentServer: AgentMcpServerInfo,
|
||||
): React.ReactNode => {
|
||||
const index = getAgentServerIndex(agentServer)
|
||||
const isSelected = selectedIndex === index
|
||||
const statusIcon = agentServer.needsAuth
|
||||
? color('warning', theme)(figures.triangleUpOutline)
|
||||
: color('inactive', theme)(figures.radioOff)
|
||||
const statusText = agentServer.needsAuth ? 'may need auth' : 'agent-only'
|
||||
|
||||
return (
|
||||
<Box key={`agent-${agentServer.name}-${index}`}>
|
||||
<Text color={isSelected ? 'suggestion' : undefined}>
|
||||
{isSelected ? `${figures.pointer} ` : ' '}
|
||||
</Text>
|
||||
<Text color={isSelected ? 'suggestion' : undefined}>
|
||||
{agentServer.name}
|
||||
</Text>
|
||||
<Text dimColor={!isSelected}> · {statusIcon} </Text>
|
||||
<Text dimColor={!isSelected}>{statusText}</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
}
|
||||
return <Box key={`${server_3.name}-${index}`}><Text color={isSelected ? "suggestion" : undefined}>{isSelected ? `${figures.pointer} ` : " "}</Text><Text color={isSelected ? "suggestion" : undefined}>{server_3.name}</Text><Text dimColor={!isSelected}> · {statusIcon} </Text><Text dimColor={!isSelected}>{statusText}</Text></Box>;
|
||||
};
|
||||
$[37] = getServerIndex;
|
||||
$[38] = selectedIndex;
|
||||
$[39] = theme;
|
||||
$[40] = t17;
|
||||
} else {
|
||||
t17 = $[40];
|
||||
}
|
||||
const renderServerItem = t17;
|
||||
let t18;
|
||||
if ($[41] !== getAgentServerIndex || $[42] !== selectedIndex || $[43] !== theme) {
|
||||
t18 = agentServer_1 => {
|
||||
const index_0 = getAgentServerIndex(agentServer_1);
|
||||
const isSelected_0 = selectedIndex === index_0;
|
||||
const statusIcon_0 = agentServer_1.needsAuth ? color("warning", theme)(figures.triangleUpOutline) : color("inactive", theme)(figures.radioOff);
|
||||
const statusText_0 = agentServer_1.needsAuth ? "may need auth" : "agent-only";
|
||||
return <Box key={`agent-${agentServer_1.name}-${index_0}`}><Text color={isSelected_0 ? "suggestion" : undefined}>{isSelected_0 ? `${figures.pointer} ` : " "}</Text><Text color={isSelected_0 ? "suggestion" : undefined}>{agentServer_1.name}</Text><Text dimColor={!isSelected_0}> · {statusIcon_0} </Text><Text dimColor={!isSelected_0}>{statusText_0}</Text></Box>;
|
||||
};
|
||||
$[41] = getAgentServerIndex;
|
||||
$[42] = selectedIndex;
|
||||
$[43] = theme;
|
||||
$[44] = t18;
|
||||
} else {
|
||||
t18 = $[44];
|
||||
}
|
||||
const renderAgentServerItem = t18;
|
||||
const totalServers = servers.length + agentServers.length;
|
||||
let t19;
|
||||
if ($[45] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t19 = <McpParsingWarnings />;
|
||||
$[45] = t19;
|
||||
} else {
|
||||
t19 = $[45];
|
||||
}
|
||||
let t20;
|
||||
if ($[46] !== totalServers) {
|
||||
t20 = plural(totalServers, "server");
|
||||
$[46] = totalServers;
|
||||
$[47] = t20;
|
||||
} else {
|
||||
t20 = $[47];
|
||||
}
|
||||
const t21 = `${totalServers} ${t20}`;
|
||||
let t22;
|
||||
if ($[48] !== renderServerItem || $[49] !== serversByScope) {
|
||||
t22 = SCOPE_ORDER.map(scope_0 => {
|
||||
const scopeServers_0 = serversByScope.get(scope_0);
|
||||
if (!scopeServers_0 || scopeServers_0.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const heading = getScopeHeading(scope_0);
|
||||
return <Box key={scope_0} flexDirection="column" marginBottom={1}><Box paddingLeft={2}><Text bold={true}>{heading.label}</Text>{heading.path && <Text dimColor={true}> ({heading.path})</Text>}</Box>{scopeServers_0.map(server_4 => renderServerItem(server_4))}</Box>;
|
||||
});
|
||||
$[48] = renderServerItem;
|
||||
$[49] = serversByScope;
|
||||
$[50] = t22;
|
||||
} else {
|
||||
t22 = $[50];
|
||||
}
|
||||
let t23;
|
||||
if ($[51] !== claudeAiServers || $[52] !== renderServerItem) {
|
||||
t23 = claudeAiServers.length > 0 && <Box flexDirection="column" marginBottom={1}><Box paddingLeft={2}><Text bold={true}>claude.ai</Text></Box>{claudeAiServers.map(server_5 => renderServerItem(server_5))}</Box>;
|
||||
$[51] = claudeAiServers;
|
||||
$[52] = renderServerItem;
|
||||
$[53] = t23;
|
||||
} else {
|
||||
t23 = $[53];
|
||||
}
|
||||
let t24;
|
||||
if ($[54] !== agentServers || $[55] !== renderAgentServerItem) {
|
||||
t24 = agentServers.length > 0 && <Box flexDirection="column" marginBottom={1}><Box paddingLeft={2}><Text bold={true}>Agent MCPs</Text></Box>{[...new Set(agentServers.flatMap(_temp6))].map(agentName => <Box key={agentName} flexDirection="column" marginTop={1}><Box paddingLeft={2}><Text dimColor={true}>@{agentName}</Text></Box>{agentServers.filter(s_3 => s_3.sourceAgents.includes(agentName)).map(agentServer_2 => renderAgentServerItem(agentServer_2))}</Box>)}</Box>;
|
||||
$[54] = agentServers;
|
||||
$[55] = renderAgentServerItem;
|
||||
$[56] = t24;
|
||||
} else {
|
||||
t24 = $[56];
|
||||
}
|
||||
let t25;
|
||||
if ($[57] !== dynamicServers || $[58] !== renderServerItem) {
|
||||
t25 = dynamicServers.length > 0 && <Box flexDirection="column" marginBottom={1}><Box paddingLeft={2}><Text bold={true}>{dynamicHeading.label}</Text>{dynamicHeading.path && <Text dimColor={true}> ({dynamicHeading.path})</Text>}</Box>{dynamicServers.map(server_6 => renderServerItem(server_6))}</Box>;
|
||||
$[57] = dynamicServers;
|
||||
$[58] = renderServerItem;
|
||||
$[59] = t25;
|
||||
} else {
|
||||
t25 = $[59];
|
||||
}
|
||||
let t26;
|
||||
if ($[60] !== hasFailedClients) {
|
||||
t26 = hasFailedClients && <Text dimColor={true}>{debugMode ? "\u203B Error logs shown inline with --debug" : "\u203B Run claude --debug to see error logs"}</Text>;
|
||||
$[60] = hasFailedClients;
|
||||
$[61] = t26;
|
||||
} else {
|
||||
t26 = $[61];
|
||||
}
|
||||
let t27;
|
||||
if ($[62] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t27 = <Text dimColor={true}><Link url="https://code.claude.com/docs/en/mcp">https://code.claude.com/docs/en/mcp</Link>{" "}for help</Text>;
|
||||
$[62] = t27;
|
||||
} else {
|
||||
t27 = $[62];
|
||||
}
|
||||
let t28;
|
||||
if ($[63] !== t26) {
|
||||
t28 = <Box flexDirection="column">{t26}{t27}</Box>;
|
||||
$[63] = t26;
|
||||
$[64] = t28;
|
||||
} else {
|
||||
t28 = $[64];
|
||||
}
|
||||
let t29;
|
||||
if ($[65] !== t22 || $[66] !== t23 || $[67] !== t24 || $[68] !== t25 || $[69] !== t28) {
|
||||
t29 = <Box flexDirection="column">{t22}{t23}{t24}{t25}{t28}</Box>;
|
||||
$[65] = t22;
|
||||
$[66] = t23;
|
||||
$[67] = t24;
|
||||
$[68] = t25;
|
||||
$[69] = t28;
|
||||
$[70] = t29;
|
||||
} else {
|
||||
t29 = $[70];
|
||||
}
|
||||
let t30;
|
||||
if ($[71] !== handleCancel || $[72] !== t21 || $[73] !== t29) {
|
||||
t30 = <Dialog title="Manage MCP servers" subtitle={t21} onCancel={handleCancel} hideInputGuide={true}>{t29}</Dialog>;
|
||||
$[71] = handleCancel;
|
||||
$[72] = t21;
|
||||
$[73] = t29;
|
||||
$[74] = t30;
|
||||
} else {
|
||||
t30 = $[74];
|
||||
}
|
||||
let t31;
|
||||
if ($[75] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t31 = <Box paddingX={1}><Text dimColor={true} italic={true}><Byline><KeyboardShortcutHint shortcut={"\u2191\u2193"} action="navigate" /><KeyboardShortcutHint shortcut="Enter" action="confirm" /><ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="cancel" /></Byline></Text></Box>;
|
||||
$[75] = t31;
|
||||
} else {
|
||||
t31 = $[75];
|
||||
}
|
||||
let t32;
|
||||
if ($[76] !== t30) {
|
||||
t32 = <Box flexDirection="column">{t19}{t30}{t31}</Box>;
|
||||
$[76] = t30;
|
||||
$[77] = t32;
|
||||
} else {
|
||||
t32 = $[77];
|
||||
}
|
||||
return t32;
|
||||
}
|
||||
function _temp6(s_2) {
|
||||
return s_2.sourceAgents;
|
||||
}
|
||||
function _temp5(s_1) {
|
||||
return s_1.client.type === "failed";
|
||||
}
|
||||
function _temp4(a_0, b_0) {
|
||||
return a_0.name.localeCompare(b_0.name);
|
||||
}
|
||||
function _temp3(a, b) {
|
||||
return a.name.localeCompare(b.name);
|
||||
}
|
||||
function _temp2(s_0) {
|
||||
return s_0.client.config.type === "claudeai-proxy";
|
||||
}
|
||||
function _temp(s) {
|
||||
return s.client.config.type !== "claudeai-proxy";
|
||||
|
||||
const totalServers = servers.length + agentServers.length
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<McpParsingWarnings />
|
||||
|
||||
<Dialog
|
||||
title="Manage MCP servers"
|
||||
subtitle={`${totalServers} ${plural(totalServers, 'server')}`}
|
||||
onCancel={handleCancel}
|
||||
hideInputGuide
|
||||
>
|
||||
<Box flexDirection="column">
|
||||
{/* Regular servers grouped by scope */}
|
||||
{SCOPE_ORDER.map(scope => {
|
||||
const scopeServers = serversByScope.get(scope)
|
||||
if (!scopeServers || scopeServers.length === 0) return null
|
||||
const heading = getScopeHeading(scope)
|
||||
return (
|
||||
<Box key={scope} flexDirection="column" marginBottom={1}>
|
||||
<Box paddingLeft={2}>
|
||||
<Text bold>{heading.label}</Text>
|
||||
{heading.path && <Text dimColor> ({heading.path})</Text>}
|
||||
</Box>
|
||||
{scopeServers.map(server => renderServerItem(server))}
|
||||
</Box>
|
||||
)
|
||||
})}
|
||||
|
||||
{/* Claude.ai servers section */}
|
||||
{claudeAiServers.length > 0 && (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Box paddingLeft={2}>
|
||||
<Text bold>claude.ai</Text>
|
||||
</Box>
|
||||
{claudeAiServers.map(server => renderServerItem(server))}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Agent servers section - grouped by source agent */}
|
||||
{agentServers.length > 0 && (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Box paddingLeft={2}>
|
||||
<Text bold>Agent MCPs</Text>
|
||||
</Box>
|
||||
{/* Group servers by source agent */}
|
||||
{[...new Set(agentServers.flatMap(s => s.sourceAgents))].map(
|
||||
agentName => (
|
||||
<Box key={agentName} flexDirection="column" marginTop={1}>
|
||||
<Box paddingLeft={2}>
|
||||
<Text dimColor>@{agentName}</Text>
|
||||
</Box>
|
||||
{agentServers
|
||||
.filter(s => s.sourceAgents.includes(agentName))
|
||||
.map(agentServer => renderAgentServerItem(agentServer))}
|
||||
</Box>
|
||||
),
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Built-in (dynamic) servers section - always last */}
|
||||
{dynamicServers.length > 0 && (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Box paddingLeft={2}>
|
||||
<Text bold>{dynamicHeading.label}</Text>
|
||||
{dynamicHeading.path && (
|
||||
<Text dimColor> ({dynamicHeading.path})</Text>
|
||||
)}
|
||||
</Box>
|
||||
{dynamicServers.map(server => renderServerItem(server))}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Footer info */}
|
||||
<Box flexDirection="column">
|
||||
{hasFailedClients && (
|
||||
<Text dimColor>
|
||||
{debugMode
|
||||
? '※ Error logs shown inline with --debug'
|
||||
: '※ Run claude --debug to see error logs'}
|
||||
</Text>
|
||||
)}
|
||||
<Text dimColor>
|
||||
<Link url="https://code.claude.com/docs/en/mcp">
|
||||
https://code.claude.com/docs/en/mcp
|
||||
</Link>{' '}
|
||||
for help
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Dialog>
|
||||
|
||||
{/* Custom footer with navigation hint */}
|
||||
<Box paddingX={1}>
|
||||
<Text dimColor italic>
|
||||
<Byline>
|
||||
<KeyboardShortcutHint shortcut="↑↓" action="navigate" />
|
||||
<KeyboardShortcutHint shortcut="Enter" action="confirm" />
|
||||
<ConfigurableShortcutHint
|
||||
action="confirm:no"
|
||||
context="Confirmation"
|
||||
fallback="Esc"
|
||||
description="cancel"
|
||||
/>
|
||||
</Byline>
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,166 +1,105 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import figures from 'figures';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import type { CommandResultDisplay } from '../../commands.js';
|
||||
import { Box, color, Text, useTheme } from '../../ink.js';
|
||||
import { useMcpReconnect } from '../../services/mcp/MCPConnectionManager.js';
|
||||
import { useAppStateStore } from '../../state/AppState.js';
|
||||
import { Spinner } from '../Spinner.js';
|
||||
import figures from 'figures'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import type { CommandResultDisplay } from '../../commands.js'
|
||||
import { Box, color, Text, useTheme } from '../../ink.js'
|
||||
import { useMcpReconnect } from '../../services/mcp/MCPConnectionManager.js'
|
||||
import { useAppStateStore } from '../../state/AppState.js'
|
||||
import { Spinner } from '../Spinner.js'
|
||||
|
||||
type Props = {
|
||||
serverName: string;
|
||||
onComplete: (result?: string, options?: {
|
||||
display?: CommandResultDisplay;
|
||||
}) => void;
|
||||
};
|
||||
export function MCPReconnect(t0) {
|
||||
const $ = _c(25);
|
||||
const {
|
||||
serverName,
|
||||
onComplete
|
||||
} = t0;
|
||||
const [theme] = useTheme();
|
||||
const store = useAppStateStore();
|
||||
const reconnectMcpServer = useMcpReconnect();
|
||||
const [isReconnecting, setIsReconnecting] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== onComplete || $[1] !== reconnectMcpServer || $[2] !== serverName || $[3] !== store) {
|
||||
t1 = () => {
|
||||
const attemptReconnect = async function attemptReconnect() {
|
||||
;
|
||||
try {
|
||||
const server = store.getState().mcp.clients.find(c => c.name === serverName);
|
||||
if (!server) {
|
||||
setError(`MCP server "${serverName}" not found`);
|
||||
setIsReconnecting(false);
|
||||
onComplete(`MCP server "${serverName}" not found`);
|
||||
return;
|
||||
}
|
||||
const result = await reconnectMcpServer(serverName);
|
||||
bb43: switch (result.client.type) {
|
||||
case "connected":
|
||||
{
|
||||
setIsReconnecting(false);
|
||||
onComplete(`Successfully reconnected to ${serverName}`);
|
||||
break bb43;
|
||||
}
|
||||
case "needs-auth":
|
||||
{
|
||||
setError(`${serverName} requires authentication`);
|
||||
setIsReconnecting(false);
|
||||
onComplete(`${serverName} requires authentication. Use /mcp to authenticate.`);
|
||||
break bb43;
|
||||
}
|
||||
case "pending":
|
||||
case "failed":
|
||||
case "disabled":
|
||||
{
|
||||
setError(`Failed to reconnect to ${serverName}`);
|
||||
setIsReconnecting(false);
|
||||
onComplete(`Failed to reconnect to ${serverName}`);
|
||||
}
|
||||
}
|
||||
} catch (t3) {
|
||||
const err = t3;
|
||||
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||
setError(errorMessage);
|
||||
setIsReconnecting(false);
|
||||
onComplete(`Error: ${errorMessage}`);
|
||||
}
|
||||
};
|
||||
attemptReconnect();
|
||||
};
|
||||
t2 = [serverName, reconnectMcpServer, store, onComplete];
|
||||
$[0] = onComplete;
|
||||
$[1] = reconnectMcpServer;
|
||||
$[2] = serverName;
|
||||
$[3] = store;
|
||||
$[4] = t1;
|
||||
$[5] = t2;
|
||||
} else {
|
||||
t1 = $[4];
|
||||
t2 = $[5];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
if (isReconnecting) {
|
||||
let t3;
|
||||
if ($[6] !== serverName) {
|
||||
t3 = <Text color="text">Reconnecting to <Text bold={true}>{serverName}</Text></Text>;
|
||||
$[6] = serverName;
|
||||
$[7] = t3;
|
||||
} else {
|
||||
t3 = $[7];
|
||||
}
|
||||
let t4;
|
||||
if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = <Box><Spinner /><Text> Establishing connection to MCP server</Text></Box>;
|
||||
$[8] = t4;
|
||||
} else {
|
||||
t4 = $[8];
|
||||
}
|
||||
let t5;
|
||||
if ($[9] !== t3) {
|
||||
t5 = <Box flexDirection="column" gap={1} padding={1}>{t3}{t4}</Box>;
|
||||
$[9] = t3;
|
||||
$[10] = t5;
|
||||
} else {
|
||||
t5 = $[10];
|
||||
}
|
||||
return t5;
|
||||
}
|
||||
if (error) {
|
||||
let t3;
|
||||
if ($[11] !== theme) {
|
||||
t3 = color("error", theme)(figures.cross);
|
||||
$[11] = theme;
|
||||
$[12] = t3;
|
||||
} else {
|
||||
t3 = $[12];
|
||||
}
|
||||
let t4;
|
||||
if ($[13] !== t3) {
|
||||
t4 = <Text>{t3} </Text>;
|
||||
$[13] = t3;
|
||||
$[14] = t4;
|
||||
} else {
|
||||
t4 = $[14];
|
||||
}
|
||||
let t5;
|
||||
if ($[15] !== serverName) {
|
||||
t5 = <Text color="error">Failed to reconnect to {serverName}</Text>;
|
||||
$[15] = serverName;
|
||||
$[16] = t5;
|
||||
} else {
|
||||
t5 = $[16];
|
||||
}
|
||||
let t6;
|
||||
if ($[17] !== t4 || $[18] !== t5) {
|
||||
t6 = <Box>{t4}{t5}</Box>;
|
||||
$[17] = t4;
|
||||
$[18] = t5;
|
||||
$[19] = t6;
|
||||
} else {
|
||||
t6 = $[19];
|
||||
}
|
||||
let t7;
|
||||
if ($[20] !== error) {
|
||||
t7 = <Text dimColor={true}>Error: {error}</Text>;
|
||||
$[20] = error;
|
||||
$[21] = t7;
|
||||
} else {
|
||||
t7 = $[21];
|
||||
}
|
||||
let t8;
|
||||
if ($[22] !== t6 || $[23] !== t7) {
|
||||
t8 = <Box flexDirection="column" gap={1} padding={1}>{t6}{t7}</Box>;
|
||||
$[22] = t6;
|
||||
$[23] = t7;
|
||||
$[24] = t8;
|
||||
} else {
|
||||
t8 = $[24];
|
||||
}
|
||||
return t8;
|
||||
}
|
||||
return null;
|
||||
serverName: string
|
||||
onComplete: (
|
||||
result?: string,
|
||||
options?: { display?: CommandResultDisplay },
|
||||
) => void
|
||||
}
|
||||
|
||||
export function MCPReconnect({
|
||||
serverName,
|
||||
onComplete,
|
||||
}: Props): React.ReactNode {
|
||||
const [theme] = useTheme()
|
||||
const store = useAppStateStore()
|
||||
const reconnectMcpServer = useMcpReconnect()
|
||||
const [isReconnecting, setIsReconnecting] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
async function attemptReconnect() {
|
||||
try {
|
||||
// Check if server exists. Read via store.getState() instead of a
|
||||
// reactive selector so this effect does not re-fire when
|
||||
// reconnectMcpServer updates mcp.clients via onConnectionAttempt.
|
||||
const server = store
|
||||
.getState()
|
||||
.mcp.clients.find(c => c.name === serverName)
|
||||
if (!server) {
|
||||
setError(`MCP server "${serverName}" not found`)
|
||||
setIsReconnecting(false)
|
||||
onComplete(`MCP server "${serverName}" not found`)
|
||||
return
|
||||
}
|
||||
|
||||
// Attempt reconnection
|
||||
const result = await reconnectMcpServer(serverName)
|
||||
|
||||
switch (result.client.type) {
|
||||
case 'connected':
|
||||
setIsReconnecting(false)
|
||||
onComplete(`Successfully reconnected to ${serverName}`)
|
||||
break
|
||||
case 'needs-auth':
|
||||
setError(`${serverName} requires authentication`)
|
||||
setIsReconnecting(false)
|
||||
onComplete(
|
||||
`${serverName} requires authentication. Use /mcp to authenticate.`,
|
||||
)
|
||||
break
|
||||
case 'pending':
|
||||
case 'failed':
|
||||
case 'disabled':
|
||||
setError(`Failed to reconnect to ${serverName}`)
|
||||
setIsReconnecting(false)
|
||||
onComplete(`Failed to reconnect to ${serverName}`)
|
||||
break
|
||||
}
|
||||
} catch (err) {
|
||||
// Only catch actual errors (like server not found)
|
||||
const errorMessage = err instanceof Error ? err.message : String(err)
|
||||
setError(errorMessage)
|
||||
setIsReconnecting(false)
|
||||
onComplete(`Error: ${errorMessage}`)
|
||||
}
|
||||
}
|
||||
|
||||
void attemptReconnect()
|
||||
}, [serverName, reconnectMcpServer, store, onComplete])
|
||||
|
||||
if (isReconnecting) {
|
||||
return (
|
||||
<Box flexDirection="column" gap={1} padding={1}>
|
||||
<Text color="text">
|
||||
Reconnecting to <Text bold>{serverName}</Text>
|
||||
</Text>
|
||||
<Box>
|
||||
<Spinner />
|
||||
<Text> Establishing connection to MCP server</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Box flexDirection="column" gap={1} padding={1}>
|
||||
<Box>
|
||||
<Text>{color('error', theme)(figures.cross)} </Text>
|
||||
<Text color="error">Failed to reconnect to {serverName}</Text>
|
||||
</Box>
|
||||
<Text dimColor>Error: {error}</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,397 +1,247 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import type { CommandResultDisplay } from '../../commands.js';
|
||||
import { ClaudeAuthProvider } from '../../services/mcp/auth.js';
|
||||
import type { McpClaudeAIProxyServerConfig, McpHTTPServerConfig, McpSSEServerConfig, McpStdioServerConfig } from '../../services/mcp/types.js';
|
||||
import { extractAgentMcpServers, filterToolsByServer } from '../../services/mcp/utils.js';
|
||||
import { useAppState } from '../../state/AppState.js';
|
||||
import { getSessionIngressAuthToken } from '../../utils/sessionIngressAuth.js';
|
||||
import { MCPAgentServerMenu } from './MCPAgentServerMenu.js';
|
||||
import { MCPListPanel } from './MCPListPanel.js';
|
||||
import { MCPRemoteServerMenu } from './MCPRemoteServerMenu.js';
|
||||
import { MCPStdioServerMenu } from './MCPStdioServerMenu.js';
|
||||
import { MCPToolDetailView } from './MCPToolDetailView.js';
|
||||
import { MCPToolListView } from './MCPToolListView.js';
|
||||
import type { AgentMcpServerInfo, MCPViewState, ServerInfo } from './types.js';
|
||||
import React, { useEffect, useMemo } from 'react'
|
||||
import type { CommandResultDisplay } from '../../commands.js'
|
||||
import { ClaudeAuthProvider } from '../../services/mcp/auth.js'
|
||||
import type {
|
||||
McpClaudeAIProxyServerConfig,
|
||||
McpHTTPServerConfig,
|
||||
McpSSEServerConfig,
|
||||
McpStdioServerConfig,
|
||||
} from '../../services/mcp/types.js'
|
||||
import {
|
||||
extractAgentMcpServers,
|
||||
filterToolsByServer,
|
||||
} from '../../services/mcp/utils.js'
|
||||
import { useAppState } from '../../state/AppState.js'
|
||||
import { getSessionIngressAuthToken } from '../../utils/sessionIngressAuth.js'
|
||||
import { MCPAgentServerMenu } from './MCPAgentServerMenu.js'
|
||||
import { MCPListPanel } from './MCPListPanel.js'
|
||||
import { MCPRemoteServerMenu } from './MCPRemoteServerMenu.js'
|
||||
import { MCPStdioServerMenu } from './MCPStdioServerMenu.js'
|
||||
import { MCPToolDetailView } from './MCPToolDetailView.js'
|
||||
import { MCPToolListView } from './MCPToolListView.js'
|
||||
import type { AgentMcpServerInfo, MCPViewState, ServerInfo } from './types.js'
|
||||
|
||||
type Props = {
|
||||
onComplete: (result?: string, options?: {
|
||||
display?: CommandResultDisplay;
|
||||
}) => void;
|
||||
};
|
||||
export function MCPSettings(t0) {
|
||||
const $ = _c(66);
|
||||
const {
|
||||
onComplete
|
||||
} = t0;
|
||||
const mcp = useAppState(_temp);
|
||||
const agentDefinitions = useAppState(_temp2);
|
||||
const mcpClients = mcp.clients;
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = {
|
||||
type: "list"
|
||||
};
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
const [viewState, setViewState] = React.useState(t1);
|
||||
let t2;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = [];
|
||||
$[1] = t2;
|
||||
} else {
|
||||
t2 = $[1];
|
||||
}
|
||||
const [servers, setServers] = React.useState(t2);
|
||||
let t3;
|
||||
if ($[2] !== agentDefinitions.allAgents) {
|
||||
t3 = extractAgentMcpServers(agentDefinitions.allAgents);
|
||||
$[2] = agentDefinitions.allAgents;
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t3 = $[3];
|
||||
}
|
||||
const agentMcpServers = t3;
|
||||
let t4;
|
||||
if ($[4] !== mcpClients) {
|
||||
t4 = mcpClients.filter(_temp3).sort(_temp4);
|
||||
$[4] = mcpClients;
|
||||
$[5] = t4;
|
||||
} else {
|
||||
t4 = $[5];
|
||||
}
|
||||
const filteredClients = t4;
|
||||
let t5;
|
||||
let t6;
|
||||
if ($[6] !== filteredClients || $[7] !== mcp.tools) {
|
||||
t5 = () => {
|
||||
let cancelled = false;
|
||||
const prepareServers = async function prepareServers() {
|
||||
const serverInfos = await Promise.all(filteredClients.map(async client_0 => {
|
||||
const scope = client_0.config.scope;
|
||||
const isSSE = client_0.config.type === "sse";
|
||||
const isHTTP = client_0.config.type === "http";
|
||||
const isClaudeAIProxy = client_0.config.type === "claudeai-proxy";
|
||||
let isAuthenticated = undefined;
|
||||
onComplete: (
|
||||
result?: string,
|
||||
options?: { display?: CommandResultDisplay },
|
||||
) => void
|
||||
}
|
||||
|
||||
export function MCPSettings({ onComplete }: Props): React.ReactNode {
|
||||
const mcp = useAppState(s => s.mcp)
|
||||
const agentDefinitions = useAppState(s => s.agentDefinitions)
|
||||
const mcpClients = mcp.clients
|
||||
const [viewState, setViewState] = React.useState<MCPViewState>({
|
||||
type: 'list',
|
||||
})
|
||||
const [servers, setServers] = React.useState<ServerInfo[]>([])
|
||||
|
||||
// Extract agent-specific MCP servers from agent definitions
|
||||
const agentMcpServers = useMemo(
|
||||
() => extractAgentMcpServers(agentDefinitions.allAgents),
|
||||
[agentDefinitions.allAgents],
|
||||
)
|
||||
|
||||
const filteredClients = React.useMemo(
|
||||
() =>
|
||||
mcpClients
|
||||
.filter(client => client.name !== 'ide')
|
||||
.sort((a, b) => a.name.localeCompare(b.name)),
|
||||
[mcpClients],
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
let cancelled = false
|
||||
async function prepareServers() {
|
||||
const serverInfos = await Promise.all(
|
||||
filteredClients.map(async client => {
|
||||
const scope = client.config.scope
|
||||
const isSSE = client.config.type === 'sse'
|
||||
const isHTTP = client.config.type === 'http'
|
||||
const isClaudeAIProxy = client.config.type === 'claudeai-proxy'
|
||||
let isAuthenticated: boolean | undefined = undefined
|
||||
|
||||
if (isSSE || isHTTP) {
|
||||
const authProvider = new ClaudeAuthProvider(client_0.name, client_0.config as McpSSEServerConfig | McpHTTPServerConfig);
|
||||
const tokens = await authProvider.tokens();
|
||||
const hasSessionAuth = getSessionIngressAuthToken() !== null && client_0.type === "connected";
|
||||
const hasToolsAndConnected = client_0.type === "connected" && filterToolsByServer(mcp.tools, client_0.name).length > 0;
|
||||
isAuthenticated = Boolean(tokens) || hasSessionAuth || hasToolsAndConnected;
|
||||
const authProvider = new ClaudeAuthProvider(
|
||||
client.name,
|
||||
client.config as McpSSEServerConfig | McpHTTPServerConfig,
|
||||
)
|
||||
const tokens = await authProvider.tokens()
|
||||
// Server is authenticated if:
|
||||
// 1. It has OAuth tokens, OR
|
||||
// 2. It's connected via session auth (has session token and is connected), OR
|
||||
// 3. It's connected and has tools (meaning it's working, regardless of auth method)
|
||||
const hasSessionAuth =
|
||||
getSessionIngressAuthToken() !== null &&
|
||||
client.type === 'connected'
|
||||
const hasToolsAndConnected =
|
||||
client.type === 'connected' &&
|
||||
filterToolsByServer(mcp.tools, client.name).length > 0
|
||||
isAuthenticated =
|
||||
Boolean(tokens) || hasSessionAuth || hasToolsAndConnected
|
||||
}
|
||||
|
||||
const baseInfo = {
|
||||
name: client_0.name,
|
||||
client: client_0,
|
||||
scope
|
||||
};
|
||||
name: client.name,
|
||||
client,
|
||||
scope,
|
||||
}
|
||||
|
||||
if (isClaudeAIProxy) {
|
||||
return {
|
||||
...baseInfo,
|
||||
transport: "claudeai-proxy" as const,
|
||||
transport: 'claudeai-proxy' as const,
|
||||
isAuthenticated: false,
|
||||
config: client_0.config as McpClaudeAIProxyServerConfig
|
||||
};
|
||||
} else {
|
||||
if (isSSE) {
|
||||
config: client.config as McpClaudeAIProxyServerConfig,
|
||||
}
|
||||
} else if (isSSE) {
|
||||
return {
|
||||
...baseInfo,
|
||||
transport: "sse" as const,
|
||||
transport: 'sse' as const,
|
||||
isAuthenticated,
|
||||
config: client_0.config as McpSSEServerConfig
|
||||
};
|
||||
} else {
|
||||
if (isHTTP) {
|
||||
config: client.config as McpSSEServerConfig,
|
||||
}
|
||||
} else if (isHTTP) {
|
||||
return {
|
||||
...baseInfo,
|
||||
transport: "http" as const,
|
||||
transport: 'http' as const,
|
||||
isAuthenticated,
|
||||
config: client_0.config as McpHTTPServerConfig
|
||||
};
|
||||
config: client.config as McpHTTPServerConfig,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
...baseInfo,
|
||||
transport: "stdio" as const,
|
||||
config: client_0.config as McpStdioServerConfig
|
||||
};
|
||||
transport: 'stdio' as const,
|
||||
config: client.config as McpStdioServerConfig,
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
if (cancelled) return
|
||||
setServers(serverInfos)
|
||||
}
|
||||
}));
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
setServers(serverInfos);
|
||||
};
|
||||
prepareServers();
|
||||
|
||||
void prepareServers()
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
};
|
||||
t6 = [filteredClients, mcp.tools];
|
||||
$[6] = filteredClients;
|
||||
$[7] = mcp.tools;
|
||||
$[8] = t5;
|
||||
$[9] = t6;
|
||||
} else {
|
||||
t5 = $[8];
|
||||
t6 = $[9];
|
||||
cancelled = true
|
||||
}
|
||||
React.useEffect(t5, t6);
|
||||
let t7;
|
||||
let t8;
|
||||
if ($[10] !== agentMcpServers.length || $[11] !== filteredClients.length || $[12] !== onComplete || $[13] !== servers.length) {
|
||||
t7 = () => {
|
||||
}, [filteredClients, mcp.tools])
|
||||
|
||||
useEffect(() => {
|
||||
if (servers.length === 0 && filteredClients.length > 0) {
|
||||
return;
|
||||
// Still loading
|
||||
return
|
||||
}
|
||||
|
||||
// Only show "no servers" message if no regular servers AND no agent servers
|
||||
if (servers.length === 0 && agentMcpServers.length === 0) {
|
||||
onComplete("No MCP servers configured. Please run /doctor if this is unexpected. Otherwise, run `claude mcp --help` or visit https://code.claude.com/docs/en/mcp to learn more.");
|
||||
onComplete(
|
||||
'No MCP servers configured. Please run /doctor if this is unexpected. Otherwise, run `claude mcp --help` or visit https://code.claude.com/docs/en/mcp to learn more.',
|
||||
)
|
||||
}
|
||||
};
|
||||
t8 = [servers.length, filteredClients.length, agentMcpServers.length, onComplete];
|
||||
$[10] = agentMcpServers.length;
|
||||
$[11] = filteredClients.length;
|
||||
$[12] = onComplete;
|
||||
$[13] = servers.length;
|
||||
$[14] = t7;
|
||||
$[15] = t8;
|
||||
} else {
|
||||
t7 = $[14];
|
||||
t8 = $[15];
|
||||
}
|
||||
useEffect(t7, t8);
|
||||
}, [
|
||||
servers.length,
|
||||
filteredClients.length,
|
||||
agentMcpServers.length,
|
||||
onComplete,
|
||||
])
|
||||
|
||||
switch (viewState.type) {
|
||||
case "list":
|
||||
{
|
||||
let t10;
|
||||
let t9;
|
||||
if ($[16] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t9 = server => setViewState({
|
||||
type: "server-menu",
|
||||
server
|
||||
});
|
||||
t10 = agentServer => setViewState({
|
||||
type: "agent-server-menu",
|
||||
agentServer
|
||||
});
|
||||
$[16] = t10;
|
||||
$[17] = t9;
|
||||
case 'list':
|
||||
return (
|
||||
<MCPListPanel
|
||||
servers={servers}
|
||||
agentServers={agentMcpServers}
|
||||
onSelectServer={server =>
|
||||
setViewState({ type: 'server-menu', server })
|
||||
}
|
||||
onSelectAgentServer={(agentServer: AgentMcpServerInfo) =>
|
||||
setViewState({ type: 'agent-server-menu', agentServer })
|
||||
}
|
||||
onComplete={onComplete}
|
||||
defaultTab={viewState.defaultTab}
|
||||
/>
|
||||
)
|
||||
|
||||
case 'server-menu': {
|
||||
const serverTools = filterToolsByServer(mcp.tools, viewState.server.name)
|
||||
|
||||
const defaultTab =
|
||||
viewState.server.transport === 'claudeai-proxy'
|
||||
? 'claude.ai'
|
||||
: 'Claude Code'
|
||||
|
||||
if (viewState.server.transport === 'stdio') {
|
||||
return (
|
||||
<MCPStdioServerMenu
|
||||
server={viewState.server}
|
||||
serverToolsCount={serverTools.length}
|
||||
onViewTools={() =>
|
||||
setViewState({ type: 'server-tools', server: viewState.server })
|
||||
}
|
||||
onCancel={() => setViewState({ type: 'list', defaultTab })}
|
||||
onComplete={onComplete}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
t10 = $[16];
|
||||
t9 = $[17];
|
||||
return (
|
||||
<MCPRemoteServerMenu
|
||||
server={viewState.server}
|
||||
serverToolsCount={serverTools.length}
|
||||
onViewTools={() =>
|
||||
setViewState({ type: 'server-tools', server: viewState.server })
|
||||
}
|
||||
let t11;
|
||||
if ($[18] !== agentMcpServers || $[19] !== onComplete || $[20] !== servers || $[21] !== viewState.defaultTab) {
|
||||
t11 = <MCPListPanel servers={servers} agentServers={agentMcpServers} onSelectServer={t9} onSelectAgentServer={t10} onComplete={onComplete} defaultTab={viewState.defaultTab} />;
|
||||
$[18] = agentMcpServers;
|
||||
$[19] = onComplete;
|
||||
$[20] = servers;
|
||||
$[21] = viewState.defaultTab;
|
||||
$[22] = t11;
|
||||
} else {
|
||||
t11 = $[22];
|
||||
}
|
||||
return t11;
|
||||
}
|
||||
case "server-menu":
|
||||
{
|
||||
let t9;
|
||||
if ($[23] !== mcp.tools || $[24] !== viewState.server.name) {
|
||||
t9 = filterToolsByServer(mcp.tools, viewState.server.name);
|
||||
$[23] = mcp.tools;
|
||||
$[24] = viewState.server.name;
|
||||
$[25] = t9;
|
||||
} else {
|
||||
t9 = $[25];
|
||||
}
|
||||
const serverTools_0 = t9;
|
||||
const defaultTab = viewState.server.transport === "claudeai-proxy" ? "claude.ai" : "Claude Code";
|
||||
if (viewState.server.transport === "stdio") {
|
||||
let t10;
|
||||
if ($[26] !== viewState.server) {
|
||||
t10 = () => setViewState({
|
||||
type: "server-tools",
|
||||
server: viewState.server
|
||||
});
|
||||
$[26] = viewState.server;
|
||||
$[27] = t10;
|
||||
} else {
|
||||
t10 = $[27];
|
||||
}
|
||||
let t11;
|
||||
if ($[28] !== defaultTab) {
|
||||
t11 = () => setViewState({
|
||||
type: "list",
|
||||
defaultTab
|
||||
});
|
||||
$[28] = defaultTab;
|
||||
$[29] = t11;
|
||||
} else {
|
||||
t11 = $[29];
|
||||
}
|
||||
let t12;
|
||||
if ($[30] !== onComplete || $[31] !== serverTools_0.length || $[32] !== t10 || $[33] !== t11 || $[34] !== viewState.server) {
|
||||
t12 = <MCPStdioServerMenu server={viewState.server} serverToolsCount={serverTools_0.length} onViewTools={t10} onCancel={t11} onComplete={onComplete} />;
|
||||
$[30] = onComplete;
|
||||
$[31] = serverTools_0.length;
|
||||
$[32] = t10;
|
||||
$[33] = t11;
|
||||
$[34] = viewState.server;
|
||||
$[35] = t12;
|
||||
} else {
|
||||
t12 = $[35];
|
||||
}
|
||||
return t12;
|
||||
} else {
|
||||
let t10;
|
||||
if ($[36] !== viewState.server) {
|
||||
t10 = () => setViewState({
|
||||
type: "server-tools",
|
||||
server: viewState.server
|
||||
});
|
||||
$[36] = viewState.server;
|
||||
$[37] = t10;
|
||||
} else {
|
||||
t10 = $[37];
|
||||
}
|
||||
let t11;
|
||||
if ($[38] !== defaultTab) {
|
||||
t11 = () => setViewState({
|
||||
type: "list",
|
||||
defaultTab
|
||||
});
|
||||
$[38] = defaultTab;
|
||||
$[39] = t11;
|
||||
} else {
|
||||
t11 = $[39];
|
||||
}
|
||||
let t12;
|
||||
if ($[40] !== onComplete || $[41] !== serverTools_0.length || $[42] !== t10 || $[43] !== t11 || $[44] !== viewState.server) {
|
||||
t12 = <MCPRemoteServerMenu server={viewState.server} serverToolsCount={serverTools_0.length} onViewTools={t10} onCancel={t11} onComplete={onComplete} />;
|
||||
$[40] = onComplete;
|
||||
$[41] = serverTools_0.length;
|
||||
$[42] = t10;
|
||||
$[43] = t11;
|
||||
$[44] = viewState.server;
|
||||
$[45] = t12;
|
||||
} else {
|
||||
t12 = $[45];
|
||||
}
|
||||
return t12;
|
||||
onCancel={() => setViewState({ type: 'list', defaultTab })}
|
||||
onComplete={onComplete}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
case "server-tools":
|
||||
{
|
||||
let t10;
|
||||
let t9;
|
||||
if ($[46] !== viewState.server) {
|
||||
t9 = (_, index) => setViewState({
|
||||
type: "server-tool-detail",
|
||||
server: viewState.server,
|
||||
toolIndex: index
|
||||
});
|
||||
t10 = () => setViewState({
|
||||
type: "server-menu",
|
||||
server: viewState.server
|
||||
});
|
||||
$[46] = viewState.server;
|
||||
$[47] = t10;
|
||||
$[48] = t9;
|
||||
} else {
|
||||
t10 = $[47];
|
||||
t9 = $[48];
|
||||
}
|
||||
let t11;
|
||||
if ($[49] !== t10 || $[50] !== t9 || $[51] !== viewState.server) {
|
||||
t11 = <MCPToolListView server={viewState.server} onSelectTool={t9} onBack={t10} />;
|
||||
$[49] = t10;
|
||||
$[50] = t9;
|
||||
$[51] = viewState.server;
|
||||
$[52] = t11;
|
||||
} else {
|
||||
t11 = $[52];
|
||||
}
|
||||
return t11;
|
||||
}
|
||||
case "server-tool-detail":
|
||||
{
|
||||
let t9;
|
||||
if ($[53] !== mcp.tools || $[54] !== viewState.server.name) {
|
||||
t9 = filterToolsByServer(mcp.tools, viewState.server.name);
|
||||
$[53] = mcp.tools;
|
||||
$[54] = viewState.server.name;
|
||||
$[55] = t9;
|
||||
} else {
|
||||
t9 = $[55];
|
||||
}
|
||||
const serverTools = t9;
|
||||
const tool = serverTools[viewState.toolIndex];
|
||||
if (!tool) {
|
||||
|
||||
case 'server-tools':
|
||||
return (
|
||||
<MCPToolListView
|
||||
server={viewState.server}
|
||||
onSelectTool={(_, index) =>
|
||||
setViewState({
|
||||
type: "server-tools",
|
||||
server: viewState.server
|
||||
});
|
||||
return null;
|
||||
type: 'server-tool-detail',
|
||||
server: viewState.server,
|
||||
toolIndex: index,
|
||||
})
|
||||
}
|
||||
let t10;
|
||||
if ($[56] !== viewState.server) {
|
||||
t10 = () => setViewState({
|
||||
type: "server-tools",
|
||||
server: viewState.server
|
||||
});
|
||||
$[56] = viewState.server;
|
||||
$[57] = t10;
|
||||
} else {
|
||||
t10 = $[57];
|
||||
onBack={() =>
|
||||
setViewState({ type: 'server-menu', server: viewState.server })
|
||||
}
|
||||
let t11;
|
||||
if ($[58] !== t10 || $[59] !== tool || $[60] !== viewState.server) {
|
||||
t11 = <MCPToolDetailView tool={tool} server={viewState.server} onBack={t10} />;
|
||||
$[58] = t10;
|
||||
$[59] = tool;
|
||||
$[60] = viewState.server;
|
||||
$[61] = t11;
|
||||
} else {
|
||||
t11 = $[61];
|
||||
/>
|
||||
)
|
||||
|
||||
case 'server-tool-detail': {
|
||||
const serverTools = filterToolsByServer(mcp.tools, viewState.server.name)
|
||||
const tool = serverTools[viewState.toolIndex]
|
||||
if (!tool) {
|
||||
setViewState({ type: 'server-tools', server: viewState.server })
|
||||
return null
|
||||
}
|
||||
return t11;
|
||||
return (
|
||||
<MCPToolDetailView
|
||||
tool={tool}
|
||||
server={viewState.server}
|
||||
onBack={() =>
|
||||
setViewState({ type: 'server-tools', server: viewState.server })
|
||||
}
|
||||
case "agent-server-menu":
|
||||
{
|
||||
let t9;
|
||||
if ($[62] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t9 = () => setViewState({
|
||||
type: "list",
|
||||
defaultTab: "Agents"
|
||||
});
|
||||
$[62] = t9;
|
||||
} else {
|
||||
t9 = $[62];
|
||||
}
|
||||
let t10;
|
||||
if ($[63] !== onComplete || $[64] !== viewState.agentServer) {
|
||||
t10 = <MCPAgentServerMenu agentServer={viewState.agentServer} onCancel={t9} onComplete={onComplete} />;
|
||||
$[63] = onComplete;
|
||||
$[64] = viewState.agentServer;
|
||||
$[65] = t10;
|
||||
} else {
|
||||
t10 = $[65];
|
||||
}
|
||||
return t10;
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
case 'agent-server-menu':
|
||||
return (
|
||||
<MCPAgentServerMenu
|
||||
agentServer={viewState.agentServer}
|
||||
onCancel={() => setViewState({ type: 'list', defaultTab: 'Agents' })}
|
||||
onComplete={onComplete}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
function _temp4(a, b) {
|
||||
return a.name.localeCompare(b.name);
|
||||
}
|
||||
function _temp3(client) {
|
||||
return client.name !== "ide";
|
||||
}
|
||||
function _temp2(s_0) {
|
||||
return s_0.agentDefinitions;
|
||||
}
|
||||
function _temp(s) {
|
||||
return s.mcp;
|
||||
}
|
||||
|
||||
@@ -1,92 +1,116 @@
|
||||
import figures from 'figures';
|
||||
import React, { useState } from 'react';
|
||||
import type { CommandResultDisplay } from '../../commands.js';
|
||||
import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js';
|
||||
import { Box, color, Text, useTheme } from '../../ink.js';
|
||||
import { getMcpConfigByName } from '../../services/mcp/config.js';
|
||||
import { useMcpReconnect, useMcpToggleEnabled } from '../../services/mcp/MCPConnectionManager.js';
|
||||
import { describeMcpConfigFilePath, filterMcpPromptsByServer } from '../../services/mcp/utils.js';
|
||||
import { useAppState } from '../../state/AppState.js';
|
||||
import { errorMessage } from '../../utils/errors.js';
|
||||
import { capitalize } from '../../utils/stringUtils.js';
|
||||
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js';
|
||||
import { Select } from '../CustomSelect/index.js';
|
||||
import { Byline } from '../design-system/Byline.js';
|
||||
import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js';
|
||||
import { Spinner } from '../Spinner.js';
|
||||
import { CapabilitiesSection } from './CapabilitiesSection.js';
|
||||
import type { StdioServerInfo } from './types.js';
|
||||
import { handleReconnectError, handleReconnectResult } from './utils/reconnectHelpers.js';
|
||||
import figures from 'figures'
|
||||
import React, { useState } from 'react'
|
||||
import type { CommandResultDisplay } from '../../commands.js'
|
||||
import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js'
|
||||
import { Box, color, Text, useTheme } from '../../ink.js'
|
||||
import { getMcpConfigByName } from '../../services/mcp/config.js'
|
||||
import {
|
||||
useMcpReconnect,
|
||||
useMcpToggleEnabled,
|
||||
} from '../../services/mcp/MCPConnectionManager.js'
|
||||
import {
|
||||
describeMcpConfigFilePath,
|
||||
filterMcpPromptsByServer,
|
||||
} from '../../services/mcp/utils.js'
|
||||
import { useAppState } from '../../state/AppState.js'
|
||||
import { errorMessage } from '../../utils/errors.js'
|
||||
import { capitalize } from '../../utils/stringUtils.js'
|
||||
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'
|
||||
import { Select } from '../CustomSelect/index.js'
|
||||
import { Byline } from '../design-system/Byline.js'
|
||||
import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'
|
||||
import { Spinner } from '../Spinner.js'
|
||||
import { CapabilitiesSection } from './CapabilitiesSection.js'
|
||||
import type { StdioServerInfo } from './types.js'
|
||||
import {
|
||||
handleReconnectError,
|
||||
handleReconnectResult,
|
||||
} from './utils/reconnectHelpers.js'
|
||||
|
||||
type Props = {
|
||||
server: StdioServerInfo;
|
||||
serverToolsCount: number;
|
||||
onViewTools: () => void;
|
||||
onCancel: () => void;
|
||||
onComplete: (result?: string, options?: {
|
||||
display?: CommandResultDisplay;
|
||||
}) => void;
|
||||
borderless?: boolean;
|
||||
};
|
||||
server: StdioServerInfo
|
||||
serverToolsCount: number
|
||||
onViewTools: () => void
|
||||
onCancel: () => void
|
||||
onComplete: (
|
||||
result?: string,
|
||||
options?: { display?: CommandResultDisplay },
|
||||
) => void
|
||||
borderless?: boolean
|
||||
}
|
||||
|
||||
export function MCPStdioServerMenu({
|
||||
server,
|
||||
serverToolsCount,
|
||||
onViewTools,
|
||||
onCancel,
|
||||
onComplete,
|
||||
borderless = false
|
||||
borderless = false,
|
||||
}: Props): React.ReactNode {
|
||||
const [theme] = useTheme();
|
||||
const exitState = useExitOnCtrlCDWithKeybindings();
|
||||
const mcp = useAppState(s => s.mcp);
|
||||
const reconnectMcpServer = useMcpReconnect();
|
||||
const toggleMcpServer = useMcpToggleEnabled();
|
||||
const [isReconnecting, setIsReconnecting] = useState(false);
|
||||
const [theme] = useTheme()
|
||||
const exitState = useExitOnCtrlCDWithKeybindings()
|
||||
const mcp = useAppState(s => s.mcp)
|
||||
const reconnectMcpServer = useMcpReconnect()
|
||||
const toggleMcpServer = useMcpToggleEnabled()
|
||||
const [isReconnecting, setIsReconnecting] = useState(false)
|
||||
|
||||
const handleToggleEnabled = React.useCallback(async () => {
|
||||
const wasEnabled = server.client.type !== 'disabled';
|
||||
const wasEnabled = server.client.type !== 'disabled'
|
||||
|
||||
try {
|
||||
await toggleMcpServer(server.name);
|
||||
await toggleMcpServer(server.name)
|
||||
// Return to the server list so user can continue managing other servers
|
||||
onCancel();
|
||||
onCancel()
|
||||
} catch (err) {
|
||||
const action = wasEnabled ? 'disable' : 'enable';
|
||||
onComplete(`Failed to ${action} MCP server '${server.name}': ${errorMessage(err)}`);
|
||||
const action = wasEnabled ? 'disable' : 'enable'
|
||||
onComplete(
|
||||
`Failed to ${action} MCP server '${server.name}': ${errorMessage(err)}`,
|
||||
)
|
||||
}
|
||||
}, [server.client.type, server.name, toggleMcpServer, onCancel, onComplete]);
|
||||
const capitalizedServerName = capitalize(String(server.name));
|
||||
}, [server.client.type, server.name, toggleMcpServer, onCancel, onComplete])
|
||||
|
||||
const capitalizedServerName = capitalize(String(server.name))
|
||||
|
||||
// Count MCP prompts for this server (skills are shown in /skills, not here)
|
||||
const serverCommandsCount = filterMcpPromptsByServer(mcp.commands, server.name).length;
|
||||
const menuOptions = [];
|
||||
const serverCommandsCount = filterMcpPromptsByServer(
|
||||
mcp.commands,
|
||||
server.name,
|
||||
).length
|
||||
|
||||
const menuOptions = []
|
||||
|
||||
// Only show "View tools" if server is not disabled and has tools
|
||||
if (server.client.type !== 'disabled' && serverToolsCount > 0) {
|
||||
menuOptions.push({
|
||||
label: 'View tools',
|
||||
value: 'tools'
|
||||
});
|
||||
value: 'tools',
|
||||
})
|
||||
}
|
||||
|
||||
// Only show reconnect option if the server is not disabled
|
||||
if (server.client.type !== 'disabled') {
|
||||
menuOptions.push({
|
||||
label: 'Reconnect',
|
||||
value: 'reconnectMcpServer'
|
||||
});
|
||||
value: 'reconnectMcpServer',
|
||||
})
|
||||
}
|
||||
|
||||
menuOptions.push({
|
||||
label: server.client.type !== 'disabled' ? 'Disable' : 'Enable',
|
||||
value: 'toggle-enabled'
|
||||
});
|
||||
value: 'toggle-enabled',
|
||||
})
|
||||
|
||||
// If there are no other options, add a back option so Select handles escape
|
||||
if (menuOptions.length === 0) {
|
||||
menuOptions.push({
|
||||
label: 'Back',
|
||||
value: 'back'
|
||||
});
|
||||
value: 'back',
|
||||
})
|
||||
}
|
||||
|
||||
if (isReconnecting) {
|
||||
return <Box flexDirection="column" gap={1} padding={1}>
|
||||
return (
|
||||
<Box flexDirection="column" gap={1} padding={1}>
|
||||
<Text color="text">
|
||||
Reconnecting to <Text bold>{server.name}</Text>
|
||||
</Text>
|
||||
@@ -95,10 +119,17 @@ export function MCPStdioServerMenu({
|
||||
<Text> Restarting MCP server process</Text>
|
||||
</Box>
|
||||
<Text dimColor>This may take a few moments.</Text>
|
||||
</Box>;
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
return <Box flexDirection="column">
|
||||
<Box flexDirection="column" paddingX={1} borderStyle={borderless ? undefined : 'round'}>
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Box
|
||||
flexDirection="column"
|
||||
paddingX={1}
|
||||
borderStyle={borderless ? undefined : 'round'}
|
||||
>
|
||||
<Box marginBottom={1}>
|
||||
<Text bold>{capitalizedServerName} MCP Server</Text>
|
||||
</Box>
|
||||
@@ -106,10 +137,18 @@ export function MCPStdioServerMenu({
|
||||
<Box flexDirection="column" gap={0}>
|
||||
<Box>
|
||||
<Text bold>Status: </Text>
|
||||
{server.client.type === 'disabled' ? <Text>{color('inactive', theme)(figures.radioOff)} disabled</Text> : server.client.type === 'connected' ? <Text>{color('success', theme)(figures.tick)} connected</Text> : server.client.type === 'pending' ? <>
|
||||
{server.client.type === 'disabled' ? (
|
||||
<Text>{color('inactive', theme)(figures.radioOff)} disabled</Text>
|
||||
) : server.client.type === 'connected' ? (
|
||||
<Text>{color('success', theme)(figures.tick)} connected</Text>
|
||||
) : server.client.type === 'pending' ? (
|
||||
<>
|
||||
<Text dimColor>{figures.radioOff}</Text>
|
||||
<Text> connecting…</Text>
|
||||
</> : <Text>{color('error', theme)(figures.cross)} failed</Text>}
|
||||
</>
|
||||
) : (
|
||||
<Text>{color('error', theme)(figures.cross)} failed</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
@@ -117,60 +156,89 @@ export function MCPStdioServerMenu({
|
||||
<Text dimColor>{server.config.command}</Text>
|
||||
</Box>
|
||||
|
||||
{server.config.args && server.config.args.length > 0 && <Box>
|
||||
{server.config.args && server.config.args.length > 0 && (
|
||||
<Box>
|
||||
<Text bold>Args: </Text>
|
||||
<Text dimColor>{server.config.args.join(' ')}</Text>
|
||||
</Box>}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box>
|
||||
<Text bold>Config location: </Text>
|
||||
<Text dimColor>
|
||||
{describeMcpConfigFilePath(getMcpConfigByName(server.name)?.scope ?? 'dynamic')}
|
||||
{describeMcpConfigFilePath(
|
||||
getMcpConfigByName(server.name)?.scope ?? 'dynamic',
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{server.client.type === 'connected' && <CapabilitiesSection serverToolsCount={serverToolsCount} serverPromptsCount={serverCommandsCount} serverResourcesCount={mcp.resources[server.name]?.length || 0} />}
|
||||
{server.client.type === 'connected' && (
|
||||
<CapabilitiesSection
|
||||
serverToolsCount={serverToolsCount}
|
||||
serverPromptsCount={serverCommandsCount}
|
||||
serverResourcesCount={mcp.resources[server.name]?.length || 0}
|
||||
/>
|
||||
)}
|
||||
|
||||
{server.client.type === 'connected' && serverToolsCount > 0 && <Box>
|
||||
{server.client.type === 'connected' && serverToolsCount > 0 && (
|
||||
<Box>
|
||||
<Text bold>Tools: </Text>
|
||||
<Text dimColor>{serverToolsCount} tools</Text>
|
||||
</Box>}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{menuOptions.length > 0 && <Box marginTop={1}>
|
||||
<Select options={menuOptions} onChange={async value => {
|
||||
{menuOptions.length > 0 && (
|
||||
<Box marginTop={1}>
|
||||
<Select
|
||||
options={menuOptions}
|
||||
onChange={async value => {
|
||||
if (value === 'tools') {
|
||||
onViewTools();
|
||||
onViewTools()
|
||||
} else if (value === 'reconnectMcpServer') {
|
||||
setIsReconnecting(true);
|
||||
setIsReconnecting(true)
|
||||
try {
|
||||
const result = await reconnectMcpServer(server.name);
|
||||
const {
|
||||
message
|
||||
} = handleReconnectResult(result, server.name);
|
||||
onComplete?.(message);
|
||||
} catch (err_0) {
|
||||
onComplete?.(handleReconnectError(err_0, server.name));
|
||||
const result = await reconnectMcpServer(server.name)
|
||||
const { message } = handleReconnectResult(
|
||||
result,
|
||||
server.name,
|
||||
)
|
||||
onComplete?.(message)
|
||||
} catch (err) {
|
||||
onComplete?.(handleReconnectError(err, server.name))
|
||||
} finally {
|
||||
setIsReconnecting(false);
|
||||
setIsReconnecting(false)
|
||||
}
|
||||
} else if (value === 'toggle-enabled') {
|
||||
await handleToggleEnabled();
|
||||
await handleToggleEnabled()
|
||||
} else if (value === 'back') {
|
||||
onCancel();
|
||||
onCancel()
|
||||
}
|
||||
}} onCancel={onCancel} />
|
||||
</Box>}
|
||||
}}
|
||||
onCancel={onCancel}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor italic>
|
||||
{exitState.pending ? <>Press {exitState.keyName} again to exit</> : <Byline>
|
||||
{exitState.pending ? (
|
||||
<>Press {exitState.keyName} again to exit</>
|
||||
) : (
|
||||
<Byline>
|
||||
<KeyboardShortcutHint shortcut="↑↓" action="navigate" />
|
||||
<KeyboardShortcutHint shortcut="Enter" action="select" />
|
||||
<ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="back" />
|
||||
</Byline>}
|
||||
<ConfigurableShortcutHint
|
||||
action="confirm:no"
|
||||
context="Confirmation"
|
||||
fallback="Esc"
|
||||
description="back"
|
||||
/>
|
||||
</Byline>
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>;
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,211 +1,142 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React from 'react';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { extractMcpToolDisplayName, getMcpDisplayName } from '../../services/mcp/mcpStringUtils.js';
|
||||
import type { Tool } from '../../Tool.js';
|
||||
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js';
|
||||
import { Dialog } from '../design-system/Dialog.js';
|
||||
import type { ServerInfo } from './types.js';
|
||||
import React from 'react'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import {
|
||||
extractMcpToolDisplayName,
|
||||
getMcpDisplayName,
|
||||
} from '../../services/mcp/mcpStringUtils.js'
|
||||
import type { Tool } from '../../Tool.js'
|
||||
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'
|
||||
import { Dialog } from '../design-system/Dialog.js'
|
||||
import type { ServerInfo } from './types.js'
|
||||
|
||||
type Props = {
|
||||
tool: Tool;
|
||||
server: ServerInfo;
|
||||
onBack: () => void;
|
||||
};
|
||||
export function MCPToolDetailView(t0) {
|
||||
const $ = _c(44);
|
||||
const {
|
||||
tool: Tool
|
||||
server: ServerInfo
|
||||
onBack: () => void
|
||||
}
|
||||
|
||||
export function MCPToolDetailView({
|
||||
tool,
|
||||
server,
|
||||
onBack
|
||||
} = t0;
|
||||
const [toolDescription, setToolDescription] = React.useState("");
|
||||
let t1;
|
||||
let toolName;
|
||||
if ($[0] !== server.name || $[1] !== tool) {
|
||||
toolName = getMcpDisplayName(tool.name, server.name);
|
||||
const fullDisplayName = tool.userFacingName ? tool.userFacingName({}) : toolName;
|
||||
t1 = extractMcpToolDisplayName(fullDisplayName);
|
||||
$[0] = server.name;
|
||||
$[1] = tool;
|
||||
$[2] = t1;
|
||||
$[3] = toolName;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
toolName = $[3];
|
||||
}
|
||||
const displayName = t1;
|
||||
let t2;
|
||||
if ($[4] !== tool) {
|
||||
t2 = tool.isReadOnly?.({}) ?? false;
|
||||
$[4] = tool;
|
||||
$[5] = t2;
|
||||
} else {
|
||||
t2 = $[5];
|
||||
}
|
||||
const isReadOnly = t2;
|
||||
let t3;
|
||||
if ($[6] !== tool) {
|
||||
t3 = tool.isDestructive?.({}) ?? false;
|
||||
$[6] = tool;
|
||||
$[7] = t3;
|
||||
} else {
|
||||
t3 = $[7];
|
||||
}
|
||||
const isDestructive = t3;
|
||||
let t4;
|
||||
if ($[8] !== tool) {
|
||||
t4 = tool.isOpenWorld?.({}) ?? false;
|
||||
$[8] = tool;
|
||||
$[9] = t4;
|
||||
} else {
|
||||
t4 = $[9];
|
||||
}
|
||||
const isOpenWorld = t4;
|
||||
let t5;
|
||||
let t6;
|
||||
if ($[10] !== tool) {
|
||||
t5 = () => {
|
||||
const loadDescription = async function loadDescription() {
|
||||
onBack,
|
||||
}: Props): React.ReactNode {
|
||||
const [toolDescription, setToolDescription] = React.useState<string>('')
|
||||
|
||||
const toolName = getMcpDisplayName(tool.name, server.name)
|
||||
const fullDisplayName = tool.userFacingName
|
||||
? tool.userFacingName({})
|
||||
: toolName
|
||||
const displayName = extractMcpToolDisplayName(fullDisplayName)
|
||||
|
||||
const isReadOnly = tool.isReadOnly?.({}) ?? false
|
||||
const isDestructive = tool.isDestructive?.({}) ?? false
|
||||
const isOpenWorld = tool.isOpenWorld?.({}) ?? false
|
||||
|
||||
React.useEffect(() => {
|
||||
async function loadDescription() {
|
||||
try {
|
||||
const desc = await tool.description({}, {
|
||||
const desc = await tool.description(
|
||||
{},
|
||||
{
|
||||
isNonInteractiveSession: false,
|
||||
toolPermissionContext: {
|
||||
mode: "default" as const,
|
||||
mode: 'default' as const,
|
||||
additionalWorkingDirectories: new Map(),
|
||||
alwaysAllowRules: {},
|
||||
alwaysDenyRules: {},
|
||||
alwaysAskRules: {},
|
||||
isBypassPermissionsModeAvailable: false
|
||||
isBypassPermissionsModeAvailable: false,
|
||||
},
|
||||
tools: []
|
||||
});
|
||||
setToolDescription(desc);
|
||||
tools: [],
|
||||
},
|
||||
)
|
||||
setToolDescription(desc)
|
||||
} catch {
|
||||
setToolDescription("Failed to load description");
|
||||
setToolDescription('Failed to load description')
|
||||
}
|
||||
};
|
||||
loadDescription();
|
||||
};
|
||||
t6 = [tool];
|
||||
$[10] = tool;
|
||||
$[11] = t5;
|
||||
$[12] = t6;
|
||||
} else {
|
||||
t5 = $[11];
|
||||
t6 = $[12];
|
||||
}
|
||||
React.useEffect(t5, t6);
|
||||
let t7;
|
||||
if ($[13] !== isReadOnly) {
|
||||
t7 = isReadOnly && <Text color="success"> [read-only]</Text>;
|
||||
$[13] = isReadOnly;
|
||||
$[14] = t7;
|
||||
} else {
|
||||
t7 = $[14];
|
||||
void loadDescription()
|
||||
}, [tool])
|
||||
|
||||
const titleContent = (
|
||||
<>
|
||||
{displayName}
|
||||
{isReadOnly && <Text color="success"> [read-only]</Text>}
|
||||
{isDestructive && <Text color="error"> [destructive]</Text>}
|
||||
{isOpenWorld && <Text dimColor> [open-world]</Text>}
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
title={titleContent}
|
||||
subtitle={server.name}
|
||||
onCancel={onBack}
|
||||
inputGuide={exitState =>
|
||||
exitState.pending ? (
|
||||
<Text>Press {exitState.keyName} again to exit</Text>
|
||||
) : (
|
||||
<ConfigurableShortcutHint
|
||||
action="confirm:no"
|
||||
context="Confirmation"
|
||||
fallback="Esc"
|
||||
description="go back"
|
||||
/>
|
||||
)
|
||||
}
|
||||
let t8;
|
||||
if ($[15] !== isDestructive) {
|
||||
t8 = isDestructive && <Text color="error"> [destructive]</Text>;
|
||||
$[15] = isDestructive;
|
||||
$[16] = t8;
|
||||
} else {
|
||||
t8 = $[16];
|
||||
}
|
||||
let t9;
|
||||
if ($[17] !== isOpenWorld) {
|
||||
t9 = isOpenWorld && <Text dimColor={true}> [open-world]</Text>;
|
||||
$[17] = isOpenWorld;
|
||||
$[18] = t9;
|
||||
} else {
|
||||
t9 = $[18];
|
||||
}
|
||||
let t10;
|
||||
if ($[19] !== displayName || $[20] !== t7 || $[21] !== t8 || $[22] !== t9) {
|
||||
t10 = <>{displayName}{t7}{t8}{t9}</>;
|
||||
$[19] = displayName;
|
||||
$[20] = t7;
|
||||
$[21] = t8;
|
||||
$[22] = t9;
|
||||
$[23] = t10;
|
||||
} else {
|
||||
t10 = $[23];
|
||||
}
|
||||
const titleContent = t10;
|
||||
let t11;
|
||||
if ($[24] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t11 = <Text bold={true}>Tool name: </Text>;
|
||||
$[24] = t11;
|
||||
} else {
|
||||
t11 = $[24];
|
||||
}
|
||||
let t12;
|
||||
if ($[25] !== toolName) {
|
||||
t12 = <Box>{t11}<Text dimColor={true}>{toolName}</Text></Box>;
|
||||
$[25] = toolName;
|
||||
$[26] = t12;
|
||||
} else {
|
||||
t12 = $[26];
|
||||
}
|
||||
let t13;
|
||||
if ($[27] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t13 = <Text bold={true}>Full name: </Text>;
|
||||
$[27] = t13;
|
||||
} else {
|
||||
t13 = $[27];
|
||||
}
|
||||
let t14;
|
||||
if ($[28] !== tool.name) {
|
||||
t14 = <Box>{t13}<Text dimColor={true}>{tool.name}</Text></Box>;
|
||||
$[28] = tool.name;
|
||||
$[29] = t14;
|
||||
} else {
|
||||
t14 = $[29];
|
||||
}
|
||||
let t15;
|
||||
if ($[30] !== toolDescription) {
|
||||
t15 = toolDescription && <Box flexDirection="column" marginTop={1}><Text bold={true}>Description:</Text><Text wrap="wrap">{toolDescription}</Text></Box>;
|
||||
$[30] = toolDescription;
|
||||
$[31] = t15;
|
||||
} else {
|
||||
t15 = $[31];
|
||||
}
|
||||
let t16;
|
||||
if ($[32] !== tool.inputJSONSchema) {
|
||||
t16 = tool.inputJSONSchema && tool.inputJSONSchema.properties && Object.keys(tool.inputJSONSchema.properties).length > 0 && <Box flexDirection="column" marginTop={1}><Text bold={true}>Parameters:</Text><Box marginLeft={2} flexDirection="column">{Object.entries(tool.inputJSONSchema.properties).map(t17 => {
|
||||
const [key, value] = t17;
|
||||
const required = tool.inputJSONSchema?.required as string[] | undefined;
|
||||
const isRequired = required?.includes(key);
|
||||
return <Text key={key}>• {key}{isRequired && <Text dimColor={true}> (required)</Text>}:{" "}<Text dimColor={true}>{typeof value === "object" && value && "type" in value ? String(value.type) : "unknown"}</Text>{typeof value === "object" && value && "description" in value && <Text dimColor={true}> - {String(value.description)}</Text>}</Text>;
|
||||
})}</Box></Box>;
|
||||
$[32] = tool.inputJSONSchema;
|
||||
$[33] = t16;
|
||||
} else {
|
||||
t16 = $[33];
|
||||
}
|
||||
let t17;
|
||||
if ($[34] !== t12 || $[35] !== t14 || $[36] !== t15 || $[37] !== t16) {
|
||||
t17 = <Box flexDirection="column">{t12}{t14}{t15}{t16}</Box>;
|
||||
$[34] = t12;
|
||||
$[35] = t14;
|
||||
$[36] = t15;
|
||||
$[37] = t16;
|
||||
$[38] = t17;
|
||||
} else {
|
||||
t17 = $[38];
|
||||
}
|
||||
let t18;
|
||||
if ($[39] !== onBack || $[40] !== server.name || $[41] !== t17 || $[42] !== titleContent) {
|
||||
t18 = <Dialog title={titleContent} subtitle={server.name} onCancel={onBack} inputGuide={_temp}>{t17}</Dialog>;
|
||||
$[39] = onBack;
|
||||
$[40] = server.name;
|
||||
$[41] = t17;
|
||||
$[42] = titleContent;
|
||||
$[43] = t18;
|
||||
} else {
|
||||
t18 = $[43];
|
||||
}
|
||||
return t18;
|
||||
}
|
||||
function _temp(exitState) {
|
||||
return exitState.pending ? <Text>Press {exitState.keyName} again to exit</Text> : <ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="go back" />;
|
||||
>
|
||||
<Box flexDirection="column">
|
||||
<Box>
|
||||
<Text bold>Tool name: </Text>
|
||||
<Text dimColor>{toolName}</Text>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text bold>Full name: </Text>
|
||||
<Text dimColor>{tool.name}</Text>
|
||||
</Box>
|
||||
|
||||
{toolDescription && (
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<Text bold>Description:</Text>
|
||||
<Text wrap="wrap">{toolDescription}</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{tool.inputJSONSchema &&
|
||||
tool.inputJSONSchema.properties &&
|
||||
Object.keys(tool.inputJSONSchema.properties).length > 0 && (
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<Text bold>Parameters:</Text>
|
||||
<Box marginLeft={2} flexDirection="column">
|
||||
{Object.entries(tool.inputJSONSchema.properties).map(
|
||||
([key, value]) => {
|
||||
const required = tool.inputJSONSchema?.required as
|
||||
| string[]
|
||||
| undefined
|
||||
const isRequired = required?.includes(key)
|
||||
return (
|
||||
<Text key={key}>
|
||||
• {key}
|
||||
{isRequired && <Text dimColor> (required)</Text>}:{' '}
|
||||
<Text dimColor>
|
||||
{typeof value === 'object' && value && 'type' in value
|
||||
? String(value.type)
|
||||
: 'unknown'}
|
||||
</Text>
|
||||
{typeof value === 'object' &&
|
||||
value &&
|
||||
'description' in value && (
|
||||
<Text dimColor> - {String(value.description)}</Text>
|
||||
)}
|
||||
</Text>
|
||||
)
|
||||
},
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,140 +1,104 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React from 'react';
|
||||
import { Text } from '../../ink.js';
|
||||
import { extractMcpToolDisplayName, getMcpDisplayName } from '../../services/mcp/mcpStringUtils.js';
|
||||
import { filterToolsByServer } from '../../services/mcp/utils.js';
|
||||
import { useAppState } from '../../state/AppState.js';
|
||||
import type { Tool } from '../../Tool.js';
|
||||
import { plural } from '../../utils/stringUtils.js';
|
||||
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js';
|
||||
import { Select } from '../CustomSelect/index.js';
|
||||
import { Byline } from '../design-system/Byline.js';
|
||||
import { Dialog } from '../design-system/Dialog.js';
|
||||
import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js';
|
||||
import type { ServerInfo } from './types.js';
|
||||
import React from 'react'
|
||||
import { Text } from '../../ink.js'
|
||||
import {
|
||||
extractMcpToolDisplayName,
|
||||
getMcpDisplayName,
|
||||
} from '../../services/mcp/mcpStringUtils.js'
|
||||
import { filterToolsByServer } from '../../services/mcp/utils.js'
|
||||
import { useAppState } from '../../state/AppState.js'
|
||||
import type { Tool } from '../../Tool.js'
|
||||
import { plural } from '../../utils/stringUtils.js'
|
||||
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'
|
||||
import { Select } from '../CustomSelect/index.js'
|
||||
import { Byline } from '../design-system/Byline.js'
|
||||
import { Dialog } from '../design-system/Dialog.js'
|
||||
import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'
|
||||
import type { ServerInfo } from './types.js'
|
||||
|
||||
type Props = {
|
||||
server: ServerInfo;
|
||||
onSelectTool: (tool: Tool, index: number) => void;
|
||||
onBack: () => void;
|
||||
};
|
||||
export function MCPToolListView(t0) {
|
||||
const $ = _c(21);
|
||||
const {
|
||||
server: ServerInfo
|
||||
onSelectTool: (tool: Tool, index: number) => void
|
||||
onBack: () => void
|
||||
}
|
||||
|
||||
export function MCPToolListView({
|
||||
server,
|
||||
onSelectTool,
|
||||
onBack
|
||||
} = t0;
|
||||
const mcpTools = useAppState(_temp);
|
||||
let t1;
|
||||
bb0: {
|
||||
if (server.client.type !== "connected") {
|
||||
let t2;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = [];
|
||||
$[0] = t2;
|
||||
} else {
|
||||
t2 = $[0];
|
||||
}
|
||||
t1 = t2;
|
||||
break bb0;
|
||||
}
|
||||
let t2;
|
||||
if ($[1] !== mcpTools || $[2] !== server.name) {
|
||||
t2 = filterToolsByServer(mcpTools, server.name);
|
||||
$[1] = mcpTools;
|
||||
$[2] = server.name;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
t1 = t2;
|
||||
}
|
||||
const serverTools = t1;
|
||||
let t2;
|
||||
if ($[4] !== server.name || $[5] !== serverTools) {
|
||||
let t3;
|
||||
if ($[7] !== server.name) {
|
||||
t3 = (tool, index) => {
|
||||
const toolName = getMcpDisplayName(tool.name, server.name);
|
||||
const fullDisplayName = tool.userFacingName ? tool.userFacingName({}) : toolName;
|
||||
const displayName = extractMcpToolDisplayName(fullDisplayName);
|
||||
const isReadOnly = tool.isReadOnly?.({}) ?? false;
|
||||
const isDestructive = tool.isDestructive?.({}) ?? false;
|
||||
const isOpenWorld = tool.isOpenWorld?.({}) ?? false;
|
||||
const annotations = [];
|
||||
if (isReadOnly) {
|
||||
annotations.push("read-only");
|
||||
}
|
||||
if (isDestructive) {
|
||||
annotations.push("destructive");
|
||||
}
|
||||
if (isOpenWorld) {
|
||||
annotations.push("open-world");
|
||||
}
|
||||
onBack,
|
||||
}: Props): React.ReactNode {
|
||||
const mcpTools = useAppState(s => s.mcp.tools)
|
||||
|
||||
const serverTools = React.useMemo(() => {
|
||||
if (server.client.type !== 'connected') return []
|
||||
return filterToolsByServer(mcpTools, server.name)
|
||||
}, [server, mcpTools])
|
||||
|
||||
const toolOptions = serverTools.map((tool, index) => {
|
||||
const toolName = getMcpDisplayName(tool.name, server.name)
|
||||
const fullDisplayName = tool.userFacingName
|
||||
? tool.userFacingName({})
|
||||
: toolName
|
||||
// Extract just the tool display name without server prefix
|
||||
const displayName = extractMcpToolDisplayName(fullDisplayName)
|
||||
|
||||
const isReadOnly = tool.isReadOnly?.({}) ?? false
|
||||
const isDestructive = tool.isDestructive?.({}) ?? false
|
||||
const isOpenWorld = tool.isOpenWorld?.({}) ?? false
|
||||
|
||||
const annotations = []
|
||||
if (isReadOnly) annotations.push('read-only')
|
||||
if (isDestructive) annotations.push('destructive')
|
||||
if (isOpenWorld) annotations.push('open-world')
|
||||
|
||||
return {
|
||||
label: displayName,
|
||||
value: index.toString(),
|
||||
description: annotations.length > 0 ? annotations.join(", ") : undefined,
|
||||
descriptionColor: isDestructive ? "error" : isReadOnly ? "success" : undefined
|
||||
};
|
||||
};
|
||||
$[7] = server.name;
|
||||
$[8] = t3;
|
||||
} else {
|
||||
t3 = $[8];
|
||||
description: annotations.length > 0 ? annotations.join(', ') : undefined,
|
||||
descriptionColor: isDestructive
|
||||
? 'error'
|
||||
: isReadOnly
|
||||
? 'success'
|
||||
: undefined,
|
||||
}
|
||||
t2 = serverTools.map(t3);
|
||||
$[4] = server.name;
|
||||
$[5] = serverTools;
|
||||
$[6] = t2;
|
||||
} else {
|
||||
t2 = $[6];
|
||||
})
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
title={`Tools for ${server.name}`}
|
||||
subtitle={`${serverTools.length} ${plural(serverTools.length, 'tool')}`}
|
||||
onCancel={onBack}
|
||||
inputGuide={exitState =>
|
||||
exitState.pending ? (
|
||||
<Text>Press {exitState.keyName} again to exit</Text>
|
||||
) : (
|
||||
<Byline>
|
||||
<KeyboardShortcutHint shortcut="↑↓" action="navigate" />
|
||||
<KeyboardShortcutHint shortcut="Enter" action="select" />
|
||||
<ConfigurableShortcutHint
|
||||
action="confirm:no"
|
||||
context="Confirmation"
|
||||
fallback="Esc"
|
||||
description="back"
|
||||
/>
|
||||
</Byline>
|
||||
)
|
||||
}
|
||||
const toolOptions = t2;
|
||||
const t3 = `Tools for ${server.name}`;
|
||||
const t4 = serverTools.length;
|
||||
let t5;
|
||||
if ($[9] !== serverTools.length) {
|
||||
t5 = plural(serverTools.length, "tool");
|
||||
$[9] = serverTools.length;
|
||||
$[10] = t5;
|
||||
} else {
|
||||
t5 = $[10];
|
||||
>
|
||||
{serverTools.length === 0 ? (
|
||||
<Text dimColor>No tools available</Text>
|
||||
) : (
|
||||
<Select
|
||||
options={toolOptions}
|
||||
onChange={value => {
|
||||
const index = parseInt(value)
|
||||
const tool = serverTools[index]
|
||||
if (tool) {
|
||||
onSelectTool(tool, index)
|
||||
}
|
||||
const t6 = `${t4} ${t5}`;
|
||||
let t7;
|
||||
if ($[11] !== onBack || $[12] !== onSelectTool || $[13] !== serverTools || $[14] !== toolOptions) {
|
||||
t7 = serverTools.length === 0 ? <Text dimColor={true}>No tools available</Text> : <Select options={toolOptions} onChange={value => {
|
||||
const index_0 = parseInt(value);
|
||||
const tool_0 = serverTools[index_0];
|
||||
if (tool_0) {
|
||||
onSelectTool(tool_0, index_0);
|
||||
}
|
||||
}} onCancel={onBack} />;
|
||||
$[11] = onBack;
|
||||
$[12] = onSelectTool;
|
||||
$[13] = serverTools;
|
||||
$[14] = toolOptions;
|
||||
$[15] = t7;
|
||||
} else {
|
||||
t7 = $[15];
|
||||
}
|
||||
let t8;
|
||||
if ($[16] !== onBack || $[17] !== t3 || $[18] !== t6 || $[19] !== t7) {
|
||||
t8 = <Dialog title={t3} subtitle={t6} onCancel={onBack} inputGuide={_temp2}>{t7}</Dialog>;
|
||||
$[16] = onBack;
|
||||
$[17] = t3;
|
||||
$[18] = t6;
|
||||
$[19] = t7;
|
||||
$[20] = t8;
|
||||
} else {
|
||||
t8 = $[20];
|
||||
}
|
||||
return t8;
|
||||
}
|
||||
function _temp2(exitState) {
|
||||
return exitState.pending ? <Text>Press {exitState.keyName} again to exit</Text> : <Byline><KeyboardShortcutHint shortcut={"\u2191\u2193"} action="navigate" /><KeyboardShortcutHint shortcut="Enter" action="select" /><ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="back" /></Byline>;
|
||||
}
|
||||
function _temp(s) {
|
||||
return s.mcp.tools;
|
||||
}}
|
||||
onCancel={onBack}
|
||||
/>
|
||||
)}
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,212 +1,147 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React, { useMemo } from 'react';
|
||||
import { getMcpConfigsByScope } from 'src/services/mcp/config.js';
|
||||
import type { ConfigScope } from 'src/services/mcp/types.js';
|
||||
import { describeMcpConfigFilePath, getScopeLabel } from 'src/services/mcp/utils.js';
|
||||
import type { ValidationError } from 'src/utils/settings/validation.js';
|
||||
import { Box, Link, Text } from '../../ink.js';
|
||||
function McpConfigErrorSection(t0) {
|
||||
const $ = _c(26);
|
||||
const {
|
||||
import React, { useMemo } from 'react'
|
||||
import { getMcpConfigsByScope } from 'src/services/mcp/config.js'
|
||||
import type { ConfigScope } from 'src/services/mcp/types.js'
|
||||
import {
|
||||
describeMcpConfigFilePath,
|
||||
getScopeLabel,
|
||||
} from 'src/services/mcp/utils.js'
|
||||
import type { ValidationError } from 'src/utils/settings/validation.js'
|
||||
import { Box, Link, Text } from '../../ink.js'
|
||||
|
||||
function McpConfigErrorSection({
|
||||
scope,
|
||||
parsingErrors,
|
||||
warnings
|
||||
} = t0;
|
||||
const hasErrors = parsingErrors.length > 0;
|
||||
const hasWarnings = warnings.length > 0;
|
||||
warnings,
|
||||
}: {
|
||||
scope: ConfigScope
|
||||
parsingErrors: ValidationError[]
|
||||
warnings: ValidationError[]
|
||||
}): React.ReactNode {
|
||||
const hasErrors = parsingErrors.length > 0
|
||||
const hasWarnings = warnings.length > 0
|
||||
|
||||
if (!hasErrors && !hasWarnings) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
let t1;
|
||||
if ($[0] !== hasErrors || $[1] !== hasWarnings) {
|
||||
t1 = (hasErrors || hasWarnings) && <Text color={hasErrors ? "error" : "warning"}>[{hasErrors ? "Failed to parse" : "Contains warnings"}]{" "}</Text>;
|
||||
$[0] = hasErrors;
|
||||
$[1] = hasWarnings;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
let t2;
|
||||
if ($[3] !== scope) {
|
||||
t2 = getScopeLabel(scope);
|
||||
$[3] = scope;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
let t3;
|
||||
if ($[5] !== t2) {
|
||||
t3 = <Text>{t2}</Text>;
|
||||
$[5] = t2;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
}
|
||||
let t4;
|
||||
if ($[7] !== t1 || $[8] !== t3) {
|
||||
t4 = <Box>{t1}{t3}</Box>;
|
||||
$[7] = t1;
|
||||
$[8] = t3;
|
||||
$[9] = t4;
|
||||
} else {
|
||||
t4 = $[9];
|
||||
}
|
||||
let t5;
|
||||
if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t5 = <Text dimColor={true}>Location: </Text>;
|
||||
$[10] = t5;
|
||||
} else {
|
||||
t5 = $[10];
|
||||
}
|
||||
let t6;
|
||||
if ($[11] !== scope) {
|
||||
t6 = describeMcpConfigFilePath(scope);
|
||||
$[11] = scope;
|
||||
$[12] = t6;
|
||||
} else {
|
||||
t6 = $[12];
|
||||
}
|
||||
let t7;
|
||||
if ($[13] !== t6) {
|
||||
t7 = <Box>{t5}<Text dimColor={true}>{t6}</Text></Box>;
|
||||
$[13] = t6;
|
||||
$[14] = t7;
|
||||
} else {
|
||||
t7 = $[14];
|
||||
}
|
||||
let t8;
|
||||
if ($[15] !== parsingErrors) {
|
||||
t8 = parsingErrors.map(_temp);
|
||||
$[15] = parsingErrors;
|
||||
$[16] = t8;
|
||||
} else {
|
||||
t8 = $[16];
|
||||
}
|
||||
let t9;
|
||||
if ($[17] !== warnings) {
|
||||
t9 = warnings.map(_temp2);
|
||||
$[17] = warnings;
|
||||
$[18] = t9;
|
||||
} else {
|
||||
t9 = $[18];
|
||||
}
|
||||
let t10;
|
||||
if ($[19] !== t8 || $[20] !== t9) {
|
||||
t10 = <Box marginLeft={1} flexDirection="column">{t8}{t9}</Box>;
|
||||
$[19] = t8;
|
||||
$[20] = t9;
|
||||
$[21] = t10;
|
||||
} else {
|
||||
t10 = $[21];
|
||||
}
|
||||
let t11;
|
||||
if ($[22] !== t10 || $[23] !== t4 || $[24] !== t7) {
|
||||
t11 = <Box flexDirection="column" marginTop={1}>{t4}{t7}{t10}</Box>;
|
||||
$[22] = t10;
|
||||
$[23] = t4;
|
||||
$[24] = t7;
|
||||
$[25] = t11;
|
||||
} else {
|
||||
t11 = $[25];
|
||||
}
|
||||
return t11;
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<Box>
|
||||
{(hasErrors || hasWarnings) && (
|
||||
<Text color={hasErrors ? 'error' : 'warning'}>
|
||||
[{hasErrors ? 'Failed to parse' : 'Contains warnings'}]{' '}
|
||||
</Text>
|
||||
)}
|
||||
<Text>{getScopeLabel(scope)}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text dimColor>Location: </Text>
|
||||
<Text dimColor>{describeMcpConfigFilePath(scope)}</Text>
|
||||
</Box>
|
||||
<Box marginLeft={1} flexDirection="column">
|
||||
{parsingErrors.map((error, i) => {
|
||||
const serverName = error.mcpErrorMetadata?.serverName
|
||||
return (
|
||||
<Box key={`error-${i}`}>
|
||||
<Text>
|
||||
<Text dimColor>└ </Text>
|
||||
<Text color="error">[Error]</Text>
|
||||
<Text dimColor>
|
||||
{' '}
|
||||
{serverName && `[${serverName}] `}
|
||||
{error.path && error.path !== '' ? `${error.path}: ` : ''}
|
||||
{error.message}
|
||||
</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
})}
|
||||
{warnings.map((warning, i) => {
|
||||
const serverName = warning.mcpErrorMetadata?.serverName
|
||||
|
||||
return (
|
||||
<Box key={`warning-${i}`}>
|
||||
<Text>
|
||||
<Text dimColor>└ </Text>
|
||||
<Text color="warning">[Warning]</Text>
|
||||
<Text dimColor>
|
||||
{' '}
|
||||
{serverName && `[${serverName}] `}
|
||||
{warning.path && warning.path !== ''
|
||||
? `${warning.path}: `
|
||||
: ''}
|
||||
{warning.message}
|
||||
</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
function _temp2(warning, i_0) {
|
||||
const serverName_0 = warning.mcpErrorMetadata?.serverName;
|
||||
return <Box key={`warning-${i_0}`}><Text><Text dimColor={true}>└ </Text><Text color="warning">[Warning]</Text><Text dimColor={true}>{" "}{serverName_0 && `[${serverName_0}] `}{warning.path && warning.path !== "" ? `${warning.path}: ` : ""}{warning.message}</Text></Text></Box>;
|
||||
}
|
||||
function _temp(error, i) {
|
||||
const serverName = error.mcpErrorMetadata?.serverName;
|
||||
return <Box key={`error-${i}`}><Text><Text dimColor={true}>└ </Text><Text color="error">[Error]</Text><Text dimColor={true}>{" "}{serverName && `[${serverName}] `}{error.path && error.path !== "" ? `${error.path}: ` : ""}{error.message}</Text></Text></Box>;
|
||||
}
|
||||
export function McpParsingWarnings() {
|
||||
const $ = _c(6);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = {
|
||||
scope: "user",
|
||||
config: getMcpConfigsByScope("user")
|
||||
};
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
let t1;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = {
|
||||
scope: "project",
|
||||
config: getMcpConfigsByScope("project")
|
||||
};
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
let t2;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = {
|
||||
scope: "local",
|
||||
config: getMcpConfigsByScope("local")
|
||||
};
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
let t3;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = [t0, t1, t2, {
|
||||
scope: "enterprise",
|
||||
config: getMcpConfigsByScope("enterprise")
|
||||
}];
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t3 = $[3];
|
||||
}
|
||||
const scopes = t3 satisfies Array<{
|
||||
scope: ConfigScope;
|
||||
config: {
|
||||
errors: ValidationError[];
|
||||
};
|
||||
}>;
|
||||
const hasParsingErrors = scopes.some(_temp3);
|
||||
const hasWarnings = scopes.some(_temp4);
|
||||
|
||||
export function McpParsingWarnings(): React.ReactNode {
|
||||
// Config files don't change during dialog lifetime; read once on mount
|
||||
// to avoid blocking file IO on every re-render.
|
||||
const scopes = useMemo(
|
||||
() =>
|
||||
[
|
||||
{ scope: 'user', config: getMcpConfigsByScope('user') },
|
||||
{ scope: 'project', config: getMcpConfigsByScope('project') },
|
||||
{ scope: 'local', config: getMcpConfigsByScope('local') },
|
||||
{ scope: 'enterprise', config: getMcpConfigsByScope('enterprise') },
|
||||
] satisfies Array<{
|
||||
scope: ConfigScope
|
||||
config: { errors: ValidationError[] }
|
||||
}>,
|
||||
[],
|
||||
)
|
||||
|
||||
const hasParsingErrors = scopes.some(
|
||||
({ config }) => filterErrors(config.errors, 'fatal').length > 0,
|
||||
)
|
||||
const hasWarnings = scopes.some(
|
||||
({ config }) => filterErrors(config.errors, 'warning').length > 0,
|
||||
)
|
||||
|
||||
if (!hasParsingErrors && !hasWarnings) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
let t4;
|
||||
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = <Text bold={true}>MCP Config Diagnostics</Text>;
|
||||
$[4] = t4;
|
||||
} else {
|
||||
t4 = $[4];
|
||||
}
|
||||
let t5;
|
||||
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t5 = <Box flexDirection="column" marginTop={1} marginBottom={1}>{t4}<Box marginTop={1}><Text dimColor={true}>For help configuring MCP servers, see:{" "}<Link url="https://code.claude.com/docs/en/mcp">https://code.claude.com/docs/en/mcp</Link></Text></Box>{scopes.map(_temp5)}</Box>;
|
||||
$[5] = t5;
|
||||
} else {
|
||||
t5 = $[5];
|
||||
}
|
||||
return t5;
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" marginTop={1} marginBottom={1}>
|
||||
<Text bold>MCP Config Diagnostics</Text>
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>
|
||||
For help configuring MCP servers, see:{' '}
|
||||
<Link url="https://code.claude.com/docs/en/mcp">
|
||||
https://code.claude.com/docs/en/mcp
|
||||
</Link>
|
||||
</Text>
|
||||
</Box>
|
||||
{scopes.map(({ scope, config }) => (
|
||||
<McpConfigErrorSection
|
||||
key={scope}
|
||||
scope={scope}
|
||||
parsingErrors={filterErrors(config.errors, 'fatal')}
|
||||
warnings={filterErrors(config.errors, 'warning')}
|
||||
/>
|
||||
))}
|
||||
{/* TODO: Add additional diagnostic sections:
|
||||
* - Duplicate Server Names (check for servers with same name across scopes)
|
||||
* This section should include:
|
||||
* - File paths where each server is defined
|
||||
* - More detailed location info for user/local scopes
|
||||
* - Approved / disabled status of servers
|
||||
*/}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
function _temp5(t0) {
|
||||
const {
|
||||
scope,
|
||||
config: config_1
|
||||
} = t0;
|
||||
return <McpConfigErrorSection key={scope} scope={scope} parsingErrors={filterErrors(config_1.errors, "fatal")} warnings={filterErrors(config_1.errors, "warning")} />;
|
||||
}
|
||||
function _temp4(t0) {
|
||||
const {
|
||||
config: config_0
|
||||
} = t0;
|
||||
return filterErrors(config_0.errors, "warning").length > 0;
|
||||
}
|
||||
function _temp3(t0) {
|
||||
const {
|
||||
config
|
||||
} = t0;
|
||||
return filterErrors(config.errors, "fatal").length > 0;
|
||||
}
|
||||
function filterErrors(errors: ValidationError[], severity: 'fatal' | 'warning'): ValidationError[] {
|
||||
return errors.filter(e => e.mcpErrorMetadata?.severity === severity);
|
||||
|
||||
function filterErrors(
|
||||
errors: ValidationError[],
|
||||
severity: 'fatal' | 'warning',
|
||||
): ValidationError[] {
|
||||
return errors.filter(e => e.mcpErrorMetadata?.severity === severity)
|
||||
}
|
||||
|
||||
@@ -1,48 +1,61 @@
|
||||
import type { Command } from '../../../commands.js';
|
||||
import type { MCPServerConnection, ServerResource } from '../../../services/mcp/types.js';
|
||||
import type { Tool } from '../../../Tool.js';
|
||||
import type { Command } from '../../../commands.js'
|
||||
import type {
|
||||
MCPServerConnection,
|
||||
ServerResource,
|
||||
} from '../../../services/mcp/types.js'
|
||||
import type { Tool } from '../../../Tool.js'
|
||||
|
||||
export interface ReconnectResult {
|
||||
message: string;
|
||||
success: boolean;
|
||||
message: string
|
||||
success: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the result of a reconnect attempt and returns an appropriate user message
|
||||
*/
|
||||
export function handleReconnectResult(result: {
|
||||
client: MCPServerConnection;
|
||||
tools: Tool[];
|
||||
commands: Command[];
|
||||
resources?: ServerResource[];
|
||||
}, serverName: string): ReconnectResult {
|
||||
export function handleReconnectResult(
|
||||
result: {
|
||||
client: MCPServerConnection
|
||||
tools: Tool[]
|
||||
commands: Command[]
|
||||
resources?: ServerResource[]
|
||||
},
|
||||
serverName: string,
|
||||
): ReconnectResult {
|
||||
switch (result.client.type) {
|
||||
case 'connected':
|
||||
return {
|
||||
message: `Reconnected to ${serverName}.`,
|
||||
success: true
|
||||
};
|
||||
success: true,
|
||||
}
|
||||
|
||||
case 'needs-auth':
|
||||
return {
|
||||
message: `${serverName} requires authentication. Use the 'Authenticate' option.`,
|
||||
success: false
|
||||
};
|
||||
success: false,
|
||||
}
|
||||
|
||||
case 'failed':
|
||||
return {
|
||||
message: `Failed to reconnect to ${serverName}.`,
|
||||
success: false
|
||||
};
|
||||
success: false,
|
||||
}
|
||||
|
||||
default:
|
||||
return {
|
||||
message: `Unknown result when reconnecting to ${serverName}.`,
|
||||
success: false
|
||||
};
|
||||
success: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles errors from reconnect attempts
|
||||
*/
|
||||
export function handleReconnectError(error: unknown, serverName: string): string {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
return `Error reconnecting to ${serverName}: ${errorMessage}`;
|
||||
export function handleReconnectError(
|
||||
error: unknown,
|
||||
serverName: string,
|
||||
): string {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
return `Error reconnecting to ${serverName}: ${errorMessage}`
|
||||
}
|
||||
|
||||
@@ -1,157 +1,85 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import figures from 'figures';
|
||||
import React from 'react';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import type { AdvisorBlock } from '../../utils/advisor.js';
|
||||
import { renderModelName } from '../../utils/model/model.js';
|
||||
import { jsonStringify } from '../../utils/slowOperations.js';
|
||||
import { CtrlOToExpand } from '../CtrlOToExpand.js';
|
||||
import { MessageResponse } from '../MessageResponse.js';
|
||||
import { ToolUseLoader } from '../ToolUseLoader.js';
|
||||
import figures from 'figures'
|
||||
import React from 'react'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import type { AdvisorBlock } from '../../utils/advisor.js'
|
||||
import { renderModelName } from '../../utils/model/model.js'
|
||||
import { jsonStringify } from '../../utils/slowOperations.js'
|
||||
import { CtrlOToExpand } from '../CtrlOToExpand.js'
|
||||
import { MessageResponse } from '../MessageResponse.js'
|
||||
import { ToolUseLoader } from '../ToolUseLoader.js'
|
||||
|
||||
type Props = {
|
||||
block: AdvisorBlock;
|
||||
addMargin: boolean;
|
||||
resolvedToolUseIDs: Set<string>;
|
||||
erroredToolUseIDs: Set<string>;
|
||||
shouldAnimate: boolean;
|
||||
verbose: boolean;
|
||||
advisorModel?: string;
|
||||
};
|
||||
export function AdvisorMessage(t0) {
|
||||
const $ = _c(30);
|
||||
const {
|
||||
block: AdvisorBlock
|
||||
addMargin: boolean
|
||||
resolvedToolUseIDs: Set<string>
|
||||
erroredToolUseIDs: Set<string>
|
||||
shouldAnimate: boolean
|
||||
verbose: boolean
|
||||
advisorModel?: string
|
||||
}
|
||||
|
||||
export function AdvisorMessage({
|
||||
block,
|
||||
addMargin,
|
||||
resolvedToolUseIDs,
|
||||
erroredToolUseIDs,
|
||||
shouldAnimate,
|
||||
verbose,
|
||||
advisorModel
|
||||
} = t0;
|
||||
if (block.type === "server_tool_use") {
|
||||
let t1;
|
||||
if ($[0] !== block.input) {
|
||||
t1 = block.input && Object.keys(block.input).length > 0 ? jsonStringify(block.input) : null;
|
||||
$[0] = block.input;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
advisorModel,
|
||||
}: Props): React.ReactNode {
|
||||
if (block.type === 'server_tool_use') {
|
||||
const input =
|
||||
block.input && Object.keys(block.input).length > 0
|
||||
? jsonStringify(block.input)
|
||||
: null
|
||||
return (
|
||||
<Box marginTop={addMargin ? 1 : 0} paddingRight={2} flexDirection="row">
|
||||
<ToolUseLoader
|
||||
shouldAnimate={shouldAnimate}
|
||||
isUnresolved={!resolvedToolUseIDs.has(block.id)}
|
||||
isError={erroredToolUseIDs.has(block.id)}
|
||||
/>
|
||||
<Text bold>Advising</Text>
|
||||
{advisorModel ? (
|
||||
<Text dimColor> using {renderModelName(advisorModel)}</Text>
|
||||
) : null}
|
||||
{input ? <Text dimColor> · {input}</Text> : null}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
const input = t1;
|
||||
const t2 = addMargin ? 1 : 0;
|
||||
let t3;
|
||||
if ($[2] !== block.id || $[3] !== resolvedToolUseIDs) {
|
||||
t3 = resolvedToolUseIDs.has(block.id);
|
||||
$[2] = block.id;
|
||||
$[3] = resolvedToolUseIDs;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
|
||||
let body: React.ReactNode
|
||||
switch (block.content.type) {
|
||||
case 'advisor_tool_result_error':
|
||||
body = (
|
||||
<Text color="error">
|
||||
Advisor unavailable ({block.content.error_code})
|
||||
</Text>
|
||||
)
|
||||
break
|
||||
case 'advisor_result':
|
||||
body = verbose ? (
|
||||
<Text dimColor>{block.content.text}</Text>
|
||||
) : (
|
||||
<Text dimColor>
|
||||
{figures.tick} Advisor has reviewed the conversation and will apply
|
||||
the feedback <CtrlOToExpand />
|
||||
</Text>
|
||||
)
|
||||
break
|
||||
case 'advisor_redacted_result':
|
||||
body = (
|
||||
<Text dimColor>
|
||||
{figures.tick} Advisor has reviewed the conversation and will apply
|
||||
the feedback
|
||||
</Text>
|
||||
)
|
||||
break
|
||||
}
|
||||
const t4 = !t3;
|
||||
let t5;
|
||||
if ($[5] !== block.id || $[6] !== erroredToolUseIDs) {
|
||||
t5 = erroredToolUseIDs.has(block.id);
|
||||
$[5] = block.id;
|
||||
$[6] = erroredToolUseIDs;
|
||||
$[7] = t5;
|
||||
} else {
|
||||
t5 = $[7];
|
||||
}
|
||||
let t6;
|
||||
if ($[8] !== shouldAnimate || $[9] !== t4 || $[10] !== t5) {
|
||||
t6 = <ToolUseLoader shouldAnimate={shouldAnimate} isUnresolved={t4} isError={t5} />;
|
||||
$[8] = shouldAnimate;
|
||||
$[9] = t4;
|
||||
$[10] = t5;
|
||||
$[11] = t6;
|
||||
} else {
|
||||
t6 = $[11];
|
||||
}
|
||||
let t7;
|
||||
if ($[12] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t7 = <Text bold={true}>Advising</Text>;
|
||||
$[12] = t7;
|
||||
} else {
|
||||
t7 = $[12];
|
||||
}
|
||||
let t8;
|
||||
if ($[13] !== advisorModel) {
|
||||
t8 = advisorModel ? <Text dimColor={true}> using {renderModelName(advisorModel)}</Text> : null;
|
||||
$[13] = advisorModel;
|
||||
$[14] = t8;
|
||||
} else {
|
||||
t8 = $[14];
|
||||
}
|
||||
let t9;
|
||||
if ($[15] !== input) {
|
||||
t9 = input ? <Text dimColor={true}> · {input}</Text> : null;
|
||||
$[15] = input;
|
||||
$[16] = t9;
|
||||
} else {
|
||||
t9 = $[16];
|
||||
}
|
||||
let t10;
|
||||
if ($[17] !== t2 || $[18] !== t6 || $[19] !== t8 || $[20] !== t9) {
|
||||
t10 = <Box marginTop={t2} paddingRight={2} flexDirection="row">{t6}{t7}{t8}{t9}</Box>;
|
||||
$[17] = t2;
|
||||
$[18] = t6;
|
||||
$[19] = t8;
|
||||
$[20] = t9;
|
||||
$[21] = t10;
|
||||
} else {
|
||||
t10 = $[21];
|
||||
}
|
||||
return t10;
|
||||
}
|
||||
let body;
|
||||
bb0: switch (block.content.type) {
|
||||
case "advisor_tool_result_error":
|
||||
{
|
||||
let t1;
|
||||
if ($[22] !== block.content.error_code) {
|
||||
t1 = <Text color="error">Advisor unavailable ({block.content.error_code})</Text>;
|
||||
$[22] = block.content.error_code;
|
||||
$[23] = t1;
|
||||
} else {
|
||||
t1 = $[23];
|
||||
}
|
||||
body = t1;
|
||||
break bb0;
|
||||
}
|
||||
case "advisor_result":
|
||||
{
|
||||
let t1;
|
||||
if ($[24] !== block.content.text || $[25] !== verbose) {
|
||||
t1 = verbose ? <Text dimColor={true}>{block.content.text}</Text> : <Text dimColor={true}>{figures.tick} Advisor has reviewed the conversation and will apply the feedback <CtrlOToExpand /></Text>;
|
||||
$[24] = block.content.text;
|
||||
$[25] = verbose;
|
||||
$[26] = t1;
|
||||
} else {
|
||||
t1 = $[26];
|
||||
}
|
||||
body = t1;
|
||||
break bb0;
|
||||
}
|
||||
case "advisor_redacted_result":
|
||||
{
|
||||
let t1;
|
||||
if ($[27] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <Text dimColor={true}>{figures.tick} Advisor has reviewed the conversation and will apply the feedback</Text>;
|
||||
$[27] = t1;
|
||||
} else {
|
||||
t1 = $[27];
|
||||
}
|
||||
body = t1;
|
||||
}
|
||||
}
|
||||
let t1;
|
||||
if ($[28] !== body) {
|
||||
t1 = <Box paddingRight={2}><MessageResponse>{body}</MessageResponse></Box>;
|
||||
$[28] = body;
|
||||
$[29] = t1;
|
||||
} else {
|
||||
t1 = $[29];
|
||||
}
|
||||
return t1;
|
||||
|
||||
return (
|
||||
<Box paddingRight={2}>
|
||||
<MessageResponse>{body}</MessageResponse>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,30 +1,18 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React from 'react';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import React from 'react'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
|
||||
type Props = {
|
||||
addMargin: boolean;
|
||||
};
|
||||
export function AssistantRedactedThinkingMessage(t0) {
|
||||
const $ = _c(3);
|
||||
const {
|
||||
addMargin: t1
|
||||
} = t0;
|
||||
const addMargin = t1 === undefined ? false : t1;
|
||||
const t2 = addMargin ? 1 : 0;
|
||||
let t3;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = <Text dimColor={true} italic={true}>✻ Thinking…</Text>;
|
||||
$[0] = t3;
|
||||
} else {
|
||||
t3 = $[0];
|
||||
}
|
||||
let t4;
|
||||
if ($[1] !== t2) {
|
||||
t4 = <Box marginTop={t2}>{t3}</Box>;
|
||||
$[1] = t2;
|
||||
$[2] = t4;
|
||||
} else {
|
||||
t4 = $[2];
|
||||
}
|
||||
return t4;
|
||||
addMargin: boolean
|
||||
}
|
||||
|
||||
export function AssistantRedactedThinkingMessage({
|
||||
addMargin = false,
|
||||
}: Props): React.ReactNode {
|
||||
return (
|
||||
<Box marginTop={addMargin ? 1 : 0}>
|
||||
<Text dimColor italic>
|
||||
✻ Thinking…
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,269 +1,222 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs';
|
||||
import React, { useContext } from 'react';
|
||||
import { ERROR_MESSAGE_USER_ABORT } from 'src/services/compact/compact.js';
|
||||
import { isRateLimitErrorMessage } from 'src/services/rateLimitMessages.js';
|
||||
import { BLACK_CIRCLE } from '../../constants/figures.js';
|
||||
import { Box, NoSelect, Text } from '../../ink.js';
|
||||
import { API_ERROR_MESSAGE_PREFIX, API_TIMEOUT_ERROR_MESSAGE, CREDIT_BALANCE_TOO_LOW_ERROR_MESSAGE, CUSTOM_OFF_SWITCH_MESSAGE, INVALID_API_KEY_ERROR_MESSAGE, INVALID_API_KEY_ERROR_MESSAGE_EXTERNAL, ORG_DISABLED_ERROR_MESSAGE_ENV_KEY, ORG_DISABLED_ERROR_MESSAGE_ENV_KEY_WITH_OAUTH, PROMPT_TOO_LONG_ERROR_MESSAGE, startsWithApiErrorPrefix, TOKEN_REVOKED_ERROR_MESSAGE } from '../../services/api/errors.js';
|
||||
import { isEmptyMessageText, NO_RESPONSE_REQUESTED } from '../../utils/messages.js';
|
||||
import { getUpgradeMessage } from '../../utils/model/contextWindowUpgradeCheck.js';
|
||||
import { getDefaultSonnetModel, renderModelName } from '../../utils/model/model.js';
|
||||
import { isMacOsKeychainLocked } from '../../utils/secureStorage/macOsKeychainStorage.js';
|
||||
import { CtrlOToExpand } from '../CtrlOToExpand.js';
|
||||
import { InterruptedByUser } from '../InterruptedByUser.js';
|
||||
import { Markdown } from '../Markdown.js';
|
||||
import { MessageResponse } from '../MessageResponse.js';
|
||||
import { MessageActionsSelectedContext } from '../messageActions.js';
|
||||
import { RateLimitMessage } from './RateLimitMessage.js';
|
||||
const MAX_API_ERROR_CHARS = 1000;
|
||||
import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
|
||||
import React, { useContext } from 'react'
|
||||
import { ERROR_MESSAGE_USER_ABORT } from 'src/services/compact/compact.js'
|
||||
import { isRateLimitErrorMessage } from 'src/services/rateLimitMessages.js'
|
||||
import { BLACK_CIRCLE } from '../../constants/figures.js'
|
||||
import { Box, NoSelect, Text } from '../../ink.js'
|
||||
import {
|
||||
API_ERROR_MESSAGE_PREFIX,
|
||||
API_TIMEOUT_ERROR_MESSAGE,
|
||||
CREDIT_BALANCE_TOO_LOW_ERROR_MESSAGE,
|
||||
CUSTOM_OFF_SWITCH_MESSAGE,
|
||||
INVALID_API_KEY_ERROR_MESSAGE,
|
||||
INVALID_API_KEY_ERROR_MESSAGE_EXTERNAL,
|
||||
ORG_DISABLED_ERROR_MESSAGE_ENV_KEY,
|
||||
ORG_DISABLED_ERROR_MESSAGE_ENV_KEY_WITH_OAUTH,
|
||||
PROMPT_TOO_LONG_ERROR_MESSAGE,
|
||||
startsWithApiErrorPrefix,
|
||||
TOKEN_REVOKED_ERROR_MESSAGE,
|
||||
} from '../../services/api/errors.js'
|
||||
import {
|
||||
isEmptyMessageText,
|
||||
NO_RESPONSE_REQUESTED,
|
||||
} from '../../utils/messages.js'
|
||||
import { getUpgradeMessage } from '../../utils/model/contextWindowUpgradeCheck.js'
|
||||
import {
|
||||
getDefaultSonnetModel,
|
||||
renderModelName,
|
||||
} from '../../utils/model/model.js'
|
||||
import { isMacOsKeychainLocked } from '../../utils/secureStorage/macOsKeychainStorage.js'
|
||||
import { CtrlOToExpand } from '../CtrlOToExpand.js'
|
||||
import { InterruptedByUser } from '../InterruptedByUser.js'
|
||||
import { Markdown } from '../Markdown.js'
|
||||
import { MessageResponse } from '../MessageResponse.js'
|
||||
import { MessageActionsSelectedContext } from '../messageActions.js'
|
||||
import { RateLimitMessage } from './RateLimitMessage.js'
|
||||
|
||||
const MAX_API_ERROR_CHARS = 1000
|
||||
|
||||
type Props = {
|
||||
param: TextBlockParam;
|
||||
addMargin: boolean;
|
||||
shouldShowDot: boolean;
|
||||
verbose: boolean;
|
||||
width?: number | string;
|
||||
onOpenRateLimitOptions?: () => void;
|
||||
};
|
||||
function InvalidApiKeyMessage() {
|
||||
const $ = _c(2);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = isMacOsKeychainLocked();
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
const isKeychainLocked = t0;
|
||||
let t1;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <MessageResponse><Box flexDirection="column"><Text color="error">{INVALID_API_KEY_ERROR_MESSAGE}</Text>{isKeychainLocked && <Text dimColor={true}>· Run in another terminal: security unlock-keychain</Text>}</Box></MessageResponse>;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
return t1;
|
||||
param: TextBlockParam
|
||||
addMargin: boolean
|
||||
shouldShowDot: boolean
|
||||
verbose: boolean
|
||||
width?: number | string
|
||||
onOpenRateLimitOptions?: () => void
|
||||
}
|
||||
export function AssistantTextMessage(t0) {
|
||||
const $ = _c(34);
|
||||
const {
|
||||
param: t1,
|
||||
|
||||
function InvalidApiKeyMessage(): React.ReactNode {
|
||||
const isKeychainLocked = isMacOsKeychainLocked()
|
||||
|
||||
return (
|
||||
<MessageResponse>
|
||||
<Box flexDirection="column">
|
||||
<Text color="error">{INVALID_API_KEY_ERROR_MESSAGE}</Text>
|
||||
{isKeychainLocked && (
|
||||
<Text dimColor>
|
||||
· Run in another terminal: security unlock-keychain
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</MessageResponse>
|
||||
)
|
||||
}
|
||||
|
||||
export function AssistantTextMessage({
|
||||
param: { text },
|
||||
addMargin,
|
||||
shouldShowDot,
|
||||
verbose,
|
||||
onOpenRateLimitOptions
|
||||
} = t0;
|
||||
const {
|
||||
text
|
||||
} = t1;
|
||||
const isSelected = useContext(MessageActionsSelectedContext);
|
||||
onOpenRateLimitOptions,
|
||||
}: Props): React.ReactNode {
|
||||
const isSelected = useContext(MessageActionsSelectedContext)
|
||||
if (isEmptyMessageText(text)) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
// Handle all rate limit error messages from getRateLimitErrorMessage
|
||||
// Use the exported function to avoid fragile string coupling
|
||||
if (isRateLimitErrorMessage(text)) {
|
||||
let t2;
|
||||
if ($[0] !== onOpenRateLimitOptions || $[1] !== text) {
|
||||
t2 = <RateLimitMessage text={text} onOpenRateLimitOptions={onOpenRateLimitOptions} />;
|
||||
$[0] = onOpenRateLimitOptions;
|
||||
$[1] = text;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
return t2;
|
||||
return (
|
||||
<RateLimitMessage
|
||||
text={text}
|
||||
onOpenRateLimitOptions={onOpenRateLimitOptions}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
switch (text) {
|
||||
// Local JSX commands don't need a response, but we still want Claude to see them
|
||||
// Tool results render their own interrupt messages
|
||||
case NO_RESPONSE_REQUESTED:
|
||||
{
|
||||
return null;
|
||||
}
|
||||
case PROMPT_TOO_LONG_ERROR_MESSAGE:
|
||||
{
|
||||
let t2;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = getUpgradeMessage("warning");
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
const upgradeHint = t2;
|
||||
let t3;
|
||||
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = <MessageResponse height={1}><Text color="error">Context limit reached · /compact or /clear to continue{upgradeHint ? ` · ${upgradeHint}` : ""}</Text></MessageResponse>;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
return t3;
|
||||
return null
|
||||
|
||||
case PROMPT_TOO_LONG_ERROR_MESSAGE: {
|
||||
const upgradeHint = getUpgradeMessage('warning')
|
||||
return (
|
||||
<MessageResponse height={1}>
|
||||
<Text color="error">
|
||||
Context limit reached · /compact or /clear to continue
|
||||
{upgradeHint ? ` · ${upgradeHint}` : ''}
|
||||
</Text>
|
||||
</MessageResponse>
|
||||
)
|
||||
}
|
||||
|
||||
case CREDIT_BALANCE_TOO_LOW_ERROR_MESSAGE:
|
||||
{
|
||||
let t2;
|
||||
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <MessageResponse height={1}><Text color="error">Credit balance too low · Add funds: https://platform.claude.com/settings/billing</Text></MessageResponse>;
|
||||
$[5] = t2;
|
||||
} else {
|
||||
t2 = $[5];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
return (
|
||||
<MessageResponse height={1}>
|
||||
<Text color="error">
|
||||
Credit balance too low · Add funds:
|
||||
https://platform.claude.com/settings/billing
|
||||
</Text>
|
||||
</MessageResponse>
|
||||
)
|
||||
|
||||
case INVALID_API_KEY_ERROR_MESSAGE:
|
||||
{
|
||||
let t2;
|
||||
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <InvalidApiKeyMessage />;
|
||||
$[6] = t2;
|
||||
} else {
|
||||
t2 = $[6];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
return <InvalidApiKeyMessage />
|
||||
|
||||
case INVALID_API_KEY_ERROR_MESSAGE_EXTERNAL:
|
||||
{
|
||||
let t2;
|
||||
if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <MessageResponse height={1}><Text color="error">{INVALID_API_KEY_ERROR_MESSAGE_EXTERNAL}</Text></MessageResponse>;
|
||||
$[7] = t2;
|
||||
} else {
|
||||
t2 = $[7];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
return (
|
||||
<MessageResponse height={1}>
|
||||
<Text color="error">{INVALID_API_KEY_ERROR_MESSAGE_EXTERNAL}</Text>
|
||||
</MessageResponse>
|
||||
)
|
||||
|
||||
case ORG_DISABLED_ERROR_MESSAGE_ENV_KEY:
|
||||
case ORG_DISABLED_ERROR_MESSAGE_ENV_KEY_WITH_OAUTH:
|
||||
{
|
||||
let t2;
|
||||
if ($[8] !== text) {
|
||||
t2 = <MessageResponse><Text color="error">{text}</Text></MessageResponse>;
|
||||
$[8] = text;
|
||||
$[9] = t2;
|
||||
} else {
|
||||
t2 = $[9];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
return (
|
||||
<MessageResponse>
|
||||
<Text color="error">{text}</Text>
|
||||
</MessageResponse>
|
||||
)
|
||||
|
||||
case TOKEN_REVOKED_ERROR_MESSAGE:
|
||||
{
|
||||
let t2;
|
||||
if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <MessageResponse height={1}><Text color="error">{TOKEN_REVOKED_ERROR_MESSAGE}</Text></MessageResponse>;
|
||||
$[10] = t2;
|
||||
} else {
|
||||
t2 = $[10];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
return (
|
||||
<MessageResponse height={1}>
|
||||
<Text color="error">{TOKEN_REVOKED_ERROR_MESSAGE}</Text>
|
||||
</MessageResponse>
|
||||
)
|
||||
|
||||
case API_TIMEOUT_ERROR_MESSAGE:
|
||||
{
|
||||
let t2;
|
||||
if ($[11] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <MessageResponse height={1}><Text color="error">{API_TIMEOUT_ERROR_MESSAGE}{process.env.API_TIMEOUT_MS && <>{" "}(API_TIMEOUT_MS={process.env.API_TIMEOUT_MS}ms, try increasing it)</>}</Text></MessageResponse>;
|
||||
$[11] = t2;
|
||||
} else {
|
||||
t2 = $[11];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
return (
|
||||
<MessageResponse height={1}>
|
||||
<Text color="error">
|
||||
{API_TIMEOUT_ERROR_MESSAGE}
|
||||
{process.env.API_TIMEOUT_MS && (
|
||||
<>
|
||||
{' '}
|
||||
(API_TIMEOUT_MS={process.env.API_TIMEOUT_MS}ms, try increasing
|
||||
it)
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
</MessageResponse>
|
||||
)
|
||||
|
||||
case CUSTOM_OFF_SWITCH_MESSAGE:
|
||||
{
|
||||
let t2;
|
||||
if ($[12] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <Text color="error">We are experiencing high demand for Opus 4.</Text>;
|
||||
$[12] = t2;
|
||||
} else {
|
||||
t2 = $[12];
|
||||
}
|
||||
let t3;
|
||||
if ($[13] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = <MessageResponse><Box flexDirection="column" gap={1}>{t2}<Text>To continue immediately, use /model to switch to{" "}{renderModelName(getDefaultSonnetModel())} and continue coding.</Text></Box></MessageResponse>;
|
||||
$[13] = t3;
|
||||
} else {
|
||||
t3 = $[13];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
return (
|
||||
<MessageResponse>
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Text color="error">
|
||||
We are experiencing high demand for Opus 4.
|
||||
</Text>
|
||||
<Text>
|
||||
To continue immediately, use /model to switch to{' '}
|
||||
{renderModelName(getDefaultSonnetModel())} and continue coding.
|
||||
</Text>
|
||||
</Box>
|
||||
</MessageResponse>
|
||||
)
|
||||
|
||||
// TODO: Move this to a user turn
|
||||
case ERROR_MESSAGE_USER_ABORT:
|
||||
{
|
||||
let t2;
|
||||
if ($[14] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <MessageResponse height={1}><InterruptedByUser /></MessageResponse>;
|
||||
$[14] = t2;
|
||||
} else {
|
||||
t2 = $[14];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
return (
|
||||
<MessageResponse height={1}>
|
||||
<InterruptedByUser />
|
||||
</MessageResponse>
|
||||
)
|
||||
|
||||
default:
|
||||
{
|
||||
if (startsWithApiErrorPrefix(text)) {
|
||||
const truncated = !verbose && text.length > MAX_API_ERROR_CHARS;
|
||||
const t2 = text === API_ERROR_MESSAGE_PREFIX ? `${API_ERROR_MESSAGE_PREFIX}: Please wait a moment and try again.` : truncated ? text.slice(0, MAX_API_ERROR_CHARS) + "\u2026" : text;
|
||||
let t3;
|
||||
if ($[15] !== t2) {
|
||||
t3 = <Text color="error">{t2}</Text>;
|
||||
$[15] = t2;
|
||||
$[16] = t3;
|
||||
} else {
|
||||
t3 = $[16];
|
||||
}
|
||||
let t4;
|
||||
if ($[17] !== truncated) {
|
||||
t4 = truncated && <CtrlOToExpand />;
|
||||
$[17] = truncated;
|
||||
$[18] = t4;
|
||||
} else {
|
||||
t4 = $[18];
|
||||
}
|
||||
let t5;
|
||||
if ($[19] !== t3 || $[20] !== t4) {
|
||||
t5 = <MessageResponse><Box flexDirection="column">{t3}{t4}</Box></MessageResponse>;
|
||||
$[19] = t3;
|
||||
$[20] = t4;
|
||||
$[21] = t5;
|
||||
} else {
|
||||
t5 = $[21];
|
||||
}
|
||||
return t5;
|
||||
}
|
||||
const t2 = addMargin ? 1 : 0;
|
||||
const t3 = isSelected ? "messageActionsBackground" : undefined;
|
||||
let t4;
|
||||
if ($[22] !== isSelected || $[23] !== shouldShowDot) {
|
||||
t4 = shouldShowDot && <NoSelect fromLeftEdge={true} minWidth={2}><Text color={isSelected ? "suggestion" : "text"}>{BLACK_CIRCLE}</Text></NoSelect>;
|
||||
$[22] = isSelected;
|
||||
$[23] = shouldShowDot;
|
||||
$[24] = t4;
|
||||
} else {
|
||||
t4 = $[24];
|
||||
}
|
||||
let t5;
|
||||
if ($[25] !== text) {
|
||||
t5 = <Box flexDirection="column"><Markdown>{text}</Markdown></Box>;
|
||||
$[25] = text;
|
||||
$[26] = t5;
|
||||
} else {
|
||||
t5 = $[26];
|
||||
}
|
||||
let t6;
|
||||
if ($[27] !== t4 || $[28] !== t5) {
|
||||
t6 = <Box flexDirection="row">{t4}{t5}</Box>;
|
||||
$[27] = t4;
|
||||
$[28] = t5;
|
||||
$[29] = t6;
|
||||
} else {
|
||||
t6 = $[29];
|
||||
}
|
||||
let t7;
|
||||
if ($[30] !== t2 || $[31] !== t3 || $[32] !== t6) {
|
||||
t7 = <Box alignItems="flex-start" flexDirection="row" justifyContent="space-between" marginTop={t2} width="100%" backgroundColor={t3}>{t6}</Box>;
|
||||
$[30] = t2;
|
||||
$[31] = t3;
|
||||
$[32] = t6;
|
||||
$[33] = t7;
|
||||
} else {
|
||||
t7 = $[33];
|
||||
}
|
||||
return t7;
|
||||
const truncated = !verbose && text.length > MAX_API_ERROR_CHARS
|
||||
return (
|
||||
<MessageResponse>
|
||||
<Box flexDirection="column">
|
||||
<Text color="error">
|
||||
{text === API_ERROR_MESSAGE_PREFIX
|
||||
? `${API_ERROR_MESSAGE_PREFIX}: Please wait a moment and try again.`
|
||||
: truncated
|
||||
? text.slice(0, MAX_API_ERROR_CHARS) + '…'
|
||||
: text}
|
||||
</Text>
|
||||
{truncated && <CtrlOToExpand />}
|
||||
</Box>
|
||||
</MessageResponse>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Box
|
||||
alignItems="flex-start"
|
||||
flexDirection="row"
|
||||
justifyContent="space-between"
|
||||
marginTop={addMargin ? 1 : 0}
|
||||
width="100%"
|
||||
backgroundColor={isSelected ? 'messageActionsBackground' : undefined}
|
||||
>
|
||||
<Box flexDirection="row">
|
||||
{shouldShowDot && (
|
||||
<NoSelect fromLeftEdge minWidth={2}>
|
||||
<Text color={isSelected ? 'suggestion' : 'text'}>
|
||||
{BLACK_CIRCLE}
|
||||
</Text>
|
||||
</NoSelect>
|
||||
)}
|
||||
<Box flexDirection="column">
|
||||
<Markdown>{text}</Markdown>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,85 +1,66 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import type { ThinkingBlock, ThinkingBlockParam } from '@anthropic-ai/sdk/resources/index.mjs';
|
||||
import React from 'react';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { CtrlOToExpand } from '../CtrlOToExpand.js';
|
||||
import { Markdown } from '../Markdown.js';
|
||||
import type {
|
||||
ThinkingBlock,
|
||||
ThinkingBlockParam,
|
||||
} from '@anthropic-ai/sdk/resources/index.mjs'
|
||||
import React from 'react'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { CtrlOToExpand } from '../CtrlOToExpand.js'
|
||||
import { Markdown } from '../Markdown.js'
|
||||
|
||||
type Props = {
|
||||
// Accept either full ThinkingBlock/ThinkingBlockParam or a minimal shape with just type and thinking
|
||||
param: ThinkingBlock | ThinkingBlockParam | {
|
||||
type: 'thinking';
|
||||
thinking: string;
|
||||
};
|
||||
addMargin: boolean;
|
||||
isTranscriptMode: boolean;
|
||||
verbose: boolean;
|
||||
param:
|
||||
| ThinkingBlock
|
||||
| ThinkingBlockParam
|
||||
| { type: 'thinking'; thinking: string }
|
||||
addMargin: boolean
|
||||
isTranscriptMode: boolean
|
||||
verbose: boolean
|
||||
/** When true, hide this thinking block entirely (used for past thinking in transcript mode) */
|
||||
hideInTranscript?: boolean;
|
||||
};
|
||||
export function AssistantThinkingMessage(t0) {
|
||||
const $ = _c(9);
|
||||
const {
|
||||
param: t1,
|
||||
addMargin: t2,
|
||||
hideInTranscript?: boolean
|
||||
}
|
||||
|
||||
export function AssistantThinkingMessage({
|
||||
param: { thinking },
|
||||
addMargin = false,
|
||||
isTranscriptMode,
|
||||
verbose,
|
||||
hideInTranscript: t3
|
||||
} = t0;
|
||||
const {
|
||||
thinking
|
||||
} = t1;
|
||||
const addMargin = t2 === undefined ? false : t2;
|
||||
const hideInTranscript = t3 === undefined ? false : t3;
|
||||
hideInTranscript = false,
|
||||
}: Props): React.ReactNode {
|
||||
if (!thinking) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
if (hideInTranscript) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
const shouldShowFullThinking = isTranscriptMode || verbose;
|
||||
|
||||
const shouldShowFullThinking = isTranscriptMode || verbose
|
||||
const label = '∴ Thinking'
|
||||
|
||||
if (!shouldShowFullThinking) {
|
||||
const t4 = addMargin ? 1 : 0;
|
||||
let t5;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t5 = <Text dimColor={true} italic={true}>{"\u2234 Thinking"} <CtrlOToExpand /></Text>;
|
||||
$[0] = t5;
|
||||
} else {
|
||||
t5 = $[0];
|
||||
return (
|
||||
<Box marginTop={addMargin ? 1 : 0}>
|
||||
<Text dimColor italic>
|
||||
{label} <CtrlOToExpand />
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
let t6;
|
||||
if ($[1] !== t4) {
|
||||
t6 = <Box marginTop={t4}>{t5}</Box>;
|
||||
$[1] = t4;
|
||||
$[2] = t6;
|
||||
} else {
|
||||
t6 = $[2];
|
||||
}
|
||||
return t6;
|
||||
}
|
||||
const t4 = addMargin ? 1 : 0;
|
||||
let t5;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t5 = <Text dimColor={true} italic={true}>{"\u2234 Thinking"}…</Text>;
|
||||
$[3] = t5;
|
||||
} else {
|
||||
t5 = $[3];
|
||||
}
|
||||
let t6;
|
||||
if ($[4] !== thinking) {
|
||||
t6 = <Box paddingLeft={2}><Markdown dimColor={true}>{thinking}</Markdown></Box>;
|
||||
$[4] = thinking;
|
||||
$[5] = t6;
|
||||
} else {
|
||||
t6 = $[5];
|
||||
}
|
||||
let t7;
|
||||
if ($[6] !== t4 || $[7] !== t6) {
|
||||
t7 = <Box flexDirection="column" gap={1} marginTop={t4} width="100%">{t5}{t6}</Box>;
|
||||
$[6] = t4;
|
||||
$[7] = t6;
|
||||
$[8] = t7;
|
||||
} else {
|
||||
t7 = $[8];
|
||||
}
|
||||
return t7;
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
gap={1}
|
||||
marginTop={addMargin ? 1 : 0}
|
||||
width="100%"
|
||||
>
|
||||
<Text dimColor italic>
|
||||
{label}…
|
||||
</Text>
|
||||
<Box paddingLeft={2}>
|
||||
<Markdown dimColor>{thinking}</Markdown>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,40 +1,44 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import type { ToolUseBlockParam } from '@anthropic-ai/sdk/resources/index.mjs';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTerminalSize } from 'src/hooks/useTerminalSize.js';
|
||||
import type { ThemeName } from 'src/utils/theme.js';
|
||||
import type { Command } from '../../commands.js';
|
||||
import { BLACK_CIRCLE } from '../../constants/figures.js';
|
||||
import { stringWidth } from '../../ink/stringWidth.js';
|
||||
import { Box, Text, useTheme } from '../../ink.js';
|
||||
import { useAppStateMaybeOutsideOfProvider } from '../../state/AppState.js';
|
||||
import { findToolByName, type Tool, type ToolProgressData, type Tools } from '../../Tool.js';
|
||||
import type { ProgressMessage } from '../../types/message.js';
|
||||
import { useIsClassifierChecking } from '../../utils/classifierApprovalsHook.js';
|
||||
import { logError } from '../../utils/log.js';
|
||||
import type { buildMessageLookups } from '../../utils/messages.js';
|
||||
import { MessageResponse } from '../MessageResponse.js';
|
||||
import { useSelectedMessageBg } from '../messageActions.js';
|
||||
import { SentryErrorBoundary } from '../SentryErrorBoundary.js';
|
||||
import { ToolUseLoader } from '../ToolUseLoader.js';
|
||||
import { HookProgressMessage } from './HookProgressMessage.js';
|
||||
import type { ToolUseBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
|
||||
import React, { useMemo } from 'react'
|
||||
import { useTerminalSize } from 'src/hooks/useTerminalSize.js'
|
||||
import type { ThemeName } from 'src/utils/theme.js'
|
||||
import type { Command } from '../../commands.js'
|
||||
import { BLACK_CIRCLE } from '../../constants/figures.js'
|
||||
import { stringWidth } from '../../ink/stringWidth.js'
|
||||
import { Box, Text, useTheme } from '../../ink.js'
|
||||
import { useAppStateMaybeOutsideOfProvider } from '../../state/AppState.js'
|
||||
import {
|
||||
findToolByName,
|
||||
type Tool,
|
||||
type ToolProgressData,
|
||||
type Tools,
|
||||
} from '../../Tool.js'
|
||||
import type { ProgressMessage } from '../../types/message.js'
|
||||
import { useIsClassifierChecking } from '../../utils/classifierApprovalsHook.js'
|
||||
import { logError } from '../../utils/log.js'
|
||||
import type { buildMessageLookups } from '../../utils/messages.js'
|
||||
import { MessageResponse } from '../MessageResponse.js'
|
||||
import { useSelectedMessageBg } from '../messageActions.js'
|
||||
import { SentryErrorBoundary } from '../SentryErrorBoundary.js'
|
||||
import { ToolUseLoader } from '../ToolUseLoader.js'
|
||||
import { HookProgressMessage } from './HookProgressMessage.js'
|
||||
|
||||
type Props = {
|
||||
param: ToolUseBlockParam;
|
||||
addMargin: boolean;
|
||||
tools: Tools;
|
||||
commands: Command[];
|
||||
verbose: boolean;
|
||||
inProgressToolUseIDs: Set<string>;
|
||||
progressMessagesForMessage: ProgressMessage[];
|
||||
shouldAnimate: boolean;
|
||||
shouldShowDot: boolean;
|
||||
inProgressToolCallCount?: number;
|
||||
lookups: ReturnType<typeof buildMessageLookups>;
|
||||
isTranscriptMode?: boolean;
|
||||
};
|
||||
export function AssistantToolUseMessage(t0) {
|
||||
const $ = _c(81);
|
||||
const {
|
||||
param: ToolUseBlockParam
|
||||
addMargin: boolean
|
||||
tools: Tools
|
||||
commands: Command[]
|
||||
verbose: boolean
|
||||
inProgressToolUseIDs: Set<string>
|
||||
progressMessagesForMessage: ProgressMessage[]
|
||||
shouldAnimate: boolean
|
||||
shouldShowDot: boolean
|
||||
inProgressToolCallCount?: number
|
||||
lookups: ReturnType<typeof buildMessageLookups>
|
||||
isTranscriptMode?: boolean
|
||||
}
|
||||
|
||||
export function AssistantToolUseMessage({
|
||||
param,
|
||||
addMargin,
|
||||
tools,
|
||||
@@ -46,322 +50,277 @@ export function AssistantToolUseMessage(t0) {
|
||||
shouldShowDot,
|
||||
inProgressToolCallCount,
|
||||
lookups,
|
||||
isTranscriptMode
|
||||
} = t0;
|
||||
const terminalSize = useTerminalSize();
|
||||
const [theme] = useTheme();
|
||||
const bg = useSelectedMessageBg();
|
||||
const pendingWorkerRequest = useAppStateMaybeOutsideOfProvider(_temp);
|
||||
const isClassifierCheckingRaw = useIsClassifierChecking(param.id);
|
||||
const permissionMode = useAppStateMaybeOutsideOfProvider(_temp2);
|
||||
const hasStrippedRules = useAppStateMaybeOutsideOfProvider(_temp3);
|
||||
const isAutoClassifier = permissionMode === "auto" || permissionMode === "plan" && hasStrippedRules;
|
||||
const isClassifierChecking = false && isClassifierCheckingRaw && permissionMode !== "auto";
|
||||
let t1;
|
||||
if ($[0] !== param.input || $[1] !== param.name || $[2] !== tools) {
|
||||
bb0: {
|
||||
if (!tools) {
|
||||
t1 = null;
|
||||
break bb0;
|
||||
}
|
||||
const tool = findToolByName(tools, param.name);
|
||||
if (!tool) {
|
||||
t1 = null;
|
||||
break bb0;
|
||||
}
|
||||
const input = tool.inputSchema.safeParse(param.input);
|
||||
const data = input.success ? input.data : undefined;
|
||||
t1 = {
|
||||
isTranscriptMode,
|
||||
}: Props): React.ReactNode {
|
||||
const terminalSize = useTerminalSize()
|
||||
const [theme] = useTheme()
|
||||
const bg = useSelectedMessageBg()
|
||||
const pendingWorkerRequest = useAppStateMaybeOutsideOfProvider(
|
||||
state => state.pendingWorkerRequest,
|
||||
)
|
||||
const isClassifierCheckingRaw = useIsClassifierChecking(param.id)
|
||||
const permissionMode = useAppStateMaybeOutsideOfProvider(
|
||||
state => state.toolPermissionContext.mode,
|
||||
)
|
||||
// strippedDangerousRules is set by stripDangerousPermissionsForAutoMode
|
||||
// (even to {}) whenever auto is active, and cleared by restoreDangerousPermissions
|
||||
// on deactivation — a reliable proxy for isAutoModeActive() during plan.
|
||||
// prePlanMode would be stale after transitionPlanAutoMode deactivates mid-plan.
|
||||
const hasStrippedRules = useAppStateMaybeOutsideOfProvider(
|
||||
state => !!state.toolPermissionContext.strippedDangerousRules,
|
||||
)
|
||||
const isAutoClassifier =
|
||||
permissionMode === 'auto' || (permissionMode === 'plan' && hasStrippedRules)
|
||||
const isClassifierChecking =
|
||||
process.env.USER_TYPE === 'ant' &&
|
||||
isClassifierCheckingRaw &&
|
||||
permissionMode !== 'auto'
|
||||
|
||||
// Memoize on param identity (stable — from the persisted message object).
|
||||
// Zod safeParse allocates per call, and some tools' userFacingName()
|
||||
// (BashTool → shouldUseSandbox → shell-quote parse) are expensive. Without
|
||||
// this, ~50 bash messages × shell-quote-per-render pushed transition
|
||||
// render past the shimmer tick → abort → infinite retry (#21605).
|
||||
const parsed = useMemo(() => {
|
||||
if (!tools) return null
|
||||
const tool = findToolByName(tools, param.name)
|
||||
if (!tool) return null
|
||||
const input = tool.inputSchema.safeParse(param.input)
|
||||
const data = input.success ? input.data : undefined
|
||||
return {
|
||||
tool,
|
||||
input,
|
||||
userFacingToolName: tool.userFacingName(data),
|
||||
userFacingToolNameBackgroundColor: tool.userFacingNameBackgroundColor?.(data),
|
||||
isTransparentWrapper: tool.isTransparentWrapper?.() ?? false
|
||||
};
|
||||
userFacingToolNameBackgroundColor:
|
||||
tool.userFacingNameBackgroundColor?.(data),
|
||||
isTransparentWrapper: tool.isTransparentWrapper?.() ?? false,
|
||||
}
|
||||
$[0] = param.input;
|
||||
$[1] = param.name;
|
||||
$[2] = tools;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
const parsed = t1;
|
||||
}, [tools, param])
|
||||
|
||||
if (!parsed) {
|
||||
logError(new Error(tools ? `Tool ${param.name} not found` : `Tools array is undefined for tool ${param.name}`));
|
||||
return null;
|
||||
// Guard against undefined tools (required prop) or unknown tool name
|
||||
logError(
|
||||
new Error(
|
||||
tools
|
||||
? `Tool ${param.name} not found`
|
||||
: `Tools array is undefined for tool ${param.name}`,
|
||||
),
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
const {
|
||||
tool: tool_0,
|
||||
input: input_0,
|
||||
tool,
|
||||
input,
|
||||
userFacingToolName,
|
||||
userFacingToolNameBackgroundColor,
|
||||
isTransparentWrapper
|
||||
} = parsed;
|
||||
let t2;
|
||||
if ($[4] !== lookups.resolvedToolUseIDs || $[5] !== param.id) {
|
||||
t2 = lookups.resolvedToolUseIDs.has(param.id);
|
||||
$[4] = lookups.resolvedToolUseIDs;
|
||||
$[5] = param.id;
|
||||
$[6] = t2;
|
||||
} else {
|
||||
t2 = $[6];
|
||||
}
|
||||
const isResolved = t2;
|
||||
let t3;
|
||||
if ($[7] !== inProgressToolUseIDs || $[8] !== isResolved || $[9] !== param.id) {
|
||||
t3 = !inProgressToolUseIDs.has(param.id) && !isResolved;
|
||||
$[7] = inProgressToolUseIDs;
|
||||
$[8] = isResolved;
|
||||
$[9] = param.id;
|
||||
$[10] = t3;
|
||||
} else {
|
||||
t3 = $[10];
|
||||
}
|
||||
const isQueued = t3;
|
||||
const isWaitingForPermission = pendingWorkerRequest?.toolUseId === param.id;
|
||||
isTransparentWrapper,
|
||||
} = parsed
|
||||
|
||||
const isResolved = lookups.resolvedToolUseIDs.has(param.id)
|
||||
const isQueued = !inProgressToolUseIDs.has(param.id) && !isResolved
|
||||
const isWaitingForPermission = pendingWorkerRequest?.toolUseId === param.id
|
||||
|
||||
if (isTransparentWrapper) {
|
||||
if (isQueued || isResolved) {
|
||||
return null;
|
||||
if (isQueued || isResolved) return null
|
||||
return (
|
||||
<Box flexDirection="column" width="100%" backgroundColor={bg}>
|
||||
{renderToolUseProgressMessage(
|
||||
tool,
|
||||
tools,
|
||||
lookups,
|
||||
param.id,
|
||||
progressMessagesForMessage,
|
||||
{ verbose, inProgressToolCallCount, isTranscriptMode },
|
||||
terminalSize,
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
let t4;
|
||||
if ($[11] !== inProgressToolCallCount || $[12] !== isTranscriptMode || $[13] !== lookups || $[14] !== param.id || $[15] !== progressMessagesForMessage || $[16] !== terminalSize || $[17] !== tool_0 || $[18] !== tools || $[19] !== verbose) {
|
||||
t4 = renderToolUseProgressMessage(tool_0, tools, lookups, param.id, progressMessagesForMessage, {
|
||||
verbose,
|
||||
inProgressToolCallCount,
|
||||
isTranscriptMode
|
||||
}, terminalSize);
|
||||
$[11] = inProgressToolCallCount;
|
||||
$[12] = isTranscriptMode;
|
||||
$[13] = lookups;
|
||||
$[14] = param.id;
|
||||
$[15] = progressMessagesForMessage;
|
||||
$[16] = terminalSize;
|
||||
$[17] = tool_0;
|
||||
$[18] = tools;
|
||||
$[19] = verbose;
|
||||
$[20] = t4;
|
||||
} else {
|
||||
t4 = $[20];
|
||||
|
||||
if (userFacingToolName === '') {
|
||||
return null
|
||||
}
|
||||
let t5;
|
||||
if ($[21] !== bg || $[22] !== t4) {
|
||||
t5 = <Box flexDirection="column" width="100%" backgroundColor={bg}>{t4}</Box>;
|
||||
$[21] = bg;
|
||||
$[22] = t4;
|
||||
$[23] = t5;
|
||||
} else {
|
||||
t5 = $[23];
|
||||
}
|
||||
return t5;
|
||||
}
|
||||
if (userFacingToolName === "") {
|
||||
return null;
|
||||
}
|
||||
let t4;
|
||||
if ($[24] !== commands || $[25] !== input_0.data || $[26] !== input_0.success || $[27] !== theme || $[28] !== tool_0 || $[29] !== verbose) {
|
||||
t4 = input_0.success ? renderToolUseMessage(tool_0, input_0.data, {
|
||||
theme,
|
||||
verbose,
|
||||
commands
|
||||
}) : null;
|
||||
$[24] = commands;
|
||||
$[25] = input_0.data;
|
||||
$[26] = input_0.success;
|
||||
$[27] = theme;
|
||||
$[28] = tool_0;
|
||||
$[29] = verbose;
|
||||
$[30] = t4;
|
||||
} else {
|
||||
t4 = $[30];
|
||||
}
|
||||
const renderedToolUseMessage = t4;
|
||||
|
||||
const renderedToolUseMessage = input.success
|
||||
? renderToolUseMessage(tool, input.data, { theme, verbose, commands })
|
||||
: null
|
||||
if (renderedToolUseMessage === null) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
const t5 = addMargin ? 1 : 0;
|
||||
const t6 = stringWidth(userFacingToolName) + (shouldShowDot ? 2 : 0);
|
||||
let t7;
|
||||
if ($[31] !== isQueued || $[32] !== isResolved || $[33] !== lookups.erroredToolUseIDs || $[34] !== param.id || $[35] !== shouldAnimate || $[36] !== shouldShowDot) {
|
||||
t7 = shouldShowDot && (isQueued ? <Box minWidth={2}><Text dimColor={isQueued}>{BLACK_CIRCLE}</Text></Box> : <ToolUseLoader shouldAnimate={shouldAnimate} isUnresolved={!isResolved} isError={lookups.erroredToolUseIDs.has(param.id)} />);
|
||||
$[31] = isQueued;
|
||||
$[32] = isResolved;
|
||||
$[33] = lookups.erroredToolUseIDs;
|
||||
$[34] = param.id;
|
||||
$[35] = shouldAnimate;
|
||||
$[36] = shouldShowDot;
|
||||
$[37] = t7;
|
||||
} else {
|
||||
t7 = $[37];
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="row"
|
||||
justifyContent="space-between"
|
||||
marginTop={addMargin ? 1 : 0}
|
||||
width="100%"
|
||||
backgroundColor={bg}
|
||||
>
|
||||
<Box flexDirection="column">
|
||||
<Box
|
||||
flexDirection="row"
|
||||
flexWrap="nowrap"
|
||||
minWidth={stringWidth(userFacingToolName) + (shouldShowDot ? 2 : 0)}
|
||||
>
|
||||
{shouldShowDot &&
|
||||
(isQueued ? (
|
||||
<Box minWidth={2}>
|
||||
<Text dimColor={isQueued}>{BLACK_CIRCLE}</Text>
|
||||
</Box>
|
||||
) : (
|
||||
// WARNING: The code here and in ToolUseLoader is particularly
|
||||
// sensitive to what *should* just be trivial refactorings. See
|
||||
// the comment in ToolUseLoader for more details.
|
||||
<ToolUseLoader
|
||||
shouldAnimate={shouldAnimate}
|
||||
isUnresolved={!isResolved}
|
||||
isError={lookups.erroredToolUseIDs.has(param.id)}
|
||||
/>
|
||||
))}
|
||||
<Box flexShrink={0}>
|
||||
<Text
|
||||
bold
|
||||
wrap="truncate-end"
|
||||
backgroundColor={userFacingToolNameBackgroundColor}
|
||||
color={
|
||||
userFacingToolNameBackgroundColor ? 'inverseText' : undefined
|
||||
}
|
||||
const t8 = userFacingToolNameBackgroundColor ? "inverseText" : undefined;
|
||||
let t9;
|
||||
if ($[38] !== t8 || $[39] !== userFacingToolName || $[40] !== userFacingToolNameBackgroundColor) {
|
||||
t9 = <Box flexShrink={0}><Text bold={true} wrap="truncate-end" backgroundColor={userFacingToolNameBackgroundColor} color={t8}>{userFacingToolName}</Text></Box>;
|
||||
$[38] = t8;
|
||||
$[39] = userFacingToolName;
|
||||
$[40] = userFacingToolNameBackgroundColor;
|
||||
$[41] = t9;
|
||||
} else {
|
||||
t9 = $[41];
|
||||
}
|
||||
let t10;
|
||||
if ($[42] !== renderedToolUseMessage) {
|
||||
t10 = renderedToolUseMessage !== "" && <Box flexWrap="nowrap"><Text>({renderedToolUseMessage})</Text></Box>;
|
||||
$[42] = renderedToolUseMessage;
|
||||
$[43] = t10;
|
||||
} else {
|
||||
t10 = $[43];
|
||||
}
|
||||
let t11;
|
||||
if ($[44] !== input_0.data || $[45] !== input_0.success || $[46] !== tool_0) {
|
||||
t11 = input_0.success && tool_0.renderToolUseTag && tool_0.renderToolUseTag(input_0.data);
|
||||
$[44] = input_0.data;
|
||||
$[45] = input_0.success;
|
||||
$[46] = tool_0;
|
||||
$[47] = t11;
|
||||
} else {
|
||||
t11 = $[47];
|
||||
}
|
||||
let t12;
|
||||
if ($[48] !== t10 || $[49] !== t11 || $[50] !== t6 || $[51] !== t7 || $[52] !== t9) {
|
||||
t12 = <Box flexDirection="row" flexWrap="nowrap" minWidth={t6}>{t7}{t9}{t10}{t11}</Box>;
|
||||
$[48] = t10;
|
||||
$[49] = t11;
|
||||
$[50] = t6;
|
||||
$[51] = t7;
|
||||
$[52] = t9;
|
||||
$[53] = t12;
|
||||
} else {
|
||||
t12 = $[53];
|
||||
}
|
||||
let t13;
|
||||
if ($[54] !== inProgressToolCallCount || $[55] !== isAutoClassifier || $[56] !== isClassifierChecking || $[57] !== isQueued || $[58] !== isResolved || $[59] !== isTranscriptMode || $[60] !== isWaitingForPermission || $[61] !== lookups || $[62] !== param.id || $[63] !== progressMessagesForMessage || $[64] !== terminalSize || $[65] !== tool_0 || $[66] !== tools || $[67] !== verbose) {
|
||||
t13 = !isResolved && !isQueued && (isClassifierChecking ? <MessageResponse height={1}><Text dimColor={true}>{isAutoClassifier ? "Auto classifier checking\u2026" : "Bash classifier checking\u2026"}</Text></MessageResponse> : isWaitingForPermission ? <MessageResponse height={1}><Text dimColor={true}>Waiting for permission…</Text></MessageResponse> : renderToolUseProgressMessage(tool_0, tools, lookups, param.id, progressMessagesForMessage, {
|
||||
>
|
||||
{userFacingToolName}
|
||||
</Text>
|
||||
</Box>
|
||||
{renderedToolUseMessage !== '' && (
|
||||
<Box flexWrap="nowrap">
|
||||
<Text>({renderedToolUseMessage})</Text>
|
||||
</Box>
|
||||
)}
|
||||
{/* Render tool-specific tags (timeout, model, resume ID, etc.) */}
|
||||
{input.success &&
|
||||
tool.renderToolUseTag &&
|
||||
tool.renderToolUseTag(input.data)}
|
||||
</Box>
|
||||
{!isResolved &&
|
||||
!isQueued &&
|
||||
(isClassifierChecking ? (
|
||||
<MessageResponse height={1}>
|
||||
<Text dimColor>
|
||||
{isAutoClassifier
|
||||
? 'Auto classifier checking\u2026'
|
||||
: 'Bash classifier checking\u2026'}
|
||||
</Text>
|
||||
</MessageResponse>
|
||||
) : isWaitingForPermission ? (
|
||||
<MessageResponse height={1}>
|
||||
<Text dimColor>Waiting for permission…</Text>
|
||||
</MessageResponse>
|
||||
) : (
|
||||
renderToolUseProgressMessage(
|
||||
tool,
|
||||
tools,
|
||||
lookups,
|
||||
param.id,
|
||||
progressMessagesForMessage,
|
||||
{
|
||||
verbose,
|
||||
inProgressToolCallCount,
|
||||
isTranscriptMode
|
||||
}, terminalSize));
|
||||
$[54] = inProgressToolCallCount;
|
||||
$[55] = isAutoClassifier;
|
||||
$[56] = isClassifierChecking;
|
||||
$[57] = isQueued;
|
||||
$[58] = isResolved;
|
||||
$[59] = isTranscriptMode;
|
||||
$[60] = isWaitingForPermission;
|
||||
$[61] = lookups;
|
||||
$[62] = param.id;
|
||||
$[63] = progressMessagesForMessage;
|
||||
$[64] = terminalSize;
|
||||
$[65] = tool_0;
|
||||
$[66] = tools;
|
||||
$[67] = verbose;
|
||||
$[68] = t13;
|
||||
} else {
|
||||
t13 = $[68];
|
||||
}
|
||||
let t14;
|
||||
if ($[69] !== isQueued || $[70] !== isResolved || $[71] !== tool_0) {
|
||||
t14 = !isResolved && isQueued && renderToolUseQueuedMessage(tool_0);
|
||||
$[69] = isQueued;
|
||||
$[70] = isResolved;
|
||||
$[71] = tool_0;
|
||||
$[72] = t14;
|
||||
} else {
|
||||
t14 = $[72];
|
||||
}
|
||||
let t15;
|
||||
if ($[73] !== t12 || $[74] !== t13 || $[75] !== t14) {
|
||||
t15 = <Box flexDirection="column">{t12}{t13}{t14}</Box>;
|
||||
$[73] = t12;
|
||||
$[74] = t13;
|
||||
$[75] = t14;
|
||||
$[76] = t15;
|
||||
} else {
|
||||
t15 = $[76];
|
||||
}
|
||||
let t16;
|
||||
if ($[77] !== bg || $[78] !== t15 || $[79] !== t5) {
|
||||
t16 = <Box flexDirection="row" justifyContent="space-between" marginTop={t5} width="100%" backgroundColor={bg}>{t15}</Box>;
|
||||
$[77] = bg;
|
||||
$[78] = t15;
|
||||
$[79] = t5;
|
||||
$[80] = t16;
|
||||
} else {
|
||||
t16 = $[80];
|
||||
}
|
||||
return t16;
|
||||
isTranscriptMode,
|
||||
},
|
||||
terminalSize,
|
||||
)
|
||||
))}
|
||||
{!isResolved && isQueued && renderToolUseQueuedMessage(tool)}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
function _temp3(state_1) {
|
||||
return !!state_1.toolPermissionContext.strippedDangerousRules;
|
||||
}
|
||||
function _temp2(state_0) {
|
||||
return state_0.toolPermissionContext.mode;
|
||||
}
|
||||
function _temp(state) {
|
||||
return state.pendingWorkerRequest;
|
||||
}
|
||||
function renderToolUseMessage(tool: Tool, input: unknown, {
|
||||
|
||||
function renderToolUseMessage(
|
||||
tool: Tool,
|
||||
input: unknown,
|
||||
{
|
||||
theme,
|
||||
verbose,
|
||||
commands
|
||||
}: {
|
||||
theme: ThemeName;
|
||||
verbose: boolean;
|
||||
commands: Command[];
|
||||
}): React.ReactNode {
|
||||
commands,
|
||||
}: { theme: ThemeName; verbose: boolean; commands: Command[] },
|
||||
): React.ReactNode {
|
||||
try {
|
||||
const parsed = tool.inputSchema.safeParse(input);
|
||||
const parsed = tool.inputSchema.safeParse(input)
|
||||
if (!parsed.success) {
|
||||
return '';
|
||||
return ''
|
||||
}
|
||||
return tool.renderToolUseMessage(parsed.data, {
|
||||
theme,
|
||||
verbose,
|
||||
commands
|
||||
});
|
||||
return tool.renderToolUseMessage(parsed.data, { theme, verbose, commands })
|
||||
} catch (error) {
|
||||
logError(new Error(`Error rendering tool use message for ${tool.name}: ${error}`));
|
||||
return '';
|
||||
logError(
|
||||
new Error(`Error rendering tool use message for ${tool.name}: ${error}`),
|
||||
)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
function renderToolUseProgressMessage(tool: Tool, tools: Tools, lookups: ReturnType<typeof buildMessageLookups>, toolUseID: string, progressMessagesForMessage: ProgressMessage[], {
|
||||
|
||||
function renderToolUseProgressMessage(
|
||||
tool: Tool,
|
||||
tools: Tools,
|
||||
lookups: ReturnType<typeof buildMessageLookups>,
|
||||
toolUseID: string,
|
||||
progressMessagesForMessage: ProgressMessage[],
|
||||
{
|
||||
verbose,
|
||||
inProgressToolCallCount,
|
||||
isTranscriptMode
|
||||
}: {
|
||||
verbose: boolean;
|
||||
inProgressToolCallCount?: number;
|
||||
isTranscriptMode?: boolean;
|
||||
}, terminalSize: {
|
||||
columns: number;
|
||||
rows: number;
|
||||
}): React.ReactNode {
|
||||
const toolProgressMessages = progressMessagesForMessage.filter((msg): msg is ProgressMessage<ToolProgressData> => (msg.data as { type?: string }).type !== 'hook_progress');
|
||||
isTranscriptMode,
|
||||
}: {
|
||||
verbose: boolean
|
||||
inProgressToolCallCount?: number
|
||||
isTranscriptMode?: boolean
|
||||
},
|
||||
terminalSize: { columns: number; rows: number },
|
||||
): React.ReactNode {
|
||||
const toolProgressMessages = progressMessagesForMessage.filter(
|
||||
(msg): msg is ProgressMessage<ToolProgressData> =>
|
||||
msg.data.type !== 'hook_progress',
|
||||
)
|
||||
try {
|
||||
const toolMessages = tool.renderToolUseProgressMessage?.(toolProgressMessages, {
|
||||
const toolMessages =
|
||||
tool.renderToolUseProgressMessage?.(toolProgressMessages, {
|
||||
tools,
|
||||
verbose,
|
||||
terminalSize,
|
||||
inProgressToolCallCount: inProgressToolCallCount ?? 1,
|
||||
isTranscriptMode
|
||||
}) ?? null;
|
||||
return <>
|
||||
isTranscriptMode,
|
||||
}) ?? null
|
||||
return (
|
||||
<>
|
||||
<SentryErrorBoundary>
|
||||
<HookProgressMessage hookEvent="PreToolUse" lookups={lookups} toolUseID={toolUseID} verbose={verbose} isTranscriptMode={isTranscriptMode} />
|
||||
<HookProgressMessage
|
||||
hookEvent="PreToolUse"
|
||||
lookups={lookups}
|
||||
toolUseID={toolUseID}
|
||||
verbose={verbose}
|
||||
isTranscriptMode={isTranscriptMode}
|
||||
/>
|
||||
</SentryErrorBoundary>
|
||||
{toolMessages}
|
||||
</>;
|
||||
</>
|
||||
)
|
||||
} catch (error) {
|
||||
logError(new Error(`Error rendering tool use progress message for ${tool.name}: ${error}`));
|
||||
return null;
|
||||
logError(
|
||||
new Error(
|
||||
`Error rendering tool use progress message for ${tool.name}: ${error}`,
|
||||
),
|
||||
)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function renderToolUseQueuedMessage(tool: Tool): React.ReactNode {
|
||||
try {
|
||||
return tool.renderToolUseQueuedMessage?.();
|
||||
return tool.renderToolUseQueuedMessage?.()
|
||||
} catch (error) {
|
||||
logError(new Error(`Error rendering tool use queued message for ${tool.name}: ${error}`));
|
||||
return null;
|
||||
logError(
|
||||
new Error(
|
||||
`Error rendering tool use queued message for ${tool.name}: ${error}`,
|
||||
),
|
||||
)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,106 +1,134 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
// biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered
|
||||
import React, { useMemo } from 'react';
|
||||
import { Ansi, Box, Text } from '../../ink.js';
|
||||
import type { Attachment } from 'src/utils/attachments.js';
|
||||
import type { NullRenderingAttachmentType } from './nullRenderingAttachments.js';
|
||||
import { type AppState, useAppState } from '../../state/AppState.js';
|
||||
import type { TaskState } from '../../tasks/types.js';
|
||||
import { getDisplayPath } from 'src/utils/file.js';
|
||||
import { formatFileSize } from 'src/utils/format.js';
|
||||
import { MessageResponse } from '../MessageResponse.js';
|
||||
import { basename, sep } from 'path';
|
||||
import { UserTextMessage } from './UserTextMessage.js';
|
||||
import { DiagnosticsDisplay } from '../DiagnosticsDisplay.js';
|
||||
import { getContentText } from 'src/utils/messages.js';
|
||||
import type { Theme } from 'src/utils/theme.js';
|
||||
import { UserImageMessage } from './UserImageMessage.js';
|
||||
import { toInkColor } from '../../utils/ink.js';
|
||||
import { jsonParse } from '../../utils/slowOperations.js';
|
||||
import { plural } from '../../utils/stringUtils.js';
|
||||
import { isEnvTruthy } from '../../utils/envUtils.js';
|
||||
import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js';
|
||||
import { tryRenderPlanApprovalMessage, formatTeammateMessageContent } from './PlanApprovalMessage.js';
|
||||
import { BLACK_CIRCLE } from '../../constants/figures.js';
|
||||
import { TeammateMessageContent } from './UserTeammateMessage.js';
|
||||
import { isShutdownApproved } from '../../utils/teammateMailbox.js';
|
||||
import { CtrlOToExpand } from '../CtrlOToExpand.js';
|
||||
import { FilePathLink } from '../FilePathLink.js';
|
||||
import { feature } from 'bun:bundle';
|
||||
import { useSelectedMessageBg } from '../messageActions.js';
|
||||
import React, { useMemo } from 'react'
|
||||
import { Ansi, Box, Text } from '../../ink.js'
|
||||
import type { Attachment } from 'src/utils/attachments.js'
|
||||
import type { NullRenderingAttachmentType } from './nullRenderingAttachments.js'
|
||||
import { useAppState } from '../../state/AppState.js'
|
||||
import { getDisplayPath } from 'src/utils/file.js'
|
||||
import { formatFileSize } from 'src/utils/format.js'
|
||||
import { MessageResponse } from '../MessageResponse.js'
|
||||
import { basename, sep } from 'path'
|
||||
import { UserTextMessage } from './UserTextMessage.js'
|
||||
import { DiagnosticsDisplay } from '../DiagnosticsDisplay.js'
|
||||
import { getContentText } from 'src/utils/messages.js'
|
||||
import type { Theme } from 'src/utils/theme.js'
|
||||
import { UserImageMessage } from './UserImageMessage.js'
|
||||
import { toInkColor } from '../../utils/ink.js'
|
||||
import { jsonParse } from '../../utils/slowOperations.js'
|
||||
import { plural } from '../../utils/stringUtils.js'
|
||||
import { isEnvTruthy } from '../../utils/envUtils.js'
|
||||
import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js'
|
||||
import {
|
||||
tryRenderPlanApprovalMessage,
|
||||
formatTeammateMessageContent,
|
||||
} from './PlanApprovalMessage.js'
|
||||
import { BLACK_CIRCLE } from '../../constants/figures.js'
|
||||
import { TeammateMessageContent } from './UserTeammateMessage.js'
|
||||
import { isShutdownApproved } from '../../utils/teammateMailbox.js'
|
||||
import { CtrlOToExpand } from '../CtrlOToExpand.js'
|
||||
import { FilePathLink } from '../FilePathLink.js'
|
||||
import { feature } from 'bun:bundle'
|
||||
import { useSelectedMessageBg } from '../messageActions.js'
|
||||
|
||||
type Props = {
|
||||
addMargin: boolean;
|
||||
attachment: Attachment;
|
||||
verbose: boolean;
|
||||
isTranscriptMode?: boolean;
|
||||
};
|
||||
addMargin: boolean
|
||||
attachment: Attachment
|
||||
verbose: boolean
|
||||
isTranscriptMode?: boolean
|
||||
}
|
||||
|
||||
export function AttachmentMessage({
|
||||
attachment,
|
||||
addMargin,
|
||||
verbose,
|
||||
isTranscriptMode
|
||||
isTranscriptMode,
|
||||
}: Props): React.ReactNode {
|
||||
const bg = useSelectedMessageBg();
|
||||
const bg = useSelectedMessageBg()
|
||||
// Hoisted to mount-time — per-message component, re-renders on every scroll.
|
||||
const isDemoEnv = feature('EXPERIMENTAL_SKILL_SEARCH') ?
|
||||
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||
useMemo(() => isEnvTruthy(process.env.IS_DEMO), []) : false;
|
||||
const isDemoEnv = feature('EXPERIMENTAL_SKILL_SEARCH')
|
||||
? // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||
useMemo(() => isEnvTruthy(process.env.IS_DEMO), [])
|
||||
: false
|
||||
// Handle teammate_mailbox BEFORE switch
|
||||
if (isAgentSwarmsEnabled() && attachment.type === 'teammate_mailbox') {
|
||||
// Filter out idle notifications BEFORE counting - they are hidden in the UI
|
||||
// so showing them in the count would be confusing ("2 messages in mailbox:" with nothing shown)
|
||||
const visibleMessages = attachment.messages.filter(msg => {
|
||||
if (isShutdownApproved(msg.text)) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
try {
|
||||
const parsed = jsonParse(msg.text);
|
||||
return parsed?.type !== 'idle_notification' && parsed?.type !== 'teammate_terminated';
|
||||
const parsed = jsonParse(msg.text)
|
||||
return (
|
||||
parsed?.type !== 'idle_notification' &&
|
||||
parsed?.type !== 'teammate_terminated'
|
||||
)
|
||||
} catch {
|
||||
return true; // Non-JSON messages are visible
|
||||
return true // Non-JSON messages are visible
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
if (visibleMessages.length === 0) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
return <Box flexDirection="column">
|
||||
{visibleMessages.map((msg_0, idx) => {
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
{visibleMessages.map((msg, idx) => {
|
||||
// Try to parse as JSON for task_assignment messages
|
||||
let parsedMsg: {
|
||||
type?: string;
|
||||
taskId?: string;
|
||||
subject?: string;
|
||||
assignedBy?: string;
|
||||
} | null = null;
|
||||
type?: string
|
||||
taskId?: string
|
||||
subject?: string
|
||||
assignedBy?: string
|
||||
} | null = null
|
||||
try {
|
||||
parsedMsg = jsonParse(msg_0.text);
|
||||
parsedMsg = jsonParse(msg.text)
|
||||
} catch {
|
||||
// Not JSON, treat as plain text
|
||||
}
|
||||
|
||||
if (parsedMsg?.type === 'task_assignment') {
|
||||
return <Box key={idx} paddingLeft={2}>
|
||||
return (
|
||||
<Box key={idx} paddingLeft={2}>
|
||||
<Text>{BLACK_CIRCLE} </Text>
|
||||
<Text>Task assigned: </Text>
|
||||
<Text bold>#{parsedMsg.taskId}</Text>
|
||||
<Text> - {parsedMsg.subject}</Text>
|
||||
<Text dimColor> (from {parsedMsg.assignedBy || msg_0.from})</Text>
|
||||
</Box>;
|
||||
<Text dimColor> (from {parsedMsg.assignedBy || msg.from})</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
// Note: idle_notification messages already filtered out above
|
||||
|
||||
// Try to render as plan approval message (request or response)
|
||||
const planApprovalElement = tryRenderPlanApprovalMessage(msg_0.text, msg_0.from);
|
||||
const planApprovalElement = tryRenderPlanApprovalMessage(
|
||||
msg.text,
|
||||
msg.from,
|
||||
)
|
||||
if (planApprovalElement) {
|
||||
return <React.Fragment key={idx}>{planApprovalElement}</React.Fragment>;
|
||||
return (
|
||||
<React.Fragment key={idx}>{planApprovalElement}</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
// Plain text message - sender header with chevron, truncated content
|
||||
const inkColor = toInkColor(msg_0.color);
|
||||
const formattedContent = formatTeammateMessageContent(msg_0.text) ?? msg_0.text;
|
||||
return <TeammateMessageContent key={idx} displayName={msg_0.from} inkColor={inkColor} content={formattedContent} summary={msg_0.summary} isTranscriptMode={isTranscriptMode} />;
|
||||
const inkColor = toInkColor(msg.color)
|
||||
const formattedContent =
|
||||
formatTeammateMessageContent(msg.text) ?? msg.text
|
||||
return (
|
||||
<TeammateMessageContent
|
||||
key={idx}
|
||||
displayName={msg.from}
|
||||
inkColor={inkColor}
|
||||
content={formattedContent}
|
||||
summary={msg.summary}
|
||||
isTranscriptMode={isTranscriptMode}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</Box>;
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
// skill_discovery rendered here (not in the switch) so the 'skill_discovery'
|
||||
@@ -108,83 +136,117 @@ export function AttachmentMessage({
|
||||
// be conditionally eliminated; an if-body can.
|
||||
if (feature('EXPERIMENTAL_SKILL_SEARCH')) {
|
||||
if (attachment.type === 'skill_discovery') {
|
||||
if (attachment.skills.length === 0) return null;
|
||||
if (attachment.skills.length === 0) return null
|
||||
// Ant users get shortIds inline so they can /skill-feedback while the
|
||||
// turn is still fresh. External users (when this un-gates) just see
|
||||
// names — shortId is undefined outside ant builds anyway.
|
||||
const names = attachment.skills.map(s => s.shortId ? `${s.name} [${s.shortId}]` : s.name).join(', ');
|
||||
const firstId = attachment.skills[0]?.shortId;
|
||||
const hint = (process.env.USER_TYPE) === 'ant' && !isDemoEnv && firstId ? ` · /skill-feedback ${firstId} 1=wrong 2=noisy 3=good [comment]` : '';
|
||||
return <Line>
|
||||
const names = attachment.skills
|
||||
.map(s => (s.shortId ? `${s.name} [${s.shortId}]` : s.name))
|
||||
.join(', ')
|
||||
const firstId = attachment.skills[0]?.shortId
|
||||
const hint =
|
||||
process.env.USER_TYPE === 'ant' && !isDemoEnv && firstId
|
||||
? ` · /skill-feedback ${firstId} 1=wrong 2=noisy 3=good [comment]`
|
||||
: ''
|
||||
return (
|
||||
<Line>
|
||||
<Text bold>{attachment.skills.length}</Text> relevant{' '}
|
||||
{plural(attachment.skills.length, 'skill')}: {names}
|
||||
{hint && <Text dimColor>{hint}</Text>}
|
||||
</Line>;
|
||||
</Line>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check -- teammate_mailbox/skill_discovery handled before switch
|
||||
switch (attachment.type) {
|
||||
case 'directory':
|
||||
return <Line>
|
||||
return (
|
||||
<Line>
|
||||
Listed directory <Text bold>{attachment.displayPath + sep}</Text>
|
||||
</Line>;
|
||||
</Line>
|
||||
)
|
||||
case 'file':
|
||||
case 'already_read_file':
|
||||
if (attachment.content.type === 'notebook') {
|
||||
return <Line>
|
||||
return (
|
||||
<Line>
|
||||
Read <Text bold>{attachment.displayPath}</Text> (
|
||||
{attachment.content.file.cells.length} cells)
|
||||
</Line>;
|
||||
</Line>
|
||||
)
|
||||
}
|
||||
if (attachment.content.type === 'file_unchanged') {
|
||||
return <Line>
|
||||
return (
|
||||
<Line>
|
||||
Read <Text bold>{attachment.displayPath}</Text> (unchanged)
|
||||
</Line>;
|
||||
}
|
||||
return <Line>
|
||||
Read <Text bold>{attachment.displayPath}</Text> (
|
||||
{attachment.content.type === 'text' ? `${attachment.content.file.numLines}${attachment.truncated ? '+' : ''} lines` : formatFileSize(attachment.content.file.originalSize)}
|
||||
</Line>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Line>
|
||||
Read <Text bold>{attachment.displayPath}</Text> (
|
||||
{attachment.content.type === 'text'
|
||||
? `${attachment.content.file.numLines}${attachment.truncated ? '+' : ''} lines`
|
||||
: formatFileSize(attachment.content.file.originalSize)}
|
||||
)
|
||||
</Line>
|
||||
)
|
||||
</Line>;
|
||||
case 'compact_file_reference':
|
||||
return <Line>
|
||||
return (
|
||||
<Line>
|
||||
Referenced file <Text bold>{attachment.displayPath}</Text>
|
||||
</Line>;
|
||||
</Line>
|
||||
)
|
||||
case 'pdf_reference':
|
||||
return <Line>
|
||||
return (
|
||||
<Line>
|
||||
Referenced PDF <Text bold>{attachment.displayPath}</Text> (
|
||||
{attachment.pageCount} pages)
|
||||
</Line>;
|
||||
</Line>
|
||||
)
|
||||
case 'selected_lines_in_ide':
|
||||
return <Line>
|
||||
return (
|
||||
<Line>
|
||||
⧉ Selected{' '}
|
||||
<Text bold>{attachment.lineEnd - attachment.lineStart + 1}</Text>{' '}
|
||||
lines from <Text bold>{attachment.displayPath}</Text> in{' '}
|
||||
{attachment.ideName}
|
||||
</Line>;
|
||||
</Line>
|
||||
)
|
||||
case 'nested_memory':
|
||||
return <Line>
|
||||
return (
|
||||
<Line>
|
||||
Loaded <Text bold>{attachment.displayPath}</Text>
|
||||
</Line>;
|
||||
</Line>
|
||||
)
|
||||
case 'relevant_memories':
|
||||
// Usually absorbed into a CollapsedReadSearchGroup (collapseReadSearch.ts)
|
||||
// so this only renders when the preceding tool was non-collapsible (Edit,
|
||||
// Write) and no group was open. Match CollapsedReadSearchContent's style:
|
||||
// 2-space gutter, dim text, count only — filenames/content in ctrl+o.
|
||||
return <Box flexDirection="column" marginTop={addMargin ? 1 : 0} backgroundColor={bg}>
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
marginTop={addMargin ? 1 : 0}
|
||||
backgroundColor={bg}
|
||||
>
|
||||
<Box flexDirection="row">
|
||||
<Box minWidth={2} />
|
||||
<Text dimColor>
|
||||
Recalled <Text bold>{attachment.memories.length}</Text>{' '}
|
||||
{attachment.memories.length === 1 ? 'memory' : 'memories'}
|
||||
{!isTranscriptMode && <>
|
||||
{!isTranscriptMode && (
|
||||
<>
|
||||
{' '}
|
||||
<CtrlOToExpand />
|
||||
</>}
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
{(verbose || isTranscriptMode) && attachment.memories.map(m => <Box key={m.path} flexDirection="column">
|
||||
{(verbose || isTranscriptMode) &&
|
||||
attachment.memories.map(m => (
|
||||
<Box key={m.path} flexDirection="column">
|
||||
<MessageResponse>
|
||||
<Text dimColor>
|
||||
<FilePathLink filePath={m.path}>
|
||||
@@ -192,156 +254,201 @@ export function AttachmentMessage({
|
||||
</FilePathLink>
|
||||
</Text>
|
||||
</MessageResponse>
|
||||
{isTranscriptMode && <Box paddingLeft={5}>
|
||||
{isTranscriptMode && (
|
||||
<Box paddingLeft={5}>
|
||||
<Text>
|
||||
<Ansi>{m.content}</Ansi>
|
||||
</Text>
|
||||
</Box>}
|
||||
</Box>)}
|
||||
</Box>;
|
||||
case 'dynamic_skill':
|
||||
{
|
||||
const skillCount = attachment.skillNames.length;
|
||||
return <Line>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)
|
||||
case 'dynamic_skill': {
|
||||
const skillCount = attachment.skillNames.length
|
||||
return (
|
||||
<Line>
|
||||
Loaded{' '}
|
||||
<Text bold>
|
||||
{skillCount} {plural(skillCount, 'skill')}
|
||||
</Text>{' '}
|
||||
from <Text bold>{attachment.displayPath}</Text>
|
||||
</Line>;
|
||||
</Line>
|
||||
)
|
||||
}
|
||||
case 'skill_listing':
|
||||
{
|
||||
case 'skill_listing': {
|
||||
if (attachment.isInitial) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
return <Line>
|
||||
return (
|
||||
<Line>
|
||||
<Text bold>{attachment.skillCount}</Text>{' '}
|
||||
{plural(attachment.skillCount, 'skill')} available
|
||||
</Line>;
|
||||
</Line>
|
||||
)
|
||||
}
|
||||
case 'agent_listing_delta':
|
||||
{
|
||||
case 'agent_listing_delta': {
|
||||
if (attachment.isInitial || attachment.addedTypes.length === 0) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
const count = attachment.addedTypes.length;
|
||||
return <Line>
|
||||
const count = attachment.addedTypes.length
|
||||
return (
|
||||
<Line>
|
||||
<Text bold>{count}</Text> agent {plural(count, 'type')} available
|
||||
</Line>;
|
||||
</Line>
|
||||
)
|
||||
}
|
||||
case 'queued_command':
|
||||
{
|
||||
const text = typeof attachment.prompt === 'string' ? attachment.prompt : getContentText(attachment.prompt) || '';
|
||||
const hasImages = attachment.imagePasteIds && attachment.imagePasteIds.length > 0;
|
||||
return <Box flexDirection="column">
|
||||
<UserTextMessage addMargin={addMargin} param={{
|
||||
text,
|
||||
type: 'text'
|
||||
}} verbose={verbose} isTranscriptMode={isTranscriptMode} />
|
||||
{hasImages && attachment.imagePasteIds?.map(id => <UserImageMessage key={id} imageId={id} />)}
|
||||
</Box>;
|
||||
case 'queued_command': {
|
||||
const text =
|
||||
typeof attachment.prompt === 'string'
|
||||
? attachment.prompt
|
||||
: getContentText(attachment.prompt) || ''
|
||||
const hasImages =
|
||||
attachment.imagePasteIds && attachment.imagePasteIds.length > 0
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<UserTextMessage
|
||||
addMargin={addMargin}
|
||||
param={{ text, type: 'text' }}
|
||||
verbose={verbose}
|
||||
isTranscriptMode={isTranscriptMode}
|
||||
/>
|
||||
{hasImages &&
|
||||
attachment.imagePasteIds?.map(id => (
|
||||
<UserImageMessage key={id} imageId={id} />
|
||||
))}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
case 'plan_file_reference':
|
||||
return <Line>
|
||||
return (
|
||||
<Line>
|
||||
Plan file referenced ({getDisplayPath(attachment.planFilePath)})
|
||||
</Line>;
|
||||
case 'invoked_skills':
|
||||
{
|
||||
</Line>
|
||||
)
|
||||
case 'invoked_skills': {
|
||||
if (attachment.skills.length === 0) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
const skillNames = attachment.skills.map(s_0 => s_0.name).join(', ');
|
||||
return <Line>Skills restored ({skillNames})</Line>;
|
||||
const skillNames = attachment.skills.map(s => s.name).join(', ')
|
||||
return <Line>Skills restored ({skillNames})</Line>
|
||||
}
|
||||
case 'diagnostics':
|
||||
return <DiagnosticsDisplay attachment={attachment} verbose={verbose} />;
|
||||
return <DiagnosticsDisplay attachment={attachment} verbose={verbose} />
|
||||
case 'mcp_resource':
|
||||
return <Line>
|
||||
return (
|
||||
<Line>
|
||||
Read MCP resource <Text bold>{attachment.name}</Text> from{' '}
|
||||
{attachment.server}
|
||||
</Line>;
|
||||
</Line>
|
||||
)
|
||||
case 'command_permissions':
|
||||
// The skill success message is rendered by SkillTool's renderToolResultMessage,
|
||||
// so we don't render anything here to avoid duplicate messages.
|
||||
return null;
|
||||
case 'async_hook_response':
|
||||
{
|
||||
return null
|
||||
case 'async_hook_response': {
|
||||
// SessionStart hook completions are only shown in verbose mode
|
||||
if (attachment.hookEvent === 'SessionStart' && !verbose) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
// Generally hide async hook completion messages unless in verbose mode
|
||||
if (!verbose && !isTranscriptMode) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
return <Line>
|
||||
return (
|
||||
<Line>
|
||||
Async hook <Text bold>{attachment.hookEvent}</Text> completed
|
||||
</Line>;
|
||||
</Line>
|
||||
)
|
||||
}
|
||||
case 'hook_blocking_error':
|
||||
{
|
||||
case 'hook_blocking_error': {
|
||||
// Stop hooks are rendered as a summary in SystemStopHookSummaryMessage
|
||||
if (attachment.hookEvent === 'Stop' || attachment.hookEvent === 'SubagentStop') {
|
||||
return null;
|
||||
if (
|
||||
attachment.hookEvent === 'Stop' ||
|
||||
attachment.hookEvent === 'SubagentStop'
|
||||
) {
|
||||
return null
|
||||
}
|
||||
// Show stderr to the user so they can understand why the hook blocked
|
||||
const stderr = attachment.blockingError.blockingError.trim();
|
||||
return <>
|
||||
const stderr = attachment.blockingError.blockingError.trim()
|
||||
return (
|
||||
<>
|
||||
<Line color="error">
|
||||
{attachment.hookName} hook returned blocking error
|
||||
</Line>
|
||||
{stderr ? <Line color="error">{stderr}</Line> : null}
|
||||
</>;
|
||||
</>
|
||||
)
|
||||
}
|
||||
case 'hook_non_blocking_error':
|
||||
{
|
||||
case 'hook_non_blocking_error': {
|
||||
// Stop hooks are rendered as a summary in SystemStopHookSummaryMessage
|
||||
if (attachment.hookEvent === 'Stop' || attachment.hookEvent === 'SubagentStop') {
|
||||
return null;
|
||||
if (
|
||||
attachment.hookEvent === 'Stop' ||
|
||||
attachment.hookEvent === 'SubagentStop'
|
||||
) {
|
||||
return null
|
||||
}
|
||||
// Full hook output is logged to debug log via hookEvents.ts
|
||||
return <Line color="error">{attachment.hookName} hook error</Line>;
|
||||
return <Line color="error">{attachment.hookName} hook error</Line>
|
||||
}
|
||||
case 'hook_error_during_execution':
|
||||
// Stop hooks are rendered as a summary in SystemStopHookSummaryMessage
|
||||
if (attachment.hookEvent === 'Stop' || attachment.hookEvent === 'SubagentStop') {
|
||||
return null;
|
||||
if (
|
||||
attachment.hookEvent === 'Stop' ||
|
||||
attachment.hookEvent === 'SubagentStop'
|
||||
) {
|
||||
return null
|
||||
}
|
||||
// Full hook output is logged to debug log via hookEvents.ts
|
||||
return <Line>{attachment.hookName} hook warning</Line>;
|
||||
return <Line>{attachment.hookName} hook warning</Line>
|
||||
case 'hook_success':
|
||||
// Full hook output is logged to debug log via hookEvents.ts
|
||||
return null;
|
||||
return null
|
||||
case 'hook_stopped_continuation':
|
||||
// Stop hooks are rendered as a summary in SystemStopHookSummaryMessage
|
||||
if (attachment.hookEvent === 'Stop' || attachment.hookEvent === 'SubagentStop') {
|
||||
return null;
|
||||
if (
|
||||
attachment.hookEvent === 'Stop' ||
|
||||
attachment.hookEvent === 'SubagentStop'
|
||||
) {
|
||||
return null
|
||||
}
|
||||
return <Line color="warning">
|
||||
return (
|
||||
<Line color="warning">
|
||||
{attachment.hookName} hook stopped continuation: {attachment.message}
|
||||
</Line>;
|
||||
</Line>
|
||||
)
|
||||
case 'hook_system_message':
|
||||
return <Line>
|
||||
return (
|
||||
<Line>
|
||||
{attachment.hookName} says: {attachment.content}
|
||||
</Line>;
|
||||
case 'hook_permission_decision':
|
||||
{
|
||||
const action = attachment.decision === 'allow' ? 'Allowed' : 'Denied';
|
||||
return <Line>
|
||||
</Line>
|
||||
)
|
||||
case 'hook_permission_decision': {
|
||||
const action = attachment.decision === 'allow' ? 'Allowed' : 'Denied'
|
||||
return (
|
||||
<Line>
|
||||
{action} by <Text bold>{attachment.hookEvent}</Text> hook
|
||||
</Line>;
|
||||
</Line>
|
||||
)
|
||||
}
|
||||
case 'task_status':
|
||||
return <TaskStatusMessage attachment={attachment} />;
|
||||
return <TaskStatusMessage attachment={attachment} />
|
||||
case 'teammate_shutdown_batch':
|
||||
return <Box flexDirection="row" width="100%" marginTop={1} backgroundColor={bg}>
|
||||
return (
|
||||
<Box
|
||||
flexDirection="row"
|
||||
width="100%"
|
||||
marginTop={1}
|
||||
backgroundColor={bg}
|
||||
>
|
||||
<Text dimColor>{BLACK_CIRCLE} </Text>
|
||||
<Text dimColor>
|
||||
{attachment.count} {plural(attachment.count, 'teammate')} shut down
|
||||
gracefully
|
||||
</Text>
|
||||
</Box>;
|
||||
</Box>
|
||||
)
|
||||
default:
|
||||
// Exhaustiveness: every type reaching here must be in NULL_RENDERING_TYPES.
|
||||
// If TS errors, a new Attachment type was added without a case above AND
|
||||
@@ -352,185 +459,110 @@ export function AttachmentMessage({
|
||||
// skill_discovery and teammate_mailbox are handled BEFORE the switch in
|
||||
// runtime-gated blocks (feature() / isAgentSwarmsEnabled()) that TS can't
|
||||
// narrow through — excluded here via type union (compile-time only, no emit).
|
||||
attachment.type satisfies NullRenderingAttachmentType | 'skill_discovery' | 'teammate_mailbox' | 'bagel_console';
|
||||
return null;
|
||||
attachment.type satisfies
|
||||
| NullRenderingAttachmentType
|
||||
| 'skill_discovery'
|
||||
| 'teammate_mailbox'
|
||||
return null
|
||||
}
|
||||
}
|
||||
type TaskStatusAttachment = Extract<Attachment, {
|
||||
type: 'task_status';
|
||||
}>;
|
||||
function TaskStatusMessage(t0) {
|
||||
const $ = _c(4);
|
||||
const {
|
||||
attachment
|
||||
} = t0;
|
||||
if (false && attachment.status === "killed") {
|
||||
return null;
|
||||
|
||||
type TaskStatusAttachment = Extract<Attachment, { type: 'task_status' }>
|
||||
|
||||
function TaskStatusMessage({
|
||||
attachment,
|
||||
}: {
|
||||
attachment: TaskStatusAttachment
|
||||
}): React.ReactNode {
|
||||
// For ants, killed task status is shown in the CoordinatorTaskPanel.
|
||||
// Don't render it again in the chat.
|
||||
if (process.env.USER_TYPE === 'ant' && attachment.status === 'killed') {
|
||||
return null
|
||||
}
|
||||
if (isAgentSwarmsEnabled() && attachment.taskType === "in_process_teammate") {
|
||||
let t1;
|
||||
if ($[0] !== attachment) {
|
||||
t1 = <TeammateTaskStatus attachment={attachment} />;
|
||||
$[0] = attachment;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
|
||||
// Only access teammate-specific code when swarms are enabled.
|
||||
// TeammateTaskStatus subscribes to AppState; by gating the mount we
|
||||
// avoid adding a store listener for every non-teammate attachment.
|
||||
if (isAgentSwarmsEnabled() && attachment.taskType === 'in_process_teammate') {
|
||||
return <TeammateTaskStatus attachment={attachment} />
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
let t1;
|
||||
if ($[2] !== attachment) {
|
||||
t1 = <GenericTaskStatus attachment={attachment} />;
|
||||
$[2] = attachment;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
return t1;
|
||||
|
||||
return <GenericTaskStatus attachment={attachment} />
|
||||
}
|
||||
function GenericTaskStatus(t0) {
|
||||
const $ = _c(9);
|
||||
const {
|
||||
attachment
|
||||
} = t0;
|
||||
const bg = useSelectedMessageBg();
|
||||
const statusText = attachment.status === "completed" ? "completed in background" : attachment.status === "killed" ? "stopped" : attachment.status === "running" ? "still running in background" : attachment.status;
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <Text dimColor={true}>{BLACK_CIRCLE} </Text>;
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
let t2;
|
||||
if ($[1] !== attachment.description) {
|
||||
t2 = <Text bold={true}>{attachment.description}</Text>;
|
||||
$[1] = attachment.description;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
let t3;
|
||||
if ($[3] !== statusText || $[4] !== t2) {
|
||||
t3 = <Text dimColor={true}>Task "{t2}" {statusText}</Text>;
|
||||
$[3] = statusText;
|
||||
$[4] = t2;
|
||||
$[5] = t3;
|
||||
} else {
|
||||
t3 = $[5];
|
||||
}
|
||||
let t4;
|
||||
if ($[6] !== bg || $[7] !== t3) {
|
||||
t4 = <Box flexDirection="row" width="100%" marginTop={1} backgroundColor={bg}>{t1}{t3}</Box>;
|
||||
$[6] = bg;
|
||||
$[7] = t3;
|
||||
$[8] = t4;
|
||||
} else {
|
||||
t4 = $[8];
|
||||
}
|
||||
return t4;
|
||||
|
||||
function GenericTaskStatus({
|
||||
attachment,
|
||||
}: {
|
||||
attachment: TaskStatusAttachment
|
||||
}): React.ReactNode {
|
||||
const bg = useSelectedMessageBg()
|
||||
const statusText =
|
||||
attachment.status === 'completed'
|
||||
? 'completed in background'
|
||||
: attachment.status === 'killed'
|
||||
? 'stopped'
|
||||
: attachment.status === 'running'
|
||||
? 'still running in background'
|
||||
: attachment.status
|
||||
return (
|
||||
<Box flexDirection="row" width="100%" marginTop={1} backgroundColor={bg}>
|
||||
<Text dimColor>{BLACK_CIRCLE} </Text>
|
||||
<Text dimColor>
|
||||
Task "<Text bold>{attachment.description}</Text>" {statusText}
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
function TeammateTaskStatus(t0: { attachment: TaskStatusAttachment }) {
|
||||
const $ = _c(16);
|
||||
const {
|
||||
attachment
|
||||
} = t0;
|
||||
const bg = useSelectedMessageBg();
|
||||
let t1: (s: AppState) => TaskState;
|
||||
if ($[0] !== attachment.taskId) {
|
||||
t1 = s => s.tasks[attachment.taskId];
|
||||
$[0] = attachment.taskId;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1] as (s: AppState) => TaskState;
|
||||
|
||||
function TeammateTaskStatus({
|
||||
attachment,
|
||||
}: {
|
||||
attachment: TaskStatusAttachment
|
||||
}): React.ReactNode {
|
||||
const bg = useSelectedMessageBg()
|
||||
// Narrow selector: only re-render when this specific task changes.
|
||||
const task = useAppState(s => s.tasks[attachment.taskId])
|
||||
if (task?.type !== 'in_process_teammate') {
|
||||
// Fall through to generic rendering (task not yet in store, or wrong type)
|
||||
return <GenericTaskStatus attachment={attachment} />
|
||||
}
|
||||
const task = useAppState(t1);
|
||||
if (task?.type !== "in_process_teammate") {
|
||||
let t2;
|
||||
if ($[2] !== attachment) {
|
||||
t2 = <GenericTaskStatus attachment={attachment} />;
|
||||
$[2] = attachment;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
let t2;
|
||||
if ($[4] !== task.identity.color) {
|
||||
t2 = toInkColor(task.identity.color);
|
||||
$[4] = task.identity.color;
|
||||
$[5] = t2;
|
||||
} else {
|
||||
t2 = $[5];
|
||||
}
|
||||
const agentColor = t2;
|
||||
const statusText = attachment.status === "completed" ? "shut down gracefully" : attachment.status;
|
||||
let t3;
|
||||
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = <Text dimColor={true}>{BLACK_CIRCLE} </Text>;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
}
|
||||
let t4;
|
||||
if ($[7] !== agentColor || $[8] !== task.identity.agentName) {
|
||||
t4 = <Text color={agentColor} bold={true} dimColor={false}>@{task.identity.agentName}</Text>;
|
||||
$[7] = agentColor;
|
||||
$[8] = task.identity.agentName;
|
||||
$[9] = t4;
|
||||
} else {
|
||||
t4 = $[9];
|
||||
}
|
||||
let t5;
|
||||
if ($[10] !== statusText || $[11] !== t4) {
|
||||
t5 = <Text dimColor={true}>Teammate{" "}{t4}{" "}{statusText}</Text>;
|
||||
$[10] = statusText;
|
||||
$[11] = t4;
|
||||
$[12] = t5;
|
||||
} else {
|
||||
t5 = $[12];
|
||||
}
|
||||
let t6;
|
||||
if ($[13] !== bg || $[14] !== t5) {
|
||||
t6 = <Box flexDirection="row" width="100%" marginTop={1} backgroundColor={bg}>{t3}{t5}</Box>;
|
||||
$[13] = bg;
|
||||
$[14] = t5;
|
||||
$[15] = t6;
|
||||
} else {
|
||||
t6 = $[15];
|
||||
}
|
||||
return t6;
|
||||
const agentColor = toInkColor(task.identity.color)
|
||||
const statusText =
|
||||
attachment.status === 'completed'
|
||||
? 'shut down gracefully'
|
||||
: attachment.status
|
||||
return (
|
||||
<Box flexDirection="row" width="100%" marginTop={1} backgroundColor={bg}>
|
||||
<Text dimColor>{BLACK_CIRCLE} </Text>
|
||||
<Text dimColor>
|
||||
Teammate{' '}
|
||||
<Text color={agentColor} bold dimColor={false}>
|
||||
@{task.identity.agentName}
|
||||
</Text>{' '}
|
||||
{statusText}
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
// We allow setting dimColor to false here to help work around the dim-bold bug.
|
||||
// https://github.com/chalk/chalk/issues/290
|
||||
function Line(t0) {
|
||||
const $ = _c(7);
|
||||
const {
|
||||
dimColor: t1,
|
||||
function Line({
|
||||
dimColor = true,
|
||||
children,
|
||||
color
|
||||
} = t0;
|
||||
const dimColor = t1 === undefined ? true : t1;
|
||||
const bg = useSelectedMessageBg();
|
||||
let t2;
|
||||
if ($[0] !== children || $[1] !== color || $[2] !== dimColor) {
|
||||
t2 = <MessageResponse><Text color={color} dimColor={dimColor} wrap="wrap">{children}</Text></MessageResponse>;
|
||||
$[0] = children;
|
||||
$[1] = color;
|
||||
$[2] = dimColor;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
let t3;
|
||||
if ($[4] !== bg || $[5] !== t2) {
|
||||
t3 = <Box backgroundColor={bg}>{t2}</Box>;
|
||||
$[4] = bg;
|
||||
$[5] = t2;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
}
|
||||
return t3;
|
||||
color,
|
||||
}: {
|
||||
dimColor?: boolean
|
||||
children: React.ReactNode
|
||||
color?: keyof Theme
|
||||
}): React.ReactNode {
|
||||
const bg = useSelectedMessageBg()
|
||||
return (
|
||||
<Box backgroundColor={bg}>
|
||||
<MessageResponse>
|
||||
<Text color={color} dimColor={dimColor} wrap="wrap">
|
||||
{children}
|
||||
</Text>
|
||||
</MessageResponse>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,144 +1,122 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { feature } from 'bun:bundle';
|
||||
import { basename } from 'path';
|
||||
import React, { useRef } from 'react';
|
||||
import { useMinDisplayTime } from '../../hooks/useMinDisplayTime.js';
|
||||
import { Ansi, Box, Text, useTheme } from '../../ink.js';
|
||||
import { findToolByName, type Tools } from '../../Tool.js';
|
||||
import { getReplPrimitiveTools } from '../../tools/REPLTool/primitiveTools.js';
|
||||
import type { CollapsedReadSearchGroup, NormalizedAssistantMessage } from '../../types/message.js';
|
||||
import { uniq } from '../../utils/array.js';
|
||||
import { getToolUseIdsFromCollapsedGroup } from '../../utils/collapseReadSearch.js';
|
||||
import { getDisplayPath } from '../../utils/file.js';
|
||||
import { formatDuration, formatSecondsShort } from '../../utils/format.js';
|
||||
import { isFullscreenEnvEnabled } from '../../utils/fullscreen.js';
|
||||
import type { buildMessageLookups } from '../../utils/messages.js';
|
||||
import type { ThemeName } from '../../utils/theme.js';
|
||||
import { CtrlOToExpand } from '../CtrlOToExpand.js';
|
||||
import { useSelectedMessageBg } from '../messageActions.js';
|
||||
import { PrBadge } from '../PrBadge.js';
|
||||
import { ToolUseLoader } from '../ToolUseLoader.js';
|
||||
import { feature } from 'bun:bundle'
|
||||
import { basename } from 'path'
|
||||
import React, { useRef } from 'react'
|
||||
import { useMinDisplayTime } from '../../hooks/useMinDisplayTime.js'
|
||||
import { Ansi, Box, Text, useTheme } from '../../ink.js'
|
||||
import { findToolByName, type Tools } from '../../Tool.js'
|
||||
import { getReplPrimitiveTools } from '../../tools/REPLTool/primitiveTools.js'
|
||||
import type {
|
||||
CollapsedReadSearchGroup,
|
||||
NormalizedAssistantMessage,
|
||||
} from '../../types/message.js'
|
||||
import { uniq } from '../../utils/array.js'
|
||||
import { getToolUseIdsFromCollapsedGroup } from '../../utils/collapseReadSearch.js'
|
||||
import { getDisplayPath } from '../../utils/file.js'
|
||||
import { formatDuration, formatSecondsShort } from '../../utils/format.js'
|
||||
import { isFullscreenEnvEnabled } from '../../utils/fullscreen.js'
|
||||
import type { buildMessageLookups } from '../../utils/messages.js'
|
||||
import type { ThemeName } from '../../utils/theme.js'
|
||||
import { CtrlOToExpand } from '../CtrlOToExpand.js'
|
||||
import { useSelectedMessageBg } from '../messageActions.js'
|
||||
import { PrBadge } from '../PrBadge.js'
|
||||
import { ToolUseLoader } from '../ToolUseLoader.js'
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
const teamMemCollapsed = feature('TEAMMEM') ? require('./teamMemCollapsed.js') as typeof import('./teamMemCollapsed.js') : null;
|
||||
const teamMemCollapsed = feature('TEAMMEM')
|
||||
? (require('./teamMemCollapsed.js') as typeof import('./teamMemCollapsed.js'))
|
||||
: null
|
||||
/* eslint-enable @typescript-eslint/no-require-imports */
|
||||
|
||||
// Hold each ⤿ hint for a minimum duration so fast-completing tool calls
|
||||
// (bash commands, file reads, search patterns) are actually readable instead
|
||||
// of flickering past in a single frame.
|
||||
const MIN_HINT_DISPLAY_MS = 700;
|
||||
const MIN_HINT_DISPLAY_MS = 700
|
||||
|
||||
type Props = {
|
||||
message: CollapsedReadSearchGroup;
|
||||
inProgressToolUseIDs: Set<string>;
|
||||
shouldAnimate: boolean;
|
||||
verbose: boolean;
|
||||
tools: Tools;
|
||||
lookups: ReturnType<typeof buildMessageLookups>;
|
||||
message: CollapsedReadSearchGroup
|
||||
inProgressToolUseIDs: Set<string>
|
||||
shouldAnimate: boolean
|
||||
verbose: boolean
|
||||
tools: Tools
|
||||
lookups: ReturnType<typeof buildMessageLookups>
|
||||
/** True if this is the currently active collapsed group (last one, still loading) */
|
||||
isActiveGroup?: boolean;
|
||||
};
|
||||
isActiveGroup?: boolean
|
||||
}
|
||||
|
||||
/** Render a single tool use in verbose mode */
|
||||
function VerboseToolUse(t0) {
|
||||
const $ = _c(24);
|
||||
const {
|
||||
function VerboseToolUse({
|
||||
content,
|
||||
tools,
|
||||
lookups,
|
||||
inProgressToolUseIDs,
|
||||
shouldAnimate,
|
||||
theme
|
||||
} = t0;
|
||||
const bg = useSelectedMessageBg();
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== bg || $[1] !== content.id || $[2] !== content.input || $[3] !== content.name || $[4] !== inProgressToolUseIDs || $[5] !== lookups || $[6] !== shouldAnimate || $[7] !== theme || $[8] !== tools) {
|
||||
t2 = Symbol.for("react.early_return_sentinel");
|
||||
bb0: {
|
||||
const tool = findToolByName(tools, content.name) ?? findToolByName(getReplPrimitiveTools(), content.name);
|
||||
if (!tool) {
|
||||
t2 = null;
|
||||
break bb0;
|
||||
}
|
||||
let t3;
|
||||
if ($[11] !== content.id || $[12] !== lookups.resolvedToolUseIDs) {
|
||||
t3 = lookups.resolvedToolUseIDs.has(content.id);
|
||||
$[11] = content.id;
|
||||
$[12] = lookups.resolvedToolUseIDs;
|
||||
$[13] = t3;
|
||||
} else {
|
||||
t3 = $[13];
|
||||
}
|
||||
const isResolved = t3;
|
||||
let t4;
|
||||
if ($[14] !== content.id || $[15] !== lookups.erroredToolUseIDs) {
|
||||
t4 = lookups.erroredToolUseIDs.has(content.id);
|
||||
$[14] = content.id;
|
||||
$[15] = lookups.erroredToolUseIDs;
|
||||
$[16] = t4;
|
||||
} else {
|
||||
t4 = $[16];
|
||||
}
|
||||
const isError = t4;
|
||||
let t5;
|
||||
if ($[17] !== content.id || $[18] !== inProgressToolUseIDs) {
|
||||
t5 = inProgressToolUseIDs.has(content.id);
|
||||
$[17] = content.id;
|
||||
$[18] = inProgressToolUseIDs;
|
||||
$[19] = t5;
|
||||
} else {
|
||||
t5 = $[19];
|
||||
}
|
||||
const isInProgress = t5;
|
||||
const resultMsg = lookups.toolResultByToolUseID.get(content.id);
|
||||
const rawToolResult = resultMsg?.type === "user" ? resultMsg.toolUseResult : undefined;
|
||||
const parsedOutput = tool.outputSchema?.safeParse(rawToolResult);
|
||||
const toolResult = parsedOutput?.success ? parsedOutput.data : undefined;
|
||||
const parsedInput = tool.inputSchema.safeParse(content.input);
|
||||
const input = parsedInput.success ? parsedInput.data : undefined;
|
||||
const userFacingName = tool.userFacingName(input);
|
||||
const toolUseMessage = input ? tool.renderToolUseMessage(input, {
|
||||
theme,
|
||||
verbose: true
|
||||
}) : null;
|
||||
const t6 = shouldAnimate && isInProgress;
|
||||
const t7 = !isResolved;
|
||||
let t8;
|
||||
if ($[20] !== isError || $[21] !== t6 || $[22] !== t7) {
|
||||
t8 = <ToolUseLoader shouldAnimate={t6} isUnresolved={t7} isError={isError} />;
|
||||
$[20] = isError;
|
||||
$[21] = t6;
|
||||
$[22] = t7;
|
||||
$[23] = t8;
|
||||
} else {
|
||||
t8 = $[23];
|
||||
}
|
||||
t1 = <Box key={content.id} flexDirection="column" marginTop={1} backgroundColor={bg}><Box flexDirection="row">{t8}<Text><Text bold={true}>{userFacingName}</Text>{toolUseMessage && <Text>({toolUseMessage})</Text>}</Text>{input && tool.renderToolUseTag?.(input)}</Box>{isResolved && !isError && toolResult !== undefined && <Box>{tool.renderToolResultMessage?.(toolResult, [], {
|
||||
}: {
|
||||
content: { type: 'tool_use'; id: string; name: string; input: unknown }
|
||||
tools: Tools
|
||||
lookups: ReturnType<typeof buildMessageLookups>
|
||||
inProgressToolUseIDs: Set<string>
|
||||
shouldAnimate: boolean
|
||||
theme: ThemeName
|
||||
}): React.ReactNode {
|
||||
const bg = useSelectedMessageBg()
|
||||
// Same REPL-primitive fallback as getToolSearchOrReadInfo — REPL mode strips
|
||||
// these from the execution tools list, but virtual messages still need them
|
||||
// to render in verbose mode.
|
||||
const tool =
|
||||
findToolByName(tools, content.name) ??
|
||||
findToolByName(getReplPrimitiveTools(), content.name)
|
||||
if (!tool) return null
|
||||
|
||||
const isResolved = lookups.resolvedToolUseIDs.has(content.id)
|
||||
const isError = lookups.erroredToolUseIDs.has(content.id)
|
||||
const isInProgress = inProgressToolUseIDs.has(content.id)
|
||||
|
||||
const resultMsg = lookups.toolResultByToolUseID.get(content.id)
|
||||
const rawToolResult =
|
||||
resultMsg?.type === 'user' ? resultMsg.toolUseResult : undefined
|
||||
const parsedOutput = tool.outputSchema?.safeParse(rawToolResult)
|
||||
const toolResult = parsedOutput?.success ? parsedOutput.data : undefined
|
||||
|
||||
const parsedInput = tool.inputSchema.safeParse(content.input)
|
||||
const input = parsedInput.success ? parsedInput.data : undefined
|
||||
const userFacingName = tool.userFacingName(input)
|
||||
const toolUseMessage = input
|
||||
? tool.renderToolUseMessage(input, { theme, verbose: true })
|
||||
: null
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={content.id}
|
||||
flexDirection="column"
|
||||
marginTop={1}
|
||||
backgroundColor={bg}
|
||||
>
|
||||
<Box flexDirection="row">
|
||||
<ToolUseLoader
|
||||
shouldAnimate={shouldAnimate && isInProgress}
|
||||
isUnresolved={!isResolved}
|
||||
isError={isError}
|
||||
/>
|
||||
<Text>
|
||||
<Text bold>{userFacingName}</Text>
|
||||
{toolUseMessage && <Text>({toolUseMessage})</Text>}
|
||||
</Text>
|
||||
{input && tool.renderToolUseTag?.(input)}
|
||||
</Box>
|
||||
{isResolved && !isError && toolResult !== undefined && (
|
||||
<Box>
|
||||
{tool.renderToolResultMessage?.(toolResult, [], {
|
||||
verbose: true,
|
||||
tools,
|
||||
theme
|
||||
})}</Box>}</Box>;
|
||||
}
|
||||
$[0] = bg;
|
||||
$[1] = content.id;
|
||||
$[2] = content.input;
|
||||
$[3] = content.name;
|
||||
$[4] = inProgressToolUseIDs;
|
||||
$[5] = lookups;
|
||||
$[6] = shouldAnimate;
|
||||
$[7] = theme;
|
||||
$[8] = tools;
|
||||
$[9] = t1;
|
||||
$[10] = t2;
|
||||
} else {
|
||||
t1 = $[9];
|
||||
t2 = $[10];
|
||||
}
|
||||
if (t2 !== Symbol.for("react.early_return_sentinel")) {
|
||||
return t2;
|
||||
}
|
||||
return t1;
|
||||
theme,
|
||||
})}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export function CollapsedReadSearchContent({
|
||||
message,
|
||||
inProgressToolUseIDs,
|
||||
@@ -146,9 +124,9 @@ export function CollapsedReadSearchContent({
|
||||
verbose,
|
||||
tools,
|
||||
lookups,
|
||||
isActiveGroup
|
||||
isActiveGroup,
|
||||
}: Props): React.ReactNode {
|
||||
const bg = useSelectedMessageBg();
|
||||
const bg = useSelectedMessageBg()
|
||||
const {
|
||||
searchCount: rawSearchCount,
|
||||
readCount: rawReadCount,
|
||||
@@ -157,94 +135,141 @@ export function CollapsedReadSearchContent({
|
||||
memorySearchCount,
|
||||
memoryReadCount,
|
||||
memoryWriteCount,
|
||||
messages: groupMessages
|
||||
} = message;
|
||||
const [theme] = useTheme();
|
||||
const toolUseIds = getToolUseIdsFromCollapsedGroup(message);
|
||||
const anyError = toolUseIds.some(id => lookups.erroredToolUseIDs.has(id));
|
||||
const hasMemoryOps = memorySearchCount > 0 || memoryReadCount > 0 || memoryWriteCount > 0;
|
||||
const hasTeamMemoryOps = feature('TEAMMEM') ? teamMemCollapsed!.checkHasTeamMemOps(message) : false;
|
||||
messages: groupMessages,
|
||||
} = message
|
||||
const [theme] = useTheme()
|
||||
const toolUseIds = getToolUseIdsFromCollapsedGroup(message)
|
||||
const anyError = toolUseIds.some(id => lookups.erroredToolUseIDs.has(id))
|
||||
const hasMemoryOps =
|
||||
memorySearchCount > 0 || memoryReadCount > 0 || memoryWriteCount > 0
|
||||
const hasTeamMemoryOps = feature('TEAMMEM')
|
||||
? teamMemCollapsed!.checkHasTeamMemOps(message)
|
||||
: false
|
||||
|
||||
// Track the max seen counts so they only ever increase. The debounce timer
|
||||
// causes extra re-renders at arbitrary times; during a brief "invisible window"
|
||||
// in the streaming executor the group count can dip, which causes jitter.
|
||||
const maxReadCountRef = useRef(0);
|
||||
const maxSearchCountRef = useRef(0);
|
||||
const maxListCountRef = useRef(0);
|
||||
const maxMcpCountRef = useRef(0);
|
||||
const maxBashCountRef = useRef(0);
|
||||
maxReadCountRef.current = Math.max(maxReadCountRef.current, rawReadCount);
|
||||
maxSearchCountRef.current = Math.max(maxSearchCountRef.current, rawSearchCount);
|
||||
maxListCountRef.current = Math.max(maxListCountRef.current, rawListCount);
|
||||
maxMcpCountRef.current = Math.max(maxMcpCountRef.current, message.mcpCallCount ?? 0);
|
||||
maxBashCountRef.current = Math.max(maxBashCountRef.current, message.bashCount ?? 0);
|
||||
const readCount = maxReadCountRef.current;
|
||||
const searchCount = maxSearchCountRef.current;
|
||||
const listCount = maxListCountRef.current;
|
||||
const mcpCallCount = maxMcpCountRef.current;
|
||||
const maxReadCountRef = useRef(0)
|
||||
const maxSearchCountRef = useRef(0)
|
||||
const maxListCountRef = useRef(0)
|
||||
const maxMcpCountRef = useRef(0)
|
||||
const maxBashCountRef = useRef(0)
|
||||
maxReadCountRef.current = Math.max(maxReadCountRef.current, rawReadCount)
|
||||
maxSearchCountRef.current = Math.max(
|
||||
maxSearchCountRef.current,
|
||||
rawSearchCount,
|
||||
)
|
||||
maxListCountRef.current = Math.max(maxListCountRef.current, rawListCount)
|
||||
maxMcpCountRef.current = Math.max(
|
||||
maxMcpCountRef.current,
|
||||
message.mcpCallCount ?? 0,
|
||||
)
|
||||
maxBashCountRef.current = Math.max(
|
||||
maxBashCountRef.current,
|
||||
message.bashCount ?? 0,
|
||||
)
|
||||
const readCount = maxReadCountRef.current
|
||||
const searchCount = maxSearchCountRef.current
|
||||
const listCount = maxListCountRef.current
|
||||
const mcpCallCount = maxMcpCountRef.current
|
||||
// Subtract commands surfaced as "Committed …" / "Created PR …" so the
|
||||
// same command isn't counted twice. gitOpBashCount is read live (no max-ref
|
||||
// needed — it's 0 until results arrive, then only grows).
|
||||
const gitOpBashCount = message.gitOpBashCount ?? 0;
|
||||
const bashCount = isFullscreenEnvEnabled() ? Math.max(0, maxBashCountRef.current - gitOpBashCount) : 0;
|
||||
const hasNonMemoryOps = searchCount > 0 || readCount > 0 || listCount > 0 || replCount > 0 || mcpCallCount > 0 || bashCount > 0 || gitOpBashCount > 0;
|
||||
const readPaths = message.readFilePaths;
|
||||
const searchArgs = message.searchArgs;
|
||||
let incomingHint = message.latestDisplayHint;
|
||||
const gitOpBashCount = message.gitOpBashCount ?? 0
|
||||
const bashCount = isFullscreenEnvEnabled()
|
||||
? Math.max(0, maxBashCountRef.current - gitOpBashCount)
|
||||
: 0
|
||||
|
||||
const hasNonMemoryOps =
|
||||
searchCount > 0 ||
|
||||
readCount > 0 ||
|
||||
listCount > 0 ||
|
||||
replCount > 0 ||
|
||||
mcpCallCount > 0 ||
|
||||
bashCount > 0 ||
|
||||
gitOpBashCount > 0
|
||||
|
||||
const readPaths = message.readFilePaths
|
||||
const searchArgs = message.searchArgs
|
||||
let incomingHint = message.latestDisplayHint
|
||||
if (incomingHint === undefined) {
|
||||
const lastSearchRaw = searchArgs?.at(-1);
|
||||
const lastSearch = lastSearchRaw !== undefined ? `"${lastSearchRaw}"` : undefined;
|
||||
const lastRead = readPaths?.at(-1);
|
||||
incomingHint = lastRead !== undefined ? getDisplayPath(lastRead) : lastSearch;
|
||||
const lastSearchRaw = searchArgs?.at(-1)
|
||||
const lastSearch =
|
||||
lastSearchRaw !== undefined ? `"${lastSearchRaw}"` : undefined
|
||||
const lastRead = readPaths?.at(-1)
|
||||
incomingHint =
|
||||
lastRead !== undefined ? getDisplayPath(lastRead) : lastSearch
|
||||
}
|
||||
|
||||
// Active REPL calls emit repl_tool_call progress with the current inner
|
||||
// tool's name+input. Virtual messages don't arrive until REPL completes,
|
||||
// so this is the only source of a live hint during execution.
|
||||
if (isActiveGroup) {
|
||||
for (const id_0 of toolUseIds) {
|
||||
if (!inProgressToolUseIDs.has(id_0)) continue;
|
||||
const latest = lookups.progressMessagesByToolUseID.get(id_0)?.at(-1)?.data as { type?: string; phase?: string; toolInput?: unknown; toolName?: string } | undefined;
|
||||
for (const id of toolUseIds) {
|
||||
if (!inProgressToolUseIDs.has(id)) continue
|
||||
const latest = lookups.progressMessagesByToolUseID.get(id)?.at(-1)?.data
|
||||
if (latest?.type === 'repl_tool_call' && latest.phase === 'start') {
|
||||
const input = latest.toolInput as {
|
||||
command?: string;
|
||||
pattern?: string;
|
||||
file_path?: string;
|
||||
};
|
||||
incomingHint = input.file_path ?? (input.pattern ? `"${input.pattern}"` : undefined) ?? input.command ?? latest.toolName;
|
||||
command?: string
|
||||
pattern?: string
|
||||
file_path?: string
|
||||
}
|
||||
incomingHint =
|
||||
input.file_path ??
|
||||
(input.pattern ? `"${input.pattern}"` : undefined) ??
|
||||
input.command ??
|
||||
latest.toolName
|
||||
}
|
||||
}
|
||||
}
|
||||
const displayedHint = useMinDisplayTime(incomingHint, MIN_HINT_DISPLAY_MS);
|
||||
|
||||
const displayedHint = useMinDisplayTime(incomingHint, MIN_HINT_DISPLAY_MS)
|
||||
|
||||
// In verbose mode, render each tool use with its 1-line result summary
|
||||
if (verbose) {
|
||||
const toolUses: NormalizedAssistantMessage[] = [];
|
||||
const toolUses: NormalizedAssistantMessage[] = []
|
||||
for (const msg of groupMessages) {
|
||||
if (msg.type === 'assistant') {
|
||||
toolUses.push(msg);
|
||||
toolUses.push(msg)
|
||||
} else if (msg.type === 'grouped_tool_use') {
|
||||
toolUses.push(...msg.messages);
|
||||
toolUses.push(...msg.messages)
|
||||
}
|
||||
}
|
||||
return <Box flexDirection="column">
|
||||
{toolUses.map(msg_0 => {
|
||||
const content = msg_0.message.content[0];
|
||||
if (!content || typeof content === 'string' || content?.type !== 'tool_use') return null;
|
||||
return <VerboseToolUse key={content.id} content={content} tools={tools} lookups={lookups} inProgressToolUseIDs={inProgressToolUseIDs} shouldAnimate={shouldAnimate} theme={theme} />;
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
{toolUses.map(msg => {
|
||||
const content = msg.message.content[0]
|
||||
if (content?.type !== 'tool_use') return null
|
||||
return (
|
||||
<VerboseToolUse
|
||||
key={content.id}
|
||||
content={content}
|
||||
tools={tools}
|
||||
lookups={lookups}
|
||||
inProgressToolUseIDs={inProgressToolUseIDs}
|
||||
shouldAnimate={shouldAnimate}
|
||||
theme={theme}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{message.hookInfos && message.hookInfos.length > 0 && <>
|
||||
{message.hookInfos && message.hookInfos.length > 0 && (
|
||||
<>
|
||||
<Text dimColor>
|
||||
{' ⎿ '}Ran {message.hookCount} PreToolUse{' '}
|
||||
{message.hookCount === 1 ? 'hook' : 'hooks'} (
|
||||
{formatSecondsShort(message.hookTotalMs ?? 0)})
|
||||
</Text>
|
||||
{message.hookInfos.map((info, idx) => <Text key={`hook-${idx}`} dimColor>
|
||||
{message.hookInfos.map((info, idx) => (
|
||||
<Text key={`hook-${idx}`} dimColor>
|
||||
{' ⎿ '}
|
||||
{info.command} ({formatSecondsShort(info.durationMs ?? 0)})
|
||||
</Text>)}
|
||||
</>}
|
||||
{message.relevantMemories?.map(m => <Box key={m.path} flexDirection="column" marginTop={1}>
|
||||
</Text>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{message.relevantMemories?.map(m => (
|
||||
<Box key={m.path} flexDirection="column" marginTop={1}>
|
||||
<Text dimColor>
|
||||
{' ⎿ '}Recalled {basename(m.path)}
|
||||
</Text>
|
||||
@@ -253,8 +278,10 @@ export function CollapsedReadSearchContent({
|
||||
<Ansi>{m.content}</Ansi>
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>)}
|
||||
</Box>;
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
// Non-verbose mode: Show counts with blinking grey dot while active, green dot when finalized
|
||||
@@ -263,70 +290,79 @@ export function CollapsedReadSearchContent({
|
||||
// Defensive: If all counts are 0, don't render the collapsed group
|
||||
// This shouldn't happen in normal operation, but handles edge cases
|
||||
if (!hasMemoryOps && !hasTeamMemoryOps && !hasNonMemoryOps) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
// Find the slowest in-progress shell command in this group. BashTool yields
|
||||
// progress every second but the collapsed renderer never showed it — long
|
||||
// commands (npm install, tests) looked frozen. Shown after 2s so fast
|
||||
// commands stay clean; the ticking counter reassures that slow ones aren't stuck.
|
||||
let shellProgressSuffix = '';
|
||||
let shellProgressSuffix = ''
|
||||
if (isFullscreenEnvEnabled() && isActiveGroup) {
|
||||
let elapsed: number | undefined;
|
||||
let lines = 0;
|
||||
for (const id_1 of toolUseIds) {
|
||||
if (!inProgressToolUseIDs.has(id_1)) continue;
|
||||
const data = lookups.progressMessagesByToolUseID.get(id_1)?.at(-1)?.data as { type?: string; elapsedTimeSeconds?: number; totalLines?: number } | undefined;
|
||||
if (data?.type !== 'bash_progress' && data?.type !== 'powershell_progress') {
|
||||
continue;
|
||||
let elapsed: number | undefined
|
||||
let lines = 0
|
||||
for (const id of toolUseIds) {
|
||||
if (!inProgressToolUseIDs.has(id)) continue
|
||||
const data = lookups.progressMessagesByToolUseID.get(id)?.at(-1)?.data
|
||||
if (
|
||||
data?.type !== 'bash_progress' &&
|
||||
data?.type !== 'powershell_progress'
|
||||
) {
|
||||
continue
|
||||
}
|
||||
if (elapsed === undefined || (data.elapsedTimeSeconds ?? 0) > elapsed) {
|
||||
elapsed = data.elapsedTimeSeconds ?? 0;
|
||||
lines = data.totalLines ?? 0;
|
||||
if (elapsed === undefined || data.elapsedTimeSeconds > elapsed) {
|
||||
elapsed = data.elapsedTimeSeconds
|
||||
lines = data.totalLines
|
||||
}
|
||||
}
|
||||
if (elapsed !== undefined && elapsed >= 2) {
|
||||
const time = formatDuration(elapsed * 1000);
|
||||
shellProgressSuffix = lines > 0 ? ` (${time} · ${lines} ${lines === 1 ? 'line' : 'lines'})` : ` (${time})`;
|
||||
const time = formatDuration(elapsed * 1000)
|
||||
shellProgressSuffix =
|
||||
lines > 0
|
||||
? ` (${time} · ${lines} ${lines === 1 ? 'line' : 'lines'})`
|
||||
: ` (${time})`
|
||||
}
|
||||
}
|
||||
|
||||
// Build non-memory parts first (search, read, repl, mcp, bash) — these render
|
||||
// before memory so the line reads "Ran 3 bash commands, recalled 1 memory".
|
||||
const nonMemParts: React.ReactNode[] = [];
|
||||
const nonMemParts: React.ReactNode[] = []
|
||||
|
||||
// Git operations lead the line — they're the load-bearing outcome.
|
||||
function pushPart(key: string, verb: string, body: React.ReactNode): void {
|
||||
const isFirst = nonMemParts.length === 0;
|
||||
if (!isFirst) nonMemParts.push(<Text key={`comma-${key}`}>, </Text>);
|
||||
nonMemParts.push(<Text key={key}>
|
||||
const isFirst = nonMemParts.length === 0
|
||||
if (!isFirst) nonMemParts.push(<Text key={`comma-${key}`}>, </Text>)
|
||||
nonMemParts.push(
|
||||
<Text key={key}>
|
||||
{isFirst ? verb[0]!.toUpperCase() + verb.slice(1) : verb} {body}
|
||||
</Text>);
|
||||
</Text>,
|
||||
)
|
||||
}
|
||||
if (isFullscreenEnvEnabled() && message.commits?.length) {
|
||||
const byKind = {
|
||||
committed: 'committed',
|
||||
amended: 'amended commit',
|
||||
'cherry-picked': 'cherry-picked'
|
||||
};
|
||||
'cherry-picked': 'cherry-picked',
|
||||
}
|
||||
for (const kind of ['committed', 'amended', 'cherry-picked'] as const) {
|
||||
const shas = message.commits.filter(c => c.kind === kind).map(c_0 => c_0.sha);
|
||||
const shas = message.commits.filter(c => c.kind === kind).map(c => c.sha)
|
||||
if (shas.length) {
|
||||
pushPart(kind, byKind[kind], <Text bold>{shas.join(', ')}</Text>);
|
||||
pushPart(kind, byKind[kind], <Text bold>{shas.join(', ')}</Text>)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isFullscreenEnvEnabled() && message.pushes?.length) {
|
||||
const branches = uniq(message.pushes.map(p => p.branch));
|
||||
pushPart('push', 'pushed to', <Text bold>{branches.join(', ')}</Text>);
|
||||
const branches = uniq(message.pushes.map(p => p.branch))
|
||||
pushPart('push', 'pushed to', <Text bold>{branches.join(', ')}</Text>)
|
||||
}
|
||||
if (isFullscreenEnvEnabled() && message.branches?.length) {
|
||||
const byAction = {
|
||||
merged: 'merged',
|
||||
rebased: 'rebased onto'
|
||||
};
|
||||
const byAction = { merged: 'merged', rebased: 'rebased onto' }
|
||||
for (const b of message.branches) {
|
||||
pushPart(`br-${b.action}-${b.ref}`, byAction[b.action], <Text bold>{b.ref}</Text>);
|
||||
pushPart(
|
||||
`br-${b.action}-${b.ref}`,
|
||||
byAction[b.action],
|
||||
<Text bold>{b.ref}</Text>,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (isFullscreenEnvEnabled() && message.prs?.length) {
|
||||
@@ -336,130 +372,224 @@ export function CollapsedReadSearchContent({
|
||||
merged: 'merged',
|
||||
commented: 'commented on',
|
||||
closed: 'closed',
|
||||
ready: 'marked ready'
|
||||
};
|
||||
ready: 'marked ready',
|
||||
}
|
||||
for (const pr of message.prs) {
|
||||
pushPart(`pr-${pr.action}-${pr.number}`, verbs[pr.action], pr.url ? <PrBadge number={pr.number} url={pr.url} bold /> : <Text bold>PR #{pr.number}</Text>);
|
||||
pushPart(
|
||||
`pr-${pr.action}-${pr.number}`,
|
||||
verbs[pr.action],
|
||||
pr.url ? (
|
||||
<PrBadge number={pr.number} url={pr.url} bold />
|
||||
) : (
|
||||
<Text bold>PR #{pr.number}</Text>
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (searchCount > 0) {
|
||||
const isFirst_0 = nonMemParts.length === 0;
|
||||
const searchVerb = isActiveGroup ? isFirst_0 ? 'Searching for' : 'searching for' : isFirst_0 ? 'Searched for' : 'searched for';
|
||||
if (!isFirst_0) {
|
||||
nonMemParts.push(<Text key="comma-s">, </Text>);
|
||||
const isFirst = nonMemParts.length === 0
|
||||
const searchVerb = isActiveGroup
|
||||
? isFirst
|
||||
? 'Searching for'
|
||||
: 'searching for'
|
||||
: isFirst
|
||||
? 'Searched for'
|
||||
: 'searched for'
|
||||
if (!isFirst) {
|
||||
nonMemParts.push(<Text key="comma-s">, </Text>)
|
||||
}
|
||||
nonMemParts.push(<Text key="search">
|
||||
nonMemParts.push(
|
||||
<Text key="search">
|
||||
{searchVerb} <Text bold>{searchCount}</Text>{' '}
|
||||
{searchCount === 1 ? 'pattern' : 'patterns'}
|
||||
</Text>);
|
||||
</Text>,
|
||||
)
|
||||
}
|
||||
|
||||
if (readCount > 0) {
|
||||
const isFirst_1 = nonMemParts.length === 0;
|
||||
const readVerb = isActiveGroup ? isFirst_1 ? 'Reading' : 'reading' : isFirst_1 ? 'Read' : 'read';
|
||||
if (!isFirst_1) {
|
||||
nonMemParts.push(<Text key="comma-r">, </Text>);
|
||||
const isFirst = nonMemParts.length === 0
|
||||
const readVerb = isActiveGroup
|
||||
? isFirst
|
||||
? 'Reading'
|
||||
: 'reading'
|
||||
: isFirst
|
||||
? 'Read'
|
||||
: 'read'
|
||||
if (!isFirst) {
|
||||
nonMemParts.push(<Text key="comma-r">, </Text>)
|
||||
}
|
||||
nonMemParts.push(<Text key="read">
|
||||
nonMemParts.push(
|
||||
<Text key="read">
|
||||
{readVerb} <Text bold>{readCount}</Text>{' '}
|
||||
{readCount === 1 ? 'file' : 'files'}
|
||||
</Text>);
|
||||
</Text>,
|
||||
)
|
||||
}
|
||||
|
||||
if (listCount > 0) {
|
||||
const isFirst_2 = nonMemParts.length === 0;
|
||||
const listVerb = isActiveGroup ? isFirst_2 ? 'Listing' : 'listing' : isFirst_2 ? 'Listed' : 'listed';
|
||||
if (!isFirst_2) {
|
||||
nonMemParts.push(<Text key="comma-l">, </Text>);
|
||||
const isFirst = nonMemParts.length === 0
|
||||
const listVerb = isActiveGroup
|
||||
? isFirst
|
||||
? 'Listing'
|
||||
: 'listing'
|
||||
: isFirst
|
||||
? 'Listed'
|
||||
: 'listed'
|
||||
if (!isFirst) {
|
||||
nonMemParts.push(<Text key="comma-l">, </Text>)
|
||||
}
|
||||
nonMemParts.push(<Text key="list">
|
||||
nonMemParts.push(
|
||||
<Text key="list">
|
||||
{listVerb} <Text bold>{listCount}</Text>{' '}
|
||||
{listCount === 1 ? 'directory' : 'directories'}
|
||||
</Text>);
|
||||
</Text>,
|
||||
)
|
||||
}
|
||||
|
||||
if (replCount > 0) {
|
||||
const replVerb = isActiveGroup ? "REPL'ing" : "REPL'd";
|
||||
const replVerb = isActiveGroup ? "REPL'ing" : "REPL'd"
|
||||
if (nonMemParts.length > 0) {
|
||||
nonMemParts.push(<Text key="comma-repl">, </Text>);
|
||||
nonMemParts.push(<Text key="comma-repl">, </Text>)
|
||||
}
|
||||
nonMemParts.push(<Text key="repl">
|
||||
nonMemParts.push(
|
||||
<Text key="repl">
|
||||
{replVerb} <Text bold>{replCount}</Text>{' '}
|
||||
{replCount === 1 ? 'time' : 'times'}
|
||||
</Text>);
|
||||
</Text>,
|
||||
)
|
||||
}
|
||||
|
||||
if (mcpCallCount > 0) {
|
||||
const serverLabel = message.mcpServerNames?.map(n => n.replace(/^claude\.ai /, '')).join(', ') || 'MCP';
|
||||
const isFirst_3 = nonMemParts.length === 0;
|
||||
const verb_0 = isActiveGroup ? isFirst_3 ? 'Querying' : 'querying' : isFirst_3 ? 'Queried' : 'queried';
|
||||
if (!isFirst_3) {
|
||||
nonMemParts.push(<Text key="comma-mcp">, </Text>);
|
||||
const serverLabel =
|
||||
message.mcpServerNames
|
||||
?.map(n => n.replace(/^claude\.ai /, ''))
|
||||
.join(', ') || 'MCP'
|
||||
const isFirst = nonMemParts.length === 0
|
||||
const verb = isActiveGroup
|
||||
? isFirst
|
||||
? 'Querying'
|
||||
: 'querying'
|
||||
: isFirst
|
||||
? 'Queried'
|
||||
: 'queried'
|
||||
if (!isFirst) {
|
||||
nonMemParts.push(<Text key="comma-mcp">, </Text>)
|
||||
}
|
||||
nonMemParts.push(<Text key="mcp">
|
||||
{verb_0} {serverLabel}
|
||||
{mcpCallCount > 1 && <>
|
||||
nonMemParts.push(
|
||||
<Text key="mcp">
|
||||
{verb} {serverLabel}
|
||||
{mcpCallCount > 1 && (
|
||||
<>
|
||||
{' '}
|
||||
<Text bold>{mcpCallCount}</Text> times
|
||||
</>}
|
||||
</Text>);
|
||||
</>
|
||||
)}
|
||||
</Text>,
|
||||
)
|
||||
}
|
||||
|
||||
if (isFullscreenEnvEnabled() && bashCount > 0) {
|
||||
const isFirst_4 = nonMemParts.length === 0;
|
||||
const verb_1 = isActiveGroup ? isFirst_4 ? 'Running' : 'running' : isFirst_4 ? 'Ran' : 'ran';
|
||||
if (!isFirst_4) {
|
||||
nonMemParts.push(<Text key="comma-bash">, </Text>);
|
||||
const isFirst = nonMemParts.length === 0
|
||||
const verb = isActiveGroup
|
||||
? isFirst
|
||||
? 'Running'
|
||||
: 'running'
|
||||
: isFirst
|
||||
? 'Ran'
|
||||
: 'ran'
|
||||
if (!isFirst) {
|
||||
nonMemParts.push(<Text key="comma-bash">, </Text>)
|
||||
}
|
||||
nonMemParts.push(<Text key="bash">
|
||||
{verb_1} <Text bold>{bashCount}</Text> bash{' '}
|
||||
nonMemParts.push(
|
||||
<Text key="bash">
|
||||
{verb} <Text bold>{bashCount}</Text> bash{' '}
|
||||
{bashCount === 1 ? 'command' : 'commands'}
|
||||
</Text>);
|
||||
</Text>,
|
||||
)
|
||||
}
|
||||
|
||||
// Build memory parts (auto-memory) — rendered after nonMemParts
|
||||
const hasPrecedingNonMem = nonMemParts.length > 0;
|
||||
const memParts: React.ReactNode[] = [];
|
||||
const hasPrecedingNonMem = nonMemParts.length > 0
|
||||
const memParts: React.ReactNode[] = []
|
||||
|
||||
if (memoryReadCount > 0) {
|
||||
const isFirst_5 = !hasPrecedingNonMem && memParts.length === 0;
|
||||
const verb_2 = isActiveGroup ? isFirst_5 ? 'Recalling' : 'recalling' : isFirst_5 ? 'Recalled' : 'recalled';
|
||||
if (!isFirst_5) {
|
||||
memParts.push(<Text key="comma-mr">, </Text>);
|
||||
const isFirst = !hasPrecedingNonMem && memParts.length === 0
|
||||
const verb = isActiveGroup
|
||||
? isFirst
|
||||
? 'Recalling'
|
||||
: 'recalling'
|
||||
: isFirst
|
||||
? 'Recalled'
|
||||
: 'recalled'
|
||||
if (!isFirst) {
|
||||
memParts.push(<Text key="comma-mr">, </Text>)
|
||||
}
|
||||
memParts.push(<Text key="mem-read">
|
||||
{verb_2} <Text bold>{memoryReadCount}</Text>{' '}
|
||||
memParts.push(
|
||||
<Text key="mem-read">
|
||||
{verb} <Text bold>{memoryReadCount}</Text>{' '}
|
||||
{memoryReadCount === 1 ? 'memory' : 'memories'}
|
||||
</Text>);
|
||||
</Text>,
|
||||
)
|
||||
}
|
||||
|
||||
if (memorySearchCount > 0) {
|
||||
const isFirst_6 = !hasPrecedingNonMem && memParts.length === 0;
|
||||
const verb_3 = isActiveGroup ? isFirst_6 ? 'Searching' : 'searching' : isFirst_6 ? 'Searched' : 'searched';
|
||||
if (!isFirst_6) {
|
||||
memParts.push(<Text key="comma-ms">, </Text>);
|
||||
const isFirst = !hasPrecedingNonMem && memParts.length === 0
|
||||
const verb = isActiveGroup
|
||||
? isFirst
|
||||
? 'Searching'
|
||||
: 'searching'
|
||||
: isFirst
|
||||
? 'Searched'
|
||||
: 'searched'
|
||||
if (!isFirst) {
|
||||
memParts.push(<Text key="comma-ms">, </Text>)
|
||||
}
|
||||
memParts.push(<Text key="mem-search">{`${verb_3} memories`}</Text>);
|
||||
memParts.push(<Text key="mem-search">{`${verb} memories`}</Text>)
|
||||
}
|
||||
|
||||
if (memoryWriteCount > 0) {
|
||||
const isFirst_7 = !hasPrecedingNonMem && memParts.length === 0;
|
||||
const verb_4 = isActiveGroup ? isFirst_7 ? 'Writing' : 'writing' : isFirst_7 ? 'Wrote' : 'wrote';
|
||||
if (!isFirst_7) {
|
||||
memParts.push(<Text key="comma-mw">, </Text>);
|
||||
const isFirst = !hasPrecedingNonMem && memParts.length === 0
|
||||
const verb = isActiveGroup
|
||||
? isFirst
|
||||
? 'Writing'
|
||||
: 'writing'
|
||||
: isFirst
|
||||
? 'Wrote'
|
||||
: 'wrote'
|
||||
if (!isFirst) {
|
||||
memParts.push(<Text key="comma-mw">, </Text>)
|
||||
}
|
||||
memParts.push(<Text key="mem-write">
|
||||
{verb_4} <Text bold>{memoryWriteCount}</Text>{' '}
|
||||
memParts.push(
|
||||
<Text key="mem-write">
|
||||
{verb} <Text bold>{memoryWriteCount}</Text>{' '}
|
||||
{memoryWriteCount === 1 ? 'memory' : 'memories'}
|
||||
</Text>);
|
||||
</Text>,
|
||||
)
|
||||
}
|
||||
return <Box flexDirection="column" marginTop={1} backgroundColor={bg}>
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" marginTop={1} backgroundColor={bg}>
|
||||
<Box flexDirection="row">
|
||||
{isActiveGroup ? <ToolUseLoader shouldAnimate isUnresolved isError={anyError} /> : <Box minWidth={2} />}
|
||||
{isActiveGroup ? (
|
||||
<ToolUseLoader shouldAnimate isUnresolved isError={anyError} />
|
||||
) : (
|
||||
<Box minWidth={2} />
|
||||
)}
|
||||
<Text dimColor={!isActiveGroup}>
|
||||
{nonMemParts}
|
||||
{memParts}
|
||||
{feature('TEAMMEM') ? teamMemCollapsed!.TeamMemCountParts({
|
||||
{feature('TEAMMEM')
|
||||
? teamMemCollapsed!.TeamMemCountParts({
|
||||
message,
|
||||
isActiveGroup,
|
||||
hasPrecedingParts: hasPrecedingNonMem || memParts.length > 0
|
||||
}) : null}
|
||||
hasPrecedingParts: hasPrecedingNonMem || memParts.length > 0,
|
||||
})
|
||||
: null}
|
||||
{isActiveGroup && <Text key="ellipsis">…</Text>} <CtrlOToExpand />
|
||||
</Text>
|
||||
</Box>
|
||||
{isActiveGroup && displayedHint !== undefined &&
|
||||
{isActiveGroup && displayedHint !== undefined && (
|
||||
// Row layout: 5-wide gutter for ⎿, then a flex column for the text.
|
||||
// Ink's wrap stays inside the right column so continuation lines
|
||||
// indent under ⎿. MAX_HINT_CHARS in commandAsHint caps total at ~5 lines.
|
||||
@@ -468,16 +598,22 @@ export function CollapsedReadSearchContent({
|
||||
<Text dimColor>{' ⎿ '}</Text>
|
||||
</Box>
|
||||
<Box flexDirection="column" flexGrow={1}>
|
||||
{displayedHint.split('\n').map((line, i, arr) => <Text key={`hint-${i}`} dimColor>
|
||||
{displayedHint.split('\n').map((line, i, arr) => (
|
||||
<Text key={`hint-${i}`} dimColor>
|
||||
{line}
|
||||
{i === arr.length - 1 && shellProgressSuffix}
|
||||
</Text>)}
|
||||
</Text>
|
||||
))}
|
||||
</Box>
|
||||
</Box>}
|
||||
{message.hookTotalMs !== undefined && message.hookTotalMs > 0 && <Text dimColor>
|
||||
</Box>
|
||||
)}
|
||||
{message.hookTotalMs !== undefined && message.hookTotalMs > 0 && (
|
||||
<Text dimColor>
|
||||
{' ⎿ '}Ran {message.hookCount} PreToolUse{' '}
|
||||
{message.hookCount === 1 ? 'hook' : 'hooks'} (
|
||||
{formatSecondsShort(message.hookTotalMs)})
|
||||
</Text>}
|
||||
</Box>;
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js';
|
||||
export function CompactBoundaryMessage() {
|
||||
const $ = _c(2);
|
||||
const historyShortcut = useShortcutDisplay("app:toggleTranscript", "Global", "ctrl+o");
|
||||
let t0;
|
||||
if ($[0] !== historyShortcut) {
|
||||
t0 = <Box marginY={1}><Text dimColor={true}>✻ Conversation compacted ({historyShortcut} for history)</Text></Box>;
|
||||
$[0] = historyShortcut;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
return t0;
|
||||
import * as React from 'react'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js'
|
||||
|
||||
export function CompactBoundaryMessage(): React.ReactNode {
|
||||
const historyShortcut = useShortcutDisplay(
|
||||
'app:toggleTranscript',
|
||||
'Global',
|
||||
'ctrl+o',
|
||||
)
|
||||
|
||||
return (
|
||||
<Box marginY={1}>
|
||||
<Text dimColor>
|
||||
✻ Conversation compacted ({historyShortcut} for history)
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,62 +1,71 @@
|
||||
import type { ToolResultBlockParam, ToolUseBlockParam } from '@anthropic-ai/sdk/resources/messages/messages.mjs';
|
||||
import * as React from 'react';
|
||||
import { filterToolProgressMessages, findToolByName, type Tools } from '../../Tool.js';
|
||||
import type { GroupedToolUseMessage } from '../../types/message.js';
|
||||
import type { buildMessageLookups } from '../../utils/messages.js';
|
||||
import type {
|
||||
ToolResultBlockParam,
|
||||
ToolUseBlockParam,
|
||||
} from '@anthropic-ai/sdk/resources/messages/messages.mjs'
|
||||
import * as React from 'react'
|
||||
import {
|
||||
filterToolProgressMessages,
|
||||
findToolByName,
|
||||
type Tools,
|
||||
} from '../../Tool.js'
|
||||
import type { GroupedToolUseMessage } from '../../types/message.js'
|
||||
import type { buildMessageLookups } from '../../utils/messages.js'
|
||||
|
||||
type Props = {
|
||||
message: GroupedToolUseMessage;
|
||||
tools: Tools;
|
||||
lookups: ReturnType<typeof buildMessageLookups>;
|
||||
inProgressToolUseIDs: Set<string>;
|
||||
shouldAnimate: boolean;
|
||||
};
|
||||
message: GroupedToolUseMessage
|
||||
tools: Tools
|
||||
lookups: ReturnType<typeof buildMessageLookups>
|
||||
inProgressToolUseIDs: Set<string>
|
||||
shouldAnimate: boolean
|
||||
}
|
||||
|
||||
export function GroupedToolUseContent({
|
||||
message,
|
||||
tools,
|
||||
lookups,
|
||||
inProgressToolUseIDs,
|
||||
shouldAnimate
|
||||
shouldAnimate,
|
||||
}: Props): React.ReactNode {
|
||||
const tool = findToolByName(tools, message.toolName);
|
||||
const tool = findToolByName(tools, message.toolName)
|
||||
if (!tool?.renderGroupedToolUse) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
// Build a map from tool_use_id to result data
|
||||
const resultsByToolUseId = new Map<string, {
|
||||
param: ToolResultBlockParam;
|
||||
output: unknown;
|
||||
}>();
|
||||
const resultsByToolUseId = new Map<
|
||||
string,
|
||||
{ param: ToolResultBlockParam; output: unknown }
|
||||
>()
|
||||
for (const resultMsg of message.results) {
|
||||
const contentArr = resultMsg.message.content;
|
||||
if (!Array.isArray(contentArr)) continue;
|
||||
for (const content of contentArr) {
|
||||
if (typeof content === 'string') continue;
|
||||
for (const content of resultMsg.message.content) {
|
||||
if (content.type === 'tool_result') {
|
||||
resultsByToolUseId.set((content as ToolResultBlockParam).tool_use_id, {
|
||||
param: content as ToolResultBlockParam,
|
||||
output: resultMsg.toolUseResult
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
const toolUsesData = message.messages.map(msg => {
|
||||
const contentArr = msg.message.content;
|
||||
const rawContent = Array.isArray(contentArr) ? contentArr[0] : undefined;
|
||||
const content = rawContent as ToolUseBlockParam;
|
||||
const result = resultsByToolUseId.get(content.id);
|
||||
return {
|
||||
resultsByToolUseId.set(content.tool_use_id, {
|
||||
param: content,
|
||||
output: resultMsg.toolUseResult,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const toolUsesData = message.messages.map(msg => {
|
||||
const content = msg.message.content[0]
|
||||
const result = resultsByToolUseId.get(content.id)
|
||||
return {
|
||||
param: content as ToolUseBlockParam,
|
||||
isResolved: lookups.resolvedToolUseIDs.has(content.id),
|
||||
isError: lookups.erroredToolUseIDs.has(content.id),
|
||||
isInProgress: inProgressToolUseIDs.has(content.id),
|
||||
progressMessages: filterToolProgressMessages(lookups.progressMessagesByToolUseID.get(content.id) ?? []),
|
||||
result
|
||||
};
|
||||
});
|
||||
const anyInProgress = toolUsesData.some(d => d.isInProgress);
|
||||
progressMessages: filterToolProgressMessages(
|
||||
lookups.progressMessagesByToolUseID.get(content.id) ?? [],
|
||||
),
|
||||
result,
|
||||
}
|
||||
})
|
||||
|
||||
const anyInProgress = toolUsesData.some(d => d.isInProgress)
|
||||
|
||||
return tool.renderGroupedToolUse(toolUsesData, {
|
||||
shouldAnimate: shouldAnimate && anyInProgress,
|
||||
tools
|
||||
});
|
||||
tools,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,161 +1,91 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import figures from 'figures';
|
||||
import * as React from 'react';
|
||||
import { useContext } from 'react';
|
||||
import { useQueuedMessage } from '../../context/QueuedMessageContext.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { formatBriefTimestamp } from '../../utils/formatBriefTimestamp.js';
|
||||
import { findThinkingTriggerPositions, getRainbowColor, isUltrathinkEnabled } from '../../utils/thinking.js';
|
||||
import { MessageActionsSelectedContext } from '../messageActions.js';
|
||||
import figures from 'figures'
|
||||
import * as React from 'react'
|
||||
import { useContext } from 'react'
|
||||
import { useQueuedMessage } from '../../context/QueuedMessageContext.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { formatBriefTimestamp } from '../../utils/formatBriefTimestamp.js'
|
||||
import {
|
||||
findThinkingTriggerPositions,
|
||||
getRainbowColor,
|
||||
isUltrathinkEnabled,
|
||||
} from '../../utils/thinking.js'
|
||||
import { MessageActionsSelectedContext } from '../messageActions.js'
|
||||
|
||||
type Props = {
|
||||
text: string;
|
||||
useBriefLayout?: boolean;
|
||||
timestamp?: string;
|
||||
};
|
||||
export function HighlightedThinkingText(t0) {
|
||||
const $ = _c(31);
|
||||
const {
|
||||
text: string
|
||||
useBriefLayout?: boolean
|
||||
timestamp?: string
|
||||
}
|
||||
|
||||
export function HighlightedThinkingText({
|
||||
text,
|
||||
useBriefLayout,
|
||||
timestamp
|
||||
} = t0;
|
||||
const isQueued = useQueuedMessage()?.isQueued ?? false;
|
||||
const isSelected = useContext(MessageActionsSelectedContext);
|
||||
const pointerColor = isSelected ? "suggestion" : "subtle";
|
||||
timestamp,
|
||||
}: Props): React.ReactNode {
|
||||
// Brief/assistant mode: chat-style "You" label instead of the ❯ highlight.
|
||||
// Parent drops its backgroundColor when this is true, so no grey shows
|
||||
// through. No manual wrap needed — Ink wraps inside the parent Box.
|
||||
const isQueued = useQueuedMessage()?.isQueued ?? false
|
||||
const isSelected = useContext(MessageActionsSelectedContext)
|
||||
const pointerColor = isSelected ? 'suggestion' : 'subtle'
|
||||
if (useBriefLayout) {
|
||||
let t1;
|
||||
if ($[0] !== timestamp) {
|
||||
t1 = timestamp ? formatBriefTimestamp(timestamp) : "";
|
||||
$[0] = timestamp;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
const ts = timestamp ? formatBriefTimestamp(timestamp) : ''
|
||||
return (
|
||||
<Box flexDirection="column" paddingLeft={2}>
|
||||
<Box flexDirection="row">
|
||||
<Text color={isQueued ? 'subtle' : 'briefLabelYou'}>You</Text>
|
||||
{ts ? <Text dimColor> {ts}</Text> : null}
|
||||
</Box>
|
||||
<Text color={isQueued ? 'subtle' : 'text'}>{text}</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
const ts = t1;
|
||||
const t2 = isQueued ? "subtle" : "briefLabelYou";
|
||||
let t3;
|
||||
if ($[2] !== t2) {
|
||||
t3 = <Text color={t2}>You</Text>;
|
||||
$[2] = t2;
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t3 = $[3];
|
||||
}
|
||||
let t4;
|
||||
if ($[4] !== ts) {
|
||||
t4 = ts ? <Text dimColor={true}> {ts}</Text> : null;
|
||||
$[4] = ts;
|
||||
$[5] = t4;
|
||||
} else {
|
||||
t4 = $[5];
|
||||
}
|
||||
let t5;
|
||||
if ($[6] !== t3 || $[7] !== t4) {
|
||||
t5 = <Box flexDirection="row">{t3}{t4}</Box>;
|
||||
$[6] = t3;
|
||||
$[7] = t4;
|
||||
$[8] = t5;
|
||||
} else {
|
||||
t5 = $[8];
|
||||
}
|
||||
const t6 = isQueued ? "subtle" : "text";
|
||||
let t7;
|
||||
if ($[9] !== t6 || $[10] !== text) {
|
||||
t7 = <Text color={t6}>{text}</Text>;
|
||||
$[9] = t6;
|
||||
$[10] = text;
|
||||
$[11] = t7;
|
||||
} else {
|
||||
t7 = $[11];
|
||||
}
|
||||
let t8;
|
||||
if ($[12] !== t5 || $[13] !== t7) {
|
||||
t8 = <Box flexDirection="column" paddingLeft={2}>{t5}{t7}</Box>;
|
||||
$[12] = t5;
|
||||
$[13] = t7;
|
||||
$[14] = t8;
|
||||
} else {
|
||||
t8 = $[14];
|
||||
}
|
||||
return t8;
|
||||
}
|
||||
let parts;
|
||||
let t1;
|
||||
if ($[15] !== pointerColor || $[16] !== text) {
|
||||
t1 = Symbol.for("react.early_return_sentinel");
|
||||
bb0: {
|
||||
const triggers = isUltrathinkEnabled() ? findThinkingTriggerPositions(text) : [];
|
||||
|
||||
const triggers = isUltrathinkEnabled()
|
||||
? findThinkingTriggerPositions(text)
|
||||
: []
|
||||
|
||||
if (triggers.length === 0) {
|
||||
let t2;
|
||||
if ($[19] !== pointerColor) {
|
||||
t2 = <Text color={pointerColor}>{figures.pointer} </Text>;
|
||||
$[19] = pointerColor;
|
||||
$[20] = t2;
|
||||
} else {
|
||||
t2 = $[20];
|
||||
return (
|
||||
<Text>
|
||||
<Text color={pointerColor}>{figures.pointer} </Text>
|
||||
<Text color="text">{text}</Text>
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
let t3;
|
||||
if ($[21] !== text) {
|
||||
t3 = <Text color="text">{text}</Text>;
|
||||
$[21] = text;
|
||||
$[22] = t3;
|
||||
} else {
|
||||
t3 = $[22];
|
||||
}
|
||||
let t4;
|
||||
if ($[23] !== t2 || $[24] !== t3) {
|
||||
t4 = <Text>{t2}{t3}</Text>;
|
||||
$[23] = t2;
|
||||
$[24] = t3;
|
||||
$[25] = t4;
|
||||
} else {
|
||||
t4 = $[25];
|
||||
}
|
||||
t1 = t4;
|
||||
break bb0;
|
||||
}
|
||||
parts = [];
|
||||
let cursor = 0;
|
||||
|
||||
// Static rainbow (no shimmer — transcript messages don't animate)
|
||||
const parts: React.ReactNode[] = []
|
||||
let cursor = 0
|
||||
for (const t of triggers) {
|
||||
if (t.start > cursor) {
|
||||
parts.push(<Text key={`plain-${cursor}`} color="text">{text.slice(cursor, t.start)}</Text>);
|
||||
parts.push(
|
||||
<Text key={`plain-${cursor}`} color="text">
|
||||
{text.slice(cursor, t.start)}
|
||||
</Text>,
|
||||
)
|
||||
}
|
||||
for (let i = t.start; i < t.end; i++) {
|
||||
parts.push(<Text key={`rb-${i}`} color={getRainbowColor(i - t.start)}>{text[i]}</Text>);
|
||||
parts.push(
|
||||
<Text key={`rb-${i}`} color={getRainbowColor(i - t.start)}>
|
||||
{text[i]}
|
||||
</Text>,
|
||||
)
|
||||
}
|
||||
cursor = t.end;
|
||||
cursor = t.end
|
||||
}
|
||||
if (cursor < text.length) {
|
||||
parts.push(<Text key={`plain-${cursor}`} color="text">{text.slice(cursor)}</Text>);
|
||||
parts.push(
|
||||
<Text key={`plain-${cursor}`} color="text">
|
||||
{text.slice(cursor)}
|
||||
</Text>,
|
||||
)
|
||||
}
|
||||
}
|
||||
$[15] = pointerColor;
|
||||
$[16] = text;
|
||||
$[17] = parts;
|
||||
$[18] = t1;
|
||||
} else {
|
||||
parts = $[17];
|
||||
t1 = $[18];
|
||||
}
|
||||
if (t1 !== Symbol.for("react.early_return_sentinel")) {
|
||||
return t1;
|
||||
}
|
||||
let t2;
|
||||
if ($[26] !== pointerColor) {
|
||||
t2 = <Text color={pointerColor}>{figures.pointer} </Text>;
|
||||
$[26] = pointerColor;
|
||||
$[27] = t2;
|
||||
} else {
|
||||
t2 = $[27];
|
||||
}
|
||||
let t3;
|
||||
if ($[28] !== parts || $[29] !== t2) {
|
||||
t3 = <Text>{t2}{parts}</Text>;
|
||||
$[28] = parts;
|
||||
$[29] = t2;
|
||||
$[30] = t3;
|
||||
} else {
|
||||
t3 = $[30];
|
||||
}
|
||||
return t3;
|
||||
|
||||
return (
|
||||
<Text>
|
||||
<Text color={pointerColor}>{figures.pointer} </Text>
|
||||
{parts}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,115 +1,67 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import type { HookEvent } from 'src/entrypoints/agentSdkTypes.js';
|
||||
import type { buildMessageLookups } from 'src/utils/messages.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { MessageResponse } from '../MessageResponse.js';
|
||||
import * as React from 'react'
|
||||
import type { HookEvent } from 'src/entrypoints/agentSdkTypes.js'
|
||||
import type { buildMessageLookups } from 'src/utils/messages.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { MessageResponse } from '../MessageResponse.js'
|
||||
|
||||
type Props = {
|
||||
hookEvent: HookEvent;
|
||||
lookups: ReturnType<typeof buildMessageLookups>;
|
||||
toolUseID: string;
|
||||
verbose: boolean;
|
||||
isTranscriptMode?: boolean;
|
||||
};
|
||||
export function HookProgressMessage(t0) {
|
||||
const $ = _c(22);
|
||||
const {
|
||||
hookEvent: HookEvent
|
||||
lookups: ReturnType<typeof buildMessageLookups>
|
||||
toolUseID: string
|
||||
verbose: boolean
|
||||
isTranscriptMode?: boolean
|
||||
}
|
||||
|
||||
export function HookProgressMessage({
|
||||
hookEvent,
|
||||
lookups,
|
||||
toolUseID,
|
||||
isTranscriptMode
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] !== hookEvent || $[1] !== lookups.inProgressHookCounts || $[2] !== toolUseID) {
|
||||
t1 = lookups.inProgressHookCounts.get(toolUseID)?.get(hookEvent) ?? 0;
|
||||
$[0] = hookEvent;
|
||||
$[1] = lookups.inProgressHookCounts;
|
||||
$[2] = toolUseID;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
const inProgressHookCount = t1;
|
||||
const resolvedHookCount = lookups.resolvedHookCounts.get(toolUseID)?.get(hookEvent) ?? 0;
|
||||
isTranscriptMode,
|
||||
}: Props): React.ReactNode {
|
||||
const inProgressHookCount =
|
||||
lookups.inProgressHookCounts.get(toolUseID)?.get(hookEvent) ?? 0
|
||||
const resolvedHookCount =
|
||||
lookups.resolvedHookCounts.get(toolUseID)?.get(hookEvent) ?? 0
|
||||
if (inProgressHookCount === 0) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
if (hookEvent === "PreToolUse" || hookEvent === "PostToolUse") {
|
||||
|
||||
if (hookEvent === 'PreToolUse' || hookEvent === 'PostToolUse') {
|
||||
// In transcript mode, show a static summary since messages never re-render
|
||||
// (so a transient "Running..." would get stuck).
|
||||
if (isTranscriptMode) {
|
||||
let t2;
|
||||
if ($[4] !== inProgressHookCount) {
|
||||
t2 = <Text dimColor={true}>{inProgressHookCount} </Text>;
|
||||
$[4] = inProgressHookCount;
|
||||
$[5] = t2;
|
||||
} else {
|
||||
t2 = $[5];
|
||||
return (
|
||||
<MessageResponse>
|
||||
<Box flexDirection="row">
|
||||
<Text dimColor>{inProgressHookCount} </Text>
|
||||
<Text dimColor bold>
|
||||
{hookEvent}
|
||||
</Text>
|
||||
<Text dimColor>
|
||||
{inProgressHookCount === 1 ? ' hook' : ' hooks'} ran
|
||||
</Text>
|
||||
</Box>
|
||||
</MessageResponse>
|
||||
)
|
||||
}
|
||||
let t3;
|
||||
if ($[6] !== hookEvent) {
|
||||
t3 = <Text dimColor={true} bold={true}>{hookEvent}</Text>;
|
||||
$[6] = hookEvent;
|
||||
$[7] = t3;
|
||||
} else {
|
||||
t3 = $[7];
|
||||
}
|
||||
const t4 = inProgressHookCount === 1 ? " hook" : " hooks";
|
||||
let t5;
|
||||
if ($[8] !== t4) {
|
||||
t5 = <Text dimColor={true}>{t4} ran</Text>;
|
||||
$[8] = t4;
|
||||
$[9] = t5;
|
||||
} else {
|
||||
t5 = $[9];
|
||||
}
|
||||
let t6;
|
||||
if ($[10] !== t2 || $[11] !== t3 || $[12] !== t5) {
|
||||
t6 = <MessageResponse><Box flexDirection="row">{t2}{t3}{t5}</Box></MessageResponse>;
|
||||
$[10] = t2;
|
||||
$[11] = t3;
|
||||
$[12] = t5;
|
||||
$[13] = t6;
|
||||
} else {
|
||||
t6 = $[13];
|
||||
}
|
||||
return t6;
|
||||
}
|
||||
return null;
|
||||
// Outside transcript mode, hide — completion info is shown via
|
||||
// async_hook_response attachments instead.
|
||||
return null
|
||||
}
|
||||
|
||||
if (resolvedHookCount === inProgressHookCount) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
let t2;
|
||||
if ($[14] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <Text dimColor={true}>Running </Text>;
|
||||
$[14] = t2;
|
||||
} else {
|
||||
t2 = $[14];
|
||||
}
|
||||
let t3;
|
||||
if ($[15] !== hookEvent) {
|
||||
t3 = <Text dimColor={true} bold={true}>{hookEvent}</Text>;
|
||||
$[15] = hookEvent;
|
||||
$[16] = t3;
|
||||
} else {
|
||||
t3 = $[16];
|
||||
}
|
||||
const t4 = inProgressHookCount === 1 ? " hook\u2026" : " hooks\u2026";
|
||||
let t5;
|
||||
if ($[17] !== t4) {
|
||||
t5 = <Text dimColor={true}>{t4}</Text>;
|
||||
$[17] = t4;
|
||||
$[18] = t5;
|
||||
} else {
|
||||
t5 = $[18];
|
||||
}
|
||||
let t6;
|
||||
if ($[19] !== t3 || $[20] !== t5) {
|
||||
t6 = <MessageResponse><Box flexDirection="row">{t2}{t3}{t5}</Box></MessageResponse>;
|
||||
$[19] = t3;
|
||||
$[20] = t5;
|
||||
$[21] = t6;
|
||||
} else {
|
||||
t6 = $[21];
|
||||
}
|
||||
return t6;
|
||||
|
||||
return (
|
||||
<MessageResponse>
|
||||
<Box flexDirection="row">
|
||||
<Text dimColor>Running </Text>
|
||||
<Text dimColor bold>
|
||||
{hookEvent}
|
||||
</Text>
|
||||
<Text dimColor>{inProgressHookCount === 1 ? ' hook…' : ' hooks…'}</Text>
|
||||
</Box>
|
||||
</MessageResponse>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,149 +1,158 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { Markdown } from '../../components/Markdown.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { jsonParse } from '../../utils/slowOperations.js';
|
||||
import { type IdleNotificationMessage, isIdleNotification, isPlanApprovalRequest, isPlanApprovalResponse, type PlanApprovalRequestMessage, type PlanApprovalResponseMessage } from '../../utils/teammateMailbox.js';
|
||||
import { getShutdownMessageSummary } from './ShutdownMessage.js';
|
||||
import { getTaskAssignmentSummary } from './TaskAssignmentMessage.js';
|
||||
import * as React from 'react'
|
||||
import { Markdown } from '../../components/Markdown.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { jsonParse } from '../../utils/slowOperations.js'
|
||||
import {
|
||||
type IdleNotificationMessage,
|
||||
isIdleNotification,
|
||||
isPlanApprovalRequest,
|
||||
isPlanApprovalResponse,
|
||||
type PlanApprovalRequestMessage,
|
||||
type PlanApprovalResponseMessage,
|
||||
} from '../../utils/teammateMailbox.js'
|
||||
import { getShutdownMessageSummary } from './ShutdownMessage.js'
|
||||
import { getTaskAssignmentSummary } from './TaskAssignmentMessage.js'
|
||||
|
||||
type PlanApprovalRequestProps = {
|
||||
request: PlanApprovalRequestMessage;
|
||||
};
|
||||
request: PlanApprovalRequestMessage
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a plan approval request with a planMode-colored border,
|
||||
* showing the plan content and instructions for approving/rejecting.
|
||||
*/
|
||||
export function PlanApprovalRequestDisplay(t0) {
|
||||
const $ = _c(10);
|
||||
const {
|
||||
request
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] !== request.from) {
|
||||
t1 = <Box marginBottom={1}><Text color="planMode" bold={true}>Plan Approval Request from {request.from}</Text></Box>;
|
||||
$[0] = request.from;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
let t2;
|
||||
if ($[2] !== request.planContent) {
|
||||
t2 = <Box borderStyle="dashed" borderColor="subtle" borderLeft={false} borderRight={false} flexDirection="column" paddingX={1} marginBottom={1}><Markdown>{request.planContent}</Markdown></Box>;
|
||||
$[2] = request.planContent;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
let t3;
|
||||
if ($[4] !== request.planFilePath) {
|
||||
t3 = <Text dimColor={true}>Plan file: {request.planFilePath}</Text>;
|
||||
$[4] = request.planFilePath;
|
||||
$[5] = t3;
|
||||
} else {
|
||||
t3 = $[5];
|
||||
}
|
||||
let t4;
|
||||
if ($[6] !== t1 || $[7] !== t2 || $[8] !== t3) {
|
||||
t4 = <Box flexDirection="column" marginY={1}><Box borderStyle="round" borderColor="planMode" flexDirection="column" paddingX={1}>{t1}{t2}{t3}</Box></Box>;
|
||||
$[6] = t1;
|
||||
$[7] = t2;
|
||||
$[8] = t3;
|
||||
$[9] = t4;
|
||||
} else {
|
||||
t4 = $[9];
|
||||
}
|
||||
return t4;
|
||||
export function PlanApprovalRequestDisplay({
|
||||
request,
|
||||
}: PlanApprovalRequestProps): React.ReactNode {
|
||||
return (
|
||||
<Box flexDirection="column" marginY={1}>
|
||||
<Box
|
||||
borderStyle="round"
|
||||
borderColor="planMode"
|
||||
flexDirection="column"
|
||||
paddingX={1}
|
||||
>
|
||||
<Box marginBottom={1}>
|
||||
<Text color="planMode" bold>
|
||||
Plan Approval Request from {request.from}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box
|
||||
borderStyle="dashed"
|
||||
borderColor="subtle"
|
||||
borderLeft={false}
|
||||
borderRight={false}
|
||||
flexDirection="column"
|
||||
paddingX={1}
|
||||
marginBottom={1}
|
||||
>
|
||||
<Markdown>{request.planContent}</Markdown>
|
||||
</Box>
|
||||
<Text dimColor>Plan file: {request.planFilePath}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
type PlanApprovalResponseProps = {
|
||||
response: PlanApprovalResponseMessage;
|
||||
senderName: string;
|
||||
};
|
||||
response: PlanApprovalResponseMessage
|
||||
senderName: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a plan approval response with a success (green) or error (red) border.
|
||||
*/
|
||||
export function PlanApprovalResponseDisplay(t0) {
|
||||
const $ = _c(13);
|
||||
const {
|
||||
export function PlanApprovalResponseDisplay({
|
||||
response,
|
||||
senderName
|
||||
} = t0;
|
||||
senderName,
|
||||
}: PlanApprovalResponseProps): React.ReactNode {
|
||||
if (response.approved) {
|
||||
let t1;
|
||||
if ($[0] !== senderName) {
|
||||
t1 = <Box><Text color="success" bold={true}>✓ Plan Approved by {senderName}</Text></Box>;
|
||||
$[0] = senderName;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
return (
|
||||
<Box flexDirection="column" marginY={1}>
|
||||
<Box
|
||||
borderStyle="round"
|
||||
borderColor="success"
|
||||
flexDirection="column"
|
||||
paddingX={1}
|
||||
paddingY={1}
|
||||
>
|
||||
<Box>
|
||||
<Text color="success" bold>
|
||||
✓ Plan Approved by {senderName}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box marginTop={1}>
|
||||
<Text>
|
||||
You can now proceed with implementation. Your plan mode
|
||||
restrictions have been lifted.
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
let t2;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <Box marginTop={1}><Text>You can now proceed with implementation. Your plan mode restrictions have been lifted.</Text></Box>;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
let t3;
|
||||
if ($[3] !== t1) {
|
||||
t3 = <Box flexDirection="column" marginY={1}><Box borderStyle="round" borderColor="success" flexDirection="column" paddingX={1} paddingY={1}>{t1}{t2}</Box></Box>;
|
||||
$[3] = t1;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
let t1;
|
||||
if ($[5] !== senderName) {
|
||||
t1 = <Box><Text color="error" bold={true}>✗ Plan Rejected by {senderName}</Text></Box>;
|
||||
$[5] = senderName;
|
||||
$[6] = t1;
|
||||
} else {
|
||||
t1 = $[6];
|
||||
}
|
||||
let t2;
|
||||
if ($[7] !== response.feedback) {
|
||||
t2 = response.feedback && <Box marginTop={1} borderStyle="dashed" borderColor="subtle" borderLeft={false} borderRight={false} paddingX={1}><Text>Feedback: {response.feedback}</Text></Box>;
|
||||
$[7] = response.feedback;
|
||||
$[8] = t2;
|
||||
} else {
|
||||
t2 = $[8];
|
||||
}
|
||||
let t3;
|
||||
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = <Box marginTop={1}><Text dimColor={true}>Please revise your plan based on the feedback and call ExitPlanMode again.</Text></Box>;
|
||||
$[9] = t3;
|
||||
} else {
|
||||
t3 = $[9];
|
||||
}
|
||||
let t4;
|
||||
if ($[10] !== t1 || $[11] !== t2) {
|
||||
t4 = <Box flexDirection="column" marginY={1}><Box borderStyle="round" borderColor="error" flexDirection="column" paddingX={1} paddingY={1}>{t1}{t2}{t3}</Box></Box>;
|
||||
$[10] = t1;
|
||||
$[11] = t2;
|
||||
$[12] = t4;
|
||||
} else {
|
||||
t4 = $[12];
|
||||
}
|
||||
return t4;
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" marginY={1}>
|
||||
<Box
|
||||
borderStyle="round"
|
||||
borderColor="error"
|
||||
flexDirection="column"
|
||||
paddingX={1}
|
||||
paddingY={1}
|
||||
>
|
||||
<Box>
|
||||
<Text color="error" bold>
|
||||
✗ Plan Rejected by {senderName}
|
||||
</Text>
|
||||
</Box>
|
||||
{response.feedback && (
|
||||
<Box
|
||||
marginTop={1}
|
||||
borderStyle="dashed"
|
||||
borderColor="subtle"
|
||||
borderLeft={false}
|
||||
borderRight={false}
|
||||
paddingX={1}
|
||||
>
|
||||
<Text>Feedback: {response.feedback}</Text>
|
||||
</Box>
|
||||
)}
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>
|
||||
Please revise your plan based on the feedback and call ExitPlanMode
|
||||
again.
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to parse and render a plan approval message from raw content.
|
||||
* Returns the rendered component if it's a plan approval message, null otherwise.
|
||||
*/
|
||||
export function tryRenderPlanApprovalMessage(content: string, senderName: string): React.ReactNode | null {
|
||||
const request = isPlanApprovalRequest(content);
|
||||
export function tryRenderPlanApprovalMessage(
|
||||
content: string,
|
||||
senderName: string,
|
||||
): React.ReactNode | null {
|
||||
const request = isPlanApprovalRequest(content)
|
||||
if (request) {
|
||||
return <PlanApprovalRequestDisplay request={request} />;
|
||||
return <PlanApprovalRequestDisplay request={request} />
|
||||
}
|
||||
const response = isPlanApprovalResponse(content);
|
||||
|
||||
const response = isPlanApprovalResponse(content)
|
||||
if (response) {
|
||||
return <PlanApprovalResponseDisplay response={response} senderName={senderName} />;
|
||||
return (
|
||||
<PlanApprovalResponseDisplay
|
||||
response={response}
|
||||
senderName={senderName}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return null;
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -152,34 +161,36 @@ export function tryRenderPlanApprovalMessage(content: string, senderName: string
|
||||
* Returns null if the content is not a plan approval message.
|
||||
*/
|
||||
function getPlanApprovalSummary(content: string): string | null {
|
||||
const request = isPlanApprovalRequest(content);
|
||||
const request = isPlanApprovalRequest(content)
|
||||
if (request) {
|
||||
return `[Plan Approval Request from ${request.from}]`;
|
||||
return `[Plan Approval Request from ${request.from}]`
|
||||
}
|
||||
const response = isPlanApprovalResponse(content);
|
||||
|
||||
const response = isPlanApprovalResponse(content)
|
||||
if (response) {
|
||||
if (response.approved) {
|
||||
return '[Plan Approved] You can now proceed with implementation';
|
||||
return '[Plan Approved] You can now proceed with implementation'
|
||||
} else {
|
||||
return `[Plan Rejected] ${response.feedback || 'Please revise your plan'}`;
|
||||
return `[Plan Rejected] ${response.feedback || 'Please revise your plan'}`
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a brief summary text for an idle notification.
|
||||
*/
|
||||
function getIdleNotificationSummary(msg: IdleNotificationMessage): string {
|
||||
const parts: string[] = ['Agent idle'];
|
||||
const parts: string[] = ['Agent idle']
|
||||
if (msg.completedTaskId) {
|
||||
const status = msg.completedStatus || 'completed';
|
||||
parts.push(`Task ${msg.completedTaskId} ${status}`);
|
||||
const status = msg.completedStatus || 'completed'
|
||||
parts.push(`Task ${msg.completedTaskId} ${status}`)
|
||||
}
|
||||
if (msg.summary) {
|
||||
parts.push(`Last DM: ${msg.summary}`);
|
||||
parts.push(`Last DM: ${msg.summary}`)
|
||||
}
|
||||
return parts.join(' · ');
|
||||
return parts.join(' · ')
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -188,34 +199,35 @@ function getIdleNotificationSummary(msg: IdleNotificationMessage): string {
|
||||
* Otherwise returns the original content.
|
||||
*/
|
||||
export function formatTeammateMessageContent(content: string): string {
|
||||
const planSummary = getPlanApprovalSummary(content);
|
||||
const planSummary = getPlanApprovalSummary(content)
|
||||
if (planSummary) {
|
||||
return planSummary;
|
||||
return planSummary
|
||||
}
|
||||
const shutdownSummary = getShutdownMessageSummary(content);
|
||||
|
||||
const shutdownSummary = getShutdownMessageSummary(content)
|
||||
if (shutdownSummary) {
|
||||
return shutdownSummary;
|
||||
return shutdownSummary
|
||||
}
|
||||
const idleMsg = isIdleNotification(content);
|
||||
|
||||
const idleMsg = isIdleNotification(content)
|
||||
if (idleMsg) {
|
||||
return getIdleNotificationSummary(idleMsg);
|
||||
return getIdleNotificationSummary(idleMsg)
|
||||
}
|
||||
const taskAssignmentSummary = getTaskAssignmentSummary(content);
|
||||
|
||||
const taskAssignmentSummary = getTaskAssignmentSummary(content)
|
||||
if (taskAssignmentSummary) {
|
||||
return taskAssignmentSummary;
|
||||
return taskAssignmentSummary
|
||||
}
|
||||
|
||||
// Check for teammate_terminated message
|
||||
try {
|
||||
const parsed = jsonParse(content) as {
|
||||
type?: string;
|
||||
message?: string;
|
||||
};
|
||||
const parsed = jsonParse(content) as { type?: string; message?: string }
|
||||
if (parsed?.type === 'teammate_terminated' && parsed.message) {
|
||||
return parsed.message;
|
||||
return parsed.message
|
||||
}
|
||||
} catch {
|
||||
// Not JSON
|
||||
}
|
||||
return content;
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
@@ -1,160 +1,131 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { extraUsage } from 'src/commands/extra-usage/index.js';
|
||||
import { Box, Text } from 'src/ink.js';
|
||||
import { useClaudeAiLimits } from 'src/services/claudeAiLimitsHook.js';
|
||||
import { shouldProcessMockLimits } from 'src/services/rateLimitMocking.js'; // Used for /mock-limits command
|
||||
import { getRateLimitTier, getSubscriptionType, isClaudeAISubscriber } from 'src/utils/auth.js';
|
||||
import { hasClaudeAiBillingAccess } from 'src/utils/billing.js';
|
||||
import { MessageResponse } from '../MessageResponse.js';
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { extraUsage } from 'src/commands/extra-usage/index.js'
|
||||
import { Box, Text } from 'src/ink.js'
|
||||
import { useClaudeAiLimits } from 'src/services/claudeAiLimitsHook.js'
|
||||
import { shouldProcessMockLimits } from 'src/services/rateLimitMocking.js' // Used for /mock-limits command
|
||||
import {
|
||||
getRateLimitTier,
|
||||
getSubscriptionType,
|
||||
isClaudeAISubscriber,
|
||||
} from 'src/utils/auth.js'
|
||||
import { hasClaudeAiBillingAccess } from 'src/utils/billing.js'
|
||||
import { MessageResponse } from '../MessageResponse.js'
|
||||
|
||||
type UpsellParams = {
|
||||
shouldShowUpsell: boolean;
|
||||
isMax20x: boolean;
|
||||
isExtraUsageCommandEnabled: boolean;
|
||||
shouldAutoOpenRateLimitOptionsMenu: boolean;
|
||||
isTeamOrEnterprise: boolean;
|
||||
hasBillingAccess: boolean;
|
||||
};
|
||||
shouldShowUpsell: boolean
|
||||
isMax20x: boolean
|
||||
isExtraUsageCommandEnabled: boolean
|
||||
shouldAutoOpenRateLimitOptionsMenu: boolean
|
||||
isTeamOrEnterprise: boolean
|
||||
hasBillingAccess: boolean
|
||||
}
|
||||
|
||||
export function getUpsellMessage({
|
||||
shouldShowUpsell,
|
||||
isMax20x,
|
||||
isExtraUsageCommandEnabled,
|
||||
shouldAutoOpenRateLimitOptionsMenu,
|
||||
isTeamOrEnterprise,
|
||||
hasBillingAccess
|
||||
hasBillingAccess,
|
||||
}: UpsellParams): string | null {
|
||||
if (!shouldShowUpsell) return null;
|
||||
if (!shouldShowUpsell) return null
|
||||
|
||||
if (isMax20x) {
|
||||
if (isExtraUsageCommandEnabled) {
|
||||
return '/extra-usage to finish what you\u2019re working on.';
|
||||
return '/extra-usage to finish what you\u2019re working on.'
|
||||
}
|
||||
return '/login to switch to an API usage-billed account.';
|
||||
return '/login to switch to an API usage-billed account.'
|
||||
}
|
||||
|
||||
if (shouldAutoOpenRateLimitOptionsMenu) {
|
||||
return 'Opening your options\u2026';
|
||||
return 'Opening your options\u2026'
|
||||
}
|
||||
|
||||
if (!isTeamOrEnterprise && !isExtraUsageCommandEnabled) {
|
||||
return '/upgrade to increase your usage limit.';
|
||||
return '/upgrade to increase your usage limit.'
|
||||
}
|
||||
|
||||
if (isTeamOrEnterprise) {
|
||||
if (!isExtraUsageCommandEnabled) return null;
|
||||
if (!isExtraUsageCommandEnabled) return null
|
||||
|
||||
if (hasBillingAccess) {
|
||||
return '/extra-usage to finish what you\u2019re working on.';
|
||||
return '/extra-usage to finish what you\u2019re working on.'
|
||||
}
|
||||
return '/extra-usage to request more usage from your admin.';
|
||||
|
||||
return '/extra-usage to request more usage from your admin.'
|
||||
}
|
||||
return '/upgrade or /extra-usage to finish what you\u2019re working on.';
|
||||
|
||||
return '/upgrade or /extra-usage to finish what you\u2019re working on.'
|
||||
}
|
||||
|
||||
type RateLimitMessageProps = {
|
||||
text: string;
|
||||
onOpenRateLimitOptions?: () => void;
|
||||
};
|
||||
export function RateLimitMessage(t0) {
|
||||
const $ = _c(16);
|
||||
const {
|
||||
text: string
|
||||
onOpenRateLimitOptions?: () => void
|
||||
}
|
||||
|
||||
export function RateLimitMessage({
|
||||
text,
|
||||
onOpenRateLimitOptions,
|
||||
}: RateLimitMessageProps): React.ReactNode {
|
||||
const subscriptionType = getSubscriptionType()
|
||||
const rateLimitTier = getRateLimitTier()
|
||||
const isTeamOrEnterprise =
|
||||
subscriptionType === 'team' || subscriptionType === 'enterprise'
|
||||
const isMax20x = rateLimitTier === 'default_claude_max_20x'
|
||||
// Always show upsell when using /mock-limits command, otherwise show for subscribers
|
||||
const shouldShowUpsell = shouldProcessMockLimits() || isClaudeAISubscriber()
|
||||
|
||||
const canSeeRateLimitOptionsUpsell = shouldShowUpsell && !isMax20x
|
||||
|
||||
const [hasOpenedInteractiveMenu, setHasOpenedInteractiveMenu] =
|
||||
useState(false)
|
||||
|
||||
// Check actual rate limit status - only auto-open if user is currently rate limited
|
||||
// AND we've verified this with the API (resetsAt is only set after API response).
|
||||
// This prevents false alerts when resuming sessions with old rate limit messages.
|
||||
const claudeAiLimits = useClaudeAiLimits()
|
||||
const isCurrentlyRateLimited =
|
||||
claudeAiLimits.status === 'rejected' &&
|
||||
claudeAiLimits.resetsAt !== undefined &&
|
||||
!claudeAiLimits.isUsingOverage
|
||||
|
||||
const shouldAutoOpenRateLimitOptionsMenu =
|
||||
canSeeRateLimitOptionsUpsell &&
|
||||
!hasOpenedInteractiveMenu &&
|
||||
isCurrentlyRateLimited &&
|
||||
onOpenRateLimitOptions
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = getSubscriptionType();
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
const subscriptionType = t1;
|
||||
let t2;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = getRateLimitTier();
|
||||
$[1] = t2;
|
||||
} else {
|
||||
t2 = $[1];
|
||||
}
|
||||
const rateLimitTier = t2;
|
||||
const isTeamOrEnterprise = subscriptionType === "team" || subscriptionType === "enterprise";
|
||||
const isMax20x = rateLimitTier === "default_claude_max_20x";
|
||||
let t3;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = shouldProcessMockLimits() || isClaudeAISubscriber();
|
||||
$[2] = t3;
|
||||
} else {
|
||||
t3 = $[2];
|
||||
}
|
||||
const shouldShowUpsell = t3;
|
||||
const canSeeRateLimitOptionsUpsell = shouldShowUpsell && !isMax20x;
|
||||
const [hasOpenedInteractiveMenu, setHasOpenedInteractiveMenu] = useState(false);
|
||||
const claudeAiLimits = useClaudeAiLimits();
|
||||
const isCurrentlyRateLimited = claudeAiLimits.status === "rejected" && claudeAiLimits.resetsAt !== undefined && !claudeAiLimits.isUsingOverage;
|
||||
const shouldAutoOpenRateLimitOptionsMenu = canSeeRateLimitOptionsUpsell && !hasOpenedInteractiveMenu && isCurrentlyRateLimited && onOpenRateLimitOptions;
|
||||
let t4;
|
||||
let t5;
|
||||
if ($[3] !== onOpenRateLimitOptions || $[4] !== shouldAutoOpenRateLimitOptionsMenu) {
|
||||
t4 = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldAutoOpenRateLimitOptionsMenu) {
|
||||
setHasOpenedInteractiveMenu(true);
|
||||
onOpenRateLimitOptions();
|
||||
setHasOpenedInteractiveMenu(true)
|
||||
onOpenRateLimitOptions()
|
||||
}
|
||||
};
|
||||
t5 = [shouldAutoOpenRateLimitOptionsMenu, onOpenRateLimitOptions];
|
||||
$[3] = onOpenRateLimitOptions;
|
||||
$[4] = shouldAutoOpenRateLimitOptionsMenu;
|
||||
$[5] = t4;
|
||||
$[6] = t5;
|
||||
} else {
|
||||
t4 = $[5];
|
||||
t5 = $[6];
|
||||
}
|
||||
useEffect(t4, t5);
|
||||
let t6;
|
||||
bb0: {
|
||||
let t7;
|
||||
if ($[7] !== shouldAutoOpenRateLimitOptionsMenu) {
|
||||
t7 = getUpsellMessage({
|
||||
}, [shouldAutoOpenRateLimitOptionsMenu, onOpenRateLimitOptions])
|
||||
|
||||
const upsell = useMemo(() => {
|
||||
const message = getUpsellMessage({
|
||||
shouldShowUpsell,
|
||||
isMax20x,
|
||||
isExtraUsageCommandEnabled: extraUsage.isEnabled(),
|
||||
shouldAutoOpenRateLimitOptionsMenu: !!shouldAutoOpenRateLimitOptionsMenu,
|
||||
isTeamOrEnterprise,
|
||||
hasBillingAccess: hasClaudeAiBillingAccess()
|
||||
});
|
||||
$[7] = shouldAutoOpenRateLimitOptionsMenu;
|
||||
$[8] = t7;
|
||||
} else {
|
||||
t7 = $[8];
|
||||
}
|
||||
const message = t7;
|
||||
if (!message) {
|
||||
t6 = null;
|
||||
break bb0;
|
||||
}
|
||||
let t8;
|
||||
if ($[9] !== message) {
|
||||
t8 = <Text dimColor={true}>{message}</Text>;
|
||||
$[9] = message;
|
||||
$[10] = t8;
|
||||
} else {
|
||||
t8 = $[10];
|
||||
}
|
||||
t6 = t8;
|
||||
}
|
||||
const upsell = t6;
|
||||
let t7;
|
||||
if ($[11] !== text) {
|
||||
t7 = <Text color="error">{text}</Text>;
|
||||
$[11] = text;
|
||||
$[12] = t7;
|
||||
} else {
|
||||
t7 = $[12];
|
||||
}
|
||||
const t8 = hasOpenedInteractiveMenu ? null : upsell;
|
||||
let t9;
|
||||
if ($[13] !== t7 || $[14] !== t8) {
|
||||
t9 = <MessageResponse><Box flexDirection="column">{t7}{t8}</Box></MessageResponse>;
|
||||
$[13] = t7;
|
||||
$[14] = t8;
|
||||
$[15] = t9;
|
||||
} else {
|
||||
t9 = $[15];
|
||||
}
|
||||
return t9;
|
||||
hasBillingAccess: hasClaudeAiBillingAccess(),
|
||||
})
|
||||
if (!message) return null
|
||||
return <Text dimColor>{message}</Text>
|
||||
}, [
|
||||
shouldShowUpsell,
|
||||
isMax20x,
|
||||
isTeamOrEnterprise,
|
||||
shouldAutoOpenRateLimitOptionsMenu,
|
||||
])
|
||||
|
||||
return (
|
||||
<MessageResponse>
|
||||
<Box flexDirection="column">
|
||||
<Text color="error">{text}</Text>
|
||||
{hasOpenedInteractiveMenu ? null : upsell}
|
||||
</Box>
|
||||
</MessageResponse>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,112 +1,113 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { isShutdownApproved, isShutdownRejected, isShutdownRequest, type ShutdownRejectedMessage, type ShutdownRequestMessage } from '../../utils/teammateMailbox.js';
|
||||
import * as React from 'react'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import {
|
||||
isShutdownApproved,
|
||||
isShutdownRejected,
|
||||
isShutdownRequest,
|
||||
type ShutdownRejectedMessage,
|
||||
type ShutdownRequestMessage,
|
||||
} from '../../utils/teammateMailbox.js'
|
||||
|
||||
type ShutdownRequestProps = {
|
||||
request: ShutdownRequestMessage;
|
||||
};
|
||||
request: ShutdownRequestMessage
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a shutdown request with a warning-colored border.
|
||||
*/
|
||||
export function ShutdownRequestDisplay(t0) {
|
||||
const $ = _c(7);
|
||||
const {
|
||||
request
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] !== request.from) {
|
||||
t1 = <Box marginBottom={1}><Text color="warning" bold={true}>Shutdown request from {request.from}</Text></Box>;
|
||||
$[0] = request.from;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
let t2;
|
||||
if ($[2] !== request.reason) {
|
||||
t2 = request.reason && <Box><Text>Reason: {request.reason}</Text></Box>;
|
||||
$[2] = request.reason;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
let t3;
|
||||
if ($[4] !== t1 || $[5] !== t2) {
|
||||
t3 = <Box flexDirection="column" marginY={1}><Box borderStyle="round" borderColor="warning" flexDirection="column" paddingX={1} paddingY={1}>{t1}{t2}</Box></Box>;
|
||||
$[4] = t1;
|
||||
$[5] = t2;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
}
|
||||
return t3;
|
||||
export function ShutdownRequestDisplay({
|
||||
request,
|
||||
}: ShutdownRequestProps): React.ReactNode {
|
||||
return (
|
||||
<Box flexDirection="column" marginY={1}>
|
||||
<Box
|
||||
borderStyle="round"
|
||||
borderColor="warning"
|
||||
flexDirection="column"
|
||||
paddingX={1}
|
||||
paddingY={1}
|
||||
>
|
||||
<Box marginBottom={1}>
|
||||
<Text color="warning" bold>
|
||||
Shutdown request from {request.from}
|
||||
</Text>
|
||||
</Box>
|
||||
{request.reason && (
|
||||
<Box>
|
||||
<Text>Reason: {request.reason}</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
type ShutdownRejectedProps = {
|
||||
response: ShutdownRejectedMessage;
|
||||
};
|
||||
response: ShutdownRejectedMessage
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a shutdown rejected message with a subtle (grey) border.
|
||||
*/
|
||||
export function ShutdownRejectedDisplay(t0) {
|
||||
const $ = _c(8);
|
||||
const {
|
||||
response
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] !== response.from) {
|
||||
t1 = <Text color="subtle" bold={true}>Shutdown rejected by {response.from}</Text>;
|
||||
$[0] = response.from;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
let t2;
|
||||
if ($[2] !== response.reason) {
|
||||
t2 = <Box marginTop={1} borderStyle="dashed" borderColor="subtle" borderLeft={false} borderRight={false} paddingX={1}><Text>Reason: {response.reason}</Text></Box>;
|
||||
$[2] = response.reason;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
let t3;
|
||||
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = <Box marginTop={1}><Text dimColor={true}>Teammate is continuing to work. You may request shutdown again later.</Text></Box>;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
let t4;
|
||||
if ($[5] !== t1 || $[6] !== t2) {
|
||||
t4 = <Box flexDirection="column" marginY={1}><Box borderStyle="round" borderColor="subtle" flexDirection="column" paddingX={1} paddingY={1}>{t1}{t2}{t3}</Box></Box>;
|
||||
$[5] = t1;
|
||||
$[6] = t2;
|
||||
$[7] = t4;
|
||||
} else {
|
||||
t4 = $[7];
|
||||
}
|
||||
return t4;
|
||||
export function ShutdownRejectedDisplay({
|
||||
response,
|
||||
}: ShutdownRejectedProps): React.ReactNode {
|
||||
return (
|
||||
<Box flexDirection="column" marginY={1}>
|
||||
<Box
|
||||
borderStyle="round"
|
||||
borderColor="subtle"
|
||||
flexDirection="column"
|
||||
paddingX={1}
|
||||
paddingY={1}
|
||||
>
|
||||
<Text color="subtle" bold>
|
||||
Shutdown rejected by {response.from}
|
||||
</Text>
|
||||
<Box
|
||||
marginTop={1}
|
||||
borderStyle="dashed"
|
||||
borderColor="subtle"
|
||||
borderLeft={false}
|
||||
borderRight={false}
|
||||
paddingX={1}
|
||||
>
|
||||
<Text>Reason: {response.reason}</Text>
|
||||
</Box>
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>
|
||||
Teammate is continuing to work. You may request shutdown again
|
||||
later.
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to parse and render a shutdown message from raw content.
|
||||
* Returns the rendered component if it's a shutdown message, null otherwise.
|
||||
*/
|
||||
export function tryRenderShutdownMessage(content: string): React.ReactNode | null {
|
||||
const request = isShutdownRequest(content);
|
||||
export function tryRenderShutdownMessage(
|
||||
content: string,
|
||||
): React.ReactNode | null {
|
||||
const request = isShutdownRequest(content)
|
||||
if (request) {
|
||||
return <ShutdownRequestDisplay request={request} />;
|
||||
return <ShutdownRequestDisplay request={request} />
|
||||
}
|
||||
|
||||
// Shutdown approved is handled inline by the caller — skip it here
|
||||
if (isShutdownApproved(content)) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
const rejected = isShutdownRejected(content);
|
||||
|
||||
const rejected = isShutdownRejected(content)
|
||||
if (rejected) {
|
||||
return <ShutdownRejectedDisplay response={rejected} />;
|
||||
return <ShutdownRejectedDisplay response={rejected} />
|
||||
}
|
||||
return null;
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,17 +116,20 @@ export function tryRenderShutdownMessage(content: string): React.ReactNode | nul
|
||||
* Returns null if the content is not a shutdown message.
|
||||
*/
|
||||
export function getShutdownMessageSummary(content: string): string | null {
|
||||
const request = isShutdownRequest(content);
|
||||
const request = isShutdownRequest(content)
|
||||
if (request) {
|
||||
return `[Shutdown Request from ${request.from}]${request.reason ? ` ${request.reason}` : ''}`;
|
||||
return `[Shutdown Request from ${request.from}]${request.reason ? ` ${request.reason}` : ''}`
|
||||
}
|
||||
const approved = isShutdownApproved(content);
|
||||
|
||||
const approved = isShutdownApproved(content)
|
||||
if (approved) {
|
||||
return `[Shutdown Approved] ${approved.from} is now exiting`;
|
||||
return `[Shutdown Approved] ${approved.from} is now exiting`
|
||||
}
|
||||
const rejected = isShutdownRejected(content);
|
||||
|
||||
const rejected = isShutdownRejected(content)
|
||||
if (rejected) {
|
||||
return `[Shutdown Rejected] ${rejected.from}: ${rejected.reason}`;
|
||||
return `[Shutdown Rejected] ${rejected.from}: ${rejected.reason}`
|
||||
}
|
||||
return null;
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,140 +1,64 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { useState } from 'react';
|
||||
import { Box, Text } from 'src/ink.js';
|
||||
import { formatAPIError } from 'src/services/api/errorUtils.js';
|
||||
import type { SystemAPIErrorMessage } from 'src/types/message.js';
|
||||
import { useInterval } from 'usehooks-ts';
|
||||
import { CtrlOToExpand } from '../CtrlOToExpand.js';
|
||||
import { MessageResponse } from '../MessageResponse.js';
|
||||
const MAX_API_ERROR_CHARS = 1000;
|
||||
import * as React from 'react'
|
||||
import { useState } from 'react'
|
||||
import { Box, Text } from 'src/ink.js'
|
||||
import { formatAPIError } from 'src/services/api/errorUtils.js'
|
||||
import type { SystemAPIErrorMessage } from 'src/types/message.js'
|
||||
import { useInterval } from 'usehooks-ts'
|
||||
import { CtrlOToExpand } from '../CtrlOToExpand.js'
|
||||
import { MessageResponse } from '../MessageResponse.js'
|
||||
|
||||
const MAX_API_ERROR_CHARS = 1000
|
||||
|
||||
type Props = {
|
||||
message: SystemAPIErrorMessage;
|
||||
verbose: boolean;
|
||||
};
|
||||
export function SystemAPIErrorMessage(t0) {
|
||||
const $ = _c(33);
|
||||
const {
|
||||
message: t1,
|
||||
verbose
|
||||
} = t0;
|
||||
const {
|
||||
retryAttempt,
|
||||
error,
|
||||
retryInMs,
|
||||
maxRetries
|
||||
} = t1;
|
||||
const hidden = true && retryAttempt < 4;
|
||||
const [countdownMs, setCountdownMs] = useState(0);
|
||||
const done = countdownMs >= retryInMs;
|
||||
let t2;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = () => setCountdownMs(_temp);
|
||||
$[0] = t2;
|
||||
} else {
|
||||
t2 = $[0];
|
||||
}
|
||||
useInterval(t2, hidden || done ? null : 1000);
|
||||
message: SystemAPIErrorMessage
|
||||
verbose: boolean
|
||||
}
|
||||
|
||||
export function SystemAPIErrorMessage({
|
||||
message: { retryAttempt, error, retryInMs, maxRetries },
|
||||
verbose,
|
||||
}: Props): React.ReactNode {
|
||||
// Hidden for early retries on external builds to avoid noise. Compute before
|
||||
// useInterval so we never register a timer that just drives a null render.
|
||||
const hidden = process.env.USER_TYPE === 'external' && retryAttempt < 4
|
||||
|
||||
const [countdownMs, setCountdownMs] = useState(0)
|
||||
const done = countdownMs >= retryInMs
|
||||
useInterval(
|
||||
() => setCountdownMs(ms => ms + 1000),
|
||||
hidden || done ? null : 1000,
|
||||
)
|
||||
|
||||
if (hidden) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
let t3;
|
||||
if ($[1] !== countdownMs || $[2] !== retryInMs) {
|
||||
t3 = Math.round((retryInMs - countdownMs) / 1000);
|
||||
$[1] = countdownMs;
|
||||
$[2] = retryInMs;
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t3 = $[3];
|
||||
}
|
||||
const retryInSecondsLive = Math.max(0, t3);
|
||||
let T0;
|
||||
let T1;
|
||||
let T2;
|
||||
let t4;
|
||||
let t5;
|
||||
let t6;
|
||||
let truncated;
|
||||
if ($[4] !== error || $[5] !== verbose) {
|
||||
const formatted = formatAPIError(error);
|
||||
truncated = !verbose && formatted.length > MAX_API_ERROR_CHARS;
|
||||
T2 = MessageResponse;
|
||||
T1 = Box;
|
||||
t6 = "column";
|
||||
T0 = Text;
|
||||
t4 = "error";
|
||||
t5 = truncated ? formatted.slice(0, MAX_API_ERROR_CHARS) + "\u2026" : formatted;
|
||||
$[4] = error;
|
||||
$[5] = verbose;
|
||||
$[6] = T0;
|
||||
$[7] = T1;
|
||||
$[8] = T2;
|
||||
$[9] = t4;
|
||||
$[10] = t5;
|
||||
$[11] = t6;
|
||||
$[12] = truncated;
|
||||
} else {
|
||||
T0 = $[6];
|
||||
T1 = $[7];
|
||||
T2 = $[8];
|
||||
t4 = $[9];
|
||||
t5 = $[10];
|
||||
t6 = $[11];
|
||||
truncated = $[12];
|
||||
}
|
||||
let t7;
|
||||
if ($[13] !== T0 || $[14] !== t4 || $[15] !== t5) {
|
||||
t7 = <T0 color={t4}>{t5}</T0>;
|
||||
$[13] = T0;
|
||||
$[14] = t4;
|
||||
$[15] = t5;
|
||||
$[16] = t7;
|
||||
} else {
|
||||
t7 = $[16];
|
||||
}
|
||||
let t8;
|
||||
if ($[17] !== truncated) {
|
||||
t8 = truncated && <CtrlOToExpand />;
|
||||
$[17] = truncated;
|
||||
$[18] = t8;
|
||||
} else {
|
||||
t8 = $[18];
|
||||
}
|
||||
const t9 = retryInSecondsLive === 1 ? "second" : "seconds";
|
||||
let t10;
|
||||
if ($[19] !== maxRetries || $[20] !== retryAttempt || $[21] !== retryInSecondsLive || $[22] !== t9) {
|
||||
t10 = <Text dimColor={true}>Retrying in {retryInSecondsLive}{" "}{t9}… (attempt{" "}{retryAttempt}/{maxRetries}){process.env.API_TIMEOUT_MS ? ` · API_TIMEOUT_MS=${process.env.API_TIMEOUT_MS}ms, try increasing it` : ""}</Text>;
|
||||
$[19] = maxRetries;
|
||||
$[20] = retryAttempt;
|
||||
$[21] = retryInSecondsLive;
|
||||
$[22] = t9;
|
||||
$[23] = t10;
|
||||
} else {
|
||||
t10 = $[23];
|
||||
}
|
||||
let t11;
|
||||
if ($[24] !== T1 || $[25] !== t10 || $[26] !== t6 || $[27] !== t7 || $[28] !== t8) {
|
||||
t11 = <T1 flexDirection={t6}>{t7}{t8}{t10}</T1>;
|
||||
$[24] = T1;
|
||||
$[25] = t10;
|
||||
$[26] = t6;
|
||||
$[27] = t7;
|
||||
$[28] = t8;
|
||||
$[29] = t11;
|
||||
} else {
|
||||
t11 = $[29];
|
||||
}
|
||||
let t12;
|
||||
if ($[30] !== T2 || $[31] !== t11) {
|
||||
t12 = <T2>{t11}</T2>;
|
||||
$[30] = T2;
|
||||
$[31] = t11;
|
||||
$[32] = t12;
|
||||
} else {
|
||||
t12 = $[32];
|
||||
}
|
||||
return t12;
|
||||
}
|
||||
function _temp(ms) {
|
||||
return ms + 1000;
|
||||
|
||||
const retryInSecondsLive = Math.max(
|
||||
0,
|
||||
Math.round((retryInMs - countdownMs) / 1000),
|
||||
)
|
||||
|
||||
const formatted = formatAPIError(error)
|
||||
const truncated = !verbose && formatted.length > MAX_API_ERROR_CHARS
|
||||
|
||||
return (
|
||||
<MessageResponse>
|
||||
<Box flexDirection="column">
|
||||
<Text color="error">
|
||||
{truncated
|
||||
? formatted.slice(0, MAX_API_ERROR_CHARS) + '…'
|
||||
: formatted}
|
||||
</Text>
|
||||
{truncated && <CtrlOToExpand />}
|
||||
<Text dimColor>
|
||||
Retrying in {retryInSecondsLive}{' '}
|
||||
{retryInSecondsLive === 1 ? 'second' : 'seconds'}… (attempt{' '}
|
||||
{retryAttempt}/{maxRetries})
|
||||
{process.env.API_TIMEOUT_MS
|
||||
? ` · API_TIMEOUT_MS=${process.env.API_TIMEOUT_MS}ms, try increasing it`
|
||||
: ''}
|
||||
</Text>
|
||||
</Box>
|
||||
</MessageResponse>
|
||||
)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,75 +1,65 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { isTaskAssignment, type TaskAssignmentMessage } from '../../utils/teammateMailbox.js';
|
||||
import * as React from 'react'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import {
|
||||
isTaskAssignment,
|
||||
type TaskAssignmentMessage,
|
||||
} from '../../utils/teammateMailbox.js'
|
||||
|
||||
type Props = {
|
||||
assignment: TaskAssignmentMessage;
|
||||
};
|
||||
assignment: TaskAssignmentMessage
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a task assignment with a cyan border (team-related color).
|
||||
*/
|
||||
export function TaskAssignmentDisplay(t0) {
|
||||
const $ = _c(11);
|
||||
const {
|
||||
assignment
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] !== assignment.assignedBy || $[1] !== assignment.taskId) {
|
||||
t1 = <Box marginBottom={1}><Text color="cyan_FOR_SUBAGENTS_ONLY" bold={true}>Task #{assignment.taskId} assigned by {assignment.assignedBy}</Text></Box>;
|
||||
$[0] = assignment.assignedBy;
|
||||
$[1] = assignment.taskId;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
let t2;
|
||||
if ($[3] !== assignment.subject) {
|
||||
t2 = <Box><Text bold={true}>{assignment.subject}</Text></Box>;
|
||||
$[3] = assignment.subject;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
let t3;
|
||||
if ($[5] !== assignment.description) {
|
||||
t3 = assignment.description && <Box marginTop={1}><Text dimColor={true}>{assignment.description}</Text></Box>;
|
||||
$[5] = assignment.description;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
}
|
||||
let t4;
|
||||
if ($[7] !== t1 || $[8] !== t2 || $[9] !== t3) {
|
||||
t4 = <Box flexDirection="column" marginY={1}><Box borderStyle="round" borderColor="cyan_FOR_SUBAGENTS_ONLY" flexDirection="column" paddingX={1} paddingY={1}>{t1}{t2}{t3}</Box></Box>;
|
||||
$[7] = t1;
|
||||
$[8] = t2;
|
||||
$[9] = t3;
|
||||
$[10] = t4;
|
||||
} else {
|
||||
t4 = $[10];
|
||||
}
|
||||
return t4;
|
||||
export function TaskAssignmentDisplay({ assignment }: Props): React.ReactNode {
|
||||
return (
|
||||
<Box flexDirection="column" marginY={1}>
|
||||
<Box
|
||||
borderStyle="round"
|
||||
borderColor="cyan_FOR_SUBAGENTS_ONLY"
|
||||
flexDirection="column"
|
||||
paddingX={1}
|
||||
paddingY={1}
|
||||
>
|
||||
<Box marginBottom={1}>
|
||||
<Text color="cyan_FOR_SUBAGENTS_ONLY" bold>
|
||||
Task #{assignment.taskId} assigned by {assignment.assignedBy}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text bold>{assignment.subject}</Text>
|
||||
</Box>
|
||||
{assignment.description && (
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>{assignment.description}</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to parse and render a task assignment message from raw content.
|
||||
*/
|
||||
export function tryRenderTaskAssignmentMessage(content: string): React.ReactNode | null {
|
||||
const assignment = isTaskAssignment(content);
|
||||
export function tryRenderTaskAssignmentMessage(
|
||||
content: string,
|
||||
): React.ReactNode | null {
|
||||
const assignment = isTaskAssignment(content)
|
||||
if (assignment) {
|
||||
return <TaskAssignmentDisplay assignment={assignment} />;
|
||||
return <TaskAssignmentDisplay assignment={assignment} />
|
||||
}
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a brief summary text for a task assignment message.
|
||||
*/
|
||||
export function getTaskAssignmentSummary(content: string): string | null {
|
||||
const assignment = isTaskAssignment(content);
|
||||
const assignment = isTaskAssignment(content)
|
||||
if (assignment) {
|
||||
return `[Task Assigned] #${assignment.taskId} - ${assignment.subject}`;
|
||||
return `[Task Assigned] #${assignment.taskId} - ${assignment.subject}`
|
||||
}
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,82 +1,42 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs';
|
||||
import * as React from 'react';
|
||||
import { BLACK_CIRCLE } from '../../constants/figures.js';
|
||||
import { Box, Text, type TextProps } from '../../ink.js';
|
||||
import { extractTag } from '../../utils/messages.js';
|
||||
import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
|
||||
import * as React from 'react'
|
||||
import { BLACK_CIRCLE } from '../../constants/figures.js'
|
||||
import { Box, Text, type TextProps } from '../../ink.js'
|
||||
import { extractTag } from '../../utils/messages.js'
|
||||
|
||||
type Props = {
|
||||
addMargin: boolean;
|
||||
param: TextBlockParam;
|
||||
};
|
||||
addMargin: boolean
|
||||
param: TextBlockParam
|
||||
}
|
||||
|
||||
function getStatusColor(status: string | null): TextProps['color'] {
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
return 'success';
|
||||
return 'success'
|
||||
case 'failed':
|
||||
return 'error';
|
||||
return 'error'
|
||||
case 'killed':
|
||||
return 'warning';
|
||||
return 'warning'
|
||||
default:
|
||||
return 'text';
|
||||
return 'text'
|
||||
}
|
||||
}
|
||||
export function UserAgentNotificationMessage(t0) {
|
||||
const $ = _c(12);
|
||||
const {
|
||||
|
||||
export function UserAgentNotificationMessage({
|
||||
addMargin,
|
||||
param: t1
|
||||
} = t0;
|
||||
const {
|
||||
text
|
||||
} = t1;
|
||||
let t2;
|
||||
if ($[0] !== text) {
|
||||
t2 = extractTag(text, "summary");
|
||||
$[0] = text;
|
||||
$[1] = t2;
|
||||
} else {
|
||||
t2 = $[1];
|
||||
}
|
||||
const summary = t2;
|
||||
if (!summary) {
|
||||
return null;
|
||||
}
|
||||
let t3;
|
||||
if ($[2] !== text) {
|
||||
const status = extractTag(text, "status");
|
||||
t3 = getStatusColor(status);
|
||||
$[2] = text;
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t3 = $[3];
|
||||
}
|
||||
const color = t3;
|
||||
const t4 = addMargin ? 1 : 0;
|
||||
let t5;
|
||||
if ($[4] !== color) {
|
||||
t5 = <Text color={color}>{BLACK_CIRCLE}</Text>;
|
||||
$[4] = color;
|
||||
$[5] = t5;
|
||||
} else {
|
||||
t5 = $[5];
|
||||
}
|
||||
let t6;
|
||||
if ($[6] !== summary || $[7] !== t5) {
|
||||
t6 = <Text>{t5} {summary}</Text>;
|
||||
$[6] = summary;
|
||||
$[7] = t5;
|
||||
$[8] = t6;
|
||||
} else {
|
||||
t6 = $[8];
|
||||
}
|
||||
let t7;
|
||||
if ($[9] !== t4 || $[10] !== t6) {
|
||||
t7 = <Box marginTop={t4}>{t6}</Box>;
|
||||
$[9] = t4;
|
||||
$[10] = t6;
|
||||
$[11] = t7;
|
||||
} else {
|
||||
t7 = $[11];
|
||||
}
|
||||
return t7;
|
||||
param: { text },
|
||||
}: Props): React.ReactNode {
|
||||
const summary = extractTag(text, 'summary')
|
||||
if (!summary) return null
|
||||
|
||||
const status = extractTag(text, 'status')
|
||||
const color = getStatusColor(status)
|
||||
|
||||
return (
|
||||
<Box marginTop={addMargin ? 1 : 0}>
|
||||
<Text>
|
||||
<Text color={color}>{BLACK_CIRCLE}</Text> {summary}
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,57 +1,30 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs';
|
||||
import * as React from 'react';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { extractTag } from '../../utils/messages.js';
|
||||
import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
|
||||
import * as React from 'react'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { extractTag } from '../../utils/messages.js'
|
||||
|
||||
type Props = {
|
||||
addMargin: boolean;
|
||||
param: TextBlockParam;
|
||||
};
|
||||
export function UserBashInputMessage(t0) {
|
||||
const $ = _c(8);
|
||||
const {
|
||||
param: t1,
|
||||
addMargin
|
||||
} = t0;
|
||||
const {
|
||||
text
|
||||
} = t1;
|
||||
let t2;
|
||||
if ($[0] !== text) {
|
||||
t2 = extractTag(text, "bash-input");
|
||||
$[0] = text;
|
||||
$[1] = t2;
|
||||
} else {
|
||||
t2 = $[1];
|
||||
}
|
||||
const input = t2;
|
||||
if (!input) {
|
||||
return null;
|
||||
}
|
||||
const t3 = addMargin ? 1 : 0;
|
||||
let t4;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = <Text color="bashBorder">! </Text>;
|
||||
$[2] = t4;
|
||||
} else {
|
||||
t4 = $[2];
|
||||
}
|
||||
let t5;
|
||||
if ($[3] !== input) {
|
||||
t5 = <Text color="text">{input}</Text>;
|
||||
$[3] = input;
|
||||
$[4] = t5;
|
||||
} else {
|
||||
t5 = $[4];
|
||||
}
|
||||
let t6;
|
||||
if ($[5] !== t3 || $[6] !== t5) {
|
||||
t6 = <Box flexDirection="row" marginTop={t3} backgroundColor="bashMessageBackgroundColor" paddingRight={1}>{t4}{t5}</Box>;
|
||||
$[5] = t3;
|
||||
$[6] = t5;
|
||||
$[7] = t6;
|
||||
} else {
|
||||
t6 = $[7];
|
||||
}
|
||||
return t6;
|
||||
addMargin: boolean
|
||||
param: TextBlockParam
|
||||
}
|
||||
|
||||
export function UserBashInputMessage({
|
||||
param: { text },
|
||||
addMargin,
|
||||
}: Props): React.ReactNode {
|
||||
const input = extractTag(text, 'bash-input')
|
||||
if (!input) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<Box
|
||||
flexDirection="row"
|
||||
marginTop={addMargin ? 1 : 0}
|
||||
backgroundColor="bashMessageBackgroundColor"
|
||||
paddingRight={1}
|
||||
>
|
||||
<Text color="bashBorder">! </Text>
|
||||
<Text color="text">{input}</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,53 +1,20 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import BashToolResultMessage from '../../tools/BashTool/BashToolResultMessage.js';
|
||||
import { extractTag } from '../../utils/messages.js';
|
||||
export function UserBashOutputMessage(t0) {
|
||||
const $ = _c(10);
|
||||
const {
|
||||
import * as React from 'react'
|
||||
import BashToolResultMessage from '../../tools/BashTool/BashToolResultMessage.js'
|
||||
import { extractTag } from '../../utils/messages.js'
|
||||
|
||||
export function UserBashOutputMessage({
|
||||
content,
|
||||
verbose
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] !== content) {
|
||||
const rawStdout = extractTag(content, "bash-stdout") ?? "";
|
||||
t1 = extractTag(rawStdout, "persisted-output") ?? rawStdout;
|
||||
$[0] = content;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const stdout = t1;
|
||||
let t2;
|
||||
if ($[2] !== content) {
|
||||
t2 = extractTag(content, "bash-stderr") ?? "";
|
||||
$[2] = content;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
const stderr = t2;
|
||||
let t3;
|
||||
if ($[4] !== stderr || $[5] !== stdout) {
|
||||
t3 = {
|
||||
stdout,
|
||||
stderr
|
||||
};
|
||||
$[4] = stderr;
|
||||
$[5] = stdout;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
}
|
||||
const t4 = !!verbose;
|
||||
let t5;
|
||||
if ($[7] !== t3 || $[8] !== t4) {
|
||||
t5 = <BashToolResultMessage content={t3} verbose={t4} />;
|
||||
$[7] = t3;
|
||||
$[8] = t4;
|
||||
$[9] = t5;
|
||||
} else {
|
||||
t5 = $[9];
|
||||
}
|
||||
return t5;
|
||||
verbose,
|
||||
}: {
|
||||
content: string
|
||||
verbose?: boolean
|
||||
}): React.ReactNode {
|
||||
const rawStdout = extractTag(content, 'bash-stdout') ?? ''
|
||||
// Unwrap <persisted-output> if present — keep the inner content (file path +
|
||||
// preview) for the user; the wrapper tag itself is model-facing signaling.
|
||||
const stdout = extractTag(rawStdout, 'persisted-output') ?? rawStdout
|
||||
const stderr = extractTag(content, 'bash-stderr') ?? ''
|
||||
return (
|
||||
<BashToolResultMessage content={{ stdout, stderr }} verbose={!!verbose} />
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,136 +1,52 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs';
|
||||
import * as React from 'react';
|
||||
import { CHANNEL_ARROW } from '../../constants/figures.js';
|
||||
import { CHANNEL_TAG } from '../../constants/xml.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { truncateToWidth } from '../../utils/format.js';
|
||||
import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
|
||||
import * as React from 'react'
|
||||
import { CHANNEL_ARROW } from '../../constants/figures.js'
|
||||
import { CHANNEL_TAG } from '../../constants/xml.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { truncateToWidth } from '../../utils/format.js'
|
||||
|
||||
type Props = {
|
||||
addMargin: boolean;
|
||||
param: TextBlockParam;
|
||||
};
|
||||
addMargin: boolean
|
||||
param: TextBlockParam
|
||||
}
|
||||
|
||||
// <channel source="..." user="..." chat_id="...">content</channel>
|
||||
// source is always first (wrapChannelMessage writes it), user is optional.
|
||||
const CHANNEL_RE = new RegExp(`<${CHANNEL_TAG}\\s+source="([^"]+)"([^>]*)>\\n?([\\s\\S]*?)\\n?</${CHANNEL_TAG}>`);
|
||||
const USER_ATTR_RE = /\buser="([^"]+)"/;
|
||||
const CHANNEL_RE = new RegExp(
|
||||
`<${CHANNEL_TAG}\\s+source="([^"]+)"([^>]*)>\\n?([\\s\\S]*?)\\n?</${CHANNEL_TAG}>`,
|
||||
)
|
||||
const USER_ATTR_RE = /\buser="([^"]+)"/
|
||||
|
||||
// Plugin-provided servers get names like plugin:slack-channel:slack via
|
||||
// addPluginScopeToServers — show just the leaf. Matches the suffix-match
|
||||
// logic in isServerInChannels.
|
||||
function displayServerName(name: string): string {
|
||||
const i = name.lastIndexOf(':');
|
||||
return i === -1 ? name : name.slice(i + 1);
|
||||
const i = name.lastIndexOf(':')
|
||||
return i === -1 ? name : name.slice(i + 1)
|
||||
}
|
||||
const TRUNCATE_AT = 60;
|
||||
export function UserChannelMessage(t0) {
|
||||
const $ = _c(29);
|
||||
const {
|
||||
|
||||
const TRUNCATE_AT = 60
|
||||
|
||||
export function UserChannelMessage({
|
||||
addMargin,
|
||||
param: t1
|
||||
} = t0;
|
||||
const {
|
||||
text
|
||||
} = t1;
|
||||
let T0;
|
||||
let T1;
|
||||
let T2;
|
||||
let t2;
|
||||
let t3;
|
||||
let t4;
|
||||
let t5;
|
||||
let t6;
|
||||
let t7;
|
||||
let truncated;
|
||||
let user;
|
||||
if ($[0] !== addMargin || $[1] !== text) {
|
||||
t7 = Symbol.for("react.early_return_sentinel");
|
||||
bb0: {
|
||||
const m = CHANNEL_RE.exec(text);
|
||||
if (!m) {
|
||||
t7 = null;
|
||||
break bb0;
|
||||
}
|
||||
const [, source, attrs, content] = m;
|
||||
user = USER_ATTR_RE.exec(attrs ?? "")?.[1];
|
||||
const body = (content ?? "").trim().replace(/\s+/g, " ");
|
||||
truncated = truncateToWidth(body, TRUNCATE_AT);
|
||||
T2 = Box;
|
||||
t6 = addMargin ? 1 : 0;
|
||||
T1 = Text;
|
||||
if ($[13] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = <Text color="suggestion">{CHANNEL_ARROW}</Text>;
|
||||
$[13] = t4;
|
||||
} else {
|
||||
t4 = $[13];
|
||||
}
|
||||
t5 = " ";
|
||||
T0 = Text;
|
||||
t2 = true;
|
||||
t3 = displayServerName(source ?? "");
|
||||
}
|
||||
$[0] = addMargin;
|
||||
$[1] = text;
|
||||
$[2] = T0;
|
||||
$[3] = T1;
|
||||
$[4] = T2;
|
||||
$[5] = t2;
|
||||
$[6] = t3;
|
||||
$[7] = t4;
|
||||
$[8] = t5;
|
||||
$[9] = t6;
|
||||
$[10] = t7;
|
||||
$[11] = truncated;
|
||||
$[12] = user;
|
||||
} else {
|
||||
T0 = $[2];
|
||||
T1 = $[3];
|
||||
T2 = $[4];
|
||||
t2 = $[5];
|
||||
t3 = $[6];
|
||||
t4 = $[7];
|
||||
t5 = $[8];
|
||||
t6 = $[9];
|
||||
t7 = $[10];
|
||||
truncated = $[11];
|
||||
user = $[12];
|
||||
}
|
||||
if (t7 !== Symbol.for("react.early_return_sentinel")) {
|
||||
return t7;
|
||||
}
|
||||
const t8 = user ? ` \u00b7 ${user}` : "";
|
||||
let t9;
|
||||
if ($[14] !== T0 || $[15] !== t2 || $[16] !== t3 || $[17] !== t8) {
|
||||
t9 = <T0 dimColor={t2}>{t3}{t8}:</T0>;
|
||||
$[14] = T0;
|
||||
$[15] = t2;
|
||||
$[16] = t3;
|
||||
$[17] = t8;
|
||||
$[18] = t9;
|
||||
} else {
|
||||
t9 = $[18];
|
||||
}
|
||||
let t10;
|
||||
if ($[19] !== T1 || $[20] !== t4 || $[21] !== t5 || $[22] !== t9 || $[23] !== truncated) {
|
||||
t10 = <T1>{t4}{t5}{t9}{" "}{truncated}</T1>;
|
||||
$[19] = T1;
|
||||
$[20] = t4;
|
||||
$[21] = t5;
|
||||
$[22] = t9;
|
||||
$[23] = truncated;
|
||||
$[24] = t10;
|
||||
} else {
|
||||
t10 = $[24];
|
||||
}
|
||||
let t11;
|
||||
if ($[25] !== T2 || $[26] !== t10 || $[27] !== t6) {
|
||||
t11 = <T2 marginTop={t6}>{t10}</T2>;
|
||||
$[25] = T2;
|
||||
$[26] = t10;
|
||||
$[27] = t6;
|
||||
$[28] = t11;
|
||||
} else {
|
||||
t11 = $[28];
|
||||
}
|
||||
return t11;
|
||||
param: { text },
|
||||
}: Props): React.ReactNode {
|
||||
const m = CHANNEL_RE.exec(text)
|
||||
if (!m) return null
|
||||
const [, source, attrs, content] = m
|
||||
const user = USER_ATTR_RE.exec(attrs ?? '')?.[1]
|
||||
const body = (content ?? '').trim().replace(/\s+/g, ' ')
|
||||
const truncated = truncateToWidth(body, TRUNCATE_AT)
|
||||
return (
|
||||
<Box marginTop={addMargin ? 1 : 0}>
|
||||
<Text>
|
||||
<Text color="suggestion">{CHANNEL_ARROW}</Text>{' '}
|
||||
<Text dimColor>
|
||||
{displayServerName(source ?? '')}
|
||||
{user ? ` \u00b7 ${user}` : ''}:
|
||||
</Text>{' '}
|
||||
{truncated}
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,107 +1,57 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs';
|
||||
import figures from 'figures';
|
||||
import * as React from 'react';
|
||||
import { COMMAND_MESSAGE_TAG } from '../../constants/xml.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { extractTag } from '../../utils/messages.js';
|
||||
import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
|
||||
import figures from 'figures'
|
||||
import * as React from 'react'
|
||||
import { COMMAND_MESSAGE_TAG } from '../../constants/xml.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { extractTag } from '../../utils/messages.js'
|
||||
|
||||
type Props = {
|
||||
addMargin: boolean;
|
||||
param: TextBlockParam;
|
||||
};
|
||||
export function UserCommandMessage(t0) {
|
||||
const $ = _c(19);
|
||||
const {
|
||||
addMargin,
|
||||
param: t1
|
||||
} = t0;
|
||||
const {
|
||||
text
|
||||
} = t1;
|
||||
let t2;
|
||||
if ($[0] !== text) {
|
||||
t2 = extractTag(text, COMMAND_MESSAGE_TAG);
|
||||
$[0] = text;
|
||||
$[1] = t2;
|
||||
} else {
|
||||
t2 = $[1];
|
||||
}
|
||||
const commandMessage = t2;
|
||||
let t3;
|
||||
if ($[2] !== text) {
|
||||
t3 = extractTag(text, "command-args");
|
||||
$[2] = text;
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t3 = $[3];
|
||||
}
|
||||
const args = t3;
|
||||
const isSkillFormat = extractTag(text, "skill-format") === "true";
|
||||
if (!commandMessage) {
|
||||
return null;
|
||||
}
|
||||
if (isSkillFormat) {
|
||||
const t4 = addMargin ? 1 : 0;
|
||||
let t5;
|
||||
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t5 = <Text color="subtle">{figures.pointer} </Text>;
|
||||
$[4] = t5;
|
||||
} else {
|
||||
t5 = $[4];
|
||||
}
|
||||
let t6;
|
||||
if ($[5] !== commandMessage) {
|
||||
t6 = <Text>{t5}<Text color="text">Skill({commandMessage})</Text></Text>;
|
||||
$[5] = commandMessage;
|
||||
$[6] = t6;
|
||||
} else {
|
||||
t6 = $[6];
|
||||
}
|
||||
let t7;
|
||||
if ($[7] !== t4 || $[8] !== t6) {
|
||||
t7 = <Box flexDirection="column" marginTop={t4} backgroundColor="userMessageBackground" paddingRight={1}>{t6}</Box>;
|
||||
$[7] = t4;
|
||||
$[8] = t6;
|
||||
$[9] = t7;
|
||||
} else {
|
||||
t7 = $[9];
|
||||
}
|
||||
return t7;
|
||||
}
|
||||
let t4;
|
||||
if ($[10] !== args || $[11] !== commandMessage) {
|
||||
t4 = [commandMessage, args].filter(Boolean);
|
||||
$[10] = args;
|
||||
$[11] = commandMessage;
|
||||
$[12] = t4;
|
||||
} else {
|
||||
t4 = $[12];
|
||||
}
|
||||
const content = `/${t4.join(" ")}`;
|
||||
const t5 = addMargin ? 1 : 0;
|
||||
let t6;
|
||||
if ($[13] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t6 = <Text color="subtle">{figures.pointer} </Text>;
|
||||
$[13] = t6;
|
||||
} else {
|
||||
t6 = $[13];
|
||||
}
|
||||
let t7;
|
||||
if ($[14] !== content) {
|
||||
t7 = <Text>{t6}<Text color="text">{content}</Text></Text>;
|
||||
$[14] = content;
|
||||
$[15] = t7;
|
||||
} else {
|
||||
t7 = $[15];
|
||||
}
|
||||
let t8;
|
||||
if ($[16] !== t5 || $[17] !== t7) {
|
||||
t8 = <Box flexDirection="column" marginTop={t5} backgroundColor="userMessageBackground" paddingRight={1}>{t7}</Box>;
|
||||
$[16] = t5;
|
||||
$[17] = t7;
|
||||
$[18] = t8;
|
||||
} else {
|
||||
t8 = $[18];
|
||||
}
|
||||
return t8;
|
||||
addMargin: boolean
|
||||
param: TextBlockParam
|
||||
}
|
||||
|
||||
export function UserCommandMessage({
|
||||
addMargin,
|
||||
param: { text },
|
||||
}: Props): React.ReactNode {
|
||||
const commandMessage = extractTag(text, COMMAND_MESSAGE_TAG)
|
||||
const args = extractTag(text, 'command-args')
|
||||
const isSkillFormat = extractTag(text, 'skill-format') === 'true'
|
||||
|
||||
if (!commandMessage) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Skills use "Skill(name)" format
|
||||
if (isSkillFormat) {
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
marginTop={addMargin ? 1 : 0}
|
||||
backgroundColor="userMessageBackground"
|
||||
paddingRight={1}
|
||||
>
|
||||
<Text>
|
||||
<Text color="subtle">{figures.pointer} </Text>
|
||||
<Text color="text">Skill({commandMessage})</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
// Slash command format: show as "❯ /command args"
|
||||
const content = `/${[commandMessage, args].filter(Boolean).join(' ')}`
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
marginTop={addMargin ? 1 : 0}
|
||||
backgroundColor="userMessageBackground"
|
||||
paddingRight={1}
|
||||
>
|
||||
<Text>
|
||||
<Text color="subtle">{figures.pointer} </Text>
|
||||
<Text color="text">{content}</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { pathToFileURL } from 'url';
|
||||
import Link from '../../ink/components/Link.js';
|
||||
import { supportsHyperlinks } from '../../ink/supports-hyperlinks.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { getStoredImagePath } from '../../utils/imageStore.js';
|
||||
import { MessageResponse } from '../MessageResponse.js';
|
||||
import * as React from 'react'
|
||||
import { pathToFileURL } from 'url'
|
||||
import Link from '../../ink/components/Link.js'
|
||||
import { supportsHyperlinks } from '../../ink/supports-hyperlinks.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { getStoredImagePath } from '../../utils/imageStore.js'
|
||||
import { MessageResponse } from '../MessageResponse.js'
|
||||
|
||||
type Props = {
|
||||
imageId?: number;
|
||||
addMargin?: boolean;
|
||||
};
|
||||
imageId?: number
|
||||
addMargin?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders an image attachment in user messages.
|
||||
@@ -17,42 +17,27 @@ type Props = {
|
||||
* Uses MessageResponse styling to appear connected to the message above,
|
||||
* unless addMargin is true (image starts a new user turn without text).
|
||||
*/
|
||||
export function UserImageMessage(t0) {
|
||||
const $ = _c(7);
|
||||
const {
|
||||
export function UserImageMessage({
|
||||
imageId,
|
||||
addMargin
|
||||
} = t0;
|
||||
const label = imageId ? `[Image #${imageId}]` : "[Image]";
|
||||
let t1;
|
||||
if ($[0] !== imageId || $[1] !== label) {
|
||||
const imagePath = imageId ? getStoredImagePath(imageId) : null;
|
||||
t1 = imagePath && supportsHyperlinks() ? <Link url={pathToFileURL(imagePath).href}><Text>{label}</Text></Link> : <Text>{label}</Text>;
|
||||
$[0] = imageId;
|
||||
$[1] = label;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
const content = t1;
|
||||
addMargin,
|
||||
}: Props): React.ReactNode {
|
||||
const label = imageId ? `[Image #${imageId}]` : '[Image]'
|
||||
const imagePath = imageId ? getStoredImagePath(imageId) : null
|
||||
|
||||
const content =
|
||||
imagePath && supportsHyperlinks() ? (
|
||||
<Link url={pathToFileURL(imagePath).href}>
|
||||
<Text>{label}</Text>
|
||||
</Link>
|
||||
) : (
|
||||
<Text>{label}</Text>
|
||||
)
|
||||
|
||||
// When this image starts a new user turn (no text before it),
|
||||
// show with margin instead of the connected line style
|
||||
if (addMargin) {
|
||||
let t2;
|
||||
if ($[3] !== content) {
|
||||
t2 = <Box marginTop={1}>{content}</Box>;
|
||||
$[3] = content;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
return <Box marginTop={1}>{content}</Box>
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
let t2;
|
||||
if ($[5] !== content) {
|
||||
t2 = <MessageResponse>{content}</MessageResponse>;
|
||||
$[5] = content;
|
||||
$[6] = t2;
|
||||
} else {
|
||||
t2 = $[6];
|
||||
}
|
||||
return t2;
|
||||
|
||||
return <MessageResponse>{content}</MessageResponse>
|
||||
}
|
||||
|
||||
@@ -1,166 +1,80 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { DIAMOND_FILLED, DIAMOND_OPEN } from '../../constants/figures.js';
|
||||
import { NO_CONTENT_MESSAGE } from '../../constants/messages.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { extractTag } from '../../utils/messages.js';
|
||||
import { Markdown } from '../Markdown.js';
|
||||
import { MessageResponse } from '../MessageResponse.js';
|
||||
import * as React from 'react'
|
||||
import { DIAMOND_FILLED, DIAMOND_OPEN } from '../../constants/figures.js'
|
||||
import { NO_CONTENT_MESSAGE } from '../../constants/messages.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { extractTag } from '../../utils/messages.js'
|
||||
import { Markdown } from '../Markdown.js'
|
||||
import { MessageResponse } from '../MessageResponse.js'
|
||||
|
||||
type Props = {
|
||||
content: string;
|
||||
};
|
||||
export function UserLocalCommandOutputMessage(t0) {
|
||||
const $ = _c(4);
|
||||
const {
|
||||
content
|
||||
} = t0;
|
||||
let lines;
|
||||
let t1;
|
||||
if ($[0] !== content) {
|
||||
t1 = Symbol.for("react.early_return_sentinel");
|
||||
bb0: {
|
||||
const stdout = extractTag(content, "local-command-stdout");
|
||||
const stderr = extractTag(content, "local-command-stderr");
|
||||
content: string
|
||||
}
|
||||
|
||||
export function UserLocalCommandOutputMessage({
|
||||
content,
|
||||
}: Props): React.ReactNode {
|
||||
const stdout = extractTag(content, 'local-command-stdout')
|
||||
const stderr = extractTag(content, 'local-command-stderr')
|
||||
if (!stdout && !stderr) {
|
||||
let t2;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <MessageResponse><Text dimColor={true}>{NO_CONTENT_MESSAGE}</Text></MessageResponse>;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
return (
|
||||
<MessageResponse>
|
||||
<Text dimColor>{NO_CONTENT_MESSAGE}</Text>
|
||||
</MessageResponse>
|
||||
)
|
||||
}
|
||||
t1 = t2;
|
||||
break bb0;
|
||||
}
|
||||
lines = [];
|
||||
|
||||
const lines: React.ReactNode[] = []
|
||||
if (stdout?.trim()) {
|
||||
lines.push(<IndentedContent key="stdout">{stdout.trim()}</IndentedContent>);
|
||||
lines.push(<IndentedContent key="stdout">{stdout.trim()}</IndentedContent>)
|
||||
}
|
||||
if (stderr?.trim()) {
|
||||
lines.push(<IndentedContent key="stderr">{stderr.trim()}</IndentedContent>);
|
||||
lines.push(<IndentedContent key="stderr">{stderr.trim()}</IndentedContent>)
|
||||
}
|
||||
}
|
||||
$[0] = content;
|
||||
$[1] = lines;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
lines = $[1];
|
||||
t1 = $[2];
|
||||
}
|
||||
if (t1 !== Symbol.for("react.early_return_sentinel")) {
|
||||
return t1;
|
||||
}
|
||||
return lines;
|
||||
return lines
|
||||
}
|
||||
function IndentedContent(t0) {
|
||||
const $ = _c(5);
|
||||
const {
|
||||
children
|
||||
} = t0;
|
||||
if (children.startsWith(`${DIAMOND_OPEN} `) || children.startsWith(`${DIAMOND_FILLED} `)) {
|
||||
let t1;
|
||||
if ($[0] !== children) {
|
||||
t1 = <CloudLaunchContent>{children}</CloudLaunchContent>;
|
||||
$[0] = children;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
|
||||
function IndentedContent({ children }: { children: string }): React.ReactNode {
|
||||
if (
|
||||
children.startsWith(`${DIAMOND_OPEN} `) ||
|
||||
children.startsWith(`${DIAMOND_FILLED} `)
|
||||
) {
|
||||
return <CloudLaunchContent>{children}</CloudLaunchContent>
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
let t1;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <Text dimColor={true}>{" \u23BF "}</Text>;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
let t2;
|
||||
if ($[3] !== children) {
|
||||
t2 = <Box flexDirection="row">{t1}<Box flexDirection="column" flexGrow={1}><Markdown>{children}</Markdown></Box></Box>;
|
||||
$[3] = children;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
return t2;
|
||||
return (
|
||||
<Box flexDirection="row">
|
||||
<Text dimColor>{' ⎿ '}</Text>
|
||||
<Box flexDirection="column" flexGrow={1}>
|
||||
<Markdown>{children}</Markdown>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
function CloudLaunchContent(t0) {
|
||||
const $ = _c(19);
|
||||
const {
|
||||
children
|
||||
} = t0;
|
||||
const diamond = children[0];
|
||||
let label;
|
||||
let rest;
|
||||
let t1;
|
||||
if ($[0] !== children) {
|
||||
const nl = children.indexOf("\n");
|
||||
const header = nl === -1 ? children.slice(2) : children.slice(2, nl);
|
||||
rest = nl === -1 ? "" : children.slice(nl + 1).trim();
|
||||
const sep = header.indexOf(" \xB7 ");
|
||||
label = sep === -1 ? header : header.slice(0, sep);
|
||||
t1 = sep === -1 ? "" : header.slice(sep);
|
||||
$[0] = children;
|
||||
$[1] = label;
|
||||
$[2] = rest;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
label = $[1];
|
||||
rest = $[2];
|
||||
t1 = $[3];
|
||||
}
|
||||
const suffix = t1;
|
||||
let t2;
|
||||
if ($[4] !== diamond) {
|
||||
t2 = <Text color="background">{diamond} </Text>;
|
||||
$[4] = diamond;
|
||||
$[5] = t2;
|
||||
} else {
|
||||
t2 = $[5];
|
||||
}
|
||||
let t3;
|
||||
if ($[6] !== label) {
|
||||
t3 = <Text bold={true}>{label}</Text>;
|
||||
$[6] = label;
|
||||
$[7] = t3;
|
||||
} else {
|
||||
t3 = $[7];
|
||||
}
|
||||
let t4;
|
||||
if ($[8] !== suffix) {
|
||||
t4 = suffix && <Text dimColor={true}>{suffix}</Text>;
|
||||
$[8] = suffix;
|
||||
$[9] = t4;
|
||||
} else {
|
||||
t4 = $[9];
|
||||
}
|
||||
let t5;
|
||||
if ($[10] !== t2 || $[11] !== t3 || $[12] !== t4) {
|
||||
t5 = <Text>{t2}{t3}{t4}</Text>;
|
||||
$[10] = t2;
|
||||
$[11] = t3;
|
||||
$[12] = t4;
|
||||
$[13] = t5;
|
||||
} else {
|
||||
t5 = $[13];
|
||||
}
|
||||
let t6;
|
||||
if ($[14] !== rest) {
|
||||
t6 = rest && <Box flexDirection="row"><Text dimColor={true}>{" \u23BF "}</Text><Text dimColor={true}>{rest}</Text></Box>;
|
||||
$[14] = rest;
|
||||
$[15] = t6;
|
||||
} else {
|
||||
t6 = $[15];
|
||||
}
|
||||
let t7;
|
||||
if ($[16] !== t5 || $[17] !== t6) {
|
||||
t7 = <Box flexDirection="column">{t5}{t6}</Box>;
|
||||
$[16] = t5;
|
||||
$[17] = t6;
|
||||
$[18] = t7;
|
||||
} else {
|
||||
t7 = $[18];
|
||||
}
|
||||
return t7;
|
||||
|
||||
function CloudLaunchContent({
|
||||
children,
|
||||
}: {
|
||||
children: string
|
||||
}): React.ReactNode {
|
||||
const diamond = children[0]!
|
||||
const nl = children.indexOf('\n')
|
||||
const header = nl === -1 ? children.slice(2) : children.slice(2, nl)
|
||||
const rest = nl === -1 ? '' : children.slice(nl + 1).trim()
|
||||
const sep = header.indexOf(' · ')
|
||||
const label = sep === -1 ? header : header.slice(0, sep)
|
||||
const suffix = sep === -1 ? '' : header.slice(sep)
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text>
|
||||
<Text color="background">{diamond} </Text>
|
||||
<Text bold>{label}</Text>
|
||||
{suffix && <Text dimColor>{suffix}</Text>}
|
||||
</Text>
|
||||
{rest && (
|
||||
<Box flexDirection="row">
|
||||
<Text dimColor>{' ⎿ '}</Text>
|
||||
<Text dimColor>{rest}</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,74 +1,44 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import sample from 'lodash-es/sample.js';
|
||||
import * as React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { extractTag } from '../../utils/messages.js';
|
||||
import { MessageResponse } from '../MessageResponse.js';
|
||||
import sample from 'lodash-es/sample.js'
|
||||
import * as React from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { extractTag } from '../../utils/messages.js'
|
||||
import { MessageResponse } from '../MessageResponse.js'
|
||||
|
||||
function getSavingMessage(): string {
|
||||
return sample(['Got it.', 'Good to know.', 'Noted.']);
|
||||
return sample(['Got it.', 'Good to know.', 'Noted.'])
|
||||
}
|
||||
|
||||
type Props = {
|
||||
addMargin: boolean;
|
||||
text: string;
|
||||
};
|
||||
export function UserMemoryInputMessage(t0) {
|
||||
const $ = _c(10);
|
||||
const {
|
||||
text,
|
||||
addMargin
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] !== text) {
|
||||
t1 = extractTag(text, "user-memory-input");
|
||||
$[0] = text;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const input = t1;
|
||||
let t2;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = getSavingMessage();
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
const savingText = t2;
|
||||
if (!input) {
|
||||
return null;
|
||||
}
|
||||
const t3 = addMargin ? 1 : 0;
|
||||
let t4;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = <Text color="remember" backgroundColor="memoryBackgroundColor">#</Text>;
|
||||
$[3] = t4;
|
||||
} else {
|
||||
t4 = $[3];
|
||||
}
|
||||
let t5;
|
||||
if ($[4] !== input) {
|
||||
t5 = <Box>{t4}<Text backgroundColor="memoryBackgroundColor" color="text">{" "}{input}{" "}</Text></Box>;
|
||||
$[4] = input;
|
||||
$[5] = t5;
|
||||
} else {
|
||||
t5 = $[5];
|
||||
}
|
||||
let t6;
|
||||
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t6 = <MessageResponse height={1}><Text dimColor={true}>{savingText}</Text></MessageResponse>;
|
||||
$[6] = t6;
|
||||
} else {
|
||||
t6 = $[6];
|
||||
}
|
||||
let t7;
|
||||
if ($[7] !== t3 || $[8] !== t5) {
|
||||
t7 = <Box flexDirection="column" marginTop={t3} width="100%">{t5}{t6}</Box>;
|
||||
$[7] = t3;
|
||||
$[8] = t5;
|
||||
$[9] = t7;
|
||||
} else {
|
||||
t7 = $[9];
|
||||
}
|
||||
return t7;
|
||||
addMargin: boolean
|
||||
text: string
|
||||
}
|
||||
|
||||
export function UserMemoryInputMessage({
|
||||
text,
|
||||
addMargin,
|
||||
}: Props): React.ReactNode {
|
||||
const input = extractTag(text, 'user-memory-input')
|
||||
const savingText = useMemo(() => getSavingMessage(), [])
|
||||
|
||||
if (!input) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" marginTop={addMargin ? 1 : 0} width="100%">
|
||||
<Box>
|
||||
<Text color="remember" backgroundColor="memoryBackgroundColor">
|
||||
#
|
||||
</Text>
|
||||
<Text backgroundColor="memoryBackgroundColor" color="text">
|
||||
{' '}
|
||||
{input}{' '}
|
||||
</Text>
|
||||
</Box>
|
||||
<MessageResponse height={1}>
|
||||
<Text dimColor>{savingText}</Text>
|
||||
</MessageResponse>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,41 +1,30 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { Markdown } from '../Markdown.js';
|
||||
import * as React from 'react'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { Markdown } from '../Markdown.js'
|
||||
|
||||
type Props = {
|
||||
addMargin: boolean;
|
||||
planContent: string;
|
||||
};
|
||||
export function UserPlanMessage(t0) {
|
||||
const $ = _c(6);
|
||||
const {
|
||||
addMargin,
|
||||
planContent
|
||||
} = t0;
|
||||
const t1 = addMargin ? 1 : 0;
|
||||
let t2;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <Box marginBottom={1}><Text bold={true} color="planMode">Plan to implement</Text></Box>;
|
||||
$[0] = t2;
|
||||
} else {
|
||||
t2 = $[0];
|
||||
}
|
||||
let t3;
|
||||
if ($[1] !== planContent) {
|
||||
t3 = <Markdown>{planContent}</Markdown>;
|
||||
$[1] = planContent;
|
||||
$[2] = t3;
|
||||
} else {
|
||||
t3 = $[2];
|
||||
}
|
||||
let t4;
|
||||
if ($[3] !== t1 || $[4] !== t3) {
|
||||
t4 = <Box flexDirection="column" borderStyle="round" borderColor="planMode" marginTop={t1} paddingX={1}>{t2}{t3}</Box>;
|
||||
$[3] = t1;
|
||||
$[4] = t3;
|
||||
$[5] = t4;
|
||||
} else {
|
||||
t4 = $[5];
|
||||
}
|
||||
return t4;
|
||||
addMargin: boolean
|
||||
planContent: string
|
||||
}
|
||||
|
||||
export function UserPlanMessage({
|
||||
addMargin,
|
||||
planContent,
|
||||
}: Props): React.ReactNode {
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
borderStyle="round"
|
||||
borderColor="planMode"
|
||||
marginTop={addMargin ? 1 : 0}
|
||||
paddingX={1}
|
||||
>
|
||||
<Box marginBottom={1}>
|
||||
<Text bold color="planMode">
|
||||
Plan to implement
|
||||
</Text>
|
||||
</Box>
|
||||
<Markdown>{planContent}</Markdown>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
import { feature } from 'bun:bundle';
|
||||
import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs';
|
||||
import React, { useContext, useMemo } from 'react';
|
||||
import { getKairosActive, getUserMsgOptIn } from '../../bootstrap/state.js';
|
||||
import { Box } from '../../ink.js';
|
||||
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js';
|
||||
import { useAppState } from '../../state/AppState.js';
|
||||
import { isEnvTruthy } from '../../utils/envUtils.js';
|
||||
import { logError } from '../../utils/log.js';
|
||||
import { countCharInString } from '../../utils/stringUtils.js';
|
||||
import { MessageActionsSelectedContext } from '../messageActions.js';
|
||||
import { HighlightedThinkingText } from './HighlightedThinkingText.js';
|
||||
import { feature } from 'bun:bundle'
|
||||
import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
|
||||
import React, { useContext, useMemo } from 'react'
|
||||
import { getKairosActive, getUserMsgOptIn } from '../../bootstrap/state.js'
|
||||
import { Box } from '../../ink.js'
|
||||
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'
|
||||
import { useAppState } from '../../state/AppState.js'
|
||||
import { isEnvTruthy } from '../../utils/envUtils.js'
|
||||
import { logError } from '../../utils/log.js'
|
||||
import { countCharInString } from '../../utils/stringUtils.js'
|
||||
import { MessageActionsSelectedContext } from '../messageActions.js'
|
||||
import { HighlightedThinkingText } from './HighlightedThinkingText.js'
|
||||
|
||||
type Props = {
|
||||
addMargin: boolean;
|
||||
param: TextBlockParam;
|
||||
isTranscriptMode?: boolean;
|
||||
timestamp?: string;
|
||||
};
|
||||
addMargin: boolean
|
||||
param: TextBlockParam
|
||||
isTranscriptMode?: boolean
|
||||
timestamp?: string
|
||||
}
|
||||
|
||||
// Hard cap on displayed prompt text. Piping large files via stdin
|
||||
// (e.g. `cat 11k-line-file | claude`) creates a single user message whose
|
||||
@@ -25,16 +26,15 @@ type Props = {
|
||||
// avoids this via <Static> (print-and-forget to terminal scrollback).
|
||||
// Head+tail because `{ cat file; echo prompt; } | claude` puts the user's
|
||||
// actual question at the end.
|
||||
const MAX_DISPLAY_CHARS = 10_000;
|
||||
const TRUNCATE_HEAD_CHARS = 2_500;
|
||||
const TRUNCATE_TAIL_CHARS = 2_500;
|
||||
const MAX_DISPLAY_CHARS = 10_000
|
||||
const TRUNCATE_HEAD_CHARS = 2_500
|
||||
const TRUNCATE_TAIL_CHARS = 2_500
|
||||
|
||||
export function UserPromptMessage({
|
||||
addMargin,
|
||||
param: {
|
||||
text
|
||||
},
|
||||
param: { text },
|
||||
isTranscriptMode,
|
||||
timestamp
|
||||
timestamp,
|
||||
}: Props): React.ReactNode {
|
||||
// REPL.tsx passes isBriefOnly={viewedTeammateTask ? false : isBriefOnly}
|
||||
// but that prop isn't threaded this deep — replicate the override by
|
||||
@@ -48,32 +48,72 @@ export function UserPromptMessage({
|
||||
// bypasses React.memo). Runtime-gated like isBriefEnabled() but inlined
|
||||
// to avoid pulling BriefTool.ts → prompt.ts tool-name strings into
|
||||
// external builds.
|
||||
const isBriefOnly = feature('KAIROS') || feature('KAIROS_BRIEF') ?
|
||||
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||
useAppState(s => s.isBriefOnly) : false;
|
||||
const viewingAgentTaskId = feature('KAIROS') || feature('KAIROS_BRIEF') ?
|
||||
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||
useAppState(s_0 => s_0.viewingAgentTaskId) : null;
|
||||
const isBriefOnly =
|
||||
feature('KAIROS') || feature('KAIROS_BRIEF')
|
||||
? // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||
useAppState(s => s.isBriefOnly)
|
||||
: false
|
||||
const viewingAgentTaskId =
|
||||
feature('KAIROS') || feature('KAIROS_BRIEF')
|
||||
? // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||
useAppState(s => s.viewingAgentTaskId)
|
||||
: null
|
||||
// Hoisted to mount-time — per-message component, re-renders on every scroll.
|
||||
const briefEnvEnabled = feature('KAIROS') || feature('KAIROS_BRIEF') ?
|
||||
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||
useMemo(() => isEnvTruthy(process.env.CLAUDE_CODE_BRIEF), []) : false;
|
||||
const useBriefLayout = feature('KAIROS') || feature('KAIROS_BRIEF') ? (getKairosActive() || getUserMsgOptIn() && (briefEnvEnabled || getFeatureValue_CACHED_MAY_BE_STALE('tengu_kairos_brief', false))) && isBriefOnly && !isTranscriptMode && !viewingAgentTaskId : false;
|
||||
const briefEnvEnabled =
|
||||
feature('KAIROS') || feature('KAIROS_BRIEF')
|
||||
? // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||
useMemo(() => isEnvTruthy(process.env.CLAUDE_CODE_BRIEF), [])
|
||||
: false
|
||||
const useBriefLayout =
|
||||
feature('KAIROS') || feature('KAIROS_BRIEF')
|
||||
? (getKairosActive() ||
|
||||
(getUserMsgOptIn() &&
|
||||
(briefEnvEnabled ||
|
||||
getFeatureValue_CACHED_MAY_BE_STALE(
|
||||
'tengu_kairos_brief',
|
||||
false,
|
||||
)))) &&
|
||||
isBriefOnly &&
|
||||
!isTranscriptMode &&
|
||||
!viewingAgentTaskId
|
||||
: false
|
||||
|
||||
// Truncate before the early return so the hook order is stable.
|
||||
const displayText = useMemo(() => {
|
||||
if (text.length <= MAX_DISPLAY_CHARS) return text;
|
||||
const head = text.slice(0, TRUNCATE_HEAD_CHARS);
|
||||
const tail = text.slice(-TRUNCATE_TAIL_CHARS);
|
||||
const hiddenLines = countCharInString(text, '\n', TRUNCATE_HEAD_CHARS) - countCharInString(tail, '\n');
|
||||
return `${head}\n… +${hiddenLines} lines …\n${tail}`;
|
||||
}, [text]);
|
||||
const isSelected = useContext(MessageActionsSelectedContext);
|
||||
if (text.length <= MAX_DISPLAY_CHARS) return text
|
||||
const head = text.slice(0, TRUNCATE_HEAD_CHARS)
|
||||
const tail = text.slice(-TRUNCATE_TAIL_CHARS)
|
||||
const hiddenLines =
|
||||
countCharInString(text, '\n', TRUNCATE_HEAD_CHARS) -
|
||||
countCharInString(tail, '\n')
|
||||
return `${head}\n… +${hiddenLines} lines …\n${tail}`
|
||||
}, [text])
|
||||
|
||||
const isSelected = useContext(MessageActionsSelectedContext)
|
||||
|
||||
if (!text) {
|
||||
logError(new Error('No content found in user prompt message'));
|
||||
return null;
|
||||
logError(new Error('No content found in user prompt message'))
|
||||
return null
|
||||
}
|
||||
return <Box flexDirection="column" marginTop={addMargin ? 1 : 0} backgroundColor={isSelected ? 'messageActionsBackground' : useBriefLayout ? undefined : 'userMessageBackground'} paddingRight={useBriefLayout ? 0 : 1}>
|
||||
<HighlightedThinkingText text={displayText} useBriefLayout={useBriefLayout} timestamp={useBriefLayout ? timestamp : undefined} />
|
||||
</Box>;
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
marginTop={addMargin ? 1 : 0}
|
||||
backgroundColor={
|
||||
isSelected
|
||||
? 'messageActionsBackground'
|
||||
: useBriefLayout
|
||||
? undefined
|
||||
: 'userMessageBackground'
|
||||
}
|
||||
paddingRight={useBriefLayout ? 0 : 1}
|
||||
>
|
||||
<HighlightedThinkingText
|
||||
text={displayText}
|
||||
useBriefLayout={useBriefLayout}
|
||||
timestamp={useBriefLayout ? timestamp : undefined}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,120 +1,91 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs';
|
||||
import * as React from 'react';
|
||||
import { REFRESH_ARROW } from '../../constants/figures.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
|
||||
import * as React from 'react'
|
||||
import { REFRESH_ARROW } from '../../constants/figures.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
|
||||
type Props = {
|
||||
addMargin: boolean;
|
||||
param: TextBlockParam;
|
||||
};
|
||||
addMargin: boolean
|
||||
param: TextBlockParam
|
||||
}
|
||||
|
||||
type ParsedUpdate = {
|
||||
kind: 'resource' | 'polling';
|
||||
server: string;
|
||||
kind: 'resource' | 'polling'
|
||||
server: string
|
||||
/** URI for resource updates, tool name for polling updates */
|
||||
target: string;
|
||||
reason?: string;
|
||||
};
|
||||
target: string
|
||||
reason?: string
|
||||
}
|
||||
|
||||
// Parse resource and polling updates from XML format
|
||||
function parseUpdates(text: string): ParsedUpdate[] {
|
||||
const updates: ParsedUpdate[] = [];
|
||||
const updates: ParsedUpdate[] = []
|
||||
|
||||
// Match <mcp-resource-update server="..." uri="...">
|
||||
const resourceRegex = /<mcp-resource-update\s+server="([^"]+)"\s+uri="([^"]+)"[^>]*>(?:[\s\S]*?<reason>([^<]+)<\/reason>)?/g;
|
||||
let match;
|
||||
const resourceRegex =
|
||||
/<mcp-resource-update\s+server="([^"]+)"\s+uri="([^"]+)"[^>]*>(?:[\s\S]*?<reason>([^<]+)<\/reason>)?/g
|
||||
let match
|
||||
while ((match = resourceRegex.exec(text)) !== null) {
|
||||
updates.push({
|
||||
kind: 'resource',
|
||||
server: match[1] ?? '',
|
||||
target: match[2] ?? '',
|
||||
reason: match[3]
|
||||
});
|
||||
reason: match[3],
|
||||
})
|
||||
}
|
||||
|
||||
// Match <mcp-polling-update type="tool" server="..." tool="...">
|
||||
const pollingRegex = /<mcp-polling-update\s+type="([^"]+)"\s+server="([^"]+)"\s+tool="([^"]+)"[^>]*>(?:[\s\S]*?<reason>([^<]+)<\/reason>)?/g;
|
||||
const pollingRegex =
|
||||
/<mcp-polling-update\s+type="([^"]+)"\s+server="([^"]+)"\s+tool="([^"]+)"[^>]*>(?:[\s\S]*?<reason>([^<]+)<\/reason>)?/g
|
||||
while ((match = pollingRegex.exec(text)) !== null) {
|
||||
updates.push({
|
||||
kind: 'polling',
|
||||
server: match[2] ?? '',
|
||||
target: match[3] ?? '',
|
||||
reason: match[4]
|
||||
});
|
||||
reason: match[4],
|
||||
})
|
||||
}
|
||||
return updates;
|
||||
|
||||
return updates
|
||||
}
|
||||
|
||||
// Format URI for display - show just the meaningful part
|
||||
function formatUri(uri: string): string {
|
||||
// For file:// URIs, show just the filename
|
||||
if (uri.startsWith('file://')) {
|
||||
const path = uri.slice(7);
|
||||
const parts = path.split('/');
|
||||
return parts[parts.length - 1] || path;
|
||||
const path = uri.slice(7)
|
||||
const parts = path.split('/')
|
||||
return parts[parts.length - 1] || path
|
||||
}
|
||||
// For other URIs, show the whole thing but truncated
|
||||
if (uri.length > 40) {
|
||||
return uri.slice(0, 39) + '\u2026';
|
||||
return uri.slice(0, 39) + '\u2026'
|
||||
}
|
||||
return uri;
|
||||
return uri
|
||||
}
|
||||
export function UserResourceUpdateMessage(t0) {
|
||||
const $ = _c(12);
|
||||
const {
|
||||
|
||||
export function UserResourceUpdateMessage({
|
||||
addMargin,
|
||||
param: t1
|
||||
} = t0;
|
||||
const {
|
||||
text
|
||||
} = t1;
|
||||
let T0;
|
||||
let t2;
|
||||
let t3;
|
||||
let t4;
|
||||
let t5;
|
||||
if ($[0] !== addMargin || $[1] !== text) {
|
||||
t5 = Symbol.for("react.early_return_sentinel");
|
||||
bb0: {
|
||||
const updates = parseUpdates(text);
|
||||
if (updates.length === 0) {
|
||||
t5 = null;
|
||||
break bb0;
|
||||
}
|
||||
T0 = Box;
|
||||
t2 = "column";
|
||||
t3 = addMargin ? 1 : 0;
|
||||
t4 = updates.map(_temp);
|
||||
}
|
||||
$[0] = addMargin;
|
||||
$[1] = text;
|
||||
$[2] = T0;
|
||||
$[3] = t2;
|
||||
$[4] = t3;
|
||||
$[5] = t4;
|
||||
$[6] = t5;
|
||||
} else {
|
||||
T0 = $[2];
|
||||
t2 = $[3];
|
||||
t3 = $[4];
|
||||
t4 = $[5];
|
||||
t5 = $[6];
|
||||
}
|
||||
if (t5 !== Symbol.for("react.early_return_sentinel")) {
|
||||
return t5;
|
||||
}
|
||||
let t6;
|
||||
if ($[7] !== T0 || $[8] !== t2 || $[9] !== t3 || $[10] !== t4) {
|
||||
t6 = <T0 flexDirection={t2} marginTop={t3}>{t4}</T0>;
|
||||
$[7] = T0;
|
||||
$[8] = t2;
|
||||
$[9] = t3;
|
||||
$[10] = t4;
|
||||
$[11] = t6;
|
||||
} else {
|
||||
t6 = $[11];
|
||||
}
|
||||
return t6;
|
||||
}
|
||||
function _temp(update, i) {
|
||||
return <Box key={i}><Text><Text color="success">{REFRESH_ARROW}</Text>{" "}<Text dimColor={true}>{update.server}:</Text>{" "}<Text color="suggestion">{update.kind === "resource" ? formatUri(update.target) : update.target}</Text>{update.reason && <Text dimColor={true}> · {update.reason}</Text>}</Text></Box>;
|
||||
param: { text },
|
||||
}: Props): React.ReactNode {
|
||||
const updates = parseUpdates(text)
|
||||
if (updates.length === 0) return null
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" marginTop={addMargin ? 1 : 0}>
|
||||
{updates.map((update, i) => (
|
||||
<Box key={i}>
|
||||
<Text>
|
||||
<Text color="success">{REFRESH_ARROW}</Text>{' '}
|
||||
<Text dimColor>{update.server}:</Text>{' '}
|
||||
<Text color="suggestion">
|
||||
{update.kind === 'resource'
|
||||
? formatUri(update.target)
|
||||
: update.target}
|
||||
</Text>
|
||||
{update.reason && <Text dimColor> · {update.reason}</Text>}
|
||||
</Text>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,28 +1,33 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs';
|
||||
import figures from 'figures';
|
||||
import * as React from 'react';
|
||||
import { TEAMMATE_MESSAGE_TAG } from '../../constants/xml.js';
|
||||
import { Ansi, Box, Text, type TextProps } from '../../ink.js';
|
||||
import { toInkColor } from '../../utils/ink.js';
|
||||
import { jsonParse } from '../../utils/slowOperations.js';
|
||||
import { isShutdownApproved } from '../../utils/teammateMailbox.js';
|
||||
import { MessageResponse } from '../MessageResponse.js';
|
||||
import { tryRenderPlanApprovalMessage } from './PlanApprovalMessage.js';
|
||||
import { tryRenderShutdownMessage } from './ShutdownMessage.js';
|
||||
import { tryRenderTaskAssignmentMessage } from './TaskAssignmentMessage.js';
|
||||
import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
|
||||
import figures from 'figures'
|
||||
import * as React from 'react'
|
||||
import { TEAMMATE_MESSAGE_TAG } from '../../constants/xml.js'
|
||||
import { Ansi, Box, Text, type TextProps } from '../../ink.js'
|
||||
import { toInkColor } from '../../utils/ink.js'
|
||||
import { jsonParse } from '../../utils/slowOperations.js'
|
||||
import { isShutdownApproved } from '../../utils/teammateMailbox.js'
|
||||
import { MessageResponse } from '../MessageResponse.js'
|
||||
import { tryRenderPlanApprovalMessage } from './PlanApprovalMessage.js'
|
||||
import { tryRenderShutdownMessage } from './ShutdownMessage.js'
|
||||
import { tryRenderTaskAssignmentMessage } from './TaskAssignmentMessage.js'
|
||||
|
||||
type Props = {
|
||||
addMargin: boolean;
|
||||
param: TextBlockParam;
|
||||
isTranscriptMode?: boolean;
|
||||
};
|
||||
addMargin: boolean
|
||||
param: TextBlockParam
|
||||
isTranscriptMode?: boolean
|
||||
}
|
||||
|
||||
type ParsedMessage = {
|
||||
teammateId: string;
|
||||
content: string;
|
||||
color?: string;
|
||||
summary?: string;
|
||||
};
|
||||
const TEAMMATE_MSG_REGEX = new RegExp(`<${TEAMMATE_MESSAGE_TAG}\\s+teammate_id="([^"]+)"(?:\\s+color="([^"]+)")?(?:\\s+summary="([^"]+)")?>\\n?([\\s\\S]*?)\\n?<\\/${TEAMMATE_MESSAGE_TAG}>`, 'g');
|
||||
teammateId: string
|
||||
content: string
|
||||
color?: string
|
||||
summary?: string
|
||||
}
|
||||
|
||||
const TEAMMATE_MSG_REGEX = new RegExp(
|
||||
`<${TEAMMATE_MESSAGE_TAG}\\s+teammate_id="([^"]+)"(?:\\s+color="([^"]+)")?(?:\\s+summary="([^"]+)")?>\\n?([\\s\\S]*?)\\n?<\\/${TEAMMATE_MESSAGE_TAG}>`,
|
||||
'g',
|
||||
)
|
||||
|
||||
/**
|
||||
* Parse all teammate messages from XML format:
|
||||
@@ -30,176 +35,169 @@ const TEAMMATE_MSG_REGEX = new RegExp(`<${TEAMMATE_MESSAGE_TAG}\\s+teammate_id="
|
||||
* Supports multiple messages in a single text block.
|
||||
*/
|
||||
function parseTeammateMessages(text: string): ParsedMessage[] {
|
||||
const messages: ParsedMessage[] = [];
|
||||
const messages: ParsedMessage[] = []
|
||||
// Use matchAll to find all matches (this is a RegExp method, not child_process)
|
||||
for (const match of text.matchAll(TEAMMATE_MSG_REGEX)) {
|
||||
if (match[1] && match[4]) {
|
||||
messages.push({
|
||||
teammateId: match[1],
|
||||
color: match[2],
|
||||
// may be undefined
|
||||
summary: match[3],
|
||||
// may be undefined
|
||||
content: match[4].trim()
|
||||
});
|
||||
color: match[2], // may be undefined
|
||||
summary: match[3], // may be undefined
|
||||
content: match[4].trim(),
|
||||
})
|
||||
}
|
||||
}
|
||||
return messages;
|
||||
|
||||
return messages
|
||||
}
|
||||
|
||||
function getDisplayName(teammateId: string): string {
|
||||
if (teammateId === 'leader') {
|
||||
return 'leader';
|
||||
return 'leader'
|
||||
}
|
||||
return teammateId;
|
||||
return teammateId
|
||||
}
|
||||
|
||||
export function UserTeammateMessage({
|
||||
addMargin,
|
||||
param: {
|
||||
text
|
||||
},
|
||||
isTranscriptMode
|
||||
param: { text },
|
||||
isTranscriptMode,
|
||||
}: Props): React.ReactNode {
|
||||
const messages = parseTeammateMessages(text).filter(msg => {
|
||||
// Pre-filter shutdown lifecycle messages to avoid empty wrapper
|
||||
// Box elements creating blank lines between model turns
|
||||
if (isShutdownApproved(msg.content)) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
try {
|
||||
const parsed = jsonParse(msg.content);
|
||||
if (parsed?.type === 'teammate_terminated') return false;
|
||||
const parsed = jsonParse(msg.content)
|
||||
if (parsed?.type === 'teammate_terminated') return false
|
||||
} catch {
|
||||
// Not JSON, keep the message
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return true
|
||||
})
|
||||
if (messages.length === 0) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
return <Box flexDirection="column" marginTop={addMargin ? 1 : 0} width="100%">
|
||||
{messages.map((msg_0, index) => {
|
||||
const inkColor = toInkColor(msg_0.color);
|
||||
const displayName = getDisplayName(msg_0.teammateId);
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" marginTop={addMargin ? 1 : 0} width="100%">
|
||||
{messages.map((msg, index) => {
|
||||
const inkColor = toInkColor(msg.color)
|
||||
const displayName = getDisplayName(msg.teammateId)
|
||||
|
||||
// Try to render as plan approval message (request or response)
|
||||
const planApprovalElement = tryRenderPlanApprovalMessage(msg_0.content, displayName);
|
||||
const planApprovalElement = tryRenderPlanApprovalMessage(
|
||||
msg.content,
|
||||
displayName,
|
||||
)
|
||||
if (planApprovalElement) {
|
||||
return <React.Fragment key={index}>{planApprovalElement}</React.Fragment>;
|
||||
return (
|
||||
<React.Fragment key={index}>{planApprovalElement}</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
// Try to render as shutdown message (request or rejected)
|
||||
const shutdownElement = tryRenderShutdownMessage(msg_0.content);
|
||||
const shutdownElement = tryRenderShutdownMessage(msg.content)
|
||||
if (shutdownElement) {
|
||||
return <React.Fragment key={index}>{shutdownElement}</React.Fragment>;
|
||||
return <React.Fragment key={index}>{shutdownElement}</React.Fragment>
|
||||
}
|
||||
|
||||
// Try to render as task assignment message
|
||||
const taskAssignmentElement = tryRenderTaskAssignmentMessage(msg_0.content);
|
||||
const taskAssignmentElement = tryRenderTaskAssignmentMessage(
|
||||
msg.content,
|
||||
)
|
||||
if (taskAssignmentElement) {
|
||||
return <React.Fragment key={index}>{taskAssignmentElement}</React.Fragment>;
|
||||
return (
|
||||
<React.Fragment key={index}>{taskAssignmentElement}</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
// Try to parse as structured JSON message
|
||||
let parsedIdleNotification: {
|
||||
type?: string;
|
||||
} | null = null;
|
||||
let parsedIdleNotification: { type?: string } | null = null
|
||||
try {
|
||||
parsedIdleNotification = jsonParse(msg_0.content);
|
||||
parsedIdleNotification = jsonParse(msg.content)
|
||||
} catch {
|
||||
// Not JSON
|
||||
}
|
||||
|
||||
// Hide idle notifications - they are processed silently
|
||||
if (parsedIdleNotification?.type === 'idle_notification') {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
// Task completed notification - show which task was completed
|
||||
if (parsedIdleNotification?.type === 'task_completed') {
|
||||
const taskCompleted = parsedIdleNotification as {
|
||||
type: string;
|
||||
from: string;
|
||||
taskId: string;
|
||||
taskSubject?: string;
|
||||
};
|
||||
return <Box key={index} flexDirection="column" marginTop={1}>
|
||||
<Text color={inkColor}>{`@${displayName}${figures.pointer}`}</Text>
|
||||
type: string
|
||||
from: string
|
||||
taskId: string
|
||||
taskSubject?: string
|
||||
}
|
||||
return (
|
||||
<Box key={index} flexDirection="column" marginTop={1}>
|
||||
<Text
|
||||
color={inkColor}
|
||||
>{`@${displayName}${figures.pointer}`}</Text>
|
||||
<MessageResponse>
|
||||
<Text color="success">✓</Text>
|
||||
<Text>
|
||||
{' '}
|
||||
Completed task #{taskCompleted.taskId}
|
||||
{taskCompleted.taskSubject && <Text dimColor> ({taskCompleted.taskSubject})</Text>}
|
||||
{taskCompleted.taskSubject && (
|
||||
<Text dimColor> ({taskCompleted.taskSubject})</Text>
|
||||
)}
|
||||
</Text>
|
||||
</MessageResponse>
|
||||
</Box>;
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
// Default: plain text message (truncated)
|
||||
return <TeammateMessageContent key={index} displayName={displayName} inkColor={inkColor} content={msg_0.content} summary={msg_0.summary} isTranscriptMode={isTranscriptMode} />;
|
||||
return (
|
||||
<TeammateMessageContent
|
||||
key={index}
|
||||
displayName={displayName}
|
||||
inkColor={inkColor}
|
||||
content={msg.content}
|
||||
summary={msg.summary}
|
||||
isTranscriptMode={isTranscriptMode}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</Box>;
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
type TeammateMessageContentProps = {
|
||||
displayName: string;
|
||||
inkColor: TextProps['color'];
|
||||
content: string;
|
||||
summary?: string;
|
||||
isTranscriptMode?: boolean;
|
||||
};
|
||||
export function TeammateMessageContent(t0) {
|
||||
const $ = _c(14);
|
||||
const {
|
||||
displayName: string
|
||||
inkColor: TextProps['color']
|
||||
content: string
|
||||
summary?: string
|
||||
isTranscriptMode?: boolean
|
||||
}
|
||||
|
||||
export function TeammateMessageContent({
|
||||
displayName,
|
||||
inkColor,
|
||||
content,
|
||||
summary,
|
||||
isTranscriptMode
|
||||
} = t0;
|
||||
const t1 = `@${displayName}${figures.pointer}`;
|
||||
let t2;
|
||||
if ($[0] !== inkColor || $[1] !== t1) {
|
||||
t2 = <Text color={inkColor}>{t1}</Text>;
|
||||
$[0] = inkColor;
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
let t3;
|
||||
if ($[3] !== summary) {
|
||||
t3 = summary && <Text> {summary}</Text>;
|
||||
$[3] = summary;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
let t4;
|
||||
if ($[5] !== t2 || $[6] !== t3) {
|
||||
t4 = <Box>{t2}{t3}</Box>;
|
||||
$[5] = t2;
|
||||
$[6] = t3;
|
||||
$[7] = t4;
|
||||
} else {
|
||||
t4 = $[7];
|
||||
}
|
||||
let t5;
|
||||
if ($[8] !== content || $[9] !== isTranscriptMode) {
|
||||
t5 = isTranscriptMode && <Box paddingLeft={2}><Text><Ansi>{content}</Ansi></Text></Box>;
|
||||
$[8] = content;
|
||||
$[9] = isTranscriptMode;
|
||||
$[10] = t5;
|
||||
} else {
|
||||
t5 = $[10];
|
||||
}
|
||||
let t6;
|
||||
if ($[11] !== t4 || $[12] !== t5) {
|
||||
t6 = <Box flexDirection="column" marginTop={1}>{t4}{t5}</Box>;
|
||||
$[11] = t4;
|
||||
$[12] = t5;
|
||||
$[13] = t6;
|
||||
} else {
|
||||
t6 = $[13];
|
||||
}
|
||||
return t6;
|
||||
isTranscriptMode,
|
||||
}: TeammateMessageContentProps): React.ReactNode {
|
||||
return (
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<Box>
|
||||
<Text color={inkColor}>{`@${displayName}${figures.pointer}`}</Text>
|
||||
{summary && <Text> {summary}</Text>}
|
||||
</Box>
|
||||
{isTranscriptMode && (
|
||||
<Box paddingLeft={2}>
|
||||
<Text>
|
||||
<Ansi>{content}</Ansi>
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,274 +1,197 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { feature } from 'bun:bundle';
|
||||
import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs';
|
||||
import * as React from 'react';
|
||||
import { NO_CONTENT_MESSAGE } from '../../constants/messages.js';
|
||||
import { COMMAND_MESSAGE_TAG, LOCAL_COMMAND_CAVEAT_TAG, TASK_NOTIFICATION_TAG, TEAMMATE_MESSAGE_TAG, TICK_TAG } from '../../constants/xml.js';
|
||||
import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js';
|
||||
import { extractTag, INTERRUPT_MESSAGE, INTERRUPT_MESSAGE_FOR_TOOL_USE } from '../../utils/messages.js';
|
||||
import { InterruptedByUser } from '../InterruptedByUser.js';
|
||||
import { MessageResponse } from '../MessageResponse.js';
|
||||
import { UserAgentNotificationMessage } from './UserAgentNotificationMessage.js';
|
||||
import { UserBashInputMessage } from './UserBashInputMessage.js';
|
||||
import { UserBashOutputMessage } from './UserBashOutputMessage.js';
|
||||
import { UserCommandMessage } from './UserCommandMessage.js';
|
||||
import { UserLocalCommandOutputMessage } from './UserLocalCommandOutputMessage.js';
|
||||
import { UserMemoryInputMessage } from './UserMemoryInputMessage.js';
|
||||
import { UserPlanMessage } from './UserPlanMessage.js';
|
||||
import { UserPromptMessage } from './UserPromptMessage.js';
|
||||
import { UserResourceUpdateMessage } from './UserResourceUpdateMessage.js';
|
||||
import { UserTeammateMessage } from './UserTeammateMessage.js';
|
||||
import { feature } from 'bun:bundle'
|
||||
import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
|
||||
import * as React from 'react'
|
||||
import { NO_CONTENT_MESSAGE } from '../../constants/messages.js'
|
||||
import {
|
||||
COMMAND_MESSAGE_TAG,
|
||||
LOCAL_COMMAND_CAVEAT_TAG,
|
||||
TASK_NOTIFICATION_TAG,
|
||||
TEAMMATE_MESSAGE_TAG,
|
||||
TICK_TAG,
|
||||
} from '../../constants/xml.js'
|
||||
import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js'
|
||||
import {
|
||||
extractTag,
|
||||
INTERRUPT_MESSAGE,
|
||||
INTERRUPT_MESSAGE_FOR_TOOL_USE,
|
||||
} from '../../utils/messages.js'
|
||||
import { InterruptedByUser } from '../InterruptedByUser.js'
|
||||
import { MessageResponse } from '../MessageResponse.js'
|
||||
import { UserAgentNotificationMessage } from './UserAgentNotificationMessage.js'
|
||||
import { UserBashInputMessage } from './UserBashInputMessage.js'
|
||||
import { UserBashOutputMessage } from './UserBashOutputMessage.js'
|
||||
import { UserCommandMessage } from './UserCommandMessage.js'
|
||||
import { UserLocalCommandOutputMessage } from './UserLocalCommandOutputMessage.js'
|
||||
import { UserMemoryInputMessage } from './UserMemoryInputMessage.js'
|
||||
import { UserPlanMessage } from './UserPlanMessage.js'
|
||||
import { UserPromptMessage } from './UserPromptMessage.js'
|
||||
import { UserResourceUpdateMessage } from './UserResourceUpdateMessage.js'
|
||||
import { UserTeammateMessage } from './UserTeammateMessage.js'
|
||||
|
||||
type Props = {
|
||||
addMargin: boolean;
|
||||
param: TextBlockParam;
|
||||
verbose: boolean;
|
||||
planContent?: string;
|
||||
isTranscriptMode?: boolean;
|
||||
timestamp?: string;
|
||||
};
|
||||
export function UserTextMessage(t0) {
|
||||
const $ = _c(49);
|
||||
const {
|
||||
addMargin: boolean
|
||||
param: TextBlockParam
|
||||
verbose: boolean
|
||||
planContent?: string
|
||||
isTranscriptMode?: boolean
|
||||
timestamp?: string
|
||||
}
|
||||
|
||||
export function UserTextMessage({
|
||||
addMargin,
|
||||
param,
|
||||
verbose,
|
||||
planContent,
|
||||
isTranscriptMode,
|
||||
timestamp
|
||||
} = t0;
|
||||
timestamp,
|
||||
}: Props): React.ReactNode {
|
||||
if (param.text.trim() === NO_CONTENT_MESSAGE) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
// Plan to implement message (cleared context flow)
|
||||
if (planContent) {
|
||||
let t1;
|
||||
if ($[0] !== addMargin || $[1] !== planContent) {
|
||||
t1 = <UserPlanMessage addMargin={addMargin} planContent={planContent} />;
|
||||
$[0] = addMargin;
|
||||
$[1] = planContent;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
return t1;
|
||||
return <UserPlanMessage addMargin={addMargin} planContent={planContent} />
|
||||
}
|
||||
|
||||
if (extractTag(param.text, TICK_TAG)) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
// Hide synthetic caveat messages (should be filtered by isMeta, this is defensive)
|
||||
if (param.text.includes(`<${LOCAL_COMMAND_CAVEAT_TAG}>`)) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
if (param.text.startsWith("<bash-stdout") || param.text.startsWith("<bash-stderr")) {
|
||||
let t1;
|
||||
if ($[3] !== param.text || $[4] !== verbose) {
|
||||
t1 = <UserBashOutputMessage content={param.text} verbose={verbose} />;
|
||||
$[3] = param.text;
|
||||
$[4] = verbose;
|
||||
$[5] = t1;
|
||||
} else {
|
||||
t1 = $[5];
|
||||
|
||||
// Show bash output
|
||||
if (
|
||||
param.text.startsWith('<bash-stdout') ||
|
||||
param.text.startsWith('<bash-stderr')
|
||||
) {
|
||||
return <UserBashOutputMessage content={param.text} verbose={verbose} />
|
||||
}
|
||||
return t1;
|
||||
|
||||
// Show command output
|
||||
if (
|
||||
param.text.startsWith('<local-command-stdout') ||
|
||||
param.text.startsWith('<local-command-stderr')
|
||||
) {
|
||||
return <UserLocalCommandOutputMessage content={param.text} />
|
||||
}
|
||||
if (param.text.startsWith("<local-command-stdout") || param.text.startsWith("<local-command-stderr")) {
|
||||
let t1;
|
||||
if ($[6] !== param.text) {
|
||||
t1 = <UserLocalCommandOutputMessage content={param.text} />;
|
||||
$[6] = param.text;
|
||||
$[7] = t1;
|
||||
} else {
|
||||
t1 = $[7];
|
||||
|
||||
// Handle interruption messages specially
|
||||
if (
|
||||
param.text === INTERRUPT_MESSAGE ||
|
||||
param.text === INTERRUPT_MESSAGE_FOR_TOOL_USE
|
||||
) {
|
||||
return (
|
||||
<MessageResponse height={1}>
|
||||
<InterruptedByUser />
|
||||
</MessageResponse>
|
||||
)
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
if (param.text === INTERRUPT_MESSAGE || param.text === INTERRUPT_MESSAGE_FOR_TOOL_USE) {
|
||||
let t1;
|
||||
if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <MessageResponse height={1}><InterruptedByUser /></MessageResponse>;
|
||||
$[8] = t1;
|
||||
} else {
|
||||
t1 = $[8];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
if (feature("KAIROS_GITHUB_WEBHOOKS")) {
|
||||
if (param.text.startsWith("<github-webhook-activity>")) {
|
||||
let t1;
|
||||
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = require("./UserGitHubWebhookMessage.js");
|
||||
$[9] = t1;
|
||||
} else {
|
||||
t1 = $[9];
|
||||
}
|
||||
const {
|
||||
UserGitHubWebhookMessage
|
||||
} = t1 as typeof import('./UserGitHubWebhookMessage.js');
|
||||
let t2;
|
||||
if ($[10] !== addMargin || $[11] !== param) {
|
||||
t2 = <UserGitHubWebhookMessage addMargin={addMargin} param={param} />;
|
||||
$[10] = addMargin;
|
||||
$[11] = param;
|
||||
$[12] = t2;
|
||||
} else {
|
||||
t2 = $[12];
|
||||
}
|
||||
return t2;
|
||||
|
||||
// GitHub webhook events (check_run, review comments, pushes) delivered via
|
||||
// bound-session routing after /subscribe-pr. The tag constant is stripped
|
||||
// from external builds — inline the literal so the import doesn't fail.
|
||||
// The require() below DCEs when both flags are off. startsWith (not
|
||||
// includes) and before the includes-checks below: defense-in-depth if
|
||||
// the sanitizer were ever weakened.
|
||||
if (feature('KAIROS_GITHUB_WEBHOOKS')) {
|
||||
if (param.text.startsWith('<github-webhook-activity>')) {
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
const { UserGitHubWebhookMessage } =
|
||||
require('./UserGitHubWebhookMessage.js') as typeof import('./UserGitHubWebhookMessage.js')
|
||||
/* eslint-enable @typescript-eslint/no-require-imports */
|
||||
return <UserGitHubWebhookMessage addMargin={addMargin} param={param} />
|
||||
}
|
||||
}
|
||||
if (param.text.includes("<bash-input>")) {
|
||||
let t1;
|
||||
if ($[13] !== addMargin || $[14] !== param) {
|
||||
t1 = <UserBashInputMessage addMargin={addMargin} param={param} />;
|
||||
$[13] = addMargin;
|
||||
$[14] = param;
|
||||
$[15] = t1;
|
||||
} else {
|
||||
t1 = $[15];
|
||||
}
|
||||
return t1;
|
||||
|
||||
// Bash inputs!
|
||||
if (param.text.includes('<bash-input>')) {
|
||||
return <UserBashInputMessage addMargin={addMargin} param={param} />
|
||||
}
|
||||
|
||||
// Slash commands/
|
||||
if (param.text.includes(`<${COMMAND_MESSAGE_TAG}>`)) {
|
||||
let t1;
|
||||
if ($[16] !== addMargin || $[17] !== param) {
|
||||
t1 = <UserCommandMessage addMargin={addMargin} param={param} />;
|
||||
$[16] = addMargin;
|
||||
$[17] = param;
|
||||
$[18] = t1;
|
||||
} else {
|
||||
t1 = $[18];
|
||||
return <UserCommandMessage addMargin={addMargin} param={param} />
|
||||
}
|
||||
return t1;
|
||||
|
||||
if (param.text.includes('<user-memory-input>')) {
|
||||
return <UserMemoryInputMessage addMargin={addMargin} text={param.text} />
|
||||
}
|
||||
if (param.text.includes("<user-memory-input>")) {
|
||||
let t1;
|
||||
if ($[19] !== addMargin || $[20] !== param.text) {
|
||||
t1 = <UserMemoryInputMessage addMargin={addMargin} text={param.text} />;
|
||||
$[19] = addMargin;
|
||||
$[20] = param.text;
|
||||
$[21] = t1;
|
||||
} else {
|
||||
t1 = $[21];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
if (isAgentSwarmsEnabled() && param.text.includes(`<${TEAMMATE_MESSAGE_TAG}`)) {
|
||||
let t1;
|
||||
if ($[22] !== addMargin || $[23] !== isTranscriptMode || $[24] !== param) {
|
||||
t1 = <UserTeammateMessage addMargin={addMargin} param={param} isTranscriptMode={isTranscriptMode} />;
|
||||
$[22] = addMargin;
|
||||
$[23] = isTranscriptMode;
|
||||
$[24] = param;
|
||||
$[25] = t1;
|
||||
} else {
|
||||
t1 = $[25];
|
||||
}
|
||||
return t1;
|
||||
|
||||
// Teammate messages - only check when swarms enabled
|
||||
if (
|
||||
isAgentSwarmsEnabled() &&
|
||||
param.text.includes(`<${TEAMMATE_MESSAGE_TAG}`)
|
||||
) {
|
||||
return (
|
||||
<UserTeammateMessage
|
||||
addMargin={addMargin}
|
||||
param={param}
|
||||
isTranscriptMode={isTranscriptMode}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// Task notifications (agent completions, bash completions, etc.)
|
||||
if (param.text.includes(`<${TASK_NOTIFICATION_TAG}`)) {
|
||||
let t1;
|
||||
if ($[26] !== addMargin || $[27] !== param) {
|
||||
t1 = <UserAgentNotificationMessage addMargin={addMargin} param={param} />;
|
||||
$[26] = addMargin;
|
||||
$[27] = param;
|
||||
$[28] = t1;
|
||||
} else {
|
||||
t1 = $[28];
|
||||
return <UserAgentNotificationMessage addMargin={addMargin} param={param} />
|
||||
}
|
||||
return t1;
|
||||
|
||||
// MCP resource and polling update notifications
|
||||
if (
|
||||
param.text.includes('<mcp-resource-update') ||
|
||||
param.text.includes('<mcp-polling-update')
|
||||
) {
|
||||
return <UserResourceUpdateMessage addMargin={addMargin} param={param} />
|
||||
}
|
||||
if (param.text.includes("<mcp-resource-update") || param.text.includes("<mcp-polling-update")) {
|
||||
let t1;
|
||||
if ($[29] !== addMargin || $[30] !== param) {
|
||||
t1 = <UserResourceUpdateMessage addMargin={addMargin} param={param} />;
|
||||
$[29] = addMargin;
|
||||
$[30] = param;
|
||||
$[31] = t1;
|
||||
} else {
|
||||
t1 = $[31];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
if (feature("FORK_SUBAGENT")) {
|
||||
if (param.text.includes("<fork-boilerplate>")) {
|
||||
let t1;
|
||||
if ($[32] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = require("./UserForkBoilerplateMessage.js");
|
||||
$[32] = t1;
|
||||
} else {
|
||||
t1 = $[32];
|
||||
}
|
||||
const {
|
||||
UserForkBoilerplateMessage
|
||||
} = t1 as typeof import('./UserForkBoilerplateMessage.js');
|
||||
let t2;
|
||||
if ($[33] !== addMargin || $[34] !== param) {
|
||||
t2 = <UserForkBoilerplateMessage addMargin={addMargin} param={param} />;
|
||||
$[33] = addMargin;
|
||||
$[34] = param;
|
||||
$[35] = t2;
|
||||
} else {
|
||||
t2 = $[35];
|
||||
}
|
||||
return t2;
|
||||
|
||||
// Fork child's first message: collapse the rules/format boilerplate, show
|
||||
// only the directive. FORK_BOILERPLATE_TAG is inlined so the import doesn't
|
||||
// ship in external builds where feature('FORK_SUBAGENT') is false.
|
||||
if (feature('FORK_SUBAGENT')) {
|
||||
if (param.text.includes('<fork-boilerplate>')) {
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
const { UserForkBoilerplateMessage } =
|
||||
require('./UserForkBoilerplateMessage.js') as typeof import('./UserForkBoilerplateMessage.js')
|
||||
/* eslint-enable @typescript-eslint/no-require-imports */
|
||||
return <UserForkBoilerplateMessage addMargin={addMargin} param={param} />
|
||||
}
|
||||
}
|
||||
if (feature("UDS_INBOX")) {
|
||||
if (param.text.includes("<cross-session-message")) {
|
||||
let t1;
|
||||
if ($[36] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = require("./UserCrossSessionMessage.js");
|
||||
$[36] = t1;
|
||||
} else {
|
||||
t1 = $[36];
|
||||
}
|
||||
const {
|
||||
UserCrossSessionMessage
|
||||
} = t1 as typeof import('./UserCrossSessionMessage.js');
|
||||
let t2;
|
||||
if ($[37] !== addMargin || $[38] !== param) {
|
||||
t2 = <UserCrossSessionMessage addMargin={addMargin} param={param} />;
|
||||
$[37] = addMargin;
|
||||
$[38] = param;
|
||||
$[39] = t2;
|
||||
} else {
|
||||
t2 = $[39];
|
||||
}
|
||||
return t2;
|
||||
|
||||
// Cross-session UDS message (from another Claude session's SendMessage).
|
||||
// CROSS_SESSION_MESSAGE_TAG is inlined so the import doesn't ship in
|
||||
// external builds where feature('UDS_INBOX') is false.
|
||||
if (feature('UDS_INBOX')) {
|
||||
if (param.text.includes('<cross-session-message')) {
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
const { UserCrossSessionMessage } =
|
||||
require('./UserCrossSessionMessage.js') as typeof import('./UserCrossSessionMessage.js')
|
||||
/* eslint-enable @typescript-eslint/no-require-imports */
|
||||
return <UserCrossSessionMessage addMargin={addMargin} param={param} />
|
||||
}
|
||||
}
|
||||
if (feature("KAIROS") || feature("KAIROS_CHANNELS")) {
|
||||
if (param.text.includes("<channel source=\"")) {
|
||||
let t1;
|
||||
if ($[40] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = require("./UserChannelMessage.js");
|
||||
$[40] = t1;
|
||||
} else {
|
||||
t1 = $[40];
|
||||
}
|
||||
const {
|
||||
UserChannelMessage
|
||||
} = t1 as typeof import('./UserChannelMessage.js');
|
||||
let t2;
|
||||
if ($[41] !== addMargin || $[42] !== param) {
|
||||
t2 = <UserChannelMessage addMargin={addMargin} param={param} />;
|
||||
$[41] = addMargin;
|
||||
$[42] = param;
|
||||
$[43] = t2;
|
||||
} else {
|
||||
t2 = $[43];
|
||||
}
|
||||
return t2;
|
||||
|
||||
// Inbound channel message (MCP server push).
|
||||
if (feature('KAIROS') || feature('KAIROS_CHANNELS')) {
|
||||
if (param.text.includes('<channel source="')) {
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
const { UserChannelMessage } =
|
||||
require('./UserChannelMessage.js') as typeof import('./UserChannelMessage.js')
|
||||
/* eslint-enable @typescript-eslint/no-require-imports */
|
||||
return <UserChannelMessage addMargin={addMargin} param={param} />
|
||||
}
|
||||
}
|
||||
let t1;
|
||||
if ($[44] !== addMargin || $[45] !== isTranscriptMode || $[46] !== param || $[47] !== timestamp) {
|
||||
t1 = <UserPromptMessage addMargin={addMargin} param={param} isTranscriptMode={isTranscriptMode} timestamp={timestamp} />;
|
||||
$[44] = addMargin;
|
||||
$[45] = isTranscriptMode;
|
||||
$[46] = param;
|
||||
$[47] = timestamp;
|
||||
$[48] = t1;
|
||||
} else {
|
||||
t1 = $[48];
|
||||
}
|
||||
return t1;
|
||||
|
||||
// User prompts>
|
||||
return (
|
||||
<UserPromptMessage
|
||||
addMargin={addMargin}
|
||||
param={param}
|
||||
isTranscriptMode={isTranscriptMode}
|
||||
timestamp={timestamp}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,30 +1,27 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { Markdown } from 'src/components/Markdown.js';
|
||||
import { MessageResponse } from 'src/components/MessageResponse.js';
|
||||
import { Box, Text } from '../../../ink.js';
|
||||
import * as React from 'react'
|
||||
import { Markdown } from 'src/components/Markdown.js'
|
||||
import { MessageResponse } from 'src/components/MessageResponse.js'
|
||||
import { Box, Text } from '../../../ink.js'
|
||||
|
||||
type Props = {
|
||||
plan: string;
|
||||
};
|
||||
export function RejectedPlanMessage(t0) {
|
||||
const $ = _c(3);
|
||||
const {
|
||||
plan
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <Text color="subtle">User rejected Claude's plan:</Text>;
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
let t2;
|
||||
if ($[1] !== plan) {
|
||||
t2 = <MessageResponse><Box flexDirection="column">{t1}<Box borderStyle="round" borderColor="planMode" paddingX={1} overflow="hidden"><Markdown>{plan}</Markdown></Box></Box></MessageResponse>;
|
||||
$[1] = plan;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
return t2;
|
||||
plan: string
|
||||
}
|
||||
|
||||
export function RejectedPlanMessage({ plan }: Props): React.ReactNode {
|
||||
return (
|
||||
<MessageResponse>
|
||||
<Box flexDirection="column">
|
||||
<Text color="subtle">User rejected Claude's plan:</Text>
|
||||
<Box
|
||||
borderStyle="round"
|
||||
borderColor="planMode"
|
||||
paddingX={1}
|
||||
// Necessary for Windows Terminal to render properly
|
||||
overflow="hidden"
|
||||
>
|
||||
<Markdown>{plan}</Markdown>
|
||||
</Box>
|
||||
</Box>
|
||||
</MessageResponse>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { Text } from '../../../ink.js';
|
||||
import { MessageResponse } from '../../MessageResponse.js';
|
||||
export function RejectedToolUseMessage() {
|
||||
const $ = _c(1);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = <MessageResponse height={1}><Text dimColor={true}>Tool use rejected</Text></MessageResponse>;
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
import * as React from 'react'
|
||||
import { Text } from '../../../ink.js'
|
||||
import { MessageResponse } from '../../MessageResponse.js'
|
||||
|
||||
export function RejectedToolUseMessage(): React.ReactNode {
|
||||
return (
|
||||
<MessageResponse height={1}>
|
||||
<Text dimColor>Tool use rejected</Text>
|
||||
</MessageResponse>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { InterruptedByUser } from 'src/components/InterruptedByUser.js';
|
||||
import { MessageResponse } from 'src/components/MessageResponse.js';
|
||||
export function UserToolCanceledMessage() {
|
||||
const $ = _c(1);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = <MessageResponse height={1}><InterruptedByUser /></MessageResponse>;
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
import * as React from 'react'
|
||||
import { InterruptedByUser } from 'src/components/InterruptedByUser.js'
|
||||
import { MessageResponse } from 'src/components/MessageResponse.js'
|
||||
|
||||
export function UserToolCanceledMessage(): React.ReactNode {
|
||||
return (
|
||||
<MessageResponse height={1}>
|
||||
<InterruptedByUser />
|
||||
</MessageResponse>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,102 +1,95 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { feature } from 'bun:bundle';
|
||||
import type { ToolResultBlockParam } from '@anthropic-ai/sdk/resources/index.mjs';
|
||||
import * as React from 'react';
|
||||
import { BULLET_OPERATOR } from '../../../constants/figures.js';
|
||||
import { Text } from '../../../ink.js';
|
||||
import { filterToolProgressMessages, type Tool, type Tools } from '../../../Tool.js';
|
||||
import type { ProgressMessage } from '../../../types/message.js';
|
||||
import { INTERRUPT_MESSAGE_FOR_TOOL_USE, isClassifierDenial, PLAN_REJECTION_PREFIX, REJECT_MESSAGE_WITH_REASON_PREFIX } from '../../../utils/messages.js';
|
||||
import { FallbackToolUseErrorMessage } from '../../FallbackToolUseErrorMessage.js';
|
||||
import { InterruptedByUser } from '../../InterruptedByUser.js';
|
||||
import { MessageResponse } from '../../MessageResponse.js';
|
||||
import { RejectedPlanMessage } from './RejectedPlanMessage.js';
|
||||
import { RejectedToolUseMessage } from './RejectedToolUseMessage.js';
|
||||
import { feature } from 'bun:bundle'
|
||||
import type { ToolResultBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
|
||||
import * as React from 'react'
|
||||
import { BULLET_OPERATOR } from '../../../constants/figures.js'
|
||||
import { Text } from '../../../ink.js'
|
||||
import {
|
||||
filterToolProgressMessages,
|
||||
type Tool,
|
||||
type Tools,
|
||||
} from '../../../Tool.js'
|
||||
import type { ProgressMessage } from '../../../types/message.js'
|
||||
import {
|
||||
INTERRUPT_MESSAGE_FOR_TOOL_USE,
|
||||
isClassifierDenial,
|
||||
PLAN_REJECTION_PREFIX,
|
||||
REJECT_MESSAGE_WITH_REASON_PREFIX,
|
||||
} from '../../../utils/messages.js'
|
||||
import { FallbackToolUseErrorMessage } from '../../FallbackToolUseErrorMessage.js'
|
||||
import { InterruptedByUser } from '../../InterruptedByUser.js'
|
||||
import { MessageResponse } from '../../MessageResponse.js'
|
||||
import { RejectedPlanMessage } from './RejectedPlanMessage.js'
|
||||
import { RejectedToolUseMessage } from './RejectedToolUseMessage.js'
|
||||
|
||||
type Props = {
|
||||
progressMessagesForMessage: ProgressMessage[];
|
||||
tool?: Tool; // undefined when resuming an old conversation that uses an old tool
|
||||
tools: Tools;
|
||||
param: ToolResultBlockParam;
|
||||
verbose: boolean;
|
||||
isTranscriptMode?: boolean;
|
||||
};
|
||||
export function UserToolErrorMessage(t0) {
|
||||
const $ = _c(14);
|
||||
const {
|
||||
progressMessagesForMessage: ProgressMessage[]
|
||||
tool?: Tool // undefined when resuming an old conversation that uses an old tool
|
||||
tools: Tools
|
||||
param: ToolResultBlockParam
|
||||
verbose: boolean
|
||||
isTranscriptMode?: boolean
|
||||
}
|
||||
|
||||
export function UserToolErrorMessage({
|
||||
progressMessagesForMessage,
|
||||
tool,
|
||||
tools,
|
||||
param,
|
||||
verbose,
|
||||
isTranscriptMode
|
||||
} = t0;
|
||||
if (typeof param.content === "string" && param.content.includes(INTERRUPT_MESSAGE_FOR_TOOL_USE)) {
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <MessageResponse height={1}><InterruptedByUser /></MessageResponse>;
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
isTranscriptMode,
|
||||
}: Props): React.ReactNode {
|
||||
if (
|
||||
typeof param.content === 'string' &&
|
||||
param.content.includes(INTERRUPT_MESSAGE_FOR_TOOL_USE)
|
||||
) {
|
||||
return (
|
||||
<MessageResponse height={1}>
|
||||
<InterruptedByUser />
|
||||
</MessageResponse>
|
||||
)
|
||||
}
|
||||
return t1;
|
||||
|
||||
if (
|
||||
typeof param.content === 'string' &&
|
||||
param.content.startsWith(PLAN_REJECTION_PREFIX)
|
||||
) {
|
||||
// Extract the plan content from the error message
|
||||
const planContent = param.content.substring(PLAN_REJECTION_PREFIX.length)
|
||||
return <RejectedPlanMessage plan={planContent} />
|
||||
}
|
||||
if (typeof param.content === "string" && param.content.startsWith(PLAN_REJECTION_PREFIX)) {
|
||||
let t1;
|
||||
if ($[1] !== param.content) {
|
||||
t1 = param.content.substring(PLAN_REJECTION_PREFIX.length);
|
||||
$[1] = param.content;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
|
||||
if (
|
||||
typeof param.content === 'string' &&
|
||||
param.content.startsWith(REJECT_MESSAGE_WITH_REASON_PREFIX)
|
||||
) {
|
||||
return <RejectedToolUseMessage />
|
||||
}
|
||||
const planContent = t1;
|
||||
let t2;
|
||||
if ($[3] !== planContent) {
|
||||
t2 = <RejectedPlanMessage plan={planContent} />;
|
||||
$[3] = planContent;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
|
||||
if (
|
||||
feature('TRANSCRIPT_CLASSIFIER') &&
|
||||
typeof param.content === 'string' &&
|
||||
isClassifierDenial(param.content)
|
||||
) {
|
||||
return (
|
||||
<MessageResponse height={1}>
|
||||
<Text dimColor>
|
||||
Denied by auto mode classifier {BULLET_OPERATOR} /feedback if
|
||||
incorrect
|
||||
</Text>
|
||||
</MessageResponse>
|
||||
)
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
if (typeof param.content === "string" && param.content.startsWith(REJECT_MESSAGE_WITH_REASON_PREFIX)) {
|
||||
let t1;
|
||||
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <RejectedToolUseMessage />;
|
||||
$[5] = t1;
|
||||
} else {
|
||||
t1 = $[5];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
if (feature("TRANSCRIPT_CLASSIFIER") && typeof param.content === "string" && isClassifierDenial(param.content)) {
|
||||
let t1;
|
||||
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <MessageResponse height={1}><Text dimColor={true}>Denied by auto mode classifier {BULLET_OPERATOR} /feedback if incorrect</Text></MessageResponse>;
|
||||
$[6] = t1;
|
||||
} else {
|
||||
t1 = $[6];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
let t1;
|
||||
if ($[7] !== isTranscriptMode || $[8] !== param.content || $[9] !== progressMessagesForMessage || $[10] !== tool || $[11] !== tools || $[12] !== verbose) {
|
||||
t1 = tool?.renderToolUseErrorMessage?.(param.content, {
|
||||
progressMessagesForMessage: filterToolProgressMessages(progressMessagesForMessage),
|
||||
|
||||
return (
|
||||
tool?.renderToolUseErrorMessage?.(param.content, {
|
||||
progressMessagesForMessage: filterToolProgressMessages(
|
||||
progressMessagesForMessage,
|
||||
),
|
||||
tools,
|
||||
verbose,
|
||||
isTranscriptMode
|
||||
}) ?? <FallbackToolUseErrorMessage result={param.content} verbose={verbose} />;
|
||||
$[7] = isTranscriptMode;
|
||||
$[8] = param.content;
|
||||
$[9] = progressMessagesForMessage;
|
||||
$[10] = tool;
|
||||
$[11] = tools;
|
||||
$[12] = verbose;
|
||||
$[13] = t1;
|
||||
} else {
|
||||
t1 = $[13];
|
||||
}
|
||||
return t1;
|
||||
isTranscriptMode,
|
||||
}) ?? (
|
||||
<FallbackToolUseErrorMessage result={param.content} verbose={verbose} />
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,94 +1,59 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { useTerminalSize } from '../../../hooks/useTerminalSize.js';
|
||||
import { useTheme } from '../../../ink.js';
|
||||
import { filterToolProgressMessages, type Tool, type Tools } from '../../../Tool.js';
|
||||
import type { ProgressMessage } from '../../../types/message.js';
|
||||
import type { buildMessageLookups } from '../../../utils/messages.js';
|
||||
import { FallbackToolUseRejectedMessage } from '../../FallbackToolUseRejectedMessage.js';
|
||||
import * as React from 'react'
|
||||
import { useTerminalSize } from '../../../hooks/useTerminalSize.js'
|
||||
import { useTheme } from '../../../ink.js'
|
||||
import {
|
||||
filterToolProgressMessages,
|
||||
type Tool,
|
||||
type Tools,
|
||||
} from '../../../Tool.js'
|
||||
import type { ProgressMessage } from '../../../types/message.js'
|
||||
import type { buildMessageLookups } from '../../../utils/messages.js'
|
||||
import { FallbackToolUseRejectedMessage } from '../../FallbackToolUseRejectedMessage.js'
|
||||
|
||||
type Props = {
|
||||
input: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
progressMessagesForMessage: ProgressMessage[];
|
||||
style?: 'condensed';
|
||||
tool?: Tool;
|
||||
tools: Tools;
|
||||
lookups: ReturnType<typeof buildMessageLookups>;
|
||||
verbose: boolean;
|
||||
isTranscriptMode?: boolean;
|
||||
};
|
||||
export function UserToolRejectMessage(t0) {
|
||||
const $ = _c(13);
|
||||
const {
|
||||
input: { [key: string]: unknown }
|
||||
progressMessagesForMessage: ProgressMessage[]
|
||||
style?: 'condensed'
|
||||
tool?: Tool
|
||||
tools: Tools
|
||||
lookups: ReturnType<typeof buildMessageLookups>
|
||||
verbose: boolean
|
||||
isTranscriptMode?: boolean
|
||||
}
|
||||
|
||||
export function UserToolRejectMessage({
|
||||
input,
|
||||
progressMessagesForMessage,
|
||||
style,
|
||||
tool,
|
||||
tools,
|
||||
verbose,
|
||||
isTranscriptMode
|
||||
} = t0;
|
||||
const {
|
||||
columns
|
||||
} = useTerminalSize();
|
||||
const [theme] = useTheme();
|
||||
isTranscriptMode,
|
||||
}: Props): React.ReactNode {
|
||||
const { columns } = useTerminalSize()
|
||||
const [theme] = useTheme()
|
||||
|
||||
if (!tool || !tool.renderToolUseRejectedMessage) {
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <FallbackToolUseRejectedMessage />;
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
return <FallbackToolUseRejectedMessage />
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
const t1 = tool.inputSchema;
|
||||
let t2;
|
||||
let t3;
|
||||
if ($[1] !== columns || $[2] !== input || $[3] !== isTranscriptMode || $[4] !== progressMessagesForMessage || $[5] !== style || $[6] !== theme || $[7] !== tool || $[8] !== tools || $[9] !== verbose) {
|
||||
t3 = Symbol.for("react.early_return_sentinel");
|
||||
bb0: {
|
||||
const parsedInput = t1.safeParse(input);
|
||||
|
||||
const parsedInput = tool.inputSchema.safeParse(input)
|
||||
if (!parsedInput.success) {
|
||||
let t4;
|
||||
if ($[12] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = <FallbackToolUseRejectedMessage />;
|
||||
$[12] = t4;
|
||||
} else {
|
||||
t4 = $[12];
|
||||
return <FallbackToolUseRejectedMessage />
|
||||
}
|
||||
t3 = t4;
|
||||
break bb0;
|
||||
}
|
||||
t2 = tool.renderToolUseRejectedMessage(parsedInput.data, {
|
||||
|
||||
return (
|
||||
tool.renderToolUseRejectedMessage(parsedInput.data, {
|
||||
columns,
|
||||
messages: [],
|
||||
tools,
|
||||
verbose,
|
||||
progressMessagesForMessage: filterToolProgressMessages(progressMessagesForMessage),
|
||||
progressMessagesForMessage: filterToolProgressMessages(
|
||||
progressMessagesForMessage,
|
||||
),
|
||||
style,
|
||||
theme,
|
||||
isTranscriptMode
|
||||
}) ?? <FallbackToolUseRejectedMessage />;
|
||||
}
|
||||
$[1] = columns;
|
||||
$[2] = input;
|
||||
$[3] = isTranscriptMode;
|
||||
$[4] = progressMessagesForMessage;
|
||||
$[5] = style;
|
||||
$[6] = theme;
|
||||
$[7] = tool;
|
||||
$[8] = tools;
|
||||
$[9] = verbose;
|
||||
$[10] = t2;
|
||||
$[11] = t3;
|
||||
} else {
|
||||
t2 = $[10];
|
||||
t3 = $[11];
|
||||
}
|
||||
if (t3 !== Symbol.for("react.early_return_sentinel")) {
|
||||
return t3;
|
||||
}
|
||||
return t2;
|
||||
isTranscriptMode,
|
||||
}) ?? <FallbackToolUseRejectedMessage />
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,28 +1,35 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import type { ToolResultBlockParam } from '@anthropic-ai/sdk/resources/index.mjs';
|
||||
import * as React from 'react';
|
||||
import type { Tools } from '../../../Tool.js';
|
||||
import type { NormalizedUserMessage, ProgressMessage } from '../../../types/message.js';
|
||||
import { type buildMessageLookups, CANCEL_MESSAGE, INTERRUPT_MESSAGE_FOR_TOOL_USE, REJECT_MESSAGE } from '../../../utils/messages.js';
|
||||
import { UserToolCanceledMessage } from './UserToolCanceledMessage.js';
|
||||
import { UserToolErrorMessage } from './UserToolErrorMessage.js';
|
||||
import { UserToolRejectMessage } from './UserToolRejectMessage.js';
|
||||
import { UserToolSuccessMessage } from './UserToolSuccessMessage.js';
|
||||
import { useGetToolFromMessages } from './utils.js';
|
||||
import type { ToolResultBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
|
||||
import * as React from 'react'
|
||||
import type { Tools } from '../../../Tool.js'
|
||||
import type {
|
||||
NormalizedUserMessage,
|
||||
ProgressMessage,
|
||||
} from '../../../types/message.js'
|
||||
import {
|
||||
type buildMessageLookups,
|
||||
CANCEL_MESSAGE,
|
||||
INTERRUPT_MESSAGE_FOR_TOOL_USE,
|
||||
REJECT_MESSAGE,
|
||||
} from '../../../utils/messages.js'
|
||||
import { UserToolCanceledMessage } from './UserToolCanceledMessage.js'
|
||||
import { UserToolErrorMessage } from './UserToolErrorMessage.js'
|
||||
import { UserToolRejectMessage } from './UserToolRejectMessage.js'
|
||||
import { UserToolSuccessMessage } from './UserToolSuccessMessage.js'
|
||||
import { useGetToolFromMessages } from './utils.js'
|
||||
|
||||
type Props = {
|
||||
param: ToolResultBlockParam;
|
||||
message: NormalizedUserMessage;
|
||||
lookups: ReturnType<typeof buildMessageLookups>;
|
||||
progressMessagesForMessage: ProgressMessage[];
|
||||
style?: 'condensed';
|
||||
tools: Tools;
|
||||
verbose: boolean;
|
||||
width: number | string;
|
||||
isTranscriptMode?: boolean;
|
||||
};
|
||||
export function UserToolResultMessage(t0) {
|
||||
const $ = _c(28);
|
||||
const {
|
||||
param: ToolResultBlockParam
|
||||
message: NormalizedUserMessage
|
||||
lookups: ReturnType<typeof buildMessageLookups>
|
||||
progressMessagesForMessage: ProgressMessage[]
|
||||
style?: 'condensed'
|
||||
tools: Tools
|
||||
verbose: boolean
|
||||
width: number | string
|
||||
isTranscriptMode?: boolean
|
||||
}
|
||||
|
||||
export function UserToolResultMessage({
|
||||
param,
|
||||
message,
|
||||
lookups,
|
||||
@@ -31,75 +38,64 @@ export function UserToolResultMessage(t0) {
|
||||
tools,
|
||||
verbose,
|
||||
width,
|
||||
isTranscriptMode
|
||||
} = t0;
|
||||
const toolUse = useGetToolFromMessages(param.tool_use_id, tools, lookups);
|
||||
isTranscriptMode,
|
||||
}: Props): React.ReactNode {
|
||||
const toolUse = useGetToolFromMessages(param.tool_use_id, tools, lookups)
|
||||
if (!toolUse) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
if (typeof param.content === "string" && param.content.startsWith(CANCEL_MESSAGE)) {
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <UserToolCanceledMessage />;
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
|
||||
if (
|
||||
typeof param.content === 'string' &&
|
||||
param.content.startsWith(CANCEL_MESSAGE)
|
||||
) {
|
||||
return <UserToolCanceledMessage />
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
if (typeof param.content === "string" && param.content.startsWith(REJECT_MESSAGE) || param.content === INTERRUPT_MESSAGE_FOR_TOOL_USE) {
|
||||
const t1 = toolUse.toolUse.input as {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
let t2;
|
||||
if ($[1] !== isTranscriptMode || $[2] !== lookups || $[3] !== progressMessagesForMessage || $[4] !== style || $[5] !== t1 || $[6] !== toolUse.tool || $[7] !== tools || $[8] !== verbose) {
|
||||
t2 = <UserToolRejectMessage input={t1} progressMessagesForMessage={progressMessagesForMessage} tool={toolUse.tool} tools={tools} lookups={lookups} style={style} verbose={verbose} isTranscriptMode={isTranscriptMode} />;
|
||||
$[1] = isTranscriptMode;
|
||||
$[2] = lookups;
|
||||
$[3] = progressMessagesForMessage;
|
||||
$[4] = style;
|
||||
$[5] = t1;
|
||||
$[6] = toolUse.tool;
|
||||
$[7] = tools;
|
||||
$[8] = verbose;
|
||||
$[9] = t2;
|
||||
} else {
|
||||
t2 = $[9];
|
||||
}
|
||||
return t2;
|
||||
|
||||
if (
|
||||
(typeof param.content === 'string' &&
|
||||
param.content.startsWith(REJECT_MESSAGE)) ||
|
||||
param.content === INTERRUPT_MESSAGE_FOR_TOOL_USE
|
||||
) {
|
||||
return (
|
||||
<UserToolRejectMessage
|
||||
input={toolUse.toolUse.input as { [key: string]: unknown }}
|
||||
progressMessagesForMessage={progressMessagesForMessage}
|
||||
tool={toolUse.tool}
|
||||
tools={tools}
|
||||
lookups={lookups}
|
||||
style={style}
|
||||
verbose={verbose}
|
||||
isTranscriptMode={isTranscriptMode}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (param.is_error) {
|
||||
let t1;
|
||||
if ($[10] !== isTranscriptMode || $[11] !== param || $[12] !== progressMessagesForMessage || $[13] !== toolUse.tool || $[14] !== tools || $[15] !== verbose) {
|
||||
t1 = <UserToolErrorMessage progressMessagesForMessage={progressMessagesForMessage} tool={toolUse.tool} tools={tools} param={param} verbose={verbose} isTranscriptMode={isTranscriptMode} />;
|
||||
$[10] = isTranscriptMode;
|
||||
$[11] = param;
|
||||
$[12] = progressMessagesForMessage;
|
||||
$[13] = toolUse.tool;
|
||||
$[14] = tools;
|
||||
$[15] = verbose;
|
||||
$[16] = t1;
|
||||
} else {
|
||||
t1 = $[16];
|
||||
return (
|
||||
<UserToolErrorMessage
|
||||
progressMessagesForMessage={progressMessagesForMessage}
|
||||
tool={toolUse.tool}
|
||||
tools={tools}
|
||||
param={param}
|
||||
verbose={verbose}
|
||||
isTranscriptMode={isTranscriptMode}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
let t1;
|
||||
if ($[17] !== isTranscriptMode || $[18] !== lookups || $[19] !== message || $[20] !== progressMessagesForMessage || $[21] !== style || $[22] !== toolUse.tool || $[23] !== toolUse.toolUse.id || $[24] !== tools || $[25] !== verbose || $[26] !== width) {
|
||||
t1 = <UserToolSuccessMessage message={message} lookups={lookups} toolUseID={toolUse.toolUse.id} progressMessagesForMessage={progressMessagesForMessage} style={style} tool={toolUse.tool} tools={tools} verbose={verbose} width={width} isTranscriptMode={isTranscriptMode} />;
|
||||
$[17] = isTranscriptMode;
|
||||
$[18] = lookups;
|
||||
$[19] = message;
|
||||
$[20] = progressMessagesForMessage;
|
||||
$[21] = style;
|
||||
$[22] = toolUse.tool;
|
||||
$[23] = toolUse.toolUse.id;
|
||||
$[24] = tools;
|
||||
$[25] = verbose;
|
||||
$[26] = width;
|
||||
$[27] = t1;
|
||||
} else {
|
||||
t1 = $[27];
|
||||
}
|
||||
return t1;
|
||||
|
||||
return (
|
||||
<UserToolSuccessMessage
|
||||
message={message}
|
||||
lookups={lookups}
|
||||
toolUseID={toolUse.toolUse.id}
|
||||
progressMessagesForMessage={progressMessagesForMessage}
|
||||
style={style}
|
||||
tool={toolUse.tool}
|
||||
tools={tools}
|
||||
verbose={verbose}
|
||||
width={width}
|
||||
isTranscriptMode={isTranscriptMode}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,27 +1,40 @@
|
||||
import { feature } from 'bun:bundle';
|
||||
import figures from 'figures';
|
||||
import * as React from 'react';
|
||||
import { SentryErrorBoundary } from 'src/components/SentryErrorBoundary.js';
|
||||
import { Box, Text, useTheme } from '../../../ink.js';
|
||||
import { useAppState } from '../../../state/AppState.js';
|
||||
import { filterToolProgressMessages, type Tool, type Tools } from '../../../Tool.js';
|
||||
import type { NormalizedUserMessage, ProgressMessage } from '../../../types/message.js';
|
||||
import { deleteClassifierApproval, getClassifierApproval, getYoloClassifierApproval } from '../../../utils/classifierApprovals.js';
|
||||
import type { buildMessageLookups } from '../../../utils/messages.js';
|
||||
import { MessageResponse } from '../../MessageResponse.js';
|
||||
import { HookProgressMessage } from '../HookProgressMessage.js';
|
||||
import { feature } from 'bun:bundle'
|
||||
import figures from 'figures'
|
||||
import * as React from 'react'
|
||||
import { SentryErrorBoundary } from 'src/components/SentryErrorBoundary.js'
|
||||
import { Box, Text, useTheme } from '../../../ink.js'
|
||||
import { useAppState } from '../../../state/AppState.js'
|
||||
import {
|
||||
filterToolProgressMessages,
|
||||
type Tool,
|
||||
type Tools,
|
||||
} from '../../../Tool.js'
|
||||
import type {
|
||||
NormalizedUserMessage,
|
||||
ProgressMessage,
|
||||
} from '../../../types/message.js'
|
||||
import {
|
||||
deleteClassifierApproval,
|
||||
getClassifierApproval,
|
||||
getYoloClassifierApproval,
|
||||
} from '../../../utils/classifierApprovals.js'
|
||||
import type { buildMessageLookups } from '../../../utils/messages.js'
|
||||
import { MessageResponse } from '../../MessageResponse.js'
|
||||
import { HookProgressMessage } from '../HookProgressMessage.js'
|
||||
|
||||
type Props = {
|
||||
message: NormalizedUserMessage;
|
||||
lookups: ReturnType<typeof buildMessageLookups>;
|
||||
toolUseID: string;
|
||||
progressMessagesForMessage: ProgressMessage[];
|
||||
style?: 'condensed';
|
||||
tool?: Tool;
|
||||
tools: Tools;
|
||||
verbose: boolean;
|
||||
width: number | string;
|
||||
isTranscriptMode?: boolean;
|
||||
};
|
||||
message: NormalizedUserMessage
|
||||
lookups: ReturnType<typeof buildMessageLookups>
|
||||
toolUseID: string
|
||||
progressMessagesForMessage: ProgressMessage[]
|
||||
style?: 'condensed'
|
||||
tool?: Tool
|
||||
tools: Tools
|
||||
verbose: boolean
|
||||
width: number | string
|
||||
isTranscriptMode?: boolean
|
||||
}
|
||||
|
||||
export function UserToolSuccessMessage({
|
||||
message,
|
||||
lookups,
|
||||
@@ -32,72 +45,105 @@ export function UserToolSuccessMessage({
|
||||
tools,
|
||||
verbose,
|
||||
width,
|
||||
isTranscriptMode
|
||||
isTranscriptMode,
|
||||
}: Props): React.ReactNode {
|
||||
const [theme] = useTheme();
|
||||
const [theme] = useTheme()
|
||||
// Hook stays inside feature() ternary so external builds don't pay a
|
||||
// per-scrollback-message store subscription — same pattern as
|
||||
// UserPromptMessage.tsx.
|
||||
const isBriefOnly = feature('KAIROS') || feature('KAIROS_BRIEF') ?
|
||||
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||
useAppState(s => s.isBriefOnly) : false;
|
||||
const isBriefOnly =
|
||||
feature('KAIROS') || feature('KAIROS_BRIEF')
|
||||
? // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||
useAppState(s => s.isBriefOnly)
|
||||
: false
|
||||
|
||||
// Capture classifier approval once on mount, then delete from Map to prevent linear growth.
|
||||
// useState lazy initializer ensures the value persists across re-renders.
|
||||
const [classifierRule] = React.useState(() => getClassifierApproval(toolUseID));
|
||||
const [yoloReason] = React.useState(() => getYoloClassifierApproval(toolUseID));
|
||||
const [classifierRule] = React.useState(() =>
|
||||
getClassifierApproval(toolUseID),
|
||||
)
|
||||
const [yoloReason] = React.useState(() =>
|
||||
getYoloClassifierApproval(toolUseID),
|
||||
)
|
||||
React.useEffect(() => {
|
||||
deleteClassifierApproval(toolUseID);
|
||||
}, [toolUseID]);
|
||||
deleteClassifierApproval(toolUseID)
|
||||
}, [toolUseID])
|
||||
|
||||
if (!message.toolUseResult || !tool) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
// Resumed transcripts deserialize toolUseResult via raw JSON.parse with no
|
||||
// validation (parseJSONL). A partial/corrupt/old-format result crashes
|
||||
// renderToolResultMessage on first field access (anthropics/claude-code#39817).
|
||||
// Validate against outputSchema before rendering — mirrors CollapsedReadSearchContent.
|
||||
const parsedOutput = tool.outputSchema?.safeParse(message.toolUseResult);
|
||||
const parsedOutput = tool.outputSchema?.safeParse(message.toolUseResult)
|
||||
if (parsedOutput && !parsedOutput.success) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
const toolResult = parsedOutput?.data ?? message.toolUseResult;
|
||||
const renderedMessage = tool.renderToolResultMessage?.(toolResult as never, filterToolProgressMessages(progressMessagesForMessage), {
|
||||
const toolResult = parsedOutput?.data ?? message.toolUseResult
|
||||
|
||||
const renderedMessage =
|
||||
tool.renderToolResultMessage?.(
|
||||
toolResult as never,
|
||||
filterToolProgressMessages(progressMessagesForMessage),
|
||||
{
|
||||
style,
|
||||
theme,
|
||||
tools,
|
||||
verbose,
|
||||
isTranscriptMode,
|
||||
isBriefOnly,
|
||||
input: lookups.toolUseByToolUseID.get(toolUseID)?.input
|
||||
}) ?? null;
|
||||
input: lookups.toolUseByToolUseID.get(toolUseID)?.input,
|
||||
},
|
||||
) ?? null
|
||||
|
||||
// Don't render anything if the tool result message is null
|
||||
if (renderedMessage === null) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
// Tools that return '' from userFacingName opt out of tool chrome and
|
||||
// render like plain assistant text. Skip the tool-result width constraint
|
||||
// so MarkdownTable's SAFETY_MARGIN=4 (tuned for the assistant-text 2-col
|
||||
// dot gutter) holds — otherwise tables wrap their box-drawing chars.
|
||||
const rendersAsAssistantText = tool.userFacingName(undefined) === '';
|
||||
return <Box flexDirection="column">
|
||||
<Box flexDirection="column" width={rendersAsAssistantText ? undefined : width}>
|
||||
const rendersAsAssistantText = tool.userFacingName(undefined) === ''
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Box
|
||||
flexDirection="column"
|
||||
width={rendersAsAssistantText ? undefined : width}
|
||||
>
|
||||
{renderedMessage}
|
||||
{feature('BASH_CLASSIFIER') ? classifierRule && <MessageResponse height={1}>
|
||||
{feature('BASH_CLASSIFIER')
|
||||
? classifierRule && (
|
||||
<MessageResponse height={1}>
|
||||
<Text dimColor>
|
||||
<Text color="success">{figures.tick}</Text>
|
||||
{' Auto-approved \u00b7 matched '}
|
||||
{`"${classifierRule}"`}
|
||||
</Text>
|
||||
</MessageResponse> : null}
|
||||
{feature('TRANSCRIPT_CLASSIFIER') ? yoloReason && <MessageResponse height={1}>
|
||||
</MessageResponse>
|
||||
)
|
||||
: null}
|
||||
{feature('TRANSCRIPT_CLASSIFIER')
|
||||
? yoloReason && (
|
||||
<MessageResponse height={1}>
|
||||
<Text dimColor>Allowed by auto mode classifier</Text>
|
||||
</MessageResponse> : null}
|
||||
</MessageResponse>
|
||||
)
|
||||
: null}
|
||||
</Box>
|
||||
<SentryErrorBoundary>
|
||||
<HookProgressMessage hookEvent="PostToolUse" lookups={lookups} toolUseID={toolUseID} verbose={verbose} isTranscriptMode={isTranscriptMode} />
|
||||
<HookProgressMessage
|
||||
hookEvent="PostToolUse"
|
||||
lookups={lookups}
|
||||
toolUseID={toolUseID}
|
||||
verbose={verbose}
|
||||
isTranscriptMode={isTranscriptMode}
|
||||
/>
|
||||
</SentryErrorBoundary>
|
||||
</Box>;
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,43 +1,22 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import type { ToolUseBlockParam } from '@anthropic-ai/sdk/resources/index.mjs';
|
||||
import { useMemo } from 'react';
|
||||
import { findToolByName, type Tool, type Tools } from '../../../Tool.js';
|
||||
import type { buildMessageLookups } from '../../../utils/messages.js';
|
||||
export function useGetToolFromMessages(toolUseID, tools, lookups) {
|
||||
const $ = _c(7);
|
||||
let t0;
|
||||
if ($[0] !== lookups.toolUseByToolUseID || $[1] !== toolUseID || $[2] !== tools) {
|
||||
bb0: {
|
||||
const toolUse = lookups.toolUseByToolUseID.get(toolUseID);
|
||||
import type { ToolUseBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
|
||||
import { useMemo } from 'react'
|
||||
import { findToolByName, type Tool, type Tools } from '../../../Tool.js'
|
||||
import type { buildMessageLookups } from '../../../utils/messages.js'
|
||||
|
||||
export function useGetToolFromMessages(
|
||||
toolUseID: string,
|
||||
tools: Tools,
|
||||
lookups: ReturnType<typeof buildMessageLookups>,
|
||||
): { tool: Tool; toolUse: ToolUseBlockParam } | null {
|
||||
return useMemo(() => {
|
||||
const toolUse = lookups.toolUseByToolUseID.get(toolUseID)
|
||||
if (!toolUse) {
|
||||
t0 = null;
|
||||
break bb0;
|
||||
return null
|
||||
}
|
||||
const tool = findToolByName(tools, toolUse.name);
|
||||
const tool = findToolByName(tools, toolUse.name)
|
||||
if (!tool) {
|
||||
t0 = null;
|
||||
break bb0;
|
||||
return null
|
||||
}
|
||||
let t1;
|
||||
if ($[4] !== tool || $[5] !== toolUse) {
|
||||
t1 = {
|
||||
tool,
|
||||
toolUse
|
||||
};
|
||||
$[4] = tool;
|
||||
$[5] = toolUse;
|
||||
$[6] = t1;
|
||||
} else {
|
||||
t1 = $[6];
|
||||
}
|
||||
t0 = t1;
|
||||
}
|
||||
$[0] = lookups.toolUseByToolUseID;
|
||||
$[1] = toolUseID;
|
||||
$[2] = tools;
|
||||
$[3] = t0;
|
||||
} else {
|
||||
t0 = $[3];
|
||||
}
|
||||
return t0;
|
||||
return { tool, toolUse }
|
||||
}, [toolUseID, lookups, tools])
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React from 'react';
|
||||
import { Text } from '../../ink.js';
|
||||
import type { CollapsedReadSearchGroup } from '../../types/message.js';
|
||||
import React from 'react'
|
||||
import { Text } from '../../ink.js'
|
||||
import type { CollapsedReadSearchGroup } from '../../types/message.js'
|
||||
|
||||
/**
|
||||
* Plain function (not a React component) so the React Compiler won't
|
||||
@@ -9,7 +8,11 @@ import type { CollapsedReadSearchGroup } from '../../types/message.js';
|
||||
* is only loaded when feature('TEAMMEM') is true.
|
||||
*/
|
||||
export function checkHasTeamMemOps(message: CollapsedReadSearchGroup): boolean {
|
||||
return (message.teamMemorySearchCount ?? 0) > 0 || (message.teamMemoryReadCount ?? 0) > 0 || (message.teamMemoryWriteCount ?? 0) > 0;
|
||||
return (
|
||||
(message.teamMemorySearchCount ?? 0) > 0 ||
|
||||
(message.teamMemoryReadCount ?? 0) > 0 ||
|
||||
(message.teamMemoryWriteCount ?? 0) > 0
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -17,123 +20,79 @@ export function checkHasTeamMemOps(message: CollapsedReadSearchGroup): boolean {
|
||||
* This module is only loaded when feature('TEAMMEM') is true,
|
||||
* so DCE removes it entirely from external builds.
|
||||
*/
|
||||
export function TeamMemCountParts(t0) {
|
||||
const $ = _c(23);
|
||||
const {
|
||||
export function TeamMemCountParts({
|
||||
message,
|
||||
isActiveGroup,
|
||||
hasPrecedingParts
|
||||
} = t0;
|
||||
const tmReadCount = message.teamMemoryReadCount ?? 0;
|
||||
const tmSearchCount = message.teamMemorySearchCount ?? 0;
|
||||
const tmWriteCount = message.teamMemoryWriteCount ?? 0;
|
||||
hasPrecedingParts,
|
||||
}: {
|
||||
message: CollapsedReadSearchGroup
|
||||
isActiveGroup: boolean | undefined
|
||||
hasPrecedingParts: boolean
|
||||
}): React.ReactNode {
|
||||
const tmReadCount = message.teamMemoryReadCount ?? 0
|
||||
const tmSearchCount = message.teamMemorySearchCount ?? 0
|
||||
const tmWriteCount = message.teamMemoryWriteCount ?? 0
|
||||
|
||||
if (tmReadCount === 0 && tmSearchCount === 0 && tmWriteCount === 0) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
let t1;
|
||||
if ($[0] !== hasPrecedingParts || $[1] !== isActiveGroup || $[2] !== tmReadCount || $[3] !== tmSearchCount || $[4] !== tmWriteCount) {
|
||||
const nodes = [];
|
||||
let count = hasPrecedingParts ? 1 : 0;
|
||||
|
||||
const nodes: React.ReactNode[] = []
|
||||
let count = hasPrecedingParts ? 1 : 0
|
||||
|
||||
if (tmReadCount > 0) {
|
||||
const verb = isActiveGroup ? count === 0 ? "Recalling" : "recalling" : count === 0 ? "Recalled" : "recalled";
|
||||
const verb = isActiveGroup
|
||||
? count === 0
|
||||
? 'Recalling'
|
||||
: 'recalling'
|
||||
: count === 0
|
||||
? 'Recalled'
|
||||
: 'recalled'
|
||||
if (count > 0) {
|
||||
let t2;
|
||||
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <Text key="comma-tmr">, </Text>;
|
||||
$[6] = t2;
|
||||
} else {
|
||||
t2 = $[6];
|
||||
nodes.push(<Text key="comma-tmr">, </Text>)
|
||||
}
|
||||
nodes.push(t2);
|
||||
}
|
||||
let t2;
|
||||
if ($[7] !== tmReadCount) {
|
||||
t2 = <Text bold={true}>{tmReadCount}</Text>;
|
||||
$[7] = tmReadCount;
|
||||
$[8] = t2;
|
||||
} else {
|
||||
t2 = $[8];
|
||||
}
|
||||
const t3 = tmReadCount === 1 ? "memory" : "memories";
|
||||
let t4;
|
||||
if ($[9] !== t2 || $[10] !== t3 || $[11] !== verb) {
|
||||
t4 = <Text key="team-mem-read">{verb} {t2} team{" "}{t3}</Text>;
|
||||
$[9] = t2;
|
||||
$[10] = t3;
|
||||
$[11] = verb;
|
||||
$[12] = t4;
|
||||
} else {
|
||||
t4 = $[12];
|
||||
}
|
||||
nodes.push(t4);
|
||||
count++;
|
||||
nodes.push(
|
||||
<Text key="team-mem-read">
|
||||
{verb} <Text bold>{tmReadCount}</Text> team{' '}
|
||||
{tmReadCount === 1 ? 'memory' : 'memories'}
|
||||
</Text>,
|
||||
)
|
||||
count++
|
||||
}
|
||||
|
||||
if (tmSearchCount > 0) {
|
||||
const verb_0 = isActiveGroup ? count === 0 ? "Searching" : "searching" : count === 0 ? "Searched" : "searched";
|
||||
const verb = isActiveGroup
|
||||
? count === 0
|
||||
? 'Searching'
|
||||
: 'searching'
|
||||
: count === 0
|
||||
? 'Searched'
|
||||
: 'searched'
|
||||
if (count > 0) {
|
||||
let t2;
|
||||
if ($[13] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <Text key="comma-tms">, </Text>;
|
||||
$[13] = t2;
|
||||
} else {
|
||||
t2 = $[13];
|
||||
nodes.push(<Text key="comma-tms">, </Text>)
|
||||
}
|
||||
nodes.push(t2);
|
||||
}
|
||||
const t2 = `${verb_0} team memories`;
|
||||
let t3;
|
||||
if ($[14] !== t2) {
|
||||
t3 = <Text key="team-mem-search">{t2}</Text>;
|
||||
$[14] = t2;
|
||||
$[15] = t3;
|
||||
} else {
|
||||
t3 = $[15];
|
||||
}
|
||||
nodes.push(t3);
|
||||
count++;
|
||||
nodes.push(<Text key="team-mem-search">{`${verb} team memories`}</Text>)
|
||||
count++
|
||||
}
|
||||
|
||||
if (tmWriteCount > 0) {
|
||||
const verb_1 = isActiveGroup ? count === 0 ? "Writing" : "writing" : count === 0 ? "Wrote" : "wrote";
|
||||
const verb = isActiveGroup
|
||||
? count === 0
|
||||
? 'Writing'
|
||||
: 'writing'
|
||||
: count === 0
|
||||
? 'Wrote'
|
||||
: 'wrote'
|
||||
if (count > 0) {
|
||||
let t2;
|
||||
if ($[16] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <Text key="comma-tmw">, </Text>;
|
||||
$[16] = t2;
|
||||
} else {
|
||||
t2 = $[16];
|
||||
nodes.push(<Text key="comma-tmw">, </Text>)
|
||||
}
|
||||
nodes.push(t2);
|
||||
nodes.push(
|
||||
<Text key="team-mem-write">
|
||||
{verb} <Text bold>{tmWriteCount}</Text> team{' '}
|
||||
{tmWriteCount === 1 ? 'memory' : 'memories'}
|
||||
</Text>,
|
||||
)
|
||||
}
|
||||
let t2;
|
||||
if ($[17] !== tmWriteCount) {
|
||||
t2 = <Text bold={true}>{tmWriteCount}</Text>;
|
||||
$[17] = tmWriteCount;
|
||||
$[18] = t2;
|
||||
} else {
|
||||
t2 = $[18];
|
||||
}
|
||||
const t3 = tmWriteCount === 1 ? "memory" : "memories";
|
||||
let t4;
|
||||
if ($[19] !== t2 || $[20] !== t3 || $[21] !== verb_1) {
|
||||
t4 = <Text key="team-mem-write">{verb_1} {t2} team{" "}{t3}</Text>;
|
||||
$[19] = t2;
|
||||
$[20] = t3;
|
||||
$[21] = verb_1;
|
||||
$[22] = t4;
|
||||
} else {
|
||||
t4 = $[22];
|
||||
}
|
||||
nodes.push(t4);
|
||||
}
|
||||
t1 = <>{nodes}</>;
|
||||
$[0] = hasPrecedingParts;
|
||||
$[1] = isActiveGroup;
|
||||
$[2] = tmReadCount;
|
||||
$[3] = tmSearchCount;
|
||||
$[4] = tmWriteCount;
|
||||
$[5] = t1;
|
||||
} else {
|
||||
t1 = $[5];
|
||||
}
|
||||
return t1;
|
||||
|
||||
return <>{nodes}</>
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,25 +1,29 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React, { Suspense, use, useMemo } from 'react';
|
||||
import { useSettings } from '../../../hooks/useSettings.js';
|
||||
import { useTerminalSize } from '../../../hooks/useTerminalSize.js';
|
||||
import { stringWidth } from '../../../ink/stringWidth.js';
|
||||
import { Ansi, Box, Text, useTheme } from '../../../ink.js';
|
||||
import { type CliHighlight, getCliHighlightPromise } from '../../../utils/cliHighlight.js';
|
||||
import { applyMarkdown } from '../../../utils/markdown.js';
|
||||
import sliceAnsi from '../../../utils/sliceAnsi.js';
|
||||
import React, { Suspense, use, useMemo } from 'react'
|
||||
import { useSettings } from '../../../hooks/useSettings.js'
|
||||
import { useTerminalSize } from '../../../hooks/useTerminalSize.js'
|
||||
import { stringWidth } from '../../../ink/stringWidth.js'
|
||||
import { Ansi, Box, Text, useTheme } from '../../../ink.js'
|
||||
import {
|
||||
type CliHighlight,
|
||||
getCliHighlightPromise,
|
||||
} from '../../../utils/cliHighlight.js'
|
||||
import { applyMarkdown } from '../../../utils/markdown.js'
|
||||
import sliceAnsi from '../../../utils/sliceAnsi.js'
|
||||
|
||||
type PreviewBoxProps = {
|
||||
/** The preview content to display. Markdown is rendered with syntax highlighting
|
||||
* for code blocks (```ts, ```py, etc.). Also supports plain multi-line text. */
|
||||
content: string;
|
||||
content: string
|
||||
/** Maximum number of lines to display before truncating. @default 20 */
|
||||
maxLines?: number;
|
||||
maxLines?: number
|
||||
/** Minimum height (in lines) for the preview box. Content will be padded if shorter. */
|
||||
minHeight?: number;
|
||||
minHeight?: number
|
||||
/** Minimum width for the preview box. @default 40 */
|
||||
minWidth?: number;
|
||||
minWidth?: number
|
||||
/** Maximum width available for this box (e.g., the container width). */
|
||||
maxWidth?: number;
|
||||
};
|
||||
maxWidth?: number
|
||||
}
|
||||
|
||||
const BOX_CHARS = {
|
||||
topLeft: '┌',
|
||||
topRight: '┐',
|
||||
@@ -28,201 +32,127 @@ const BOX_CHARS = {
|
||||
horizontal: '─',
|
||||
vertical: '│',
|
||||
teeLeft: '├',
|
||||
teeRight: '┤'
|
||||
};
|
||||
teeRight: '┤',
|
||||
}
|
||||
|
||||
/**
|
||||
* A bordered monospace box for displaying preview content.
|
||||
* Truncates content that exceeds maxLines with an indicator.
|
||||
* The parent component should pass maxLines based on its available height budget.
|
||||
*/
|
||||
export function PreviewBox(props) {
|
||||
const $ = _c(4);
|
||||
const settings = useSettings();
|
||||
export function PreviewBox(props: PreviewBoxProps): React.ReactNode {
|
||||
const settings = useSettings()
|
||||
if (settings.syntaxHighlightingDisabled) {
|
||||
let t0;
|
||||
if ($[0] !== props) {
|
||||
t0 = <PreviewBoxBody {...props} highlight={null} />;
|
||||
$[0] = props;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
return <PreviewBoxBody {...props} highlight={null} />
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
let t0;
|
||||
if ($[2] !== props) {
|
||||
t0 = <Suspense fallback={<PreviewBoxBody {...props} highlight={null} />}><PreviewBoxWithHighlight {...props} /></Suspense>;
|
||||
$[2] = props;
|
||||
$[3] = t0;
|
||||
} else {
|
||||
t0 = $[3];
|
||||
}
|
||||
return t0;
|
||||
return (
|
||||
<Suspense fallback={<PreviewBoxBody {...props} highlight={null} />}>
|
||||
<PreviewBoxWithHighlight {...props} />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
function PreviewBoxWithHighlight(props) {
|
||||
const $ = _c(4);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = getCliHighlightPromise();
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
const highlight = use(t0);
|
||||
let t1;
|
||||
if ($[1] !== highlight || $[2] !== props) {
|
||||
t1 = <PreviewBoxBody {...props} highlight={highlight} />;
|
||||
$[1] = highlight;
|
||||
$[2] = props;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
return t1;
|
||||
|
||||
function PreviewBoxWithHighlight(props: PreviewBoxProps): React.ReactNode {
|
||||
const highlight = use(getCliHighlightPromise())
|
||||
return <PreviewBoxBody {...props} highlight={highlight} />
|
||||
}
|
||||
function PreviewBoxBody(t0) {
|
||||
const $ = _c(34);
|
||||
const {
|
||||
|
||||
function PreviewBoxBody({
|
||||
content,
|
||||
maxLines,
|
||||
minHeight,
|
||||
minWidth: t1,
|
||||
minWidth = 40,
|
||||
maxWidth,
|
||||
highlight
|
||||
} = t0;
|
||||
const minWidth = t1 === undefined ? 40 : t1;
|
||||
const {
|
||||
columns: terminalWidth
|
||||
} = useTerminalSize();
|
||||
const [theme] = useTheme();
|
||||
const effectiveMaxWidth = maxWidth ?? terminalWidth - 4;
|
||||
const effectiveMaxLines = maxLines ?? 20;
|
||||
let t2;
|
||||
if ($[0] !== content || $[1] !== highlight || $[2] !== theme) {
|
||||
t2 = applyMarkdown(content, theme, highlight);
|
||||
$[0] = content;
|
||||
$[1] = highlight;
|
||||
$[2] = theme;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
const rendered = t2;
|
||||
let T0;
|
||||
let bottomBorder;
|
||||
let t3;
|
||||
let t4;
|
||||
let t5;
|
||||
let truncationBar;
|
||||
if ($[4] !== effectiveMaxLines || $[5] !== effectiveMaxWidth || $[6] !== minHeight || $[7] !== minWidth || $[8] !== rendered) {
|
||||
const contentLines = rendered.split("\n");
|
||||
const isTruncated = contentLines.length > effectiveMaxLines;
|
||||
const truncatedLines = isTruncated ? contentLines.slice(0, effectiveMaxLines) : contentLines;
|
||||
const effectiveMinHeight = Math.min(minHeight ?? 0, effectiveMaxLines);
|
||||
const paddingNeeded = Math.max(0, effectiveMinHeight - truncatedLines.length - (isTruncated ? 1 : 0));
|
||||
const lines = paddingNeeded > 0 ? [...truncatedLines, ...Array(paddingNeeded).fill("")] : truncatedLines;
|
||||
const contentWidth = Math.max(minWidth, ...lines.map(_temp));
|
||||
const boxWidth = Math.min(contentWidth + 4, effectiveMaxWidth);
|
||||
const innerWidth = boxWidth - 4;
|
||||
let t6;
|
||||
if ($[15] !== boxWidth) {
|
||||
t6 = BOX_CHARS.horizontal.repeat(boxWidth - 2);
|
||||
$[15] = boxWidth;
|
||||
$[16] = t6;
|
||||
} else {
|
||||
t6 = $[16];
|
||||
}
|
||||
const topBorder = `${BOX_CHARS.topLeft}${t6}${BOX_CHARS.topRight}`;
|
||||
let t7;
|
||||
if ($[17] !== boxWidth) {
|
||||
t7 = BOX_CHARS.horizontal.repeat(boxWidth - 2);
|
||||
$[17] = boxWidth;
|
||||
$[18] = t7;
|
||||
} else {
|
||||
t7 = $[18];
|
||||
}
|
||||
bottomBorder = `${BOX_CHARS.bottomLeft}${t7}${BOX_CHARS.bottomRight}`;
|
||||
truncationBar = isTruncated ? (() => {
|
||||
const hiddenCount = contentLines.length - effectiveMaxLines;
|
||||
const label = `${BOX_CHARS.horizontal.repeat(3)} \u2702 ${BOX_CHARS.horizontal.repeat(3)} ${hiddenCount} lines hidden `;
|
||||
const labelWidth = stringWidth(label);
|
||||
const fillWidth = Math.max(0, boxWidth - 2 - labelWidth);
|
||||
return `${BOX_CHARS.teeLeft}${label}${BOX_CHARS.horizontal.repeat(fillWidth)}${BOX_CHARS.teeRight}`;
|
||||
})() : null;
|
||||
T0 = Box;
|
||||
t3 = "column";
|
||||
if ($[19] !== topBorder) {
|
||||
t4 = <Text dimColor={true}>{topBorder}</Text>;
|
||||
$[19] = topBorder;
|
||||
$[20] = t4;
|
||||
} else {
|
||||
t4 = $[20];
|
||||
}
|
||||
let t8;
|
||||
if ($[21] !== innerWidth) {
|
||||
t8 = (line_0, index) => {
|
||||
const lineWidth = stringWidth(line_0);
|
||||
const displayLine = lineWidth > innerWidth ? sliceAnsi(line_0, 0, innerWidth) : line_0;
|
||||
const padding = " ".repeat(Math.max(0, innerWidth - stringWidth(displayLine)));
|
||||
return <Box key={index} flexDirection="row"><Text dimColor={true}>{BOX_CHARS.vertical} </Text><Ansi>{displayLine}</Ansi><Text dimColor={true}>{padding} {BOX_CHARS.vertical}</Text></Box>;
|
||||
};
|
||||
$[21] = innerWidth;
|
||||
$[22] = t8;
|
||||
} else {
|
||||
t8 = $[22];
|
||||
}
|
||||
t5 = lines.map(t8);
|
||||
$[4] = effectiveMaxLines;
|
||||
$[5] = effectiveMaxWidth;
|
||||
$[6] = minHeight;
|
||||
$[7] = minWidth;
|
||||
$[8] = rendered;
|
||||
$[9] = T0;
|
||||
$[10] = bottomBorder;
|
||||
$[11] = t3;
|
||||
$[12] = t4;
|
||||
$[13] = t5;
|
||||
$[14] = truncationBar;
|
||||
} else {
|
||||
T0 = $[9];
|
||||
bottomBorder = $[10];
|
||||
t3 = $[11];
|
||||
t4 = $[12];
|
||||
t5 = $[13];
|
||||
truncationBar = $[14];
|
||||
}
|
||||
let t6;
|
||||
if ($[23] !== truncationBar) {
|
||||
t6 = truncationBar && <Text color="warning">{truncationBar}</Text>;
|
||||
$[23] = truncationBar;
|
||||
$[24] = t6;
|
||||
} else {
|
||||
t6 = $[24];
|
||||
}
|
||||
let t7;
|
||||
if ($[25] !== bottomBorder) {
|
||||
t7 = <Text dimColor={true}>{bottomBorder}</Text>;
|
||||
$[25] = bottomBorder;
|
||||
$[26] = t7;
|
||||
} else {
|
||||
t7 = $[26];
|
||||
}
|
||||
let t8;
|
||||
if ($[27] !== T0 || $[28] !== t3 || $[29] !== t4 || $[30] !== t5 || $[31] !== t6 || $[32] !== t7) {
|
||||
t8 = <T0 flexDirection={t3}>{t4}{t5}{t6}{t7}</T0>;
|
||||
$[27] = T0;
|
||||
$[28] = t3;
|
||||
$[29] = t4;
|
||||
$[30] = t5;
|
||||
$[31] = t6;
|
||||
$[32] = t7;
|
||||
$[33] = t8;
|
||||
} else {
|
||||
t8 = $[33];
|
||||
}
|
||||
return t8;
|
||||
}
|
||||
function _temp(line) {
|
||||
return stringWidth(line);
|
||||
highlight,
|
||||
}: PreviewBoxProps & { highlight: CliHighlight | null }): React.ReactNode {
|
||||
const { columns: terminalWidth } = useTerminalSize()
|
||||
const [theme] = useTheme()
|
||||
const effectiveMaxWidth = maxWidth ?? terminalWidth - 4
|
||||
|
||||
// Use provided maxLines, or a reasonable default
|
||||
const effectiveMaxLines = maxLines ?? 20
|
||||
|
||||
// Render markdown with syntax highlighting for code blocks. applyMarkdown
|
||||
// returns an ANSI-styled string (bold, colors, etc.) that we split into
|
||||
// lines. stringWidth and sliceAnsi below correctly handle ANSI codes.
|
||||
const rendered = useMemo(
|
||||
() => applyMarkdown(content, theme, highlight),
|
||||
[content, theme, highlight],
|
||||
)
|
||||
const contentLines = rendered.split('\n')
|
||||
const isTruncated = contentLines.length > effectiveMaxLines
|
||||
|
||||
// Truncate to effectiveMaxLines
|
||||
const truncatedLines = isTruncated
|
||||
? contentLines.slice(0, effectiveMaxLines)
|
||||
: contentLines
|
||||
|
||||
// Pad content with empty lines if shorter than minHeight, but never exceed
|
||||
// the truncation limit — otherwise padding undoes the truncation
|
||||
const effectiveMinHeight = Math.min(minHeight ?? 0, effectiveMaxLines)
|
||||
const paddingNeeded = Math.max(
|
||||
0,
|
||||
effectiveMinHeight - truncatedLines.length - (isTruncated ? 1 : 0),
|
||||
)
|
||||
const lines =
|
||||
paddingNeeded > 0
|
||||
? [...truncatedLines, ...Array<string>(paddingNeeded).fill('')]
|
||||
: truncatedLines
|
||||
|
||||
// Calculate content width (max visual line width, handling unicode/emoji/CJK)
|
||||
const contentWidth = Math.max(
|
||||
minWidth,
|
||||
...lines.map(line => stringWidth(line)),
|
||||
)
|
||||
// Add 2 for border padding, cap at the container width to prevent line wrapping
|
||||
const boxWidth = Math.min(contentWidth + 4, effectiveMaxWidth)
|
||||
const innerWidth = boxWidth - 4 // Account for borders and padding
|
||||
|
||||
// Render top border
|
||||
const topBorder = `${BOX_CHARS.topLeft}${BOX_CHARS.horizontal.repeat(boxWidth - 2)}${BOX_CHARS.topRight}`
|
||||
|
||||
// Render bottom border
|
||||
const bottomBorder = `${BOX_CHARS.bottomLeft}${BOX_CHARS.horizontal.repeat(boxWidth - 2)}${BOX_CHARS.bottomRight}`
|
||||
|
||||
// Build the truncation separator bar (e.g. ├─── ✂ ─── 42 lines hidden ──────┤)
|
||||
const truncationBar = isTruncated
|
||||
? (() => {
|
||||
const hiddenCount = contentLines.length - effectiveMaxLines
|
||||
const label = `${BOX_CHARS.horizontal.repeat(3)} \u2702 ${BOX_CHARS.horizontal.repeat(3)} ${hiddenCount} lines hidden `
|
||||
const labelWidth = stringWidth(label)
|
||||
const fillWidth = Math.max(0, boxWidth - 2 - labelWidth)
|
||||
return `${BOX_CHARS.teeLeft}${label}${BOX_CHARS.horizontal.repeat(fillWidth)}${BOX_CHARS.teeRight}`
|
||||
})()
|
||||
: null
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text dimColor>{topBorder}</Text>
|
||||
|
||||
{lines.map((line, index) => {
|
||||
// Pad or truncate line to fit inner width (using visual width for unicode/emoji/CJK).
|
||||
// sliceAnsi handles ANSI escape codes correctly; stringWidth strips them before measuring.
|
||||
const lineWidth = stringWidth(line)
|
||||
const displayLine =
|
||||
lineWidth > innerWidth ? sliceAnsi(line, 0, innerWidth) : line
|
||||
const padding = ' '.repeat(
|
||||
Math.max(0, innerWidth - stringWidth(displayLine)),
|
||||
)
|
||||
|
||||
return (
|
||||
<Box key={index} flexDirection="row">
|
||||
<Text dimColor>{BOX_CHARS.vertical} </Text>
|
||||
<Ansi>{displayLine}</Ansi>
|
||||
<Text dimColor>
|
||||
{padding} {BOX_CHARS.vertical}
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
})}
|
||||
|
||||
{truncationBar && <Text color="warning">{truncationBar}</Text>}
|
||||
|
||||
<Text dimColor>{bottomBorder}</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,38 +1,51 @@
|
||||
import figures from 'figures';
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { useTerminalSize } from '../../../hooks/useTerminalSize.js';
|
||||
import type { KeyboardEvent } from '../../../ink/events/keyboard-event.js';
|
||||
import { Box, Text } from '../../../ink.js';
|
||||
import { useKeybinding, useKeybindings } from '../../../keybindings/useKeybinding.js';
|
||||
import { useAppState } from '../../../state/AppState.js';
|
||||
import type { Question } from '../../../tools/AskUserQuestionTool/AskUserQuestionTool.js';
|
||||
import { getExternalEditor } from '../../../utils/editor.js';
|
||||
import { toIDEDisplayName } from '../../../utils/ide.js';
|
||||
import { editPromptInEditor } from '../../../utils/promptEditor.js';
|
||||
import { Divider } from '../../design-system/Divider.js';
|
||||
import TextInput from '../../TextInput.js';
|
||||
import { PermissionRequestTitle } from '../PermissionRequestTitle.js';
|
||||
import { PreviewBox } from './PreviewBox.js';
|
||||
import { QuestionNavigationBar } from './QuestionNavigationBar.js';
|
||||
import type { QuestionState } from './use-multiple-choice-state.js';
|
||||
import figures from 'figures'
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react'
|
||||
import { useTerminalSize } from '../../../hooks/useTerminalSize.js'
|
||||
import type { KeyboardEvent } from '../../../ink/events/keyboard-event.js'
|
||||
import { Box, Text } from '../../../ink.js'
|
||||
import {
|
||||
useKeybinding,
|
||||
useKeybindings,
|
||||
} from '../../../keybindings/useKeybinding.js'
|
||||
import { useAppState } from '../../../state/AppState.js'
|
||||
import type { Question } from '../../../tools/AskUserQuestionTool/AskUserQuestionTool.js'
|
||||
import { getExternalEditor } from '../../../utils/editor.js'
|
||||
import { toIDEDisplayName } from '../../../utils/ide.js'
|
||||
import { editPromptInEditor } from '../../../utils/promptEditor.js'
|
||||
import { Divider } from '../../design-system/Divider.js'
|
||||
import TextInput from '../../TextInput.js'
|
||||
import { PermissionRequestTitle } from '../PermissionRequestTitle.js'
|
||||
import { PreviewBox } from './PreviewBox.js'
|
||||
import { QuestionNavigationBar } from './QuestionNavigationBar.js'
|
||||
import type { QuestionState } from './use-multiple-choice-state.js'
|
||||
|
||||
type Props = {
|
||||
question: Question;
|
||||
questions: Question[];
|
||||
currentQuestionIndex: number;
|
||||
answers: Record<string, string>;
|
||||
questionStates: Record<string, QuestionState>;
|
||||
hideSubmitTab?: boolean;
|
||||
minContentHeight?: number;
|
||||
minContentWidth?: number;
|
||||
onUpdateQuestionState: (questionText: string, updates: Partial<QuestionState>, isMultiSelect: boolean) => void;
|
||||
onAnswer: (questionText: string, label: string | string[], textInput?: string, shouldAdvance?: boolean) => void;
|
||||
onTextInputFocus: (isInInput: boolean) => void;
|
||||
onCancel: () => void;
|
||||
onTabPrev?: () => void;
|
||||
onTabNext?: () => void;
|
||||
onRespondToClaude: () => void;
|
||||
onFinishPlanInterview: () => void;
|
||||
};
|
||||
question: Question
|
||||
questions: Question[]
|
||||
currentQuestionIndex: number
|
||||
answers: Record<string, string>
|
||||
questionStates: Record<string, QuestionState>
|
||||
hideSubmitTab?: boolean
|
||||
minContentHeight?: number
|
||||
minContentWidth?: number
|
||||
onUpdateQuestionState: (
|
||||
questionText: string,
|
||||
updates: Partial<QuestionState>,
|
||||
isMultiSelect: boolean,
|
||||
) => void
|
||||
onAnswer: (
|
||||
questionText: string,
|
||||
label: string | string[],
|
||||
textInput?: string,
|
||||
shouldAdvance?: boolean,
|
||||
) => void
|
||||
onTextInputFocus: (isInInput: boolean) => void
|
||||
onCancel: () => void
|
||||
onTabPrev?: () => void
|
||||
onTabNext?: () => void
|
||||
onRespondToClaude: () => void
|
||||
onFinishPlanInterview: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* A side-by-side question view for questions with preview content.
|
||||
@@ -54,188 +67,235 @@ export function PreviewQuestionView({
|
||||
onTabPrev,
|
||||
onTabNext,
|
||||
onRespondToClaude,
|
||||
onFinishPlanInterview
|
||||
onFinishPlanInterview,
|
||||
}: Props): React.ReactNode {
|
||||
const isInPlanMode = useAppState(s => s.toolPermissionContext.mode) === 'plan';
|
||||
const [isFooterFocused, setIsFooterFocused] = useState(false);
|
||||
const [footerIndex, setFooterIndex] = useState(0);
|
||||
const [isInNotesInput, setIsInNotesInput] = useState(false);
|
||||
const [cursorOffset, setCursorOffset] = useState(0);
|
||||
const editor = getExternalEditor();
|
||||
const editorName = editor ? toIDEDisplayName(editor) : null;
|
||||
const questionText = question.question;
|
||||
const questionState = questionStates[questionText];
|
||||
const isInPlanMode = useAppState(s => s.toolPermissionContext.mode) === 'plan'
|
||||
const [isFooterFocused, setIsFooterFocused] = useState(false)
|
||||
const [footerIndex, setFooterIndex] = useState(0)
|
||||
const [isInNotesInput, setIsInNotesInput] = useState(false)
|
||||
const [cursorOffset, setCursorOffset] = useState(0)
|
||||
|
||||
const editor = getExternalEditor()
|
||||
const editorName = editor ? toIDEDisplayName(editor) : null
|
||||
|
||||
const questionText = question.question
|
||||
const questionState = questionStates[questionText]
|
||||
|
||||
// Only real options — no "Other" for preview questions
|
||||
const allOptions = question.options;
|
||||
const allOptions = question.options
|
||||
|
||||
// Track which option is focused (for preview display)
|
||||
const [focusedIndex, setFocusedIndex] = useState(0);
|
||||
const [focusedIndex, setFocusedIndex] = useState(0)
|
||||
|
||||
// Reset focusedIndex when navigating to a different question
|
||||
const prevQuestionText = useRef(questionText);
|
||||
const prevQuestionText = useRef(questionText)
|
||||
if (prevQuestionText.current !== questionText) {
|
||||
prevQuestionText.current = questionText;
|
||||
const selected = questionState?.selectedValue as string | undefined;
|
||||
const idx = selected ? allOptions.findIndex(opt => opt.label === selected) : -1;
|
||||
setFocusedIndex(idx >= 0 ? idx : 0);
|
||||
prevQuestionText.current = questionText
|
||||
const selected = questionState?.selectedValue as string | undefined
|
||||
const idx = selected
|
||||
? allOptions.findIndex(opt => opt.label === selected)
|
||||
: -1
|
||||
setFocusedIndex(idx >= 0 ? idx : 0)
|
||||
}
|
||||
const focusedOption = allOptions[focusedIndex];
|
||||
const selectedValue = questionState?.selectedValue as string | undefined;
|
||||
const notesValue = questionState?.textInputValue || '';
|
||||
const handleSelectOption = useCallback((index: number) => {
|
||||
const option = allOptions[index];
|
||||
if (!option) return;
|
||||
setFocusedIndex(index);
|
||||
onUpdateQuestionState(questionText, {
|
||||
selectedValue: option.label
|
||||
}, false);
|
||||
onAnswer(questionText, option.label);
|
||||
}, [allOptions, questionText, onUpdateQuestionState, onAnswer]);
|
||||
const handleNavigate = useCallback((direction: 'up' | 'down' | number) => {
|
||||
if (isInNotesInput) return;
|
||||
let newIndex: number;
|
||||
|
||||
const focusedOption = allOptions[focusedIndex]
|
||||
const selectedValue = questionState?.selectedValue as string | undefined
|
||||
const notesValue = questionState?.textInputValue || ''
|
||||
|
||||
const handleSelectOption = useCallback(
|
||||
(index: number) => {
|
||||
const option = allOptions[index]
|
||||
if (!option) return
|
||||
|
||||
setFocusedIndex(index)
|
||||
onUpdateQuestionState(
|
||||
questionText,
|
||||
{ selectedValue: option.label },
|
||||
false,
|
||||
)
|
||||
|
||||
onAnswer(questionText, option.label)
|
||||
},
|
||||
[allOptions, questionText, onUpdateQuestionState, onAnswer],
|
||||
)
|
||||
|
||||
const handleNavigate = useCallback(
|
||||
(direction: 'up' | 'down' | number) => {
|
||||
if (isInNotesInput) return
|
||||
|
||||
let newIndex: number
|
||||
if (typeof direction === 'number') {
|
||||
newIndex = direction;
|
||||
newIndex = direction
|
||||
} else if (direction === 'up') {
|
||||
newIndex = focusedIndex > 0 ? focusedIndex - 1 : focusedIndex;
|
||||
newIndex = focusedIndex > 0 ? focusedIndex - 1 : focusedIndex
|
||||
} else {
|
||||
newIndex = focusedIndex < allOptions.length - 1 ? focusedIndex + 1 : focusedIndex;
|
||||
newIndex =
|
||||
focusedIndex < allOptions.length - 1 ? focusedIndex + 1 : focusedIndex
|
||||
}
|
||||
|
||||
if (newIndex >= 0 && newIndex < allOptions.length) {
|
||||
setFocusedIndex(newIndex);
|
||||
setFocusedIndex(newIndex)
|
||||
}
|
||||
}, [focusedIndex, allOptions.length, isInNotesInput]);
|
||||
},
|
||||
[focusedIndex, allOptions.length, isInNotesInput],
|
||||
)
|
||||
|
||||
// Handle ctrl+g to open external editor for notes
|
||||
useKeybinding('chat:externalEditor', async () => {
|
||||
const currentValue = questionState?.textInputValue || '';
|
||||
const result = await editPromptInEditor(currentValue);
|
||||
useKeybinding(
|
||||
'chat:externalEditor',
|
||||
async () => {
|
||||
const currentValue = questionState?.textInputValue || ''
|
||||
const result = await editPromptInEditor(currentValue)
|
||||
if (result.content !== null && result.content !== currentValue) {
|
||||
onUpdateQuestionState(questionText, {
|
||||
textInputValue: result.content
|
||||
}, false);
|
||||
onUpdateQuestionState(
|
||||
questionText,
|
||||
{ textInputValue: result.content },
|
||||
false,
|
||||
)
|
||||
}
|
||||
}, {
|
||||
context: 'Chat',
|
||||
isActive: isInNotesInput && !!editor
|
||||
});
|
||||
},
|
||||
{ context: 'Chat', isActive: isInNotesInput && !!editor },
|
||||
)
|
||||
|
||||
// Handle left/right arrow and tab for question navigation.
|
||||
// This must be in the child component (not just the parent) because child useInput
|
||||
// handlers register first on the event emitter and fire before parent handlers.
|
||||
// Without this, the parent's useKeybindings may not fire reliably depending on
|
||||
// listener ordering in the event emitter.
|
||||
useKeybindings({
|
||||
useKeybindings(
|
||||
{
|
||||
'tabs:previous': () => onTabPrev?.(),
|
||||
'tabs:next': () => onTabNext?.()
|
||||
}, {
|
||||
context: 'Tabs',
|
||||
isActive: !isInNotesInput && !isFooterFocused
|
||||
});
|
||||
'tabs:next': () => onTabNext?.(),
|
||||
},
|
||||
{ context: 'Tabs', isActive: !isInNotesInput && !isFooterFocused },
|
||||
)
|
||||
|
||||
// Re-submit the answer (plain label) when exiting notes input.
|
||||
// Notes are stored in questionStates and collected at submit time via annotations.
|
||||
const handleNotesExit = useCallback(() => {
|
||||
setIsInNotesInput(false);
|
||||
onTextInputFocus(false);
|
||||
setIsInNotesInput(false)
|
||||
onTextInputFocus(false)
|
||||
if (selectedValue) {
|
||||
onAnswer(questionText, selectedValue);
|
||||
onAnswer(questionText, selectedValue)
|
||||
}
|
||||
}, [selectedValue, questionText, onAnswer, onTextInputFocus]);
|
||||
}, [selectedValue, questionText, onAnswer, onTextInputFocus])
|
||||
|
||||
const handleDownFromPreview = useCallback(() => {
|
||||
setIsFooterFocused(true);
|
||||
}, []);
|
||||
setIsFooterFocused(true)
|
||||
}, [])
|
||||
|
||||
const handleUpFromFooter = useCallback(() => {
|
||||
setIsFooterFocused(false);
|
||||
}, []);
|
||||
setIsFooterFocused(false)
|
||||
}, [])
|
||||
|
||||
// Handle keyboard input for option/footer/notes navigation.
|
||||
// Always active — the handler routes internally based on isFooterFocused/isInNotesInput.
|
||||
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (isFooterFocused) {
|
||||
if (e.key === 'up' || e.ctrl && e.key === 'p') {
|
||||
e.preventDefault();
|
||||
if (e.key === 'up' || (e.ctrl && e.key === 'p')) {
|
||||
e.preventDefault()
|
||||
if (footerIndex === 0) {
|
||||
handleUpFromFooter();
|
||||
handleUpFromFooter()
|
||||
} else {
|
||||
setFooterIndex(0);
|
||||
setFooterIndex(0)
|
||||
}
|
||||
return;
|
||||
return
|
||||
}
|
||||
if (e.key === 'down' || e.ctrl && e.key === 'n') {
|
||||
e.preventDefault();
|
||||
|
||||
if (e.key === 'down' || (e.ctrl && e.key === 'n')) {
|
||||
e.preventDefault()
|
||||
if (isInPlanMode && footerIndex === 0) {
|
||||
setFooterIndex(1);
|
||||
setFooterIndex(1)
|
||||
}
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
if (e.key === 'return') {
|
||||
e.preventDefault();
|
||||
e.preventDefault()
|
||||
if (footerIndex === 0) {
|
||||
onRespondToClaude();
|
||||
onRespondToClaude()
|
||||
} else {
|
||||
onFinishPlanInterview();
|
||||
onFinishPlanInterview()
|
||||
}
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
if (e.key === 'escape') {
|
||||
e.preventDefault();
|
||||
onCancel();
|
||||
e.preventDefault()
|
||||
onCancel()
|
||||
}
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
if (isInNotesInput) {
|
||||
// In notes input mode, handle escape to exit back to option navigation
|
||||
if (e.key === 'escape') {
|
||||
e.preventDefault();
|
||||
handleNotesExit();
|
||||
e.preventDefault()
|
||||
handleNotesExit()
|
||||
}
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
// Handle option navigation (vertical)
|
||||
if (e.key === 'up' || e.ctrl && e.key === 'p') {
|
||||
e.preventDefault();
|
||||
if (e.key === 'up' || (e.ctrl && e.key === 'p')) {
|
||||
e.preventDefault()
|
||||
if (focusedIndex > 0) {
|
||||
handleNavigate('up');
|
||||
handleNavigate('up')
|
||||
}
|
||||
} else if (e.key === 'down' || e.ctrl && e.key === 'n') {
|
||||
e.preventDefault();
|
||||
} else if (e.key === 'down' || (e.ctrl && e.key === 'n')) {
|
||||
e.preventDefault()
|
||||
if (focusedIndex === allOptions.length - 1) {
|
||||
// At bottom of options, go to footer
|
||||
handleDownFromPreview();
|
||||
handleDownFromPreview()
|
||||
} else {
|
||||
handleNavigate('down');
|
||||
handleNavigate('down')
|
||||
}
|
||||
} else if (e.key === 'return') {
|
||||
e.preventDefault();
|
||||
handleSelectOption(focusedIndex);
|
||||
e.preventDefault()
|
||||
handleSelectOption(focusedIndex)
|
||||
} else if (e.key === 'n' && !e.ctrl && !e.meta) {
|
||||
// Press 'n' to focus the notes input
|
||||
e.preventDefault();
|
||||
setIsInNotesInput(true);
|
||||
onTextInputFocus(true);
|
||||
e.preventDefault()
|
||||
setIsInNotesInput(true)
|
||||
onTextInputFocus(true)
|
||||
} else if (e.key === 'escape') {
|
||||
e.preventDefault();
|
||||
onCancel();
|
||||
e.preventDefault()
|
||||
onCancel()
|
||||
} else if (e.key.length === 1 && e.key >= '1' && e.key <= '9') {
|
||||
e.preventDefault();
|
||||
const idx_0 = parseInt(e.key, 10) - 1;
|
||||
if (idx_0 < allOptions.length) {
|
||||
handleNavigate(idx_0);
|
||||
e.preventDefault()
|
||||
const idx = parseInt(e.key, 10) - 1
|
||||
if (idx < allOptions.length) {
|
||||
handleNavigate(idx)
|
||||
}
|
||||
}
|
||||
}, [isFooterFocused, footerIndex, isInPlanMode, isInNotesInput, focusedIndex, allOptions.length, handleUpFromFooter, handleDownFromPreview, handleNavigate, handleSelectOption, handleNotesExit, onRespondToClaude, onFinishPlanInterview, onCancel, onTextInputFocus]);
|
||||
const previewContent = focusedOption?.preview || null;
|
||||
},
|
||||
[
|
||||
isFooterFocused,
|
||||
footerIndex,
|
||||
isInPlanMode,
|
||||
isInNotesInput,
|
||||
focusedIndex,
|
||||
allOptions.length,
|
||||
handleUpFromFooter,
|
||||
handleDownFromPreview,
|
||||
handleNavigate,
|
||||
handleSelectOption,
|
||||
handleNotesExit,
|
||||
onRespondToClaude,
|
||||
onFinishPlanInterview,
|
||||
onCancel,
|
||||
onTextInputFocus,
|
||||
],
|
||||
)
|
||||
|
||||
const previewContent = focusedOption?.preview || null
|
||||
|
||||
// The right panel's available width is terminal minus the left panel and gap.
|
||||
const LEFT_PANEL_WIDTH = 30;
|
||||
const GAP = 4;
|
||||
const {
|
||||
columns
|
||||
} = useTerminalSize();
|
||||
const previewMaxWidth = columns - LEFT_PANEL_WIDTH - GAP;
|
||||
const LEFT_PANEL_WIDTH = 30
|
||||
const GAP = 4
|
||||
const { columns } = useTerminalSize()
|
||||
const previewMaxWidth = columns - LEFT_PANEL_WIDTH - GAP
|
||||
|
||||
// Lines used within the content area that aren't preview content:
|
||||
// 1: marginTop on side-by-side box
|
||||
@@ -245,19 +305,34 @@ export function PreviewQuestionView({
|
||||
// 1: "Chat about this" line
|
||||
// 1: plan mode line (may or may not show)
|
||||
// 2: help text (marginTop=1 + text)
|
||||
const PREVIEW_OVERHEAD = 11;
|
||||
const PREVIEW_OVERHEAD = 11
|
||||
|
||||
// Compute the max lines available for preview content from the parent's
|
||||
// height budget to prevent terminal overflow. We do NOT pad shorter options
|
||||
// to match the tallest — the outer box's minHeight handles cross-question
|
||||
// layout consistency, and within-question shifts are acceptable.
|
||||
const previewMaxLines = useMemo(() => {
|
||||
return minContentHeight ? Math.max(1, minContentHeight - PREVIEW_OVERHEAD) : undefined;
|
||||
}, [minContentHeight]);
|
||||
return <Box flexDirection="column" marginTop={1} tabIndex={0} autoFocus onKeyDown={handleKeyDown}>
|
||||
return minContentHeight
|
||||
? Math.max(1, minContentHeight - PREVIEW_OVERHEAD)
|
||||
: undefined
|
||||
}, [minContentHeight])
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
marginTop={1}
|
||||
tabIndex={0}
|
||||
autoFocus
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<Divider color="inactive" />
|
||||
<Box flexDirection="column" paddingTop={0}>
|
||||
<QuestionNavigationBar questions={questions} currentQuestionIndex={currentQuestionIndex} answers={answers} hideSubmitTab={hideSubmitTab} />
|
||||
<QuestionNavigationBar
|
||||
questions={questions}
|
||||
currentQuestionIndex={currentQuestionIndex}
|
||||
answers={answers}
|
||||
hideSubmitTab={hideSubmitTab}
|
||||
/>
|
||||
<PermissionRequestTitle title={question.question} color={'text'} />
|
||||
|
||||
<Box flexDirection="column" minHeight={minContentHeight}>
|
||||
@@ -265,33 +340,71 @@ export function PreviewQuestionView({
|
||||
<Box marginTop={1} flexDirection="row" gap={4}>
|
||||
{/* Left panel: vertical option list */}
|
||||
<Box flexDirection="column" width={30}>
|
||||
{allOptions.map((option_0, index_0) => {
|
||||
const isFocused = focusedIndex === index_0;
|
||||
const isSelected = selectedValue === option_0.label;
|
||||
return <Box key={option_0.label} flexDirection="row">
|
||||
{isFocused ? <Text color="suggestion">{figures.pointer}</Text> : <Text> </Text>}
|
||||
<Text dimColor> {index_0 + 1}.</Text>
|
||||
<Text color={isSelected ? 'success' : isFocused ? 'suggestion' : undefined} bold={isFocused}>
|
||||
{allOptions.map((option, index) => {
|
||||
const isFocused = focusedIndex === index
|
||||
const isSelected = selectedValue === option.label
|
||||
|
||||
return (
|
||||
<Box key={option.label} flexDirection="row">
|
||||
{isFocused ? (
|
||||
<Text color="suggestion">{figures.pointer}</Text>
|
||||
) : (
|
||||
<Text> </Text>
|
||||
)}
|
||||
<Text dimColor> {index + 1}.</Text>
|
||||
<Text
|
||||
color={
|
||||
isSelected
|
||||
? 'success'
|
||||
: isFocused
|
||||
? 'suggestion'
|
||||
: undefined
|
||||
}
|
||||
bold={isFocused}
|
||||
>
|
||||
{' '}
|
||||
{option_0.label}
|
||||
{option.label}
|
||||
</Text>
|
||||
{isSelected && <Text color="success"> {figures.tick}</Text>}
|
||||
</Box>;
|
||||
</Box>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
|
||||
{/* Right panel: preview + notes */}
|
||||
<Box flexDirection="column" flexGrow={1}>
|
||||
<PreviewBox content={previewContent || 'No preview available'} maxLines={previewMaxLines} minWidth={minContentWidth} maxWidth={previewMaxWidth} />
|
||||
<PreviewBox
|
||||
content={previewContent || 'No preview available'}
|
||||
maxLines={previewMaxLines}
|
||||
minWidth={minContentWidth}
|
||||
maxWidth={previewMaxWidth}
|
||||
/>
|
||||
<Box marginTop={1} flexDirection="row" gap={1}>
|
||||
<Text color="suggestion">Notes:</Text>
|
||||
{isInNotesInput ? <TextInput value={notesValue} placeholder="Add notes on this design…" onChange={value => {
|
||||
onUpdateQuestionState(questionText, {
|
||||
textInputValue: value
|
||||
}, false);
|
||||
}} onSubmit={handleNotesExit} onExit={handleNotesExit} focus={true} showCursor={true} columns={60} cursorOffset={cursorOffset} onChangeCursorOffset={setCursorOffset} /> : <Text dimColor italic>
|
||||
{isInNotesInput ? (
|
||||
<TextInput
|
||||
value={notesValue}
|
||||
placeholder="Add notes on this design…"
|
||||
onChange={value => {
|
||||
onUpdateQuestionState(
|
||||
questionText,
|
||||
{ textInputValue: value },
|
||||
false,
|
||||
)
|
||||
}}
|
||||
onSubmit={handleNotesExit}
|
||||
onExit={handleNotesExit}
|
||||
focus={true}
|
||||
showCursor={true}
|
||||
columns={60}
|
||||
cursorOffset={cursorOffset}
|
||||
onChangeCursorOffset={setCursorOffset}
|
||||
/>
|
||||
) : (
|
||||
<Text dimColor italic>
|
||||
{notesValue || 'press n to add notes'}
|
||||
</Text>}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -300,28 +413,53 @@ export function PreviewQuestionView({
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<Divider color="inactive" />
|
||||
<Box flexDirection="row" gap={1}>
|
||||
{isFooterFocused && footerIndex === 0 ? <Text color="suggestion">{figures.pointer}</Text> : <Text> </Text>}
|
||||
<Text color={isFooterFocused && footerIndex === 0 ? 'suggestion' : undefined}>
|
||||
{isFooterFocused && footerIndex === 0 ? (
|
||||
<Text color="suggestion">{figures.pointer}</Text>
|
||||
) : (
|
||||
<Text> </Text>
|
||||
)}
|
||||
<Text
|
||||
color={
|
||||
isFooterFocused && footerIndex === 0
|
||||
? 'suggestion'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
Chat about this
|
||||
</Text>
|
||||
</Box>
|
||||
{isInPlanMode && <Box flexDirection="row" gap={1}>
|
||||
{isFooterFocused && footerIndex === 1 ? <Text color="suggestion">{figures.pointer}</Text> : <Text> </Text>}
|
||||
<Text color={isFooterFocused && footerIndex === 1 ? 'suggestion' : undefined}>
|
||||
{isInPlanMode && (
|
||||
<Box flexDirection="row" gap={1}>
|
||||
{isFooterFocused && footerIndex === 1 ? (
|
||||
<Text color="suggestion">{figures.pointer}</Text>
|
||||
) : (
|
||||
<Text> </Text>
|
||||
)}
|
||||
<Text
|
||||
color={
|
||||
isFooterFocused && footerIndex === 1
|
||||
? 'suggestion'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
Skip interview and plan immediately
|
||||
</Text>
|
||||
</Box>}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<Box marginTop={1}>
|
||||
<Text color="inactive" dimColor>
|
||||
Enter to select · {figures.arrowUp}/{figures.arrowDown} to
|
||||
navigate · n to add notes
|
||||
{questions.length > 1 && <> · Tab to switch questions</>}
|
||||
{isInNotesInput && editorName && <> · ctrl+g to edit in {editorName}</>}{' '}
|
||||
{isInNotesInput && editorName && (
|
||||
<> · ctrl+g to edit in {editorName}</>
|
||||
)}{' '}
|
||||
· Esc to cancel
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>;
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,177 +1,151 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import figures from 'figures';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTerminalSize } from '../../../hooks/useTerminalSize.js';
|
||||
import { stringWidth } from '../../../ink/stringWidth.js';
|
||||
import { Box, Text } from '../../../ink.js';
|
||||
import type { Question } from '../../../tools/AskUserQuestionTool/AskUserQuestionTool.js';
|
||||
import { truncateToWidth } from '../../../utils/format.js';
|
||||
import figures from 'figures'
|
||||
import React, { useMemo } from 'react'
|
||||
import { useTerminalSize } from '../../../hooks/useTerminalSize.js'
|
||||
import { stringWidth } from '../../../ink/stringWidth.js'
|
||||
import { Box, Text } from '../../../ink.js'
|
||||
import type { Question } from '../../../tools/AskUserQuestionTool/AskUserQuestionTool.js'
|
||||
import { truncateToWidth } from '../../../utils/format.js'
|
||||
|
||||
type Props = {
|
||||
questions: Question[];
|
||||
currentQuestionIndex: number;
|
||||
answers: Record<string, string>;
|
||||
hideSubmitTab?: boolean;
|
||||
};
|
||||
export function QuestionNavigationBar(t0) {
|
||||
const $ = _c(39);
|
||||
const {
|
||||
questions: Question[]
|
||||
currentQuestionIndex: number
|
||||
answers: Record<string, string>
|
||||
hideSubmitTab?: boolean
|
||||
}
|
||||
|
||||
export function QuestionNavigationBar({
|
||||
questions,
|
||||
currentQuestionIndex,
|
||||
answers,
|
||||
hideSubmitTab: t1
|
||||
} = t0;
|
||||
const hideSubmitTab = t1 === undefined ? false : t1;
|
||||
const {
|
||||
columns
|
||||
} = useTerminalSize();
|
||||
let t2;
|
||||
if ($[0] !== columns || $[1] !== currentQuestionIndex || $[2] !== hideSubmitTab || $[3] !== questions) {
|
||||
bb0: {
|
||||
const submitText = hideSubmitTab ? "" : ` ${figures.tick} Submit `;
|
||||
const fixedWidth = stringWidth("\u2190 ") + stringWidth(" \u2192") + stringWidth(submitText);
|
||||
const availableForTabs = columns - fixedWidth;
|
||||
hideSubmitTab = false,
|
||||
}: Props): React.ReactNode {
|
||||
const { columns } = useTerminalSize()
|
||||
|
||||
// Calculate the display text for each tab based on available width
|
||||
const tabDisplayTexts = useMemo(() => {
|
||||
// Calculate fixed width elements
|
||||
const leftArrow = '← '
|
||||
const rightArrow = ' →'
|
||||
const submitText = hideSubmitTab ? '' : ` ${figures.tick} Submit `
|
||||
const checkboxWidth = 2 // checkbox + space
|
||||
const paddingPerTab = 2 // space before and after each tab text
|
||||
|
||||
const fixedWidth =
|
||||
stringWidth(leftArrow) + stringWidth(rightArrow) + stringWidth(submitText)
|
||||
|
||||
// Available width for all question tabs
|
||||
const availableForTabs = columns - fixedWidth
|
||||
|
||||
if (availableForTabs <= 0) {
|
||||
let t3;
|
||||
if ($[5] !== currentQuestionIndex || $[6] !== questions) {
|
||||
let t4;
|
||||
if ($[8] !== currentQuestionIndex) {
|
||||
t4 = (q, index) => {
|
||||
const header = q?.header || `Q${index + 1}`;
|
||||
return index === currentQuestionIndex ? header.slice(0, 3) : "";
|
||||
};
|
||||
$[8] = currentQuestionIndex;
|
||||
$[9] = t4;
|
||||
} else {
|
||||
t4 = $[9];
|
||||
// Terminal too narrow, fallback to minimal display
|
||||
return questions.map((q: Question, index: number) => {
|
||||
const header = q?.header || `Q${index + 1}`
|
||||
return index === currentQuestionIndex ? header.slice(0, 3) : ''
|
||||
})
|
||||
}
|
||||
t3 = questions.map(t4);
|
||||
$[5] = currentQuestionIndex;
|
||||
$[6] = questions;
|
||||
$[7] = t3;
|
||||
} else {
|
||||
t3 = $[7];
|
||||
}
|
||||
t2 = t3;
|
||||
break bb0;
|
||||
}
|
||||
const tabHeaders = questions.map(_temp);
|
||||
const idealWidths = tabHeaders.map(_temp2);
|
||||
const totalIdealWidth = idealWidths.reduce(_temp3, 0);
|
||||
|
||||
// Calculate ideal width for each tab (checkbox + padding + text)
|
||||
const tabHeaders = questions.map(
|
||||
(q: Question, index: number) => q?.header || `Q${index + 1}`,
|
||||
)
|
||||
const idealWidths = tabHeaders.map(
|
||||
header => checkboxWidth + paddingPerTab + stringWidth(header),
|
||||
)
|
||||
|
||||
// Calculate total ideal width
|
||||
const totalIdealWidth = idealWidths.reduce((sum, w) => sum + w, 0)
|
||||
|
||||
// If everything fits, use full headers
|
||||
if (totalIdealWidth <= availableForTabs) {
|
||||
t2 = tabHeaders;
|
||||
break bb0;
|
||||
return tabHeaders
|
||||
}
|
||||
const currentHeader = tabHeaders[currentQuestionIndex] || "";
|
||||
const currentIdealWidth = 4 + stringWidth(currentHeader);
|
||||
const currentTabWidth = Math.min(currentIdealWidth, availableForTabs / 2);
|
||||
const remainingWidth = availableForTabs - currentTabWidth;
|
||||
const otherTabCount = questions.length - 1;
|
||||
const widthPerOtherTab = Math.max(6, Math.floor(remainingWidth / Math.max(otherTabCount, 1)));
|
||||
let t3;
|
||||
if ($[10] !== currentQuestionIndex || $[11] !== currentTabWidth || $[12] !== widthPerOtherTab) {
|
||||
t3 = (header_1, index_1) => {
|
||||
if (index_1 === currentQuestionIndex) {
|
||||
const maxTextWidth = currentTabWidth - 2 - 2;
|
||||
return truncateToWidth(header_1, maxTextWidth);
|
||||
|
||||
// Need to truncate - prioritize current tab
|
||||
const currentHeader = tabHeaders[currentQuestionIndex] || ''
|
||||
const currentIdealWidth =
|
||||
checkboxWidth + paddingPerTab + stringWidth(currentHeader)
|
||||
|
||||
// Minimum width for other tabs (checkbox + padding + 1 char + ellipsis)
|
||||
const minWidthPerTab = checkboxWidth + paddingPerTab + 2 // "X…"
|
||||
|
||||
// Calculate space for current tab (try to show full text)
|
||||
const currentTabWidth = Math.min(currentIdealWidth, availableForTabs / 2)
|
||||
const remainingWidth = availableForTabs - currentTabWidth
|
||||
|
||||
// Calculate space for other tabs
|
||||
const otherTabCount = questions.length - 1
|
||||
const widthPerOtherTab = Math.max(
|
||||
minWidthPerTab,
|
||||
Math.floor(remainingWidth / Math.max(otherTabCount, 1)),
|
||||
)
|
||||
|
||||
return tabHeaders.map((header, index) => {
|
||||
if (index === currentQuestionIndex) {
|
||||
// Current tab - show as much as possible
|
||||
const maxTextWidth = currentTabWidth - checkboxWidth - paddingPerTab
|
||||
return truncateToWidth(header, maxTextWidth)
|
||||
} else {
|
||||
const maxTextWidth_0 = widthPerOtherTab - 2 - 2;
|
||||
return truncateToWidth(header_1, maxTextWidth_0);
|
||||
// Other tabs - truncate to fit
|
||||
const maxTextWidth = widthPerOtherTab - checkboxWidth - paddingPerTab
|
||||
return truncateToWidth(header, maxTextWidth)
|
||||
}
|
||||
};
|
||||
$[10] = currentQuestionIndex;
|
||||
$[11] = currentTabWidth;
|
||||
$[12] = widthPerOtherTab;
|
||||
$[13] = t3;
|
||||
} else {
|
||||
t3 = $[13];
|
||||
})
|
||||
}, [questions, currentQuestionIndex, columns, hideSubmitTab])
|
||||
|
||||
const hideArrows = questions.length === 1 && hideSubmitTab
|
||||
|
||||
return (
|
||||
<Box flexDirection="row" marginBottom={1}>
|
||||
{!hideArrows && (
|
||||
<Text color={currentQuestionIndex === 0 ? 'inactive' : undefined}>
|
||||
←{' '}
|
||||
</Text>
|
||||
)}
|
||||
{questions.map((q: Question, index: number) => {
|
||||
const isSelected = index === currentQuestionIndex
|
||||
const isAnswered = q?.question && !!answers[q.question]
|
||||
const checkbox = isAnswered ? figures.checkboxOn : figures.checkboxOff
|
||||
const displayText =
|
||||
tabDisplayTexts[index] || q?.header || `Q${index + 1}`
|
||||
|
||||
return (
|
||||
<Box key={q?.question || `question-${index}`}>
|
||||
{isSelected ? (
|
||||
<Text backgroundColor="permission" color="inverseText">
|
||||
{' '}
|
||||
{checkbox} {displayText}{' '}
|
||||
</Text>
|
||||
) : (
|
||||
<Text>
|
||||
{' '}
|
||||
{checkbox} {displayText}{' '}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
})}
|
||||
{!hideSubmitTab && (
|
||||
<Box key="submit">
|
||||
{currentQuestionIndex === questions.length ? (
|
||||
<Text backgroundColor="permission" color="inverseText">
|
||||
{' '}
|
||||
{figures.tick} Submit{' '}
|
||||
</Text>
|
||||
) : (
|
||||
<Text> {figures.tick} Submit </Text>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
{!hideArrows && (
|
||||
<Text
|
||||
color={
|
||||
currentQuestionIndex === questions.length ? 'inactive' : undefined
|
||||
}
|
||||
t2 = tabHeaders.map(t3);
|
||||
}
|
||||
$[0] = columns;
|
||||
$[1] = currentQuestionIndex;
|
||||
$[2] = hideSubmitTab;
|
||||
$[3] = questions;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
const tabDisplayTexts = t2;
|
||||
const hideArrows = questions.length === 1 && hideSubmitTab;
|
||||
let t3;
|
||||
if ($[14] !== currentQuestionIndex || $[15] !== hideArrows) {
|
||||
t3 = !hideArrows && <Text color={currentQuestionIndex === 0 ? "inactive" : undefined}>←{" "}</Text>;
|
||||
$[14] = currentQuestionIndex;
|
||||
$[15] = hideArrows;
|
||||
$[16] = t3;
|
||||
} else {
|
||||
t3 = $[16];
|
||||
}
|
||||
let t4;
|
||||
if ($[17] !== answers || $[18] !== currentQuestionIndex || $[19] !== questions || $[20] !== tabDisplayTexts) {
|
||||
let t5;
|
||||
if ($[22] !== answers || $[23] !== currentQuestionIndex || $[24] !== tabDisplayTexts) {
|
||||
t5 = (q_1, index_2) => {
|
||||
const isSelected = index_2 === currentQuestionIndex;
|
||||
const isAnswered = q_1?.question && !!answers[q_1.question];
|
||||
const checkbox = isAnswered ? figures.checkboxOn : figures.checkboxOff;
|
||||
const displayText = tabDisplayTexts[index_2] || q_1?.header || `Q${index_2 + 1}`;
|
||||
return <Box key={q_1?.question || `question-${index_2}`}>{isSelected ? <Text backgroundColor="permission" color="inverseText">{" "}{checkbox} {displayText}{" "}</Text> : <Text>{" "}{checkbox} {displayText}{" "}</Text>}</Box>;
|
||||
};
|
||||
$[22] = answers;
|
||||
$[23] = currentQuestionIndex;
|
||||
$[24] = tabDisplayTexts;
|
||||
$[25] = t5;
|
||||
} else {
|
||||
t5 = $[25];
|
||||
}
|
||||
t4 = questions.map(t5);
|
||||
$[17] = answers;
|
||||
$[18] = currentQuestionIndex;
|
||||
$[19] = questions;
|
||||
$[20] = tabDisplayTexts;
|
||||
$[21] = t4;
|
||||
} else {
|
||||
t4 = $[21];
|
||||
}
|
||||
let t5;
|
||||
if ($[26] !== currentQuestionIndex || $[27] !== hideSubmitTab || $[28] !== questions.length) {
|
||||
t5 = !hideSubmitTab && <Box key="submit">{currentQuestionIndex === questions.length ? <Text backgroundColor="permission" color="inverseText">{" "}{figures.tick} Submit{" "}</Text> : <Text> {figures.tick} Submit </Text>}</Box>;
|
||||
$[26] = currentQuestionIndex;
|
||||
$[27] = hideSubmitTab;
|
||||
$[28] = questions.length;
|
||||
$[29] = t5;
|
||||
} else {
|
||||
t5 = $[29];
|
||||
}
|
||||
let t6;
|
||||
if ($[30] !== currentQuestionIndex || $[31] !== hideArrows || $[32] !== questions.length) {
|
||||
t6 = !hideArrows && <Text color={currentQuestionIndex === questions.length ? "inactive" : undefined}>{" "}→</Text>;
|
||||
$[30] = currentQuestionIndex;
|
||||
$[31] = hideArrows;
|
||||
$[32] = questions.length;
|
||||
$[33] = t6;
|
||||
} else {
|
||||
t6 = $[33];
|
||||
}
|
||||
let t7;
|
||||
if ($[34] !== t3 || $[35] !== t4 || $[36] !== t5 || $[37] !== t6) {
|
||||
t7 = <Box flexDirection="row" marginBottom={1}>{t3}{t4}{t5}{t6}</Box>;
|
||||
$[34] = t3;
|
||||
$[35] = t4;
|
||||
$[36] = t5;
|
||||
$[37] = t6;
|
||||
$[38] = t7;
|
||||
} else {
|
||||
t7 = $[38];
|
||||
}
|
||||
return t7;
|
||||
}
|
||||
function _temp3(sum, w) {
|
||||
return sum + w;
|
||||
}
|
||||
function _temp2(header_0) {
|
||||
return 4 + stringWidth(header_0);
|
||||
}
|
||||
function _temp(q_0, index_0) {
|
||||
return q_0?.header || `Q${index_0 + 1}`;
|
||||
>
|
||||
{' '}
|
||||
→
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,54 +1,75 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import figures from 'figures';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import type { KeyboardEvent } from '../../../ink/events/keyboard-event.js';
|
||||
import { Box, Text } from '../../../ink.js';
|
||||
import { useAppState } from '../../../state/AppState.js';
|
||||
import type { Question, QuestionOption } from '../../../tools/AskUserQuestionTool/AskUserQuestionTool.js';
|
||||
import type { PastedContent } from '../../../utils/config.js';
|
||||
import { getExternalEditor } from '../../../utils/editor.js';
|
||||
import { toIDEDisplayName } from '../../../utils/ide.js';
|
||||
import type { ImageDimensions } from '../../../utils/imageResizer.js';
|
||||
import { editPromptInEditor } from '../../../utils/promptEditor.js';
|
||||
import { type OptionWithDescription, Select, SelectMulti } from '../../CustomSelect/index.js';
|
||||
import { Divider } from '../../design-system/Divider.js';
|
||||
import { FilePathLink } from '../../FilePathLink.js';
|
||||
import { PermissionRequestTitle } from '../PermissionRequestTitle.js';
|
||||
import { PreviewQuestionView } from './PreviewQuestionView.js';
|
||||
import { QuestionNavigationBar } from './QuestionNavigationBar.js';
|
||||
import type { QuestionState } from './use-multiple-choice-state.js';
|
||||
import figures from 'figures'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import type { KeyboardEvent } from '../../../ink/events/keyboard-event.js'
|
||||
import { Box, Text } from '../../../ink.js'
|
||||
import { useAppState } from '../../../state/AppState.js'
|
||||
import type {
|
||||
Question,
|
||||
QuestionOption,
|
||||
} from '../../../tools/AskUserQuestionTool/AskUserQuestionTool.js'
|
||||
import type { PastedContent } from '../../../utils/config.js'
|
||||
import { getExternalEditor } from '../../../utils/editor.js'
|
||||
import { toIDEDisplayName } from '../../../utils/ide.js'
|
||||
import type { ImageDimensions } from '../../../utils/imageResizer.js'
|
||||
import { editPromptInEditor } from '../../../utils/promptEditor.js'
|
||||
import {
|
||||
type OptionWithDescription,
|
||||
Select,
|
||||
SelectMulti,
|
||||
} from '../../CustomSelect/index.js'
|
||||
import { Divider } from '../../design-system/Divider.js'
|
||||
import { FilePathLink } from '../../FilePathLink.js'
|
||||
import { PermissionRequestTitle } from '../PermissionRequestTitle.js'
|
||||
import { PreviewQuestionView } from './PreviewQuestionView.js'
|
||||
import { QuestionNavigationBar } from './QuestionNavigationBar.js'
|
||||
import type { QuestionState } from './use-multiple-choice-state.js'
|
||||
|
||||
type Props = {
|
||||
question: Question;
|
||||
questions: Question[];
|
||||
currentQuestionIndex: number;
|
||||
answers: Record<string, string>;
|
||||
questionStates: Record<string, QuestionState>;
|
||||
hideSubmitTab?: boolean;
|
||||
planFilePath?: string;
|
||||
pastedContents?: Record<number, PastedContent>;
|
||||
minContentHeight?: number;
|
||||
minContentWidth?: number;
|
||||
onUpdateQuestionState: (questionText: string, updates: Partial<QuestionState>, isMultiSelect: boolean) => void;
|
||||
onAnswer: (questionText: string, label: string | string[], textInput?: string, shouldAdvance?: boolean) => void;
|
||||
onTextInputFocus: (isInInput: boolean) => void;
|
||||
onCancel: () => void;
|
||||
onSubmit: () => void;
|
||||
onTabPrev?: () => void;
|
||||
onTabNext?: () => void;
|
||||
onRespondToClaude: () => void;
|
||||
onFinishPlanInterview: () => void;
|
||||
onImagePaste?: (base64Image: string, mediaType?: string, filename?: string, dimensions?: ImageDimensions, sourcePath?: string) => void;
|
||||
onRemoveImage?: (id: number) => void;
|
||||
};
|
||||
export function QuestionView(t0) {
|
||||
const $ = _c(114);
|
||||
const {
|
||||
question: Question
|
||||
questions: Question[]
|
||||
currentQuestionIndex: number
|
||||
answers: Record<string, string>
|
||||
questionStates: Record<string, QuestionState>
|
||||
hideSubmitTab?: boolean
|
||||
planFilePath?: string
|
||||
pastedContents?: Record<number, PastedContent>
|
||||
minContentHeight?: number
|
||||
minContentWidth?: number
|
||||
onUpdateQuestionState: (
|
||||
questionText: string,
|
||||
updates: Partial<QuestionState>,
|
||||
isMultiSelect: boolean,
|
||||
) => void
|
||||
onAnswer: (
|
||||
questionText: string,
|
||||
label: string | string[],
|
||||
textInput?: string,
|
||||
shouldAdvance?: boolean,
|
||||
) => void
|
||||
onTextInputFocus: (isInInput: boolean) => void
|
||||
onCancel: () => void
|
||||
onSubmit: () => void
|
||||
onTabPrev?: () => void
|
||||
onTabNext?: () => void
|
||||
onRespondToClaude: () => void
|
||||
onFinishPlanInterview: () => void
|
||||
onImagePaste?: (
|
||||
base64Image: string,
|
||||
mediaType?: string,
|
||||
filename?: string,
|
||||
dimensions?: ImageDimensions,
|
||||
sourcePath?: string,
|
||||
) => void
|
||||
onRemoveImage?: (id: number) => void
|
||||
}
|
||||
|
||||
export function QuestionView({
|
||||
question,
|
||||
questions,
|
||||
currentQuestionIndex,
|
||||
answers,
|
||||
questionStates,
|
||||
hideSubmitTab: t1,
|
||||
hideSubmitTab = false,
|
||||
planFilePath,
|
||||
minContentHeight,
|
||||
minContentWidth,
|
||||
@@ -63,402 +84,315 @@ export function QuestionView(t0) {
|
||||
onFinishPlanInterview,
|
||||
onImagePaste,
|
||||
pastedContents,
|
||||
onRemoveImage
|
||||
} = t0;
|
||||
const hideSubmitTab = t1 === undefined ? false : t1;
|
||||
const isInPlanMode = useAppState(_temp) === "plan";
|
||||
const [isFooterFocused, setIsFooterFocused] = useState(false);
|
||||
const [footerIndex, setFooterIndex] = useState(0);
|
||||
const [isOtherFocused, setIsOtherFocused] = useState(false);
|
||||
let t2;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
const editor = getExternalEditor();
|
||||
t2 = editor ? toIDEDisplayName(editor) : null;
|
||||
$[0] = t2;
|
||||
} else {
|
||||
t2 = $[0];
|
||||
}
|
||||
const editorName = t2;
|
||||
let t3;
|
||||
if ($[1] !== onTextInputFocus) {
|
||||
t3 = value => {
|
||||
const isOther = value === "__other__";
|
||||
setIsOtherFocused(isOther);
|
||||
onTextInputFocus(isOther);
|
||||
};
|
||||
$[1] = onTextInputFocus;
|
||||
$[2] = t3;
|
||||
} else {
|
||||
t3 = $[2];
|
||||
}
|
||||
const handleFocus = t3;
|
||||
let t4;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = () => {
|
||||
setIsFooterFocused(true);
|
||||
};
|
||||
$[3] = t4;
|
||||
} else {
|
||||
t4 = $[3];
|
||||
}
|
||||
const handleDownFromLastItem = t4;
|
||||
let t5;
|
||||
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t5 = () => {
|
||||
setIsFooterFocused(false);
|
||||
};
|
||||
$[4] = t5;
|
||||
} else {
|
||||
t5 = $[4];
|
||||
}
|
||||
const handleUpFromFooter = t5;
|
||||
let t6;
|
||||
if ($[5] !== footerIndex || $[6] !== isFooterFocused || $[7] !== isInPlanMode || $[8] !== onCancel || $[9] !== onFinishPlanInterview || $[10] !== onRespondToClaude) {
|
||||
t6 = e => {
|
||||
if (!isFooterFocused) {
|
||||
return;
|
||||
}
|
||||
if (e.key === "up" || e.ctrl && e.key === "p") {
|
||||
e.preventDefault();
|
||||
onRemoveImage,
|
||||
}: Props): React.ReactNode {
|
||||
const isInPlanMode = useAppState(s => s.toolPermissionContext.mode) === 'plan'
|
||||
const [isFooterFocused, setIsFooterFocused] = useState(false)
|
||||
const [footerIndex, setFooterIndex] = useState(0)
|
||||
const [isOtherFocused, setIsOtherFocused] = useState(false)
|
||||
|
||||
const editor = getExternalEditor()
|
||||
const editorName = editor ? toIDEDisplayName(editor) : null
|
||||
|
||||
const handleFocus = useCallback(
|
||||
(value: string) => {
|
||||
const isOther = value === '__other__'
|
||||
setIsOtherFocused(isOther)
|
||||
onTextInputFocus(isOther)
|
||||
},
|
||||
[onTextInputFocus],
|
||||
)
|
||||
|
||||
const handleDownFromLastItem = useCallback(() => {
|
||||
setIsFooterFocused(true)
|
||||
}, [])
|
||||
|
||||
const handleUpFromFooter = useCallback(() => {
|
||||
setIsFooterFocused(false)
|
||||
}, [])
|
||||
|
||||
// Handle keyboard input when footer is focused
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (!isFooterFocused) return
|
||||
|
||||
if (e.key === 'up' || (e.ctrl && e.key === 'p')) {
|
||||
e.preventDefault()
|
||||
if (footerIndex === 0) {
|
||||
handleUpFromFooter();
|
||||
handleUpFromFooter()
|
||||
} else {
|
||||
setFooterIndex(0);
|
||||
setFooterIndex(0)
|
||||
}
|
||||
return;
|
||||
return
|
||||
}
|
||||
if (e.key === "down" || e.ctrl && e.key === "n") {
|
||||
e.preventDefault();
|
||||
|
||||
if (e.key === 'down' || (e.ctrl && e.key === 'n')) {
|
||||
e.preventDefault()
|
||||
if (isInPlanMode && footerIndex === 0) {
|
||||
setFooterIndex(1);
|
||||
setFooterIndex(1)
|
||||
}
|
||||
return;
|
||||
return
|
||||
}
|
||||
if (e.key === "return") {
|
||||
e.preventDefault();
|
||||
|
||||
if (e.key === 'return') {
|
||||
e.preventDefault()
|
||||
if (footerIndex === 0) {
|
||||
onRespondToClaude();
|
||||
onRespondToClaude()
|
||||
} else {
|
||||
onFinishPlanInterview();
|
||||
onFinishPlanInterview()
|
||||
}
|
||||
return;
|
||||
return
|
||||
}
|
||||
if (e.key === "escape") {
|
||||
e.preventDefault();
|
||||
onCancel();
|
||||
|
||||
if (e.key === 'escape') {
|
||||
e.preventDefault()
|
||||
onCancel()
|
||||
}
|
||||
};
|
||||
$[5] = footerIndex;
|
||||
$[6] = isFooterFocused;
|
||||
$[7] = isInPlanMode;
|
||||
$[8] = onCancel;
|
||||
$[9] = onFinishPlanInterview;
|
||||
$[10] = onRespondToClaude;
|
||||
$[11] = t6;
|
||||
} else {
|
||||
t6 = $[11];
|
||||
}
|
||||
const handleKeyDown = t6;
|
||||
let handleOpenEditor;
|
||||
let questionText;
|
||||
let t7;
|
||||
if ($[12] !== onUpdateQuestionState || $[13] !== question || $[14] !== questionStates) {
|
||||
const textOptions = question.options.map(_temp2);
|
||||
questionText = question.question;
|
||||
const questionState = questionStates[questionText];
|
||||
let t8;
|
||||
if ($[18] !== onUpdateQuestionState || $[19] !== question.multiSelect || $[20] !== questionText) {
|
||||
t8 = async (currentValue, setValue) => {
|
||||
const result = await editPromptInEditor(currentValue);
|
||||
if (result.content !== null && result.content !== currentValue) {
|
||||
setValue(result.content);
|
||||
onUpdateQuestionState(questionText, {
|
||||
textInputValue: result.content
|
||||
}, question.multiSelect ?? false);
|
||||
}
|
||||
};
|
||||
$[18] = onUpdateQuestionState;
|
||||
$[19] = question.multiSelect;
|
||||
$[20] = questionText;
|
||||
$[21] = t8;
|
||||
} else {
|
||||
t8 = $[21];
|
||||
}
|
||||
handleOpenEditor = t8;
|
||||
const t9 = question.multiSelect ? "Type something" : "Type something.";
|
||||
const t10 = questionState?.textInputValue ?? "";
|
||||
let t11;
|
||||
if ($[22] !== onUpdateQuestionState || $[23] !== question.multiSelect || $[24] !== questionText) {
|
||||
t11 = value_0 => {
|
||||
onUpdateQuestionState(questionText, {
|
||||
textInputValue: value_0
|
||||
}, question.multiSelect ?? false);
|
||||
};
|
||||
$[22] = onUpdateQuestionState;
|
||||
$[23] = question.multiSelect;
|
||||
$[24] = questionText;
|
||||
$[25] = t11;
|
||||
} else {
|
||||
t11 = $[25];
|
||||
}
|
||||
let t12;
|
||||
if ($[26] !== t10 || $[27] !== t11 || $[28] !== t9) {
|
||||
t12 = {
|
||||
type: "input" as const,
|
||||
value: "__other__",
|
||||
label: "Other",
|
||||
placeholder: t9,
|
||||
initialValue: t10,
|
||||
onChange: t11
|
||||
};
|
||||
$[26] = t10;
|
||||
$[27] = t11;
|
||||
$[28] = t9;
|
||||
$[29] = t12;
|
||||
} else {
|
||||
t12 = $[29];
|
||||
}
|
||||
const otherOption = t12;
|
||||
t7 = [...textOptions, otherOption];
|
||||
$[12] = onUpdateQuestionState;
|
||||
$[13] = question;
|
||||
$[14] = questionStates;
|
||||
$[15] = handleOpenEditor;
|
||||
$[16] = questionText;
|
||||
$[17] = t7;
|
||||
} else {
|
||||
handleOpenEditor = $[15];
|
||||
questionText = $[16];
|
||||
t7 = $[17];
|
||||
}
|
||||
const options = t7;
|
||||
const hasAnyPreview = !question.multiSelect && question.options.some(_temp3);
|
||||
if (hasAnyPreview) {
|
||||
let t8;
|
||||
if ($[30] !== answers || $[31] !== currentQuestionIndex || $[32] !== hideSubmitTab || $[33] !== minContentHeight || $[34] !== minContentWidth || $[35] !== onAnswer || $[36] !== onCancel || $[37] !== onFinishPlanInterview || $[38] !== onRespondToClaude || $[39] !== onTabNext || $[40] !== onTabPrev || $[41] !== onTextInputFocus || $[42] !== onUpdateQuestionState || $[43] !== question || $[44] !== questionStates || $[45] !== questions) {
|
||||
t8 = <PreviewQuestionView question={question} questions={questions} currentQuestionIndex={currentQuestionIndex} answers={answers} questionStates={questionStates} hideSubmitTab={hideSubmitTab} minContentHeight={minContentHeight} minContentWidth={minContentWidth} onUpdateQuestionState={onUpdateQuestionState} onAnswer={onAnswer} onTextInputFocus={onTextInputFocus} onCancel={onCancel} onTabPrev={onTabPrev} onTabNext={onTabNext} onRespondToClaude={onRespondToClaude} onFinishPlanInterview={onFinishPlanInterview} />;
|
||||
$[30] = answers;
|
||||
$[31] = currentQuestionIndex;
|
||||
$[32] = hideSubmitTab;
|
||||
$[33] = minContentHeight;
|
||||
$[34] = minContentWidth;
|
||||
$[35] = onAnswer;
|
||||
$[36] = onCancel;
|
||||
$[37] = onFinishPlanInterview;
|
||||
$[38] = onRespondToClaude;
|
||||
$[39] = onTabNext;
|
||||
$[40] = onTabPrev;
|
||||
$[41] = onTextInputFocus;
|
||||
$[42] = onUpdateQuestionState;
|
||||
$[43] = question;
|
||||
$[44] = questionStates;
|
||||
$[45] = questions;
|
||||
$[46] = t8;
|
||||
} else {
|
||||
t8 = $[46];
|
||||
}
|
||||
return t8;
|
||||
}
|
||||
let t8;
|
||||
if ($[47] !== isInPlanMode || $[48] !== planFilePath) {
|
||||
t8 = isInPlanMode && planFilePath && <Box flexDirection="column" gap={0}><Divider color="inactive" /><Text color="inactive">Planning: <FilePathLink filePath={planFilePath} /></Text></Box>;
|
||||
$[47] = isInPlanMode;
|
||||
$[48] = planFilePath;
|
||||
$[49] = t8;
|
||||
} else {
|
||||
t8 = $[49];
|
||||
}
|
||||
let t9;
|
||||
if ($[50] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t9 = <Box marginTop={-1}><Divider color="inactive" /></Box>;
|
||||
$[50] = t9;
|
||||
} else {
|
||||
t9 = $[50];
|
||||
}
|
||||
let t10;
|
||||
if ($[51] !== answers || $[52] !== currentQuestionIndex || $[53] !== hideSubmitTab || $[54] !== questions) {
|
||||
t10 = <QuestionNavigationBar questions={questions} currentQuestionIndex={currentQuestionIndex} answers={answers} hideSubmitTab={hideSubmitTab} />;
|
||||
$[51] = answers;
|
||||
$[52] = currentQuestionIndex;
|
||||
$[53] = hideSubmitTab;
|
||||
$[54] = questions;
|
||||
$[55] = t10;
|
||||
} else {
|
||||
t10 = $[55];
|
||||
}
|
||||
let t11;
|
||||
if ($[56] !== question.question) {
|
||||
t11 = <PermissionRequestTitle title={question.question} color="text" />;
|
||||
$[56] = question.question;
|
||||
$[57] = t11;
|
||||
} else {
|
||||
t11 = $[57];
|
||||
}
|
||||
let t12;
|
||||
if ($[58] !== currentQuestionIndex || $[59] !== handleFocus || $[60] !== handleOpenEditor || $[61] !== isFooterFocused || $[62] !== onAnswer || $[63] !== onCancel || $[64] !== onImagePaste || $[65] !== onRemoveImage || $[66] !== onSubmit || $[67] !== onUpdateQuestionState || $[68] !== options || $[69] !== pastedContents || $[70] !== question.multiSelect || $[71] !== question.question || $[72] !== questionStates || $[73] !== questionText || $[74] !== questions.length) {
|
||||
t12 = <Box marginTop={1}>{question.multiSelect ? <SelectMulti key={question.question} options={options} defaultValue={questionStates[question.question]?.selectedValue as string[] | undefined} onChange={values => {
|
||||
onUpdateQuestionState(questionText, {
|
||||
selectedValue: values
|
||||
}, true);
|
||||
const textInput = values.includes("__other__") ? questionStates[questionText]?.textInputValue : undefined;
|
||||
const finalValues = values.filter(_temp4).concat(textInput ? [textInput] : []);
|
||||
onAnswer(questionText, finalValues, undefined, false);
|
||||
}} onFocus={handleFocus} onCancel={onCancel} submitButtonText={currentQuestionIndex === questions.length - 1 ? "Submit" : "Next"} onSubmit={onSubmit} onDownFromLastItem={handleDownFromLastItem} isDisabled={isFooterFocused} onOpenEditor={handleOpenEditor} onImagePaste={onImagePaste} pastedContents={pastedContents} onRemoveImage={onRemoveImage} /> : <Select key={question.question} options={options} defaultValue={questionStates[question.question]?.selectedValue as string | undefined} onChange={value_1 => {
|
||||
onUpdateQuestionState(questionText, {
|
||||
selectedValue: value_1
|
||||
}, false);
|
||||
const textInput_0 = value_1 === "__other__" ? questionStates[questionText]?.textInputValue : undefined;
|
||||
onAnswer(questionText, value_1, textInput_0);
|
||||
}} onFocus={handleFocus} onCancel={onCancel} onDownFromLastItem={handleDownFromLastItem} isDisabled={isFooterFocused} layout="compact-vertical" onOpenEditor={handleOpenEditor} onImagePaste={onImagePaste} pastedContents={pastedContents} onRemoveImage={onRemoveImage} />}</Box>;
|
||||
$[58] = currentQuestionIndex;
|
||||
$[59] = handleFocus;
|
||||
$[60] = handleOpenEditor;
|
||||
$[61] = isFooterFocused;
|
||||
$[62] = onAnswer;
|
||||
$[63] = onCancel;
|
||||
$[64] = onImagePaste;
|
||||
$[65] = onRemoveImage;
|
||||
$[66] = onSubmit;
|
||||
$[67] = onUpdateQuestionState;
|
||||
$[68] = options;
|
||||
$[69] = pastedContents;
|
||||
$[70] = question.multiSelect;
|
||||
$[71] = question.question;
|
||||
$[72] = questionStates;
|
||||
$[73] = questionText;
|
||||
$[74] = questions.length;
|
||||
$[75] = t12;
|
||||
} else {
|
||||
t12 = $[75];
|
||||
}
|
||||
let t13;
|
||||
if ($[76] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t13 = <Divider color="inactive" />;
|
||||
$[76] = t13;
|
||||
} else {
|
||||
t13 = $[76];
|
||||
}
|
||||
let t14;
|
||||
if ($[77] !== footerIndex || $[78] !== isFooterFocused) {
|
||||
t14 = isFooterFocused && footerIndex === 0 ? <Text color="suggestion">{figures.pointer}</Text> : <Text> </Text>;
|
||||
$[77] = footerIndex;
|
||||
$[78] = isFooterFocused;
|
||||
$[79] = t14;
|
||||
} else {
|
||||
t14 = $[79];
|
||||
}
|
||||
const t15 = isFooterFocused && footerIndex === 0 ? "suggestion" : undefined;
|
||||
const t16 = options.length + 1;
|
||||
let t17;
|
||||
if ($[80] !== t15 || $[81] !== t16) {
|
||||
t17 = <Text color={t15}>{t16}. Chat about this</Text>;
|
||||
$[80] = t15;
|
||||
$[81] = t16;
|
||||
$[82] = t17;
|
||||
} else {
|
||||
t17 = $[82];
|
||||
}
|
||||
let t18;
|
||||
if ($[83] !== t14 || $[84] !== t17) {
|
||||
t18 = <Box flexDirection="row" gap={1}>{t14}{t17}</Box>;
|
||||
$[83] = t14;
|
||||
$[84] = t17;
|
||||
$[85] = t18;
|
||||
} else {
|
||||
t18 = $[85];
|
||||
}
|
||||
let t19;
|
||||
if ($[86] !== footerIndex || $[87] !== isFooterFocused || $[88] !== isInPlanMode || $[89] !== options.length) {
|
||||
t19 = isInPlanMode && <Box flexDirection="row" gap={1}>{isFooterFocused && footerIndex === 1 ? <Text color="suggestion">{figures.pointer}</Text> : <Text> </Text>}<Text color={isFooterFocused && footerIndex === 1 ? "suggestion" : undefined}>{options.length + 2}. Skip interview and plan immediately</Text></Box>;
|
||||
$[86] = footerIndex;
|
||||
$[87] = isFooterFocused;
|
||||
$[88] = isInPlanMode;
|
||||
$[89] = options.length;
|
||||
$[90] = t19;
|
||||
} else {
|
||||
t19 = $[90];
|
||||
}
|
||||
let t20;
|
||||
if ($[91] !== t18 || $[92] !== t19) {
|
||||
t20 = <Box flexDirection="column">{t13}{t18}{t19}</Box>;
|
||||
$[91] = t18;
|
||||
$[92] = t19;
|
||||
$[93] = t20;
|
||||
} else {
|
||||
t20 = $[93];
|
||||
}
|
||||
let t21;
|
||||
if ($[94] !== questions.length) {
|
||||
t21 = questions.length === 1 ? <>{figures.arrowUp}/{figures.arrowDown} to navigate</> : "Tab/Arrow keys to navigate";
|
||||
$[94] = questions.length;
|
||||
$[95] = t21;
|
||||
} else {
|
||||
t21 = $[95];
|
||||
}
|
||||
let t22;
|
||||
if ($[96] !== isOtherFocused) {
|
||||
t22 = isOtherFocused && editorName && <> · ctrl+g to edit in {editorName}</>;
|
||||
$[96] = isOtherFocused;
|
||||
$[97] = t22;
|
||||
} else {
|
||||
t22 = $[97];
|
||||
}
|
||||
let t23;
|
||||
if ($[98] !== t21 || $[99] !== t22) {
|
||||
t23 = <Box marginTop={1}><Text color="inactive" dimColor={true}>Enter to select ·{" "}{t21}{t22}{" "}· Esc to cancel</Text></Box>;
|
||||
$[98] = t21;
|
||||
$[99] = t22;
|
||||
$[100] = t23;
|
||||
} else {
|
||||
t23 = $[100];
|
||||
}
|
||||
let t24;
|
||||
if ($[101] !== minContentHeight || $[102] !== t12 || $[103] !== t20 || $[104] !== t23) {
|
||||
t24 = <Box flexDirection="column" minHeight={minContentHeight}>{t12}{t20}{t23}</Box>;
|
||||
$[101] = minContentHeight;
|
||||
$[102] = t12;
|
||||
$[103] = t20;
|
||||
$[104] = t23;
|
||||
$[105] = t24;
|
||||
} else {
|
||||
t24 = $[105];
|
||||
}
|
||||
let t25;
|
||||
if ($[106] !== t10 || $[107] !== t11 || $[108] !== t24) {
|
||||
t25 = <Box flexDirection="column" paddingTop={0}>{t10}{t11}{t24}</Box>;
|
||||
$[106] = t10;
|
||||
$[107] = t11;
|
||||
$[108] = t24;
|
||||
$[109] = t25;
|
||||
} else {
|
||||
t25 = $[109];
|
||||
}
|
||||
let t26;
|
||||
if ($[110] !== handleKeyDown || $[111] !== t25 || $[112] !== t8) {
|
||||
t26 = <Box flexDirection="column" marginTop={0} tabIndex={0} autoFocus={true} onKeyDown={handleKeyDown}>{t8}{t9}{t25}</Box>;
|
||||
$[110] = handleKeyDown;
|
||||
$[111] = t25;
|
||||
$[112] = t8;
|
||||
$[113] = t26;
|
||||
} else {
|
||||
t26 = $[113];
|
||||
}
|
||||
return t26;
|
||||
}
|
||||
function _temp4(v) {
|
||||
return v !== "__other__";
|
||||
}
|
||||
function _temp3(opt_0) {
|
||||
return opt_0.preview;
|
||||
}
|
||||
function _temp2(opt) {
|
||||
return {
|
||||
type: "text" as const,
|
||||
},
|
||||
[
|
||||
isFooterFocused,
|
||||
footerIndex,
|
||||
isInPlanMode,
|
||||
handleUpFromFooter,
|
||||
onRespondToClaude,
|
||||
onFinishPlanInterview,
|
||||
onCancel,
|
||||
],
|
||||
)
|
||||
|
||||
const textOptions: OptionWithDescription<string>[] = question.options.map(
|
||||
(opt: QuestionOption) => ({
|
||||
type: 'text' as const,
|
||||
value: opt.label,
|
||||
label: opt.label,
|
||||
description: opt.description
|
||||
};
|
||||
}
|
||||
function _temp(s) {
|
||||
return s.toolPermissionContext.mode;
|
||||
description: opt.description,
|
||||
}),
|
||||
)
|
||||
|
||||
const questionText = question.question
|
||||
const questionState = questionStates[questionText]
|
||||
|
||||
const handleOpenEditor = useCallback(
|
||||
async (currentValue: string, setValue: (value: string) => void) => {
|
||||
const result = await editPromptInEditor(currentValue)
|
||||
|
||||
if (result.content !== null && result.content !== currentValue) {
|
||||
// Update the Select's internal state for immediate UI update
|
||||
setValue(result.content)
|
||||
// Also update the question state for persistence
|
||||
onUpdateQuestionState(
|
||||
questionText,
|
||||
{ textInputValue: result.content },
|
||||
question.multiSelect ?? false,
|
||||
)
|
||||
}
|
||||
},
|
||||
[questionText, onUpdateQuestionState, question.multiSelect],
|
||||
)
|
||||
|
||||
const otherOption: OptionWithDescription<string> = {
|
||||
type: 'input' as const,
|
||||
value: '__other__',
|
||||
label: 'Other',
|
||||
placeholder: question.multiSelect ? 'Type something' : 'Type something.',
|
||||
initialValue: questionState?.textInputValue ?? '',
|
||||
onChange: (value: string) => {
|
||||
onUpdateQuestionState(
|
||||
questionText,
|
||||
{ textInputValue: value },
|
||||
question.multiSelect ?? false,
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
const options = [...textOptions, otherOption]
|
||||
|
||||
// Check if any option has a preview and it's not multi-select
|
||||
// Previews only supported for single-select questions
|
||||
const hasAnyPreview =
|
||||
!question.multiSelect && question.options.some(opt => opt.preview)
|
||||
|
||||
// Delegate to PreviewQuestionView for carousel-style preview mode
|
||||
if (hasAnyPreview) {
|
||||
return (
|
||||
<PreviewQuestionView
|
||||
question={question}
|
||||
questions={questions}
|
||||
currentQuestionIndex={currentQuestionIndex}
|
||||
answers={answers}
|
||||
questionStates={questionStates}
|
||||
hideSubmitTab={hideSubmitTab}
|
||||
minContentHeight={minContentHeight}
|
||||
minContentWidth={minContentWidth}
|
||||
onUpdateQuestionState={onUpdateQuestionState}
|
||||
onAnswer={onAnswer}
|
||||
onTextInputFocus={onTextInputFocus}
|
||||
onCancel={onCancel}
|
||||
onTabPrev={onTabPrev}
|
||||
onTabNext={onTabNext}
|
||||
onRespondToClaude={onRespondToClaude}
|
||||
onFinishPlanInterview={onFinishPlanInterview}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
marginTop={0}
|
||||
tabIndex={0}
|
||||
autoFocus
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
{isInPlanMode && planFilePath && (
|
||||
<Box flexDirection="column" gap={0}>
|
||||
<Divider color="inactive" />
|
||||
<Text color="inactive">
|
||||
Planning: <FilePathLink filePath={planFilePath} />
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
<Box marginTop={-1}>
|
||||
<Divider color="inactive" />
|
||||
</Box>
|
||||
<Box flexDirection="column" paddingTop={0}>
|
||||
<QuestionNavigationBar
|
||||
questions={questions}
|
||||
currentQuestionIndex={currentQuestionIndex}
|
||||
answers={answers}
|
||||
hideSubmitTab={hideSubmitTab}
|
||||
/>
|
||||
<PermissionRequestTitle title={question.question} color={'text'} />
|
||||
|
||||
<Box flexDirection="column" minHeight={minContentHeight}>
|
||||
<Box marginTop={1}>
|
||||
{question.multiSelect ? (
|
||||
<SelectMulti
|
||||
key={question.question}
|
||||
options={options}
|
||||
defaultValue={
|
||||
questionStates[question.question]?.selectedValue as
|
||||
| string[]
|
||||
| undefined
|
||||
}
|
||||
onChange={(values: string[]) => {
|
||||
onUpdateQuestionState(
|
||||
questionText,
|
||||
{ selectedValue: values },
|
||||
true,
|
||||
)
|
||||
const textInput = values.includes('__other__')
|
||||
? questionStates[questionText]?.textInputValue
|
||||
: undefined
|
||||
const finalValues = values
|
||||
.filter(v => v !== '__other__')
|
||||
.concat(textInput ? [textInput] : [])
|
||||
onAnswer(questionText, finalValues, undefined, false)
|
||||
}}
|
||||
onFocus={handleFocus}
|
||||
onCancel={onCancel}
|
||||
submitButtonText={
|
||||
currentQuestionIndex === questions.length - 1
|
||||
? 'Submit'
|
||||
: 'Next'
|
||||
}
|
||||
onSubmit={onSubmit}
|
||||
onDownFromLastItem={handleDownFromLastItem}
|
||||
isDisabled={isFooterFocused}
|
||||
onOpenEditor={handleOpenEditor}
|
||||
onImagePaste={onImagePaste}
|
||||
pastedContents={pastedContents}
|
||||
onRemoveImage={onRemoveImage}
|
||||
/>
|
||||
) : (
|
||||
<Select
|
||||
key={question.question}
|
||||
options={options}
|
||||
defaultValue={
|
||||
questionStates[question.question]?.selectedValue as
|
||||
| string
|
||||
| undefined
|
||||
}
|
||||
onChange={(value: string) => {
|
||||
onUpdateQuestionState(
|
||||
questionText,
|
||||
{ selectedValue: value },
|
||||
false,
|
||||
)
|
||||
const textInput =
|
||||
value === '__other__'
|
||||
? questionStates[questionText]?.textInputValue
|
||||
: undefined
|
||||
onAnswer(questionText, value, textInput)
|
||||
}}
|
||||
onFocus={handleFocus}
|
||||
onCancel={onCancel}
|
||||
onDownFromLastItem={handleDownFromLastItem}
|
||||
isDisabled={isFooterFocused}
|
||||
layout="compact-vertical"
|
||||
onOpenEditor={handleOpenEditor}
|
||||
onImagePaste={onImagePaste}
|
||||
pastedContents={pastedContents}
|
||||
onRemoveImage={onRemoveImage}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
{/* Footer section - always visible, separate from Select */}
|
||||
<Box flexDirection="column">
|
||||
<Divider color="inactive" />
|
||||
<Box flexDirection="row" gap={1}>
|
||||
{isFooterFocused && footerIndex === 0 ? (
|
||||
<Text color="suggestion">{figures.pointer}</Text>
|
||||
) : (
|
||||
<Text> </Text>
|
||||
)}
|
||||
<Text
|
||||
color={
|
||||
isFooterFocused && footerIndex === 0
|
||||
? 'suggestion'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{options.length + 1}. Chat about this
|
||||
</Text>
|
||||
</Box>
|
||||
{isInPlanMode && (
|
||||
<Box flexDirection="row" gap={1}>
|
||||
{isFooterFocused && footerIndex === 1 ? (
|
||||
<Text color="suggestion">{figures.pointer}</Text>
|
||||
) : (
|
||||
<Text> </Text>
|
||||
)}
|
||||
<Text
|
||||
color={
|
||||
isFooterFocused && footerIndex === 1
|
||||
? 'suggestion'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{options.length + 2}. Skip interview and plan immediately
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<Box marginTop={1}>
|
||||
<Text color="inactive" dimColor>
|
||||
Enter to select ·{' '}
|
||||
{questions.length === 1 ? (
|
||||
<>
|
||||
{figures.arrowUp}/{figures.arrowDown} to navigate
|
||||
</>
|
||||
) : (
|
||||
'Tab/Arrow keys to navigate'
|
||||
)}
|
||||
{isOtherFocused && editorName && (
|
||||
<> · ctrl+g to edit in {editorName}</>
|
||||
)}{' '}
|
||||
· Esc to cancel
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,143 +1,104 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import figures from 'figures';
|
||||
import React from 'react';
|
||||
import { Box, Text } from '../../../ink.js';
|
||||
import type { Question } from '../../../tools/AskUserQuestionTool/AskUserQuestionTool.js';
|
||||
import type { PermissionDecision } from '../../../utils/permissions/PermissionResult.js';
|
||||
import { Select } from '../../CustomSelect/index.js';
|
||||
import { Divider } from '../../design-system/Divider.js';
|
||||
import { PermissionRequestTitle } from '../PermissionRequestTitle.js';
|
||||
import { PermissionRuleExplanation } from '../PermissionRuleExplanation.js';
|
||||
import { QuestionNavigationBar } from './QuestionNavigationBar.js';
|
||||
import figures from 'figures'
|
||||
import React from 'react'
|
||||
import { Box, Text } from '../../../ink.js'
|
||||
import type { Question } from '../../../tools/AskUserQuestionTool/AskUserQuestionTool.js'
|
||||
import type { PermissionDecision } from '../../../utils/permissions/PermissionResult.js'
|
||||
import { Select } from '../../CustomSelect/index.js'
|
||||
import { Divider } from '../../design-system/Divider.js'
|
||||
import { PermissionRequestTitle } from '../PermissionRequestTitle.js'
|
||||
import { PermissionRuleExplanation } from '../PermissionRuleExplanation.js'
|
||||
import { QuestionNavigationBar } from './QuestionNavigationBar.js'
|
||||
|
||||
type Props = {
|
||||
questions: Question[];
|
||||
currentQuestionIndex: number;
|
||||
answers: Record<string, string>;
|
||||
allQuestionsAnswered: boolean;
|
||||
permissionResult: PermissionDecision;
|
||||
minContentHeight?: number;
|
||||
onFinalResponse: (value: 'submit' | 'cancel') => void;
|
||||
};
|
||||
export function SubmitQuestionsView(t0) {
|
||||
const $ = _c(27);
|
||||
const {
|
||||
questions: Question[]
|
||||
currentQuestionIndex: number
|
||||
answers: Record<string, string>
|
||||
allQuestionsAnswered: boolean
|
||||
permissionResult: PermissionDecision
|
||||
minContentHeight?: number
|
||||
onFinalResponse: (value: 'submit' | 'cancel') => void
|
||||
}
|
||||
|
||||
export function SubmitQuestionsView({
|
||||
questions,
|
||||
currentQuestionIndex,
|
||||
answers,
|
||||
allQuestionsAnswered,
|
||||
permissionResult,
|
||||
minContentHeight,
|
||||
onFinalResponse
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <Divider color="inactive" />;
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
let t2;
|
||||
if ($[1] !== answers || $[2] !== currentQuestionIndex || $[3] !== questions) {
|
||||
t2 = <QuestionNavigationBar questions={questions} currentQuestionIndex={currentQuestionIndex} answers={answers} />;
|
||||
$[1] = answers;
|
||||
$[2] = currentQuestionIndex;
|
||||
$[3] = questions;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
let t3;
|
||||
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = <PermissionRequestTitle title="Review your answers" color="text" />;
|
||||
$[5] = t3;
|
||||
} else {
|
||||
t3 = $[5];
|
||||
}
|
||||
let t4;
|
||||
if ($[6] !== allQuestionsAnswered) {
|
||||
t4 = !allQuestionsAnswered && <Box marginBottom={1}><Text color="warning">{figures.warning} You have not answered all questions</Text></Box>;
|
||||
$[6] = allQuestionsAnswered;
|
||||
$[7] = t4;
|
||||
} else {
|
||||
t4 = $[7];
|
||||
}
|
||||
let t5;
|
||||
if ($[8] !== answers || $[9] !== questions) {
|
||||
t5 = Object.keys(answers).length > 0 && <Box flexDirection="column" marginBottom={1}>{questions.filter(q => q?.question && answers[q.question]).map(q_0 => {
|
||||
const answer = answers[q_0?.question];
|
||||
return <Box key={q_0?.question || "answer"} flexDirection="column" marginLeft={1}><Text>{figures.bullet} {q_0?.question || "Question"}</Text><Box marginLeft={2}><Text color="success">{figures.arrowRight} {answer}</Text></Box></Box>;
|
||||
})}</Box>;
|
||||
$[8] = answers;
|
||||
$[9] = questions;
|
||||
$[10] = t5;
|
||||
} else {
|
||||
t5 = $[10];
|
||||
}
|
||||
let t6;
|
||||
if ($[11] !== permissionResult) {
|
||||
t6 = <PermissionRuleExplanation permissionResult={permissionResult} toolType="tool" />;
|
||||
$[11] = permissionResult;
|
||||
$[12] = t6;
|
||||
} else {
|
||||
t6 = $[12];
|
||||
}
|
||||
let t7;
|
||||
if ($[13] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t7 = <Text color="inactive">Ready to submit your answers?</Text>;
|
||||
$[13] = t7;
|
||||
} else {
|
||||
t7 = $[13];
|
||||
}
|
||||
let t8;
|
||||
if ($[14] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t8 = {
|
||||
type: "text" as const,
|
||||
label: "Submit answers",
|
||||
value: "submit"
|
||||
};
|
||||
$[14] = t8;
|
||||
} else {
|
||||
t8 = $[14];
|
||||
}
|
||||
let t9;
|
||||
if ($[15] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t9 = [t8, {
|
||||
type: "text" as const,
|
||||
label: "Cancel",
|
||||
value: "cancel"
|
||||
}];
|
||||
$[15] = t9;
|
||||
} else {
|
||||
t9 = $[15];
|
||||
}
|
||||
let t10;
|
||||
if ($[16] !== onFinalResponse) {
|
||||
t10 = <Box marginTop={1}><Select options={t9} onChange={value => onFinalResponse(value as 'submit' | 'cancel')} onCancel={() => onFinalResponse("cancel")} /></Box>;
|
||||
$[16] = onFinalResponse;
|
||||
$[17] = t10;
|
||||
} else {
|
||||
t10 = $[17];
|
||||
}
|
||||
let t11;
|
||||
if ($[18] !== minContentHeight || $[19] !== t10 || $[20] !== t4 || $[21] !== t5 || $[22] !== t6) {
|
||||
t11 = <Box flexDirection="column" marginTop={1} minHeight={minContentHeight}>{t4}{t5}{t6}{t7}{t10}</Box>;
|
||||
$[18] = minContentHeight;
|
||||
$[19] = t10;
|
||||
$[20] = t4;
|
||||
$[21] = t5;
|
||||
$[22] = t6;
|
||||
$[23] = t11;
|
||||
} else {
|
||||
t11 = $[23];
|
||||
}
|
||||
let t12;
|
||||
if ($[24] !== t11 || $[25] !== t2) {
|
||||
t12 = <Box flexDirection="column" marginTop={1}>{t1}<Box flexDirection="column" borderTop={true} borderColor="inactive" paddingTop={0}>{t2}{t3}{t11}</Box></Box>;
|
||||
$[24] = t11;
|
||||
$[25] = t2;
|
||||
$[26] = t12;
|
||||
} else {
|
||||
t12 = $[26];
|
||||
}
|
||||
return t12;
|
||||
onFinalResponse,
|
||||
}: Props): React.ReactNode {
|
||||
return (
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<Divider color="inactive" />
|
||||
<Box
|
||||
flexDirection="column"
|
||||
borderTop
|
||||
borderColor="inactive"
|
||||
paddingTop={0}
|
||||
>
|
||||
<QuestionNavigationBar
|
||||
questions={questions}
|
||||
currentQuestionIndex={currentQuestionIndex}
|
||||
answers={answers}
|
||||
/>
|
||||
<PermissionRequestTitle title="Review your answers" color="text" />
|
||||
<Box flexDirection="column" marginTop={1} minHeight={minContentHeight}>
|
||||
{!allQuestionsAnswered && (
|
||||
<Box marginBottom={1}>
|
||||
<Text color="warning">
|
||||
{figures.warning} You have not answered all questions
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
{Object.keys(answers).length > 0 && (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
{questions
|
||||
.filter((q: Question) => q?.question && answers[q.question])
|
||||
.map((q: Question) => {
|
||||
const answer = answers[q?.question]
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={q?.question || 'answer'}
|
||||
flexDirection="column"
|
||||
marginLeft={1}
|
||||
>
|
||||
<Text>
|
||||
{figures.bullet} {q?.question || 'Question'}
|
||||
</Text>
|
||||
<Box marginLeft={2}>
|
||||
<Text color="success">
|
||||
{figures.arrowRight} {answer}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<PermissionRuleExplanation
|
||||
permissionResult={permissionResult}
|
||||
toolType="tool"
|
||||
/>
|
||||
<Text color="inactive">Ready to submit your answers?</Text>
|
||||
<Box marginTop={1}>
|
||||
<Select
|
||||
options={[
|
||||
{
|
||||
type: 'text' as const,
|
||||
label: 'Submit answers',
|
||||
value: 'submit',
|
||||
},
|
||||
{ type: 'text' as const, label: 'Cancel', value: 'cancel' },
|
||||
]}
|
||||
onChange={value => onFinalResponse(value as 'submit' | 'cancel')}
|
||||
onCancel={() => onFinalResponse('cancel')}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,37 +1,51 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { feature } from 'bun:bundle';
|
||||
import figures from 'figures';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Box, Text, useTheme } from '../../../ink.js';
|
||||
import { useKeybinding } from '../../../keybindings/useKeybinding.js';
|
||||
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../../services/analytics/growthbook.js';
|
||||
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../../services/analytics/index.js';
|
||||
import { sanitizeToolNameForAnalytics } from '../../../services/analytics/metadata.js';
|
||||
import { useAppState } from '../../../state/AppState.js';
|
||||
import { BashTool } from '../../../tools/BashTool/BashTool.js';
|
||||
import { getFirstWordPrefix, getSimpleCommandPrefix } from '../../../tools/BashTool/bashPermissions.js';
|
||||
import { getDestructiveCommandWarning } from '../../../tools/BashTool/destructiveCommandWarning.js';
|
||||
import { parseSedEditCommand } from '../../../tools/BashTool/sedEditParser.js';
|
||||
import { shouldUseSandbox } from '../../../tools/BashTool/shouldUseSandbox.js';
|
||||
import { getCompoundCommandPrefixesStatic } from '../../../utils/bash/prefix.js';
|
||||
import { createPromptRuleContent, generateGenericDescription, getBashPromptAllowDescriptions, isClassifierPermissionsEnabled } from '../../../utils/permissions/bashClassifier.js';
|
||||
import { extractRules } from '../../../utils/permissions/PermissionUpdate.js';
|
||||
import type { PermissionUpdate } from '../../../utils/permissions/PermissionUpdateSchema.js';
|
||||
import { SandboxManager } from '../../../utils/sandbox/sandbox-adapter.js';
|
||||
import { Select } from '../../CustomSelect/select.js';
|
||||
import { ShimmerChar } from '../../Spinner/ShimmerChar.js';
|
||||
import { useShimmerAnimation } from '../../Spinner/useShimmerAnimation.js';
|
||||
import { type UnaryEvent, usePermissionRequestLogging } from '../hooks.js';
|
||||
import { PermissionDecisionDebugInfo } from '../PermissionDecisionDebugInfo.js';
|
||||
import { PermissionDialog } from '../PermissionDialog.js';
|
||||
import { PermissionExplainerContent, usePermissionExplainerUI } from '../PermissionExplanation.js';
|
||||
import type { PermissionRequestProps } from '../PermissionRequest.js';
|
||||
import { PermissionRuleExplanation } from '../PermissionRuleExplanation.js';
|
||||
import { SedEditPermissionRequest } from '../SedEditPermissionRequest/SedEditPermissionRequest.js';
|
||||
import { useShellPermissionFeedback } from '../useShellPermissionFeedback.js';
|
||||
import { logUnaryPermissionEvent } from '../utils.js';
|
||||
import { bashToolUseOptions } from './bashToolUseOptions.js';
|
||||
const CHECKING_TEXT = 'Attempting to auto-approve\u2026';
|
||||
import { feature } from 'bun:bundle'
|
||||
import figures from 'figures'
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Box, Text, useTheme } from '../../../ink.js'
|
||||
import { useKeybinding } from '../../../keybindings/useKeybinding.js'
|
||||
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../../services/analytics/growthbook.js'
|
||||
import {
|
||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
logEvent,
|
||||
} from '../../../services/analytics/index.js'
|
||||
import { sanitizeToolNameForAnalytics } from '../../../services/analytics/metadata.js'
|
||||
import { useAppState } from '../../../state/AppState.js'
|
||||
import { BashTool } from '../../../tools/BashTool/BashTool.js'
|
||||
import {
|
||||
getFirstWordPrefix,
|
||||
getSimpleCommandPrefix,
|
||||
} from '../../../tools/BashTool/bashPermissions.js'
|
||||
import { getDestructiveCommandWarning } from '../../../tools/BashTool/destructiveCommandWarning.js'
|
||||
import { parseSedEditCommand } from '../../../tools/BashTool/sedEditParser.js'
|
||||
import { shouldUseSandbox } from '../../../tools/BashTool/shouldUseSandbox.js'
|
||||
import { getCompoundCommandPrefixesStatic } from '../../../utils/bash/prefix.js'
|
||||
import {
|
||||
createPromptRuleContent,
|
||||
generateGenericDescription,
|
||||
getBashPromptAllowDescriptions,
|
||||
isClassifierPermissionsEnabled,
|
||||
} from '../../../utils/permissions/bashClassifier.js'
|
||||
import { extractRules } from '../../../utils/permissions/PermissionUpdate.js'
|
||||
import type { PermissionUpdate } from '../../../utils/permissions/PermissionUpdateSchema.js'
|
||||
import { SandboxManager } from '../../../utils/sandbox/sandbox-adapter.js'
|
||||
import { Select } from '../../CustomSelect/select.js'
|
||||
import { ShimmerChar } from '../../Spinner/ShimmerChar.js'
|
||||
import { useShimmerAnimation } from '../../Spinner/useShimmerAnimation.js'
|
||||
import { type UnaryEvent, usePermissionRequestLogging } from '../hooks.js'
|
||||
import { PermissionDecisionDebugInfo } from '../PermissionDecisionDebugInfo.js'
|
||||
import { PermissionDialog } from '../PermissionDialog.js'
|
||||
import {
|
||||
PermissionExplainerContent,
|
||||
usePermissionExplainerUI,
|
||||
} from '../PermissionExplanation.js'
|
||||
import type { PermissionRequestProps } from '../PermissionRequest.js'
|
||||
import { PermissionRuleExplanation } from '../PermissionRuleExplanation.js'
|
||||
import { SedEditPermissionRequest } from '../SedEditPermissionRequest/SedEditPermissionRequest.js'
|
||||
import { useShellPermissionFeedback } from '../useShellPermissionFeedback.js'
|
||||
import { logUnaryPermissionEvent } from '../utils.js'
|
||||
import { bashToolUseOptions } from './bashToolUseOptions.js'
|
||||
|
||||
const CHECKING_TEXT = 'Attempting to auto-approve\u2026'
|
||||
|
||||
// Isolates the 20fps shimmer clock from BashPermissionRequestInner. Before this
|
||||
// extraction, useShimmerAnimation lived inside the 535-line Inner body, so every
|
||||
@@ -39,97 +53,77 @@ const CHECKING_TEXT = 'Attempting to auto-approve\u2026';
|
||||
// all children) for the ~1-3 seconds the classifier typically takes. Inner also
|
||||
// has a Compiler bailout (see below), so nothing was auto-memoized — the full
|
||||
// JSX tree was reconstructed 20-60 times per classifier check.
|
||||
function ClassifierCheckingSubtitle() {
|
||||
const $ = _c(6);
|
||||
const [ref, glimmerIndex] = useShimmerAnimation("requesting", CHECKING_TEXT, false);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = [...CHECKING_TEXT];
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
let t1;
|
||||
if ($[1] !== glimmerIndex) {
|
||||
t1 = <Text>{t0.map((char, i) => <ShimmerChar key={i} char={char} index={i} glimmerIndex={glimmerIndex} messageColor="inactive" shimmerColor="subtle" />)}</Text>;
|
||||
$[1] = glimmerIndex;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
let t2;
|
||||
if ($[3] !== ref || $[4] !== t1) {
|
||||
t2 = <Box ref={ref}>{t1}</Box>;
|
||||
$[3] = ref;
|
||||
$[4] = t1;
|
||||
$[5] = t2;
|
||||
} else {
|
||||
t2 = $[5];
|
||||
}
|
||||
return t2;
|
||||
function ClassifierCheckingSubtitle(): React.ReactNode {
|
||||
const [ref, glimmerIndex] = useShimmerAnimation(
|
||||
'requesting',
|
||||
CHECKING_TEXT,
|
||||
false,
|
||||
)
|
||||
return (
|
||||
<Box ref={ref}>
|
||||
<Text>
|
||||
{[...CHECKING_TEXT].map((char, i) => (
|
||||
<ShimmerChar
|
||||
key={i}
|
||||
char={char}
|
||||
index={i}
|
||||
glimmerIndex={glimmerIndex}
|
||||
messageColor="inactive"
|
||||
shimmerColor="subtle"
|
||||
/>
|
||||
))}
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
export function BashPermissionRequest(props) {
|
||||
const $ = _c(21);
|
||||
|
||||
export function BashPermissionRequest(
|
||||
props: PermissionRequestProps,
|
||||
): React.ReactNode {
|
||||
const {
|
||||
toolUseConfirm,
|
||||
toolUseContext,
|
||||
onDone,
|
||||
onReject,
|
||||
verbose,
|
||||
workerBadge
|
||||
} = props;
|
||||
let command;
|
||||
let description;
|
||||
let t0;
|
||||
if ($[0] !== toolUseConfirm.input) {
|
||||
({
|
||||
command,
|
||||
description
|
||||
} = BashTool.inputSchema.parse(toolUseConfirm.input));
|
||||
t0 = parseSedEditCommand(command);
|
||||
$[0] = toolUseConfirm.input;
|
||||
$[1] = command;
|
||||
$[2] = description;
|
||||
$[3] = t0;
|
||||
} else {
|
||||
command = $[1];
|
||||
description = $[2];
|
||||
t0 = $[3];
|
||||
}
|
||||
const sedInfo = t0;
|
||||
workerBadge,
|
||||
} = props
|
||||
|
||||
const { command, description } = BashTool.inputSchema.parse(
|
||||
toolUseConfirm.input,
|
||||
)
|
||||
|
||||
// Detect sed in-place edit commands and delegate to SedEditPermissionRequest
|
||||
// This renders sed edits like file edits with a diff view
|
||||
const sedInfo = parseSedEditCommand(command)
|
||||
|
||||
if (sedInfo) {
|
||||
let t1;
|
||||
if ($[4] !== onDone || $[5] !== onReject || $[6] !== sedInfo || $[7] !== toolUseConfirm || $[8] !== toolUseContext || $[9] !== verbose || $[10] !== workerBadge) {
|
||||
t1 = <SedEditPermissionRequest toolUseConfirm={toolUseConfirm} toolUseContext={toolUseContext} onDone={onDone} onReject={onReject} verbose={verbose} workerBadge={workerBadge} sedInfo={sedInfo} />;
|
||||
$[4] = onDone;
|
||||
$[5] = onReject;
|
||||
$[6] = sedInfo;
|
||||
$[7] = toolUseConfirm;
|
||||
$[8] = toolUseContext;
|
||||
$[9] = verbose;
|
||||
$[10] = workerBadge;
|
||||
$[11] = t1;
|
||||
} else {
|
||||
t1 = $[11];
|
||||
return (
|
||||
<SedEditPermissionRequest
|
||||
toolUseConfirm={toolUseConfirm}
|
||||
toolUseContext={toolUseContext}
|
||||
onDone={onDone}
|
||||
onReject={onReject}
|
||||
verbose={verbose}
|
||||
workerBadge={workerBadge}
|
||||
sedInfo={sedInfo}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
let t1;
|
||||
if ($[12] !== command || $[13] !== description || $[14] !== onDone || $[15] !== onReject || $[16] !== toolUseConfirm || $[17] !== toolUseContext || $[18] !== verbose || $[19] !== workerBadge) {
|
||||
t1 = <BashPermissionRequestInner toolUseConfirm={toolUseConfirm} toolUseContext={toolUseContext} onDone={onDone} onReject={onReject} verbose={verbose} workerBadge={workerBadge} command={command} description={description} />;
|
||||
$[12] = command;
|
||||
$[13] = description;
|
||||
$[14] = onDone;
|
||||
$[15] = onReject;
|
||||
$[16] = toolUseConfirm;
|
||||
$[17] = toolUseContext;
|
||||
$[18] = verbose;
|
||||
$[19] = workerBadge;
|
||||
$[20] = t1;
|
||||
} else {
|
||||
t1 = $[20];
|
||||
}
|
||||
return t1;
|
||||
|
||||
// Regular bash command - render with hooks
|
||||
return (
|
||||
<BashPermissionRequestInner
|
||||
toolUseConfirm={toolUseConfirm}
|
||||
toolUseContext={toolUseContext}
|
||||
onDone={onDone}
|
||||
onReject={onReject}
|
||||
verbose={verbose}
|
||||
workerBadge={workerBadge}
|
||||
command={command}
|
||||
description={description}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// Inner component that uses hooks - only called for non-MCP CLI commands
|
||||
@@ -141,19 +135,19 @@ function BashPermissionRequestInner({
|
||||
verbose: _verbose,
|
||||
workerBadge,
|
||||
command,
|
||||
description
|
||||
description,
|
||||
}: PermissionRequestProps & {
|
||||
command: string;
|
||||
description?: string;
|
||||
command: string
|
||||
description?: string
|
||||
}): React.ReactNode {
|
||||
const [theme] = useTheme();
|
||||
const toolPermissionContext = useAppState(s => s.toolPermissionContext);
|
||||
const [theme] = useTheme()
|
||||
const toolPermissionContext = useAppState(s => s.toolPermissionContext)
|
||||
const explainerState = usePermissionExplainerUI({
|
||||
toolName: toolUseConfirm.tool.name,
|
||||
toolInput: toolUseConfirm.input,
|
||||
toolDescription: toolUseConfirm.description,
|
||||
messages: toolUseContext.messages
|
||||
});
|
||||
messages: toolUseContext.messages,
|
||||
})
|
||||
const {
|
||||
yesInputMode,
|
||||
noInputMode,
|
||||
@@ -166,31 +160,39 @@ function BashPermissionRequestInner({
|
||||
focusedOption,
|
||||
handleInputModeToggle,
|
||||
handleReject,
|
||||
handleFocus
|
||||
handleFocus,
|
||||
} = useShellPermissionFeedback({
|
||||
toolUseConfirm,
|
||||
onDone,
|
||||
onReject,
|
||||
explainerVisible: explainerState.visible
|
||||
});
|
||||
const [showPermissionDebug, setShowPermissionDebug] = useState(false);
|
||||
const [classifierDescription, setClassifierDescription] = useState(description || '');
|
||||
explainerVisible: explainerState.visible,
|
||||
})
|
||||
const [showPermissionDebug, setShowPermissionDebug] = useState(false)
|
||||
const [classifierDescription, setClassifierDescription] = useState(
|
||||
description || '',
|
||||
)
|
||||
// Track whether the initial description (from prop or async generation) was empty.
|
||||
// Once we receive a non-empty description, this stays false.
|
||||
const [initialClassifierDescriptionEmpty, setInitialClassifierDescriptionEmpty] = useState(!description?.trim());
|
||||
const [
|
||||
initialClassifierDescriptionEmpty,
|
||||
setInitialClassifierDescriptionEmpty,
|
||||
] = useState(!description?.trim())
|
||||
|
||||
// Asynchronously generate a generic description for the classifier
|
||||
useEffect(() => {
|
||||
if (!isClassifierPermissionsEnabled()) return;
|
||||
const abortController = new AbortController();
|
||||
generateGenericDescription(command, description, abortController.signal).then(generic => {
|
||||
if (!isClassifierPermissionsEnabled()) return
|
||||
|
||||
const abortController = new AbortController()
|
||||
generateGenericDescription(command, description, abortController.signal)
|
||||
.then(generic => {
|
||||
if (generic && !abortController.signal.aborted) {
|
||||
setClassifierDescription(generic);
|
||||
setInitialClassifierDescriptionEmpty(false);
|
||||
setClassifierDescription(generic)
|
||||
setInitialClassifierDescriptionEmpty(false)
|
||||
}
|
||||
}).catch(() => {}); // Keep original on error
|
||||
return () => abortController.abort();
|
||||
}, [command, description]);
|
||||
})
|
||||
.catch(() => {}) // Keep original on error
|
||||
return () => abortController.abort()
|
||||
}, [command, description])
|
||||
|
||||
// GH#11380: For compound commands (cd src && git status && npm test), the
|
||||
// backend already computed correct per-subcommand suggestions via tree-sitter
|
||||
@@ -206,7 +208,8 @@ function BashPermissionRequestInner({
|
||||
// from the backend rule. When compound with 2+ rules, editablePrefix stays
|
||||
// undefined so bashToolUseOptions falls through to yes-apply-suggestions,
|
||||
// which saves all per-subcommand rules atomically.
|
||||
const isCompound = toolUseConfirm.permissionResult.decisionReason?.type === 'subcommandResults';
|
||||
const isCompound =
|
||||
toolUseConfirm.permissionResult.decisionReason?.type === 'subcommandResults'
|
||||
|
||||
// Editable prefix — initialize synchronously with the best prefix we can
|
||||
// extract without tree-sitter, then refine via tree-sitter for compound
|
||||
@@ -216,49 +219,63 @@ function BashPermissionRequestInner({
|
||||
//
|
||||
// Lazy initializer: this runs regex + split on every render if left in
|
||||
// the render body; it's only needed for initial state.
|
||||
const [editablePrefix, setEditablePrefix] = useState<string | undefined>(() => {
|
||||
const [editablePrefix, setEditablePrefix] = useState<string | undefined>(
|
||||
() => {
|
||||
if (isCompound) {
|
||||
// Backend suggestion is the source of truth for compound commands.
|
||||
// Single rule → seed the editable input so the user can refine it.
|
||||
// Multiple/zero rules → undefined → yes-apply-suggestions handles it.
|
||||
const backendBashRules = extractRules('suggestions' in toolUseConfirm.permissionResult ? toolUseConfirm.permissionResult.suggestions : undefined).filter(r => r.toolName === BashTool.name && r.ruleContent);
|
||||
return backendBashRules.length === 1 ? backendBashRules[0]!.ruleContent : undefined;
|
||||
const backendBashRules = extractRules(
|
||||
'suggestions' in toolUseConfirm.permissionResult
|
||||
? toolUseConfirm.permissionResult.suggestions
|
||||
: undefined,
|
||||
).filter(r => r.toolName === BashTool.name && r.ruleContent)
|
||||
return backendBashRules.length === 1
|
||||
? backendBashRules[0]!.ruleContent
|
||||
: undefined
|
||||
}
|
||||
const two = getSimpleCommandPrefix(command);
|
||||
if (two) return `${two}:*`;
|
||||
const one = getFirstWordPrefix(command);
|
||||
if (one) return `${one}:*`;
|
||||
return command;
|
||||
});
|
||||
const hasUserEditedPrefix = useRef(false);
|
||||
const two = getSimpleCommandPrefix(command)
|
||||
if (two) return `${two}:*`
|
||||
const one = getFirstWordPrefix(command)
|
||||
if (one) return `${one}:*`
|
||||
return command
|
||||
},
|
||||
)
|
||||
const hasUserEditedPrefix = useRef(false)
|
||||
const onEditablePrefixChange = useCallback((value: string) => {
|
||||
hasUserEditedPrefix.current = true;
|
||||
setEditablePrefix(value);
|
||||
}, []);
|
||||
hasUserEditedPrefix.current = true
|
||||
setEditablePrefix(value)
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
// Skip async refinement for compound commands — the backend already ran
|
||||
// the full per-subcommand analysis and its suggestion is correct.
|
||||
if (isCompound) return;
|
||||
let cancelled = false;
|
||||
getCompoundCommandPrefixesStatic(command, subcmd => BashTool.isReadOnly({
|
||||
command: subcmd
|
||||
})).then(prefixes => {
|
||||
if (cancelled || hasUserEditedPrefix.current) return;
|
||||
if (isCompound) return
|
||||
let cancelled = false
|
||||
getCompoundCommandPrefixesStatic(command, subcmd =>
|
||||
BashTool.isReadOnly({ command: subcmd }),
|
||||
)
|
||||
.then(prefixes => {
|
||||
if (cancelled || hasUserEditedPrefix.current) return
|
||||
if (prefixes.length > 0) {
|
||||
setEditablePrefix(`${prefixes[0]}:*`);
|
||||
setEditablePrefix(`${prefixes[0]}:*`)
|
||||
}
|
||||
}).catch(() => {}); // Keep sync prefix on tree-sitter failure
|
||||
})
|
||||
.catch(() => {}) // Keep sync prefix on tree-sitter failure
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [command, isCompound]);
|
||||
cancelled = true
|
||||
}
|
||||
}, [command, isCompound])
|
||||
|
||||
// Track whether classifier check was ever in progress (persists after completion).
|
||||
// classifierCheckInProgress is set once at queue-push time (interactiveHandler)
|
||||
// and only ever transitions true→false, so capturing the mount-time value is
|
||||
// sufficient — no latch/ref needed. The feature() ternary keeps the property
|
||||
// read out of external builds (forbidden-string check).
|
||||
const [classifierWasChecking] = useState(feature('BASH_CLASSIFIER') ? !!toolUseConfirm.classifierCheckInProgress : false);
|
||||
const [classifierWasChecking] = useState(
|
||||
feature('BASH_CLASSIFIER')
|
||||
? !!toolUseConfirm.classifierCheckInProgress
|
||||
: false,
|
||||
)
|
||||
|
||||
// These derive solely from the tool input (fixed for the dialog lifetime).
|
||||
// The shimmer clock used to live in this component and re-render it at 20fps
|
||||
@@ -266,28 +283,40 @@ function BashPermissionRequestInner({
|
||||
// extraction). React Compiler can't auto-memoize imported functions (can't
|
||||
// prove side-effect freedom), so this useMemo still guards against any
|
||||
// re-render source (e.g. Inner state updates). Same pattern as PR#20730.
|
||||
const {
|
||||
destructiveWarning: destructiveWarning_0,
|
||||
sandboxingEnabled: sandboxingEnabled_0,
|
||||
isSandboxed: isSandboxed_0
|
||||
} = useMemo(() => {
|
||||
const destructiveWarning = getFeatureValue_CACHED_MAY_BE_STALE('tengu_destructive_command_warning', false) ? getDestructiveCommandWarning(command) : null;
|
||||
const sandboxingEnabled = SandboxManager.isSandboxingEnabled();
|
||||
const isSandboxed = sandboxingEnabled && shouldUseSandbox(toolUseConfirm.input);
|
||||
return {
|
||||
destructiveWarning,
|
||||
sandboxingEnabled,
|
||||
isSandboxed
|
||||
};
|
||||
}, [command, toolUseConfirm.input]);
|
||||
const unaryEvent = useMemo<UnaryEvent>(() => ({
|
||||
completion_type: 'tool_use_single',
|
||||
language_name: 'none'
|
||||
}), []);
|
||||
usePermissionRequestLogging(toolUseConfirm, unaryEvent);
|
||||
const existingAllowDescriptions = useMemo(() => getBashPromptAllowDescriptions(toolPermissionContext), [toolPermissionContext]);
|
||||
const options = useMemo(() => bashToolUseOptions({
|
||||
suggestions: toolUseConfirm.permissionResult.behavior === 'ask' ? toolUseConfirm.permissionResult.suggestions : undefined,
|
||||
const { destructiveWarning, sandboxingEnabled, isSandboxed } = useMemo(() => {
|
||||
const destructiveWarning = getFeatureValue_CACHED_MAY_BE_STALE(
|
||||
'tengu_destructive_command_warning',
|
||||
false,
|
||||
)
|
||||
? getDestructiveCommandWarning(command)
|
||||
: null
|
||||
|
||||
const sandboxingEnabled = SandboxManager.isSandboxingEnabled()
|
||||
const isSandboxed =
|
||||
sandboxingEnabled && shouldUseSandbox(toolUseConfirm.input)
|
||||
|
||||
return { destructiveWarning, sandboxingEnabled, isSandboxed }
|
||||
}, [command, toolUseConfirm.input])
|
||||
|
||||
const unaryEvent = useMemo<UnaryEvent>(
|
||||
() => ({ completion_type: 'tool_use_single', language_name: 'none' }),
|
||||
[],
|
||||
)
|
||||
|
||||
usePermissionRequestLogging(toolUseConfirm, unaryEvent)
|
||||
|
||||
const existingAllowDescriptions = useMemo(
|
||||
() => getBashPromptAllowDescriptions(toolPermissionContext),
|
||||
[toolPermissionContext],
|
||||
)
|
||||
|
||||
const options = useMemo(
|
||||
() =>
|
||||
bashToolUseOptions({
|
||||
suggestions:
|
||||
toolUseConfirm.permissionResult.behavior === 'ask'
|
||||
? toolUseConfirm.permissionResult.suggestions
|
||||
: undefined,
|
||||
decisionReason: toolUseConfirm.permissionResult.decisionReason,
|
||||
onRejectFeedbackChange: setRejectFeedback,
|
||||
onAcceptFeedbackChange: setAcceptFeedback,
|
||||
@@ -298,116 +327,148 @@ function BashPermissionRequestInner({
|
||||
yesInputMode,
|
||||
noInputMode,
|
||||
editablePrefix,
|
||||
onEditablePrefixChange
|
||||
}), [toolUseConfirm, classifierDescription, initialClassifierDescriptionEmpty, existingAllowDescriptions, yesInputMode, noInputMode, editablePrefix, onEditablePrefixChange]);
|
||||
onEditablePrefixChange,
|
||||
}),
|
||||
[
|
||||
toolUseConfirm,
|
||||
classifierDescription,
|
||||
initialClassifierDescriptionEmpty,
|
||||
existingAllowDescriptions,
|
||||
yesInputMode,
|
||||
noInputMode,
|
||||
editablePrefix,
|
||||
onEditablePrefixChange,
|
||||
],
|
||||
)
|
||||
|
||||
// Toggle permission debug info with keybinding
|
||||
const handleToggleDebug = useCallback(() => {
|
||||
setShowPermissionDebug(prev => !prev);
|
||||
}, []);
|
||||
setShowPermissionDebug(prev => !prev)
|
||||
}, [])
|
||||
useKeybinding('permission:toggleDebug', handleToggleDebug, {
|
||||
context: 'Confirmation'
|
||||
});
|
||||
context: 'Confirmation',
|
||||
})
|
||||
|
||||
// Allow Esc to dismiss the checkmark after auto-approval
|
||||
const handleDismissCheckmark = useCallback(() => {
|
||||
toolUseConfirm.onDismissCheckmark?.();
|
||||
}, [toolUseConfirm]);
|
||||
toolUseConfirm.onDismissCheckmark?.()
|
||||
}, [toolUseConfirm])
|
||||
useKeybinding('confirm:no', handleDismissCheckmark, {
|
||||
context: 'Confirmation',
|
||||
isActive: feature('BASH_CLASSIFIER') ? !!toolUseConfirm.classifierAutoApproved : false
|
||||
});
|
||||
function onSelect(value_0: string) {
|
||||
isActive: feature('BASH_CLASSIFIER')
|
||||
? !!toolUseConfirm.classifierAutoApproved
|
||||
: false,
|
||||
})
|
||||
|
||||
function onSelect(value: string) {
|
||||
// Map options to numeric values for analytics (strings not allowed in logEvent)
|
||||
let optionIndex: Record<string, number> = {
|
||||
yes: 1,
|
||||
'yes-apply-suggestions': 2,
|
||||
'yes-prefix-edited': 2,
|
||||
no: 3
|
||||
};
|
||||
no: 3,
|
||||
}
|
||||
if (feature('BASH_CLASSIFIER')) {
|
||||
optionIndex = {
|
||||
yes: 1,
|
||||
'yes-apply-suggestions': 2,
|
||||
'yes-prefix-edited': 2,
|
||||
'yes-classifier-reviewed': 3,
|
||||
no: 4
|
||||
};
|
||||
no: 4,
|
||||
}
|
||||
}
|
||||
logEvent('tengu_permission_request_option_selected', {
|
||||
option_index: optionIndex[value_0],
|
||||
explainer_visible: explainerState.visible
|
||||
});
|
||||
const toolNameForAnalytics = sanitizeToolNameForAnalytics(toolUseConfirm.tool.name) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS;
|
||||
if (value_0 === 'yes-prefix-edited') {
|
||||
const trimmedPrefix = (editablePrefix ?? '').trim();
|
||||
logUnaryPermissionEvent('tool_use_single', toolUseConfirm, 'accept');
|
||||
option_index: optionIndex[value],
|
||||
explainer_visible: explainerState.visible,
|
||||
})
|
||||
|
||||
const toolNameForAnalytics = sanitizeToolNameForAnalytics(
|
||||
toolUseConfirm.tool.name,
|
||||
) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
|
||||
if (value === 'yes-prefix-edited') {
|
||||
const trimmedPrefix = (editablePrefix ?? '').trim()
|
||||
logUnaryPermissionEvent('tool_use_single', toolUseConfirm, 'accept')
|
||||
if (!trimmedPrefix) {
|
||||
toolUseConfirm.onAllow(toolUseConfirm.input, []);
|
||||
toolUseConfirm.onAllow(toolUseConfirm.input, [])
|
||||
} else {
|
||||
const prefixUpdates: PermissionUpdate[] = [{
|
||||
type: 'addRules',
|
||||
rules: [{
|
||||
toolName: BashTool.name,
|
||||
ruleContent: trimmedPrefix
|
||||
}],
|
||||
behavior: 'allow',
|
||||
destination: 'localSettings'
|
||||
}];
|
||||
toolUseConfirm.onAllow(toolUseConfirm.input, prefixUpdates);
|
||||
}
|
||||
onDone();
|
||||
return;
|
||||
}
|
||||
if (feature('BASH_CLASSIFIER') && value_0 === 'yes-classifier-reviewed') {
|
||||
const trimmedDescription = classifierDescription.trim();
|
||||
logUnaryPermissionEvent('tool_use_single', toolUseConfirm, 'accept');
|
||||
if (!trimmedDescription) {
|
||||
toolUseConfirm.onAllow(toolUseConfirm.input, []);
|
||||
} else {
|
||||
const permissionUpdates: PermissionUpdate[] = [{
|
||||
type: 'addRules',
|
||||
rules: [{
|
||||
toolName: BashTool.name,
|
||||
ruleContent: createPromptRuleContent(trimmedDescription)
|
||||
}],
|
||||
behavior: 'allow',
|
||||
destination: 'session'
|
||||
}];
|
||||
toolUseConfirm.onAllow(toolUseConfirm.input, permissionUpdates);
|
||||
}
|
||||
onDone();
|
||||
return;
|
||||
}
|
||||
switch (value_0) {
|
||||
case 'yes':
|
||||
const prefixUpdates: PermissionUpdate[] = [
|
||||
{
|
||||
const trimmedFeedback_0 = acceptFeedback.trim();
|
||||
logUnaryPermissionEvent('tool_use_single', toolUseConfirm, 'accept');
|
||||
type: 'addRules',
|
||||
rules: [
|
||||
{
|
||||
toolName: BashTool.name,
|
||||
ruleContent: trimmedPrefix,
|
||||
},
|
||||
],
|
||||
behavior: 'allow',
|
||||
destination: 'localSettings',
|
||||
},
|
||||
]
|
||||
toolUseConfirm.onAllow(toolUseConfirm.input, prefixUpdates)
|
||||
}
|
||||
onDone()
|
||||
return
|
||||
}
|
||||
|
||||
if (feature('BASH_CLASSIFIER') && value === 'yes-classifier-reviewed') {
|
||||
const trimmedDescription = classifierDescription.trim()
|
||||
logUnaryPermissionEvent('tool_use_single', toolUseConfirm, 'accept')
|
||||
if (!trimmedDescription) {
|
||||
toolUseConfirm.onAllow(toolUseConfirm.input, [])
|
||||
} else {
|
||||
const permissionUpdates: PermissionUpdate[] = [
|
||||
{
|
||||
type: 'addRules',
|
||||
rules: [
|
||||
{
|
||||
toolName: BashTool.name,
|
||||
ruleContent: createPromptRuleContent(trimmedDescription),
|
||||
},
|
||||
],
|
||||
behavior: 'allow',
|
||||
destination: 'session',
|
||||
},
|
||||
]
|
||||
toolUseConfirm.onAllow(toolUseConfirm.input, permissionUpdates)
|
||||
}
|
||||
onDone()
|
||||
return
|
||||
}
|
||||
|
||||
switch (value) {
|
||||
case 'yes': {
|
||||
const trimmedFeedback = acceptFeedback.trim()
|
||||
logUnaryPermissionEvent('tool_use_single', toolUseConfirm, 'accept')
|
||||
// Log accept submission with feedback context
|
||||
logEvent('tengu_accept_submitted', {
|
||||
toolName: toolNameForAnalytics,
|
||||
isMcp: toolUseConfirm.tool.isMcp ?? false,
|
||||
has_instructions: !!trimmedFeedback_0,
|
||||
instructions_length: trimmedFeedback_0.length,
|
||||
entered_feedback_mode: yesFeedbackModeEntered
|
||||
});
|
||||
toolUseConfirm.onAllow(toolUseConfirm.input, [], trimmedFeedback_0 || undefined);
|
||||
onDone();
|
||||
break;
|
||||
has_instructions: !!trimmedFeedback,
|
||||
instructions_length: trimmedFeedback.length,
|
||||
entered_feedback_mode: yesFeedbackModeEntered,
|
||||
})
|
||||
toolUseConfirm.onAllow(
|
||||
toolUseConfirm.input,
|
||||
[],
|
||||
trimmedFeedback || undefined,
|
||||
)
|
||||
onDone()
|
||||
break
|
||||
}
|
||||
case 'yes-apply-suggestions':
|
||||
{
|
||||
logUnaryPermissionEvent('tool_use_single', toolUseConfirm, 'accept');
|
||||
case 'yes-apply-suggestions': {
|
||||
logUnaryPermissionEvent('tool_use_single', toolUseConfirm, 'accept')
|
||||
// Extract suggestions if present (works for both 'ask' and 'passthrough' behaviors)
|
||||
const permissionUpdates_0 = 'suggestions' in toolUseConfirm.permissionResult ? toolUseConfirm.permissionResult.suggestions || [] : [];
|
||||
toolUseConfirm.onAllow(toolUseConfirm.input, permissionUpdates_0);
|
||||
onDone();
|
||||
break;
|
||||
const permissionUpdates =
|
||||
'suggestions' in toolUseConfirm.permissionResult
|
||||
? toolUseConfirm.permissionResult.suggestions || []
|
||||
: []
|
||||
toolUseConfirm.onAllow(toolUseConfirm.input, permissionUpdates)
|
||||
onDone()
|
||||
break
|
||||
}
|
||||
case 'no':
|
||||
{
|
||||
const trimmedFeedback = rejectFeedback.trim();
|
||||
case 'no': {
|
||||
const trimmedFeedback = rejectFeedback.trim()
|
||||
|
||||
// Log reject submission with feedback context
|
||||
logEvent('tengu_reject_submitted', {
|
||||
@@ -415,67 +476,137 @@ function BashPermissionRequestInner({
|
||||
isMcp: toolUseConfirm.tool.isMcp ?? false,
|
||||
has_instructions: !!trimmedFeedback,
|
||||
instructions_length: trimmedFeedback.length,
|
||||
entered_feedback_mode: noFeedbackModeEntered
|
||||
});
|
||||
entered_feedback_mode: noFeedbackModeEntered,
|
||||
})
|
||||
|
||||
// Process rejection (with or without feedback)
|
||||
handleReject(trimmedFeedback || undefined);
|
||||
break;
|
||||
handleReject(trimmedFeedback || undefined)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
const classifierSubtitle = feature('BASH_CLASSIFIER') ? toolUseConfirm.classifierAutoApproved ? <Text>
|
||||
|
||||
const classifierSubtitle = feature('BASH_CLASSIFIER') ? (
|
||||
toolUseConfirm.classifierAutoApproved ? (
|
||||
<Text>
|
||||
<Text color="success">{figures.tick} Auto-approved</Text>
|
||||
{toolUseConfirm.classifierMatchedRule && <Text dimColor>
|
||||
{toolUseConfirm.classifierMatchedRule && (
|
||||
<Text dimColor>
|
||||
{' \u00b7 matched "'}
|
||||
{toolUseConfirm.classifierMatchedRule}
|
||||
{'"'}
|
||||
</Text>}
|
||||
</Text> : toolUseConfirm.classifierCheckInProgress ? <ClassifierCheckingSubtitle /> : classifierWasChecking ? <Text dimColor>Requires manual approval</Text> : undefined : undefined;
|
||||
return <PermissionDialog workerBadge={workerBadge} title={sandboxingEnabled_0 && !isSandboxed_0 ? 'Bash command (unsandboxed)' : 'Bash command'} subtitle={classifierSubtitle}>
|
||||
<Box flexDirection="column" paddingX={2} paddingY={1}>
|
||||
<Text dimColor={explainerState.visible}>
|
||||
{BashTool.renderToolUseMessage({
|
||||
command,
|
||||
description
|
||||
}, {
|
||||
theme,
|
||||
verbose: true
|
||||
} // always show the full command
|
||||
</Text>
|
||||
)}
|
||||
</Text>
|
||||
{!explainerState.visible && <Text dimColor>{toolUseConfirm.description}</Text>}
|
||||
<PermissionExplainerContent visible={explainerState.visible} promise={explainerState.promise} />
|
||||
</Box>
|
||||
{showPermissionDebug ? <>
|
||||
<PermissionDecisionDebugInfo permissionResult={toolUseConfirm.permissionResult} toolName="Bash" />
|
||||
{toolUseContext.options.debug && <Box justifyContent="flex-end" marginTop={1}>
|
||||
<Text dimColor>Ctrl-D to hide debug info</Text>
|
||||
</Box>}
|
||||
</> : <>
|
||||
<Box flexDirection="column">
|
||||
<PermissionRuleExplanation permissionResult={toolUseConfirm.permissionResult} toolType="command" />
|
||||
{destructiveWarning_0 && <Box marginBottom={1}>
|
||||
<Text color="warning" dimColor={feature('BASH_CLASSIFIER') ? toolUseConfirm.classifierAutoApproved : false}>
|
||||
{destructiveWarning_0}
|
||||
) : toolUseConfirm.classifierCheckInProgress ? (
|
||||
<ClassifierCheckingSubtitle />
|
||||
) : classifierWasChecking ? (
|
||||
<Text dimColor>Requires manual approval</Text>
|
||||
) : undefined
|
||||
) : undefined
|
||||
|
||||
return (
|
||||
<PermissionDialog
|
||||
workerBadge={workerBadge}
|
||||
title={
|
||||
sandboxingEnabled && !isSandboxed
|
||||
? 'Bash command (unsandboxed)'
|
||||
: 'Bash command'
|
||||
}
|
||||
subtitle={classifierSubtitle}
|
||||
>
|
||||
<Box flexDirection="column" paddingX={2} paddingY={1}>
|
||||
<Text dimColor={explainerState.visible}>
|
||||
{BashTool.renderToolUseMessage(
|
||||
{ command, description },
|
||||
{ theme, verbose: true }, // always show the full command
|
||||
)}
|
||||
</Text>
|
||||
</Box>}
|
||||
<Text dimColor={feature('BASH_CLASSIFIER') ? toolUseConfirm.classifierAutoApproved : false}>
|
||||
{!explainerState.visible && (
|
||||
<Text dimColor>{toolUseConfirm.description}</Text>
|
||||
)}
|
||||
<PermissionExplainerContent
|
||||
visible={explainerState.visible}
|
||||
promise={explainerState.promise}
|
||||
/>
|
||||
</Box>
|
||||
{showPermissionDebug ? (
|
||||
<>
|
||||
<PermissionDecisionDebugInfo
|
||||
permissionResult={toolUseConfirm.permissionResult}
|
||||
toolName="Bash"
|
||||
/>
|
||||
{toolUseContext.options.debug && (
|
||||
<Box justifyContent="flex-end" marginTop={1}>
|
||||
<Text dimColor>Ctrl-D to hide debug info</Text>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Box flexDirection="column">
|
||||
<PermissionRuleExplanation
|
||||
permissionResult={toolUseConfirm.permissionResult}
|
||||
toolType="command"
|
||||
/>
|
||||
{destructiveWarning && (
|
||||
<Box marginBottom={1}>
|
||||
<Text
|
||||
color="warning"
|
||||
dimColor={
|
||||
feature('BASH_CLASSIFIER')
|
||||
? toolUseConfirm.classifierAutoApproved
|
||||
: false
|
||||
}
|
||||
>
|
||||
{destructiveWarning}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
<Text
|
||||
dimColor={
|
||||
feature('BASH_CLASSIFIER')
|
||||
? toolUseConfirm.classifierAutoApproved
|
||||
: false
|
||||
}
|
||||
>
|
||||
Do you want to proceed?
|
||||
</Text>
|
||||
<Select options={feature('BASH_CLASSIFIER') ? toolUseConfirm.classifierAutoApproved ? options.map(o => ({
|
||||
...o,
|
||||
disabled: true
|
||||
})) : options : options} isDisabled={feature('BASH_CLASSIFIER') ? toolUseConfirm.classifierAutoApproved : false} inlineDescriptions onChange={onSelect} onCancel={() => handleReject()} onFocus={handleFocus} onInputModeToggle={handleInputModeToggle} />
|
||||
<Select
|
||||
options={
|
||||
feature('BASH_CLASSIFIER')
|
||||
? toolUseConfirm.classifierAutoApproved
|
||||
? options.map(o => ({ ...o, disabled: true }))
|
||||
: options
|
||||
: options
|
||||
}
|
||||
isDisabled={
|
||||
feature('BASH_CLASSIFIER')
|
||||
? toolUseConfirm.classifierAutoApproved
|
||||
: false
|
||||
}
|
||||
inlineDescriptions
|
||||
onChange={onSelect}
|
||||
onCancel={() => handleReject()}
|
||||
onFocus={handleFocus}
|
||||
onInputModeToggle={handleInputModeToggle}
|
||||
/>
|
||||
</Box>
|
||||
<Box justifyContent="space-between" marginTop={1}>
|
||||
<Text dimColor>
|
||||
Esc to cancel
|
||||
{(focusedOption === 'yes' && !yesInputMode || focusedOption === 'no' && !noInputMode) && ' · Tab to amend'}
|
||||
{explainerState.enabled && ` · ctrl+e to ${explainerState.visible ? 'hide' : 'explain'}`}
|
||||
{((focusedOption === 'yes' && !yesInputMode) ||
|
||||
(focusedOption === 'no' && !noInputMode)) &&
|
||||
' · Tab to amend'}
|
||||
{explainerState.enabled &&
|
||||
` · ctrl+e to ${explainerState.visible ? 'hide' : 'explain'}`}
|
||||
</Text>
|
||||
{toolUseContext.options.debug && <Text dimColor>Ctrl+d to show debug info</Text>}
|
||||
{toolUseContext.options.debug && (
|
||||
<Text dimColor>Ctrl+d to show debug info</Text>
|
||||
)}
|
||||
</Box>
|
||||
</>}
|
||||
</PermissionDialog>;
|
||||
</>
|
||||
)}
|
||||
</PermissionDialog>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,33 +1,43 @@
|
||||
import { BASH_TOOL_NAME } from '../../../tools/BashTool/toolName.js';
|
||||
import { extractOutputRedirections } from '../../../utils/bash/commands.js';
|
||||
import { isClassifierPermissionsEnabled } from '../../../utils/permissions/bashClassifier.js';
|
||||
import type { PermissionDecisionReason } from '../../../utils/permissions/PermissionResult.js';
|
||||
import type { PermissionUpdate } from '../../../utils/permissions/PermissionUpdateSchema.js';
|
||||
import { shouldShowAlwaysAllowOptions } from '../../../utils/permissions/permissionsLoader.js';
|
||||
import type { OptionWithDescription } from '../../CustomSelect/select.js';
|
||||
import { generateShellSuggestionsLabel } from '../shellPermissionHelpers.js';
|
||||
export type BashToolUseOption = 'yes' | 'yes-apply-suggestions' | 'yes-prefix-edited' | 'yes-classifier-reviewed' | 'no';
|
||||
import { BASH_TOOL_NAME } from '../../../tools/BashTool/toolName.js'
|
||||
import { extractOutputRedirections } from '../../../utils/bash/commands.js'
|
||||
import { isClassifierPermissionsEnabled } from '../../../utils/permissions/bashClassifier.js'
|
||||
import type { PermissionDecisionReason } from '../../../utils/permissions/PermissionResult.js'
|
||||
import type { PermissionUpdate } from '../../../utils/permissions/PermissionUpdateSchema.js'
|
||||
import { shouldShowAlwaysAllowOptions } from '../../../utils/permissions/permissionsLoader.js'
|
||||
import type { OptionWithDescription } from '../../CustomSelect/select.js'
|
||||
import { generateShellSuggestionsLabel } from '../shellPermissionHelpers.js'
|
||||
|
||||
export type BashToolUseOption =
|
||||
| 'yes'
|
||||
| 'yes-apply-suggestions'
|
||||
| 'yes-prefix-edited'
|
||||
| 'yes-classifier-reviewed'
|
||||
| 'no'
|
||||
|
||||
/**
|
||||
* Check if a description already exists in the allow list.
|
||||
* Compares lowercase and trailing-whitespace-trimmed versions.
|
||||
*/
|
||||
function descriptionAlreadyExists(description: string, existingDescriptions: string[]): boolean {
|
||||
const normalized = description.toLowerCase().trimEnd();
|
||||
return existingDescriptions.some(existing => existing.toLowerCase().trimEnd() === normalized);
|
||||
function descriptionAlreadyExists(
|
||||
description: string,
|
||||
existingDescriptions: string[],
|
||||
): boolean {
|
||||
const normalized = description.toLowerCase().trimEnd()
|
||||
return existingDescriptions.some(
|
||||
existing => existing.toLowerCase().trimEnd() === normalized,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip output redirections so filenames don't show as commands in the label.
|
||||
*/
|
||||
function stripBashRedirections(command: string): string {
|
||||
const {
|
||||
commandWithoutRedirections,
|
||||
redirections
|
||||
} = extractOutputRedirections(command);
|
||||
const { commandWithoutRedirections, redirections } =
|
||||
extractOutputRedirections(command)
|
||||
// Only use stripped version if there were actual redirections
|
||||
return redirections.length > 0 ? commandWithoutRedirections : command;
|
||||
return redirections.length > 0 ? commandWithoutRedirections : command
|
||||
}
|
||||
|
||||
export function bashToolUseOptions({
|
||||
suggestions = [],
|
||||
decisionReason,
|
||||
@@ -40,25 +50,26 @@ export function bashToolUseOptions({
|
||||
yesInputMode = false,
|
||||
noInputMode = false,
|
||||
editablePrefix,
|
||||
onEditablePrefixChange
|
||||
onEditablePrefixChange,
|
||||
}: {
|
||||
suggestions?: PermissionUpdate[];
|
||||
decisionReason?: PermissionDecisionReason;
|
||||
onRejectFeedbackChange: (value: string) => void;
|
||||
onAcceptFeedbackChange: (value: string) => void;
|
||||
onClassifierDescriptionChange?: (value: string) => void;
|
||||
classifierDescription?: string;
|
||||
suggestions?: PermissionUpdate[]
|
||||
decisionReason?: PermissionDecisionReason
|
||||
onRejectFeedbackChange: (value: string) => void
|
||||
onAcceptFeedbackChange: (value: string) => void
|
||||
onClassifierDescriptionChange?: (value: string) => void
|
||||
classifierDescription?: string
|
||||
/** Whether the initial classifier description was empty. When true, hides the option. */
|
||||
initialClassifierDescriptionEmpty?: boolean;
|
||||
existingAllowDescriptions?: string[];
|
||||
yesInputMode?: boolean;
|
||||
noInputMode?: boolean;
|
||||
initialClassifierDescriptionEmpty?: boolean
|
||||
existingAllowDescriptions?: string[]
|
||||
yesInputMode?: boolean
|
||||
noInputMode?: boolean
|
||||
/** Editable prefix rule content (e.g., "npm run:*"). When set, replaces Haiku-based suggestions. */
|
||||
editablePrefix?: string;
|
||||
editablePrefix?: string
|
||||
/** Callback when the user edits the prefix value. */
|
||||
onEditablePrefixChange?: (value: string) => void;
|
||||
onEditablePrefixChange?: (value: string) => void
|
||||
}): OptionWithDescription<BashToolUseOption>[] {
|
||||
const options: OptionWithDescription<BashToolUseOption>[] = [];
|
||||
const options: OptionWithDescription<BashToolUseOption>[] = []
|
||||
|
||||
if (yesInputMode) {
|
||||
options.push({
|
||||
type: 'input',
|
||||
@@ -66,13 +77,13 @@ export function bashToolUseOptions({
|
||||
value: 'yes',
|
||||
placeholder: 'and tell Claude what to do next',
|
||||
onChange: onAcceptFeedbackChange,
|
||||
allowEmptySubmitToCancel: true
|
||||
});
|
||||
allowEmptySubmitToCancel: true,
|
||||
})
|
||||
} else {
|
||||
options.push({
|
||||
label: 'Yes',
|
||||
value: 'yes'
|
||||
});
|
||||
value: 'yes',
|
||||
})
|
||||
}
|
||||
|
||||
// Only show "always allow" options when not restricted by allowManagedPermissionRulesOnly
|
||||
@@ -81,8 +92,18 @@ export function bashToolUseOptions({
|
||||
// Haiku-generated suggestion label — but only when the suggestions
|
||||
// don't contain non-Bash items (addDirectories, Read rules) that
|
||||
// the editable prefix can't represent.
|
||||
const hasNonBashSuggestions = suggestions.some(s => s.type === 'addDirectories' || s.type === 'addRules' && s.rules?.some(r => r.toolName !== BASH_TOOL_NAME));
|
||||
if (editablePrefix !== undefined && onEditablePrefixChange && !hasNonBashSuggestions && suggestions.length > 0) {
|
||||
const hasNonBashSuggestions = suggestions.some(
|
||||
s =>
|
||||
s.type === 'addDirectories' ||
|
||||
(s.type === 'addRules' &&
|
||||
s.rules?.some(r => r.toolName !== BASH_TOOL_NAME)),
|
||||
)
|
||||
if (
|
||||
editablePrefix !== undefined &&
|
||||
onEditablePrefixChange &&
|
||||
!hasNonBashSuggestions &&
|
||||
suggestions.length > 0
|
||||
) {
|
||||
options.push({
|
||||
type: 'input',
|
||||
label: 'Yes, and don\u2019t ask again for',
|
||||
@@ -93,15 +114,20 @@ export function bashToolUseOptions({
|
||||
allowEmptySubmitToCancel: true,
|
||||
showLabelWithValue: true,
|
||||
labelValueSeparator: ': ',
|
||||
resetCursorOnUpdate: true
|
||||
});
|
||||
resetCursorOnUpdate: true,
|
||||
})
|
||||
} else if (suggestions.length > 0) {
|
||||
const label = generateShellSuggestionsLabel(suggestions, BASH_TOOL_NAME, stripBashRedirections);
|
||||
const label = generateShellSuggestionsLabel(
|
||||
suggestions,
|
||||
BASH_TOOL_NAME,
|
||||
stripBashRedirections,
|
||||
)
|
||||
|
||||
if (label) {
|
||||
options.push({
|
||||
label,
|
||||
value: 'yes-apply-suggestions'
|
||||
});
|
||||
value: 'yes-apply-suggestions',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,8 +137,21 @@ export function bashToolUseOptions({
|
||||
// (prompt-based rules don't help when the server-side classifier triggers first).
|
||||
// Skip when the editable prefix option is already shown — they serve the
|
||||
// same role and having two identical-looking "don't ask again" inputs is confusing.
|
||||
const editablePrefixShown = options.some(o => o.value === 'yes-prefix-edited');
|
||||
if ((process.env.USER_TYPE) === 'ant' && !editablePrefixShown && isClassifierPermissionsEnabled() && onClassifierDescriptionChange && !initialClassifierDescriptionEmpty && !descriptionAlreadyExists(classifierDescription ?? '', existingAllowDescriptions) && decisionReason?.type !== 'classifier') {
|
||||
const editablePrefixShown = options.some(
|
||||
o => o.value === 'yes-prefix-edited',
|
||||
)
|
||||
if (
|
||||
process.env.USER_TYPE === 'ant' &&
|
||||
!editablePrefixShown &&
|
||||
isClassifierPermissionsEnabled() &&
|
||||
onClassifierDescriptionChange &&
|
||||
!initialClassifierDescriptionEmpty &&
|
||||
!descriptionAlreadyExists(
|
||||
classifierDescription ?? '',
|
||||
existingAllowDescriptions,
|
||||
) &&
|
||||
decisionReason?.type !== 'classifier'
|
||||
) {
|
||||
options.push({
|
||||
type: 'input',
|
||||
label: 'Yes, and don\u2019t ask again for',
|
||||
@@ -123,10 +162,11 @@ export function bashToolUseOptions({
|
||||
allowEmptySubmitToCancel: true,
|
||||
showLabelWithValue: true,
|
||||
labelValueSeparator: ': ',
|
||||
resetCursorOnUpdate: true
|
||||
});
|
||||
resetCursorOnUpdate: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (noInputMode) {
|
||||
options.push({
|
||||
type: 'input',
|
||||
@@ -134,13 +174,14 @@ export function bashToolUseOptions({
|
||||
value: 'no',
|
||||
placeholder: 'and tell Claude what to do differently',
|
||||
onChange: onRejectFeedbackChange,
|
||||
allowEmptySubmitToCancel: true
|
||||
});
|
||||
allowEmptySubmitToCancel: true,
|
||||
})
|
||||
} else {
|
||||
options.push({
|
||||
label: 'No',
|
||||
value: 'no'
|
||||
});
|
||||
value: 'no',
|
||||
})
|
||||
}
|
||||
return options;
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
@@ -1,25 +1,29 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { getSentinelCategory } from '@ant/computer-use-mcp/sentinelApps';
|
||||
import type { CuPermissionRequest, CuPermissionResponse } from '@ant/computer-use-mcp/types';
|
||||
import { DEFAULT_GRANT_FLAGS } from '@ant/computer-use-mcp/types';
|
||||
import figures from 'figures';
|
||||
import * as React from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Box, Text } from '../../../ink.js';
|
||||
import { execFileNoThrow } from '../../../utils/execFileNoThrow.js';
|
||||
import { plural } from '../../../utils/stringUtils.js';
|
||||
import type { OptionWithDescription } from '../../CustomSelect/select.js';
|
||||
import { Select } from '../../CustomSelect/select.js';
|
||||
import { Dialog } from '../../design-system/Dialog.js';
|
||||
import { getSentinelCategory } from '@ant/computer-use-mcp/sentinelApps'
|
||||
import type {
|
||||
CuPermissionRequest,
|
||||
CuPermissionResponse,
|
||||
} from '@ant/computer-use-mcp/types'
|
||||
import { DEFAULT_GRANT_FLAGS } from '@ant/computer-use-mcp/types'
|
||||
import figures from 'figures'
|
||||
import * as React from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { Box, Text } from '../../../ink.js'
|
||||
import { execFileNoThrow } from '../../../utils/execFileNoThrow.js'
|
||||
import { plural } from '../../../utils/stringUtils.js'
|
||||
import type { OptionWithDescription } from '../../CustomSelect/select.js'
|
||||
import { Select } from '../../CustomSelect/select.js'
|
||||
import { Dialog } from '../../design-system/Dialog.js'
|
||||
|
||||
type ComputerUseApprovalProps = {
|
||||
request: CuPermissionRequest;
|
||||
onDone: (response: CuPermissionResponse) => void;
|
||||
};
|
||||
request: CuPermissionRequest
|
||||
onDone: (response: CuPermissionResponse) => void
|
||||
}
|
||||
|
||||
const DENY_ALL_RESPONSE: CuPermissionResponse = {
|
||||
granted: [],
|
||||
denied: [],
|
||||
flags: DEFAULT_GRANT_FLAGS
|
||||
};
|
||||
flags: DEFAULT_GRANT_FLAGS,
|
||||
}
|
||||
|
||||
/**
|
||||
* Two-panel dispatcher. When `request.tccState` is present, macOS permissions
|
||||
@@ -27,414 +31,271 @@ const DENY_ALL_RESPONSE: CuPermissionResponse = {
|
||||
* irrelevant — show a TCC panel that opens System Settings. Otherwise show the
|
||||
* app allowlist + grant-flags panel.
|
||||
*/
|
||||
export function ComputerUseApproval(t0) {
|
||||
const $ = _c(3);
|
||||
const {
|
||||
export function ComputerUseApproval({
|
||||
request,
|
||||
onDone
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] !== onDone || $[1] !== request) {
|
||||
t1 = request.tccState ? <ComputerUseTccPanel tccState={request.tccState} onDone={() => onDone(DENY_ALL_RESPONSE)} /> : <ComputerUseAppListPanel request={request} onDone={onDone} />;
|
||||
$[0] = onDone;
|
||||
$[1] = request;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
return t1;
|
||||
onDone,
|
||||
}: ComputerUseApprovalProps): React.ReactNode {
|
||||
return request.tccState ? (
|
||||
<ComputerUseTccPanel
|
||||
tccState={request.tccState}
|
||||
onDone={() => onDone(DENY_ALL_RESPONSE)}
|
||||
/>
|
||||
) : (
|
||||
<ComputerUseAppListPanel request={request} onDone={onDone} />
|
||||
)
|
||||
}
|
||||
|
||||
// ── TCC panel ─────────────────────────────────────────────────────────────
|
||||
|
||||
type TccOption = 'open_accessibility' | 'open_screen_recording' | 'retry';
|
||||
function ComputerUseTccPanel(t0) {
|
||||
const $ = _c(26);
|
||||
const {
|
||||
type TccOption = 'open_accessibility' | 'open_screen_recording' | 'retry'
|
||||
|
||||
function ComputerUseTccPanel({
|
||||
tccState,
|
||||
onDone
|
||||
} = t0;
|
||||
let opts;
|
||||
if ($[0] !== tccState.accessibility || $[1] !== tccState.screenRecording) {
|
||||
opts = [];
|
||||
onDone,
|
||||
}: {
|
||||
tccState: NonNullable<CuPermissionRequest['tccState']>
|
||||
onDone: () => void
|
||||
}): React.ReactNode {
|
||||
const options = useMemo<OptionWithDescription<TccOption>[]>(() => {
|
||||
const opts: OptionWithDescription<TccOption>[] = []
|
||||
if (!tccState.accessibility) {
|
||||
let t1;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = {
|
||||
label: "Open System Settings \u2192 Accessibility",
|
||||
value: "open_accessibility"
|
||||
};
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
opts.push(t1);
|
||||
opts.push({
|
||||
label: 'Open System Settings → Accessibility',
|
||||
value: 'open_accessibility',
|
||||
})
|
||||
}
|
||||
if (!tccState.screenRecording) {
|
||||
let t1;
|
||||
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = {
|
||||
label: "Open System Settings \u2192 Screen Recording",
|
||||
value: "open_screen_recording"
|
||||
};
|
||||
$[4] = t1;
|
||||
} else {
|
||||
t1 = $[4];
|
||||
opts.push({
|
||||
label: 'Open System Settings → Screen Recording',
|
||||
value: 'open_screen_recording',
|
||||
})
|
||||
}
|
||||
opts.push(t1);
|
||||
}
|
||||
let t1;
|
||||
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = {
|
||||
label: "Try again",
|
||||
value: "retry"
|
||||
};
|
||||
$[5] = t1;
|
||||
} else {
|
||||
t1 = $[5];
|
||||
}
|
||||
opts.push(t1);
|
||||
$[0] = tccState.accessibility;
|
||||
$[1] = tccState.screenRecording;
|
||||
$[2] = opts;
|
||||
} else {
|
||||
opts = $[2];
|
||||
}
|
||||
const options = opts;
|
||||
let t1;
|
||||
if ($[6] !== onDone) {
|
||||
t1 = function onChange(value) {
|
||||
opts.push({ label: 'Try again', value: 'retry' })
|
||||
return opts
|
||||
}, [tccState.accessibility, tccState.screenRecording])
|
||||
|
||||
function onChange(value: TccOption): void {
|
||||
switch (value) {
|
||||
case "open_accessibility":
|
||||
{
|
||||
execFileNoThrow("open", ["x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"], {
|
||||
useCwd: false
|
||||
});
|
||||
return;
|
||||
}
|
||||
case "open_screen_recording":
|
||||
{
|
||||
execFileNoThrow("open", ["x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture"], {
|
||||
useCwd: false
|
||||
});
|
||||
return;
|
||||
}
|
||||
case "retry":
|
||||
{
|
||||
onDone();
|
||||
return;
|
||||
case 'open_accessibility':
|
||||
void execFileNoThrow(
|
||||
'open',
|
||||
[
|
||||
'x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility',
|
||||
],
|
||||
{ useCwd: false },
|
||||
)
|
||||
return
|
||||
case 'open_screen_recording':
|
||||
void execFileNoThrow(
|
||||
'open',
|
||||
[
|
||||
'x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture',
|
||||
],
|
||||
{ useCwd: false },
|
||||
)
|
||||
return
|
||||
case 'retry':
|
||||
// Resolve with deny-all — the model re-calls request_access, which
|
||||
// re-checks TCC and renders the app list if now granted.
|
||||
onDone()
|
||||
return
|
||||
}
|
||||
}
|
||||
};
|
||||
$[6] = onDone;
|
||||
$[7] = t1;
|
||||
} else {
|
||||
t1 = $[7];
|
||||
}
|
||||
const onChange = t1;
|
||||
const t2 = tccState.accessibility ? `${figures.tick} granted` : `${figures.cross} not granted`;
|
||||
let t3;
|
||||
if ($[8] !== t2) {
|
||||
t3 = <Text>Accessibility:{" "}{t2}</Text>;
|
||||
$[8] = t2;
|
||||
$[9] = t3;
|
||||
} else {
|
||||
t3 = $[9];
|
||||
}
|
||||
const t4 = tccState.screenRecording ? `${figures.tick} granted` : `${figures.cross} not granted`;
|
||||
let t5;
|
||||
if ($[10] !== t4) {
|
||||
t5 = <Text>Screen Recording:{" "}{t4}</Text>;
|
||||
$[10] = t4;
|
||||
$[11] = t5;
|
||||
} else {
|
||||
t5 = $[11];
|
||||
}
|
||||
let t6;
|
||||
if ($[12] !== t3 || $[13] !== t5) {
|
||||
t6 = <Box flexDirection="column">{t3}{t5}</Box>;
|
||||
$[12] = t3;
|
||||
$[13] = t5;
|
||||
$[14] = t6;
|
||||
} else {
|
||||
t6 = $[14];
|
||||
}
|
||||
let t7;
|
||||
if ($[15] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t7 = <Text dimColor={true}>Grant the missing permissions in System Settings, then select "Try again". macOS may require you to restart Claude Code after granting Screen Recording.</Text>;
|
||||
$[15] = t7;
|
||||
} else {
|
||||
t7 = $[15];
|
||||
}
|
||||
let t8;
|
||||
if ($[16] !== onChange || $[17] !== onDone || $[18] !== options) {
|
||||
t8 = <Select options={options} onChange={onChange} onCancel={onDone} />;
|
||||
$[16] = onChange;
|
||||
$[17] = onDone;
|
||||
$[18] = options;
|
||||
$[19] = t8;
|
||||
} else {
|
||||
t8 = $[19];
|
||||
}
|
||||
let t9;
|
||||
if ($[20] !== t6 || $[21] !== t8) {
|
||||
t9 = <Box flexDirection="column" paddingX={1} paddingY={1} gap={1}>{t6}{t7}{t8}</Box>;
|
||||
$[20] = t6;
|
||||
$[21] = t8;
|
||||
$[22] = t9;
|
||||
} else {
|
||||
t9 = $[22];
|
||||
}
|
||||
let t10;
|
||||
if ($[23] !== onDone || $[24] !== t9) {
|
||||
t10 = <Dialog title="Computer Use needs macOS permissions" onCancel={onDone}>{t9}</Dialog>;
|
||||
$[23] = onDone;
|
||||
$[24] = t9;
|
||||
$[25] = t10;
|
||||
} else {
|
||||
t10 = $[25];
|
||||
}
|
||||
return t10;
|
||||
|
||||
return (
|
||||
<Dialog title="Computer Use needs macOS permissions" onCancel={onDone}>
|
||||
<Box flexDirection="column" paddingX={1} paddingY={1} gap={1}>
|
||||
<Box flexDirection="column">
|
||||
<Text>
|
||||
Accessibility:{' '}
|
||||
{tccState.accessibility
|
||||
? `${figures.tick} granted`
|
||||
: `${figures.cross} not granted`}
|
||||
</Text>
|
||||
<Text>
|
||||
Screen Recording:{' '}
|
||||
{tccState.screenRecording
|
||||
? `${figures.tick} granted`
|
||||
: `${figures.cross} not granted`}
|
||||
</Text>
|
||||
</Box>
|
||||
<Text dimColor>
|
||||
Grant the missing permissions in System Settings, then select
|
||||
"Try again". macOS may require you to restart Claude Code
|
||||
after granting Screen Recording.
|
||||
</Text>
|
||||
<Select options={options} onChange={onChange} onCancel={onDone} />
|
||||
</Box>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
// ── App allowlist panel ───────────────────────────────────────────────────
|
||||
|
||||
type AppListOption = 'allow_all' | 'deny';
|
||||
const SENTINEL_WARNING: Record<NonNullable<ReturnType<typeof getSentinelCategory>>, string> = {
|
||||
type AppListOption = 'allow_all' | 'deny'
|
||||
|
||||
const SENTINEL_WARNING: Record<
|
||||
NonNullable<ReturnType<typeof getSentinelCategory>>,
|
||||
string
|
||||
> = {
|
||||
shell: 'equivalent to shell access',
|
||||
filesystem: 'can read/write any file',
|
||||
system_settings: 'can change system settings'
|
||||
};
|
||||
function ComputerUseAppListPanel(t0) {
|
||||
const $ = _c(48);
|
||||
const {
|
||||
system_settings: 'can change system settings',
|
||||
}
|
||||
|
||||
function ComputerUseAppListPanel({
|
||||
request,
|
||||
onDone
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] !== request.apps) {
|
||||
t1 = () => new Set(request.apps.flatMap(_temp));
|
||||
$[0] = request.apps;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const [checked] = useState(t1);
|
||||
let t2;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = ["clipboardRead", "clipboardWrite", "systemKeyCombos"];
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
const ALL_FLAG_KEYS = t2;
|
||||
let t3;
|
||||
if ($[3] !== request.requestedFlags) {
|
||||
t3 = ALL_FLAG_KEYS.filter(k => request.requestedFlags[k]);
|
||||
$[3] = request.requestedFlags;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
const requestedFlagKeys = t3;
|
||||
const t4 = checked.size;
|
||||
let t5;
|
||||
if ($[5] !== checked.size) {
|
||||
t5 = plural(checked.size, "app");
|
||||
$[5] = checked.size;
|
||||
$[6] = t5;
|
||||
} else {
|
||||
t5 = $[6];
|
||||
}
|
||||
const t6 = `Allow for this session (${t4} ${t5})`;
|
||||
let t7;
|
||||
if ($[7] !== t6) {
|
||||
t7 = {
|
||||
label: t6,
|
||||
value: "allow_all"
|
||||
};
|
||||
$[7] = t6;
|
||||
$[8] = t7;
|
||||
} else {
|
||||
t7 = $[8];
|
||||
}
|
||||
let t8;
|
||||
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t8 = {
|
||||
label: <Text>Deny, and tell Claude what to do differently <Text bold={true}>(esc)</Text></Text>,
|
||||
value: "deny"
|
||||
};
|
||||
$[9] = t8;
|
||||
} else {
|
||||
t8 = $[9];
|
||||
}
|
||||
let t9;
|
||||
if ($[10] !== t7) {
|
||||
t9 = [t7, t8];
|
||||
$[10] = t7;
|
||||
$[11] = t9;
|
||||
} else {
|
||||
t9 = $[11];
|
||||
}
|
||||
const options = t9;
|
||||
let t10;
|
||||
if ($[12] !== checked || $[13] !== onDone || $[14] !== request.apps || $[15] !== requestedFlagKeys) {
|
||||
t10 = function respond(allow) {
|
||||
onDone,
|
||||
}: ComputerUseApprovalProps): React.ReactNode {
|
||||
// Pre-check every resolved, not-yet-granted app. Sentinels stay checked
|
||||
// too — the warning text is the signal, not an unchecked box.
|
||||
// Per-item toggles are a follow-up; for now every resolved app is granted
|
||||
// when the user accepts. `setChecked` is unused until then.
|
||||
const [checked] = useState<ReadonlySet<string>>(
|
||||
() =>
|
||||
new Set(
|
||||
request.apps.flatMap(a =>
|
||||
a.resolved && !a.alreadyGranted ? [a.resolved.bundleId] : [],
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
type FlagKey = keyof typeof DEFAULT_GRANT_FLAGS
|
||||
const ALL_FLAG_KEYS: FlagKey[] = [
|
||||
'clipboardRead',
|
||||
'clipboardWrite',
|
||||
'systemKeyCombos',
|
||||
]
|
||||
const requestedFlagKeys = useMemo(
|
||||
(): FlagKey[] => ALL_FLAG_KEYS.filter(k => request.requestedFlags[k]),
|
||||
[request.requestedFlags],
|
||||
)
|
||||
|
||||
const options = useMemo<OptionWithDescription<AppListOption>[]>(
|
||||
() => [
|
||||
{
|
||||
label: `Allow for this session (${checked.size} ${plural(checked.size, 'app')})`,
|
||||
value: 'allow_all',
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<Text>
|
||||
Deny, and tell Claude what to do differently <Text bold>(esc)</Text>
|
||||
</Text>
|
||||
),
|
||||
value: 'deny',
|
||||
},
|
||||
],
|
||||
[checked.size],
|
||||
)
|
||||
|
||||
function respond(allow: boolean): void {
|
||||
if (!allow) {
|
||||
onDone(DENY_ALL_RESPONSE);
|
||||
return;
|
||||
onDone(DENY_ALL_RESPONSE)
|
||||
return
|
||||
}
|
||||
const now = Date.now();
|
||||
const granted = request.apps.flatMap(a_0 => a_0.resolved && checked.has(a_0.resolved.bundleId) ? [{
|
||||
bundleId: a_0.resolved.bundleId,
|
||||
displayName: a_0.resolved.displayName,
|
||||
grantedAt: now
|
||||
}] : []);
|
||||
const denied = request.apps.filter(a_1 => !a_1.resolved || !checked.has(a_1.resolved.bundleId)).map(_temp2);
|
||||
const now = Date.now()
|
||||
const granted = request.apps.flatMap(a =>
|
||||
a.resolved && checked.has(a.resolved.bundleId)
|
||||
? [
|
||||
{
|
||||
bundleId: a.resolved.bundleId,
|
||||
displayName: a.resolved.displayName,
|
||||
grantedAt: now,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
)
|
||||
const denied = request.apps
|
||||
.filter(a => !a.resolved || !checked.has(a.resolved.bundleId))
|
||||
.map(a => ({
|
||||
bundleId: a.resolved?.bundleId ?? a.requestedName,
|
||||
reason: a.resolved
|
||||
? ('user_denied' as const)
|
||||
: ('not_installed' as const),
|
||||
}))
|
||||
// Grant all requested flags on allow — per-flag toggles are a follow-up.
|
||||
const flags = {
|
||||
...DEFAULT_GRANT_FLAGS,
|
||||
...Object.fromEntries(requestedFlagKeys.map(_temp3))
|
||||
};
|
||||
onDone({
|
||||
granted,
|
||||
denied,
|
||||
flags
|
||||
});
|
||||
};
|
||||
$[12] = checked;
|
||||
$[13] = onDone;
|
||||
$[14] = request.apps;
|
||||
$[15] = requestedFlagKeys;
|
||||
$[16] = t10;
|
||||
} else {
|
||||
t10 = $[16];
|
||||
...Object.fromEntries(requestedFlagKeys.map(k => [k, true] as const)),
|
||||
}
|
||||
const respond = t10;
|
||||
let t11;
|
||||
if ($[17] !== respond) {
|
||||
t11 = () => respond(false);
|
||||
$[17] = respond;
|
||||
$[18] = t11;
|
||||
} else {
|
||||
t11 = $[18];
|
||||
onDone({ granted, denied, flags })
|
||||
}
|
||||
let t12;
|
||||
if ($[19] !== request.reason) {
|
||||
t12 = request.reason ? <Text dimColor={true}>{request.reason}</Text> : null;
|
||||
$[19] = request.reason;
|
||||
$[20] = t12;
|
||||
} else {
|
||||
t12 = $[20];
|
||||
}
|
||||
let t13;
|
||||
if ($[21] !== checked || $[22] !== request.apps) {
|
||||
let t14;
|
||||
if ($[24] !== checked) {
|
||||
t14 = a_3 => {
|
||||
const resolved = a_3.resolved;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
title="Computer Use wants to control these apps"
|
||||
onCancel={() => respond(false)}
|
||||
>
|
||||
<Box flexDirection="column" paddingX={1} paddingY={1} gap={1}>
|
||||
{request.reason ? <Text dimColor>{request.reason}</Text> : null}
|
||||
|
||||
<Box flexDirection="column">
|
||||
{request.apps.map(a => {
|
||||
const resolved = a.resolved
|
||||
if (!resolved) {
|
||||
return <Text key={a_3.requestedName} dimColor={true}>{" "}{figures.circle} {a_3.requestedName}{" "}<Text dimColor={true}>(not installed)</Text></Text>;
|
||||
return (
|
||||
<Text key={a.requestedName} dimColor>
|
||||
{' '}
|
||||
{figures.circle} {a.requestedName}{' '}
|
||||
<Text dimColor>(not installed)</Text>
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
if (a_3.alreadyGranted) {
|
||||
return <Text key={resolved.bundleId} dimColor={true}>{" "}{figures.tick} {resolved.displayName}{" "}<Text dimColor={true}>(already granted)</Text></Text>;
|
||||
if (a.alreadyGranted) {
|
||||
return (
|
||||
<Text key={resolved.bundleId} dimColor>
|
||||
{' '}
|
||||
{figures.tick} {resolved.displayName}{' '}
|
||||
<Text dimColor>(already granted)</Text>
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
const sentinel = getSentinelCategory(resolved.bundleId);
|
||||
const isChecked = checked.has(resolved.bundleId);
|
||||
return <Box key={resolved.bundleId} flexDirection="column"><Text>{" "}{isChecked ? figures.circleFilled : figures.circle}{" "}{resolved.displayName}</Text>{sentinel ? <Text bold={true}>{" "}{figures.warning} {SENTINEL_WARNING[sentinel]}</Text> : null}</Box>;
|
||||
};
|
||||
$[24] = checked;
|
||||
$[25] = t14;
|
||||
} else {
|
||||
t14 = $[25];
|
||||
}
|
||||
t13 = request.apps.map(t14);
|
||||
$[21] = checked;
|
||||
$[22] = request.apps;
|
||||
$[23] = t13;
|
||||
} else {
|
||||
t13 = $[23];
|
||||
}
|
||||
let t14;
|
||||
if ($[26] !== t13) {
|
||||
t14 = <Box flexDirection="column">{t13}</Box>;
|
||||
$[26] = t13;
|
||||
$[27] = t14;
|
||||
} else {
|
||||
t14 = $[27];
|
||||
}
|
||||
let t15;
|
||||
if ($[28] !== requestedFlagKeys) {
|
||||
t15 = requestedFlagKeys.length > 0 ? <Box flexDirection="column"><Text dimColor={true}>Also requested:</Text>{requestedFlagKeys.map(_temp4)}</Box> : null;
|
||||
$[28] = requestedFlagKeys;
|
||||
$[29] = t15;
|
||||
} else {
|
||||
t15 = $[29];
|
||||
}
|
||||
let t16;
|
||||
if ($[30] !== request.willHide) {
|
||||
t16 = request.willHide && request.willHide.length > 0 ? <Text dimColor={true}>{request.willHide.length} other{" "}{plural(request.willHide.length, "app")} will be hidden while Claude works.</Text> : null;
|
||||
$[30] = request.willHide;
|
||||
$[31] = t16;
|
||||
} else {
|
||||
t16 = $[31];
|
||||
}
|
||||
let t17;
|
||||
let t18;
|
||||
if ($[32] !== respond) {
|
||||
t17 = v => respond(v === "allow_all");
|
||||
t18 = () => respond(false);
|
||||
$[32] = respond;
|
||||
$[33] = t17;
|
||||
$[34] = t18;
|
||||
} else {
|
||||
t17 = $[33];
|
||||
t18 = $[34];
|
||||
}
|
||||
let t19;
|
||||
if ($[35] !== options || $[36] !== t17 || $[37] !== t18) {
|
||||
t19 = <Select options={options} onChange={t17} onCancel={t18} />;
|
||||
$[35] = options;
|
||||
$[36] = t17;
|
||||
$[37] = t18;
|
||||
$[38] = t19;
|
||||
} else {
|
||||
t19 = $[38];
|
||||
}
|
||||
let t20;
|
||||
if ($[39] !== t12 || $[40] !== t14 || $[41] !== t15 || $[42] !== t16 || $[43] !== t19) {
|
||||
t20 = <Box flexDirection="column" paddingX={1} paddingY={1} gap={1}>{t12}{t14}{t15}{t16}{t19}</Box>;
|
||||
$[39] = t12;
|
||||
$[40] = t14;
|
||||
$[41] = t15;
|
||||
$[42] = t16;
|
||||
$[43] = t19;
|
||||
$[44] = t20;
|
||||
} else {
|
||||
t20 = $[44];
|
||||
}
|
||||
let t21;
|
||||
if ($[45] !== t11 || $[46] !== t20) {
|
||||
t21 = <Dialog title="Computer Use wants to control these apps" onCancel={t11}>{t20}</Dialog>;
|
||||
$[45] = t11;
|
||||
$[46] = t20;
|
||||
$[47] = t21;
|
||||
} else {
|
||||
t21 = $[47];
|
||||
}
|
||||
return t21;
|
||||
}
|
||||
function _temp4(flag) {
|
||||
return <Text key={flag} dimColor={true}>{" "}· {flag}</Text>;
|
||||
}
|
||||
function _temp3(k_0) {
|
||||
return [k_0, true] as const;
|
||||
}
|
||||
function _temp2(a_2) {
|
||||
return {
|
||||
bundleId: a_2.resolved?.bundleId ?? a_2.requestedName,
|
||||
reason: a_2.resolved ? "user_denied" as const : "not_installed" as const
|
||||
};
|
||||
}
|
||||
function _temp(a) {
|
||||
return a.resolved && !a.alreadyGranted ? [a.resolved.bundleId] : [];
|
||||
const sentinel = getSentinelCategory(resolved.bundleId)
|
||||
const isChecked = checked.has(resolved.bundleId)
|
||||
return (
|
||||
<Box key={resolved.bundleId} flexDirection="column">
|
||||
<Text>
|
||||
{' '}
|
||||
{isChecked ? figures.circleFilled : figures.circle}{' '}
|
||||
{resolved.displayName}
|
||||
</Text>
|
||||
{sentinel ? (
|
||||
<Text bold>
|
||||
{' '}
|
||||
{figures.warning} {SENTINEL_WARNING[sentinel]}
|
||||
</Text>
|
||||
) : null}
|
||||
</Box>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
|
||||
{requestedFlagKeys.length > 0 ? (
|
||||
<Box flexDirection="column">
|
||||
<Text dimColor>Also requested:</Text>
|
||||
{requestedFlagKeys.map(flag => (
|
||||
<Text key={flag} dimColor>
|
||||
{' '}· {flag}
|
||||
</Text>
|
||||
))}
|
||||
</Box>
|
||||
) : null}
|
||||
|
||||
{request.willHide && request.willHide.length > 0 ? (
|
||||
<Text dimColor>
|
||||
{request.willHide.length} other{' '}
|
||||
{plural(request.willHide.length, 'app')} will be hidden while Claude
|
||||
works.
|
||||
</Text>
|
||||
) : null}
|
||||
|
||||
<Select
|
||||
options={options}
|
||||
onChange={v => respond(v === 'allow_all')}
|
||||
onCancel={() => respond(false)}
|
||||
/>
|
||||
</Box>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,121 +1,82 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React from 'react';
|
||||
import { handlePlanModeTransition } from '../../../bootstrap/state.js';
|
||||
import { Box, Text } from '../../../ink.js';
|
||||
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../../services/analytics/index.js';
|
||||
import { useAppState } from '../../../state/AppState.js';
|
||||
import { isPlanModeInterviewPhaseEnabled } from '../../../utils/planModeV2.js';
|
||||
import { Select } from '../../CustomSelect/index.js';
|
||||
import { PermissionDialog } from '../PermissionDialog.js';
|
||||
import type { PermissionRequestProps } from '../PermissionRequest.js';
|
||||
export function EnterPlanModePermissionRequest(t0) {
|
||||
const $ = _c(18);
|
||||
const {
|
||||
import React from 'react'
|
||||
import { handlePlanModeTransition } from '../../../bootstrap/state.js'
|
||||
import { Box, Text } from '../../../ink.js'
|
||||
import {
|
||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
logEvent,
|
||||
} from '../../../services/analytics/index.js'
|
||||
import { useAppState } from '../../../state/AppState.js'
|
||||
import { isPlanModeInterviewPhaseEnabled } from '../../../utils/planModeV2.js'
|
||||
import { Select } from '../../CustomSelect/index.js'
|
||||
import { PermissionDialog } from '../PermissionDialog.js'
|
||||
import type { PermissionRequestProps } from '../PermissionRequest.js'
|
||||
|
||||
export function EnterPlanModePermissionRequest({
|
||||
toolUseConfirm,
|
||||
onDone,
|
||||
onReject,
|
||||
workerBadge
|
||||
} = t0;
|
||||
const toolPermissionContextMode = useAppState(_temp);
|
||||
let t1;
|
||||
if ($[0] !== onDone || $[1] !== onReject || $[2] !== toolPermissionContextMode || $[3] !== toolUseConfirm) {
|
||||
t1 = function handleResponse(value) {
|
||||
if (value === "yes") {
|
||||
logEvent("tengu_plan_enter", {
|
||||
workerBadge,
|
||||
}: PermissionRequestProps): React.ReactNode {
|
||||
const toolPermissionContextMode = useAppState(
|
||||
s => s.toolPermissionContext.mode,
|
||||
)
|
||||
|
||||
function handleResponse(value: 'yes' | 'no'): void {
|
||||
if (value === 'yes') {
|
||||
logEvent('tengu_plan_enter', {
|
||||
interviewPhaseEnabled: isPlanModeInterviewPhaseEnabled(),
|
||||
entryMethod: "tool" as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
handlePlanModeTransition(toolPermissionContextMode, "plan");
|
||||
onDone();
|
||||
toolUseConfirm.onAllow({}, [{
|
||||
type: "setMode",
|
||||
mode: "plan",
|
||||
destination: "session"
|
||||
}]);
|
||||
entryMethod:
|
||||
'tool' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
handlePlanModeTransition(toolPermissionContextMode, 'plan')
|
||||
onDone()
|
||||
toolUseConfirm.onAllow({}, [
|
||||
{ type: 'setMode', mode: 'plan', destination: 'session' },
|
||||
])
|
||||
} else {
|
||||
onDone();
|
||||
onReject();
|
||||
toolUseConfirm.onReject();
|
||||
onDone()
|
||||
onReject()
|
||||
toolUseConfirm.onReject()
|
||||
}
|
||||
};
|
||||
$[0] = onDone;
|
||||
$[1] = onReject;
|
||||
$[2] = toolPermissionContextMode;
|
||||
$[3] = toolUseConfirm;
|
||||
$[4] = t1;
|
||||
} else {
|
||||
t1 = $[4];
|
||||
}
|
||||
const handleResponse = t1;
|
||||
let t2;
|
||||
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <Text>Claude wants to enter plan mode to explore and design an implementation approach.</Text>;
|
||||
$[5] = t2;
|
||||
} else {
|
||||
t2 = $[5];
|
||||
}
|
||||
let t3;
|
||||
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = <Box marginTop={1} flexDirection="column"><Text dimColor={true}>In plan mode, Claude will:</Text><Text dimColor={true}> · Explore the codebase thoroughly</Text><Text dimColor={true}> · Identify existing patterns</Text><Text dimColor={true}> · Design an implementation strategy</Text><Text dimColor={true}> · Present a plan for your approval</Text></Box>;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
}
|
||||
let t4;
|
||||
if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = <Box marginTop={1}><Text dimColor={true}>No code changes will be made until you approve the plan.</Text></Box>;
|
||||
$[7] = t4;
|
||||
} else {
|
||||
t4 = $[7];
|
||||
}
|
||||
let t5;
|
||||
if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t5 = {
|
||||
label: "Yes, enter plan mode",
|
||||
value: "yes" as const
|
||||
};
|
||||
$[8] = t5;
|
||||
} else {
|
||||
t5 = $[8];
|
||||
}
|
||||
let t6;
|
||||
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t6 = [t5, {
|
||||
label: "No, start implementing now",
|
||||
value: "no" as const
|
||||
}];
|
||||
$[9] = t6;
|
||||
} else {
|
||||
t6 = $[9];
|
||||
}
|
||||
let t7;
|
||||
if ($[10] !== handleResponse) {
|
||||
t7 = () => handleResponse("no");
|
||||
$[10] = handleResponse;
|
||||
$[11] = t7;
|
||||
} else {
|
||||
t7 = $[11];
|
||||
}
|
||||
let t8;
|
||||
if ($[12] !== handleResponse || $[13] !== t7) {
|
||||
t8 = <Box flexDirection="column" marginTop={1} paddingX={1}>{t2}{t3}{t4}<Box marginTop={1}><Select options={t6} onChange={handleResponse} onCancel={t7} /></Box></Box>;
|
||||
$[12] = handleResponse;
|
||||
$[13] = t7;
|
||||
$[14] = t8;
|
||||
} else {
|
||||
t8 = $[14];
|
||||
}
|
||||
let t9;
|
||||
if ($[15] !== t8 || $[16] !== workerBadge) {
|
||||
t9 = <PermissionDialog color="planMode" title="Enter plan mode?" workerBadge={workerBadge}>{t8}</PermissionDialog>;
|
||||
$[15] = t8;
|
||||
$[16] = workerBadge;
|
||||
$[17] = t9;
|
||||
} else {
|
||||
t9 = $[17];
|
||||
}
|
||||
return t9;
|
||||
}
|
||||
function _temp(s) {
|
||||
return s.toolPermissionContext.mode;
|
||||
|
||||
return (
|
||||
<PermissionDialog
|
||||
color="planMode"
|
||||
title="Enter plan mode?"
|
||||
workerBadge={workerBadge}
|
||||
>
|
||||
<Box flexDirection="column" marginTop={1} paddingX={1}>
|
||||
<Text>
|
||||
Claude wants to enter plan mode to explore and design an
|
||||
implementation approach.
|
||||
</Text>
|
||||
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
<Text dimColor>In plan mode, Claude will:</Text>
|
||||
<Text dimColor> · Explore the codebase thoroughly</Text>
|
||||
<Text dimColor> · Identify existing patterns</Text>
|
||||
<Text dimColor> · Design an implementation strategy</Text>
|
||||
<Text dimColor> · Present a plan for your approval</Text>
|
||||
</Box>
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>
|
||||
No code changes will be made until you approve the plan.
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Select
|
||||
options={[
|
||||
{ label: 'Yes, enter plan mode', value: 'yes' as const },
|
||||
{ label: 'No, start implementing now', value: 'no' as const },
|
||||
]}
|
||||
onChange={handleResponse}
|
||||
onCancel={() => handleResponse('no')}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</PermissionDialog>
|
||||
)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,332 +1,196 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { getOriginalCwd } from '../../bootstrap/state.js';
|
||||
import { Box, Text, useTheme } from '../../ink.js';
|
||||
import { sanitizeToolNameForAnalytics } from '../../services/analytics/metadata.js';
|
||||
import { env } from '../../utils/env.js';
|
||||
import { shouldShowAlwaysAllowOptions } from '../../utils/permissions/permissionsLoader.js';
|
||||
import { truncateToLines } from '../../utils/stringUtils.js';
|
||||
import { logUnaryEvent } from '../../utils/unaryLogging.js';
|
||||
import { type UnaryEvent, usePermissionRequestLogging } from './hooks.js';
|
||||
import { PermissionDialog } from './PermissionDialog.js';
|
||||
import { PermissionPrompt, type PermissionPromptOption, type ToolAnalyticsContext } from './PermissionPrompt.js';
|
||||
import type { PermissionRequestProps } from './PermissionRequest.js';
|
||||
import { PermissionRuleExplanation } from './PermissionRuleExplanation.js';
|
||||
type FallbackOptionValue = 'yes' | 'yes-dont-ask-again' | 'no';
|
||||
export function FallbackPermissionRequest(t0) {
|
||||
const $ = _c(58);
|
||||
const {
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import { getOriginalCwd } from '../../bootstrap/state.js'
|
||||
import { Box, Text, useTheme } from '../../ink.js'
|
||||
import { sanitizeToolNameForAnalytics } from '../../services/analytics/metadata.js'
|
||||
import { env } from '../../utils/env.js'
|
||||
import { shouldShowAlwaysAllowOptions } from '../../utils/permissions/permissionsLoader.js'
|
||||
import { truncateToLines } from '../../utils/stringUtils.js'
|
||||
import { logUnaryEvent } from '../../utils/unaryLogging.js'
|
||||
import { type UnaryEvent, usePermissionRequestLogging } from './hooks.js'
|
||||
import { PermissionDialog } from './PermissionDialog.js'
|
||||
import {
|
||||
PermissionPrompt,
|
||||
type PermissionPromptOption,
|
||||
type ToolAnalyticsContext,
|
||||
} from './PermissionPrompt.js'
|
||||
import type { PermissionRequestProps } from './PermissionRequest.js'
|
||||
import { PermissionRuleExplanation } from './PermissionRuleExplanation.js'
|
||||
|
||||
type FallbackOptionValue = 'yes' | 'yes-dont-ask-again' | 'no'
|
||||
|
||||
export function FallbackPermissionRequest({
|
||||
toolUseConfirm,
|
||||
onDone,
|
||||
onReject,
|
||||
workerBadge
|
||||
} = t0;
|
||||
const [theme] = useTheme();
|
||||
let originalUserFacingName;
|
||||
let t1;
|
||||
if ($[0] !== toolUseConfirm.input || $[1] !== toolUseConfirm.tool) {
|
||||
originalUserFacingName = toolUseConfirm.tool.userFacingName(toolUseConfirm.input as never);
|
||||
t1 = originalUserFacingName.endsWith(" (MCP)") ? originalUserFacingName.slice(0, -6) : originalUserFacingName;
|
||||
$[0] = toolUseConfirm.input;
|
||||
$[1] = toolUseConfirm.tool;
|
||||
$[2] = originalUserFacingName;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
originalUserFacingName = $[2];
|
||||
t1 = $[3];
|
||||
}
|
||||
const userFacingName = t1;
|
||||
let t2;
|
||||
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = {
|
||||
completion_type: "tool_use_single",
|
||||
language_name: "none"
|
||||
};
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
const unaryEvent = t2;
|
||||
usePermissionRequestLogging(toolUseConfirm, unaryEvent);
|
||||
let t3;
|
||||
if ($[5] !== onDone || $[6] !== onReject || $[7] !== toolUseConfirm) {
|
||||
t3 = (value, feedback) => {
|
||||
bb8: switch (value) {
|
||||
case "yes":
|
||||
verbose: _verbose,
|
||||
workerBadge,
|
||||
}: PermissionRequestProps): React.ReactNode {
|
||||
const [theme] = useTheme()
|
||||
// TODO: Avoid these special cases
|
||||
const originalUserFacingName = toolUseConfirm.tool.userFacingName(
|
||||
toolUseConfirm.input as never,
|
||||
)
|
||||
const userFacingName = originalUserFacingName.endsWith(' (MCP)')
|
||||
? originalUserFacingName.slice(0, -6)
|
||||
: originalUserFacingName
|
||||
|
||||
const unaryEvent = useMemo<UnaryEvent>(
|
||||
() => ({
|
||||
completion_type: 'tool_use_single',
|
||||
language_name: 'none',
|
||||
}),
|
||||
[],
|
||||
)
|
||||
|
||||
usePermissionRequestLogging(toolUseConfirm, unaryEvent)
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(value: FallbackOptionValue, feedback?: string) => {
|
||||
switch (value) {
|
||||
case 'yes':
|
||||
void logUnaryEvent({
|
||||
completion_type: 'tool_use_single',
|
||||
event: 'accept',
|
||||
metadata: {
|
||||
language_name: 'none',
|
||||
message_id: toolUseConfirm.assistantMessage.message.id,
|
||||
platform: env.platform,
|
||||
},
|
||||
})
|
||||
toolUseConfirm.onAllow(toolUseConfirm.input, [], feedback)
|
||||
onDone()
|
||||
break
|
||||
case 'yes-dont-ask-again': {
|
||||
void logUnaryEvent({
|
||||
completion_type: 'tool_use_single',
|
||||
event: 'accept',
|
||||
metadata: {
|
||||
language_name: 'none',
|
||||
message_id: toolUseConfirm.assistantMessage.message.id,
|
||||
platform: env.platform,
|
||||
},
|
||||
})
|
||||
|
||||
toolUseConfirm.onAllow(toolUseConfirm.input, [
|
||||
{
|
||||
logUnaryEvent({
|
||||
completion_type: "tool_use_single",
|
||||
event: "accept",
|
||||
metadata: {
|
||||
language_name: "none",
|
||||
message_id: toolUseConfirm.assistantMessage.message.id,
|
||||
platform: env.platform
|
||||
}
|
||||
});
|
||||
toolUseConfirm.onAllow(toolUseConfirm.input, [], feedback);
|
||||
onDone();
|
||||
break bb8;
|
||||
}
|
||||
case "yes-dont-ask-again":
|
||||
type: 'addRules',
|
||||
rules: [
|
||||
{
|
||||
logUnaryEvent({
|
||||
completion_type: "tool_use_single",
|
||||
event: "accept",
|
||||
toolName: toolUseConfirm.tool.name,
|
||||
},
|
||||
],
|
||||
behavior: 'allow',
|
||||
destination: 'localSettings',
|
||||
},
|
||||
])
|
||||
onDone()
|
||||
break
|
||||
}
|
||||
case 'no':
|
||||
void logUnaryEvent({
|
||||
completion_type: 'tool_use_single',
|
||||
event: 'reject',
|
||||
metadata: {
|
||||
language_name: "none",
|
||||
language_name: 'none',
|
||||
message_id: toolUseConfirm.assistantMessage.message.id,
|
||||
platform: env.platform
|
||||
platform: env.platform,
|
||||
},
|
||||
})
|
||||
toolUseConfirm.onReject(feedback)
|
||||
onReject()
|
||||
onDone()
|
||||
break
|
||||
}
|
||||
});
|
||||
toolUseConfirm.onAllow(toolUseConfirm.input, [{
|
||||
type: "addRules",
|
||||
rules: [{
|
||||
toolName: toolUseConfirm.tool.name
|
||||
}],
|
||||
behavior: "allow",
|
||||
destination: "localSettings"
|
||||
}]);
|
||||
onDone();
|
||||
break bb8;
|
||||
}
|
||||
case "no":
|
||||
},
|
||||
[toolUseConfirm, onDone, onReject],
|
||||
)
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
void logUnaryEvent({
|
||||
completion_type: 'tool_use_single',
|
||||
event: 'reject',
|
||||
metadata: {
|
||||
language_name: 'none',
|
||||
message_id: toolUseConfirm.assistantMessage.message.id,
|
||||
platform: env.platform,
|
||||
},
|
||||
})
|
||||
toolUseConfirm.onReject()
|
||||
onReject()
|
||||
onDone()
|
||||
}, [toolUseConfirm, onDone, onReject])
|
||||
|
||||
const originalCwd = getOriginalCwd()
|
||||
const showAlwaysAllowOptions = shouldShowAlwaysAllowOptions()
|
||||
const options = useMemo((): PermissionPromptOption<FallbackOptionValue>[] => {
|
||||
const result: PermissionPromptOption<FallbackOptionValue>[] = [
|
||||
{
|
||||
logUnaryEvent({
|
||||
completion_type: "tool_use_single",
|
||||
event: "reject",
|
||||
metadata: {
|
||||
language_name: "none",
|
||||
message_id: toolUseConfirm.assistantMessage.message.id,
|
||||
platform: env.platform
|
||||
}
|
||||
});
|
||||
toolUseConfirm.onReject(feedback);
|
||||
onReject();
|
||||
onDone();
|
||||
}
|
||||
}
|
||||
};
|
||||
$[5] = onDone;
|
||||
$[6] = onReject;
|
||||
$[7] = toolUseConfirm;
|
||||
$[8] = t3;
|
||||
} else {
|
||||
t3 = $[8];
|
||||
}
|
||||
const handleSelect = t3;
|
||||
let t4;
|
||||
if ($[9] !== onDone || $[10] !== onReject || $[11] !== toolUseConfirm) {
|
||||
t4 = () => {
|
||||
logUnaryEvent({
|
||||
completion_type: "tool_use_single",
|
||||
event: "reject",
|
||||
metadata: {
|
||||
language_name: "none",
|
||||
message_id: toolUseConfirm.assistantMessage.message.id,
|
||||
platform: env.platform
|
||||
}
|
||||
});
|
||||
toolUseConfirm.onReject();
|
||||
onReject();
|
||||
onDone();
|
||||
};
|
||||
$[9] = onDone;
|
||||
$[10] = onReject;
|
||||
$[11] = toolUseConfirm;
|
||||
$[12] = t4;
|
||||
} else {
|
||||
t4 = $[12];
|
||||
}
|
||||
const handleCancel = t4;
|
||||
let t5;
|
||||
if ($[13] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t5 = getOriginalCwd();
|
||||
$[13] = t5;
|
||||
} else {
|
||||
t5 = $[13];
|
||||
}
|
||||
const originalCwd = t5;
|
||||
let t6;
|
||||
if ($[14] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t6 = shouldShowAlwaysAllowOptions();
|
||||
$[14] = t6;
|
||||
} else {
|
||||
t6 = $[14];
|
||||
}
|
||||
const showAlwaysAllowOptions = t6;
|
||||
let t7;
|
||||
if ($[15] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t7 = {
|
||||
label: "Yes",
|
||||
value: "yes",
|
||||
feedbackConfig: {
|
||||
type: "accept"
|
||||
}
|
||||
};
|
||||
$[15] = t7;
|
||||
} else {
|
||||
t7 = $[15];
|
||||
}
|
||||
let result;
|
||||
if ($[16] !== userFacingName) {
|
||||
result = [t7];
|
||||
label: 'Yes',
|
||||
value: 'yes',
|
||||
feedbackConfig: { type: 'accept' },
|
||||
},
|
||||
]
|
||||
|
||||
if (showAlwaysAllowOptions) {
|
||||
const t8 = <Text bold={true}>{userFacingName}</Text>;
|
||||
let t9;
|
||||
if ($[18] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t9 = <Text bold={true}>{originalCwd}</Text>;
|
||||
$[18] = t9;
|
||||
} else {
|
||||
t9 = $[18];
|
||||
result.push({
|
||||
label: (
|
||||
<Text>
|
||||
Yes, and don't ask again for <Text bold>{userFacingName}</Text>{' '}
|
||||
commands in <Text bold>{originalCwd}</Text>
|
||||
</Text>
|
||||
),
|
||||
value: 'yes-dont-ask-again',
|
||||
})
|
||||
}
|
||||
let t10;
|
||||
if ($[19] !== t8) {
|
||||
t10 = {
|
||||
label: <Text>Yes, and don't ask again for {t8}{" "}commands in {t9}</Text>,
|
||||
value: "yes-dont-ask-again"
|
||||
};
|
||||
$[19] = t8;
|
||||
$[20] = t10;
|
||||
} else {
|
||||
t10 = $[20];
|
||||
}
|
||||
result.push(t10);
|
||||
}
|
||||
let t8;
|
||||
if ($[21] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t8 = {
|
||||
label: "No",
|
||||
value: "no",
|
||||
feedbackConfig: {
|
||||
type: "reject"
|
||||
}
|
||||
};
|
||||
$[21] = t8;
|
||||
} else {
|
||||
t8 = $[21];
|
||||
}
|
||||
result.push(t8);
|
||||
$[16] = userFacingName;
|
||||
$[17] = result;
|
||||
} else {
|
||||
result = $[17];
|
||||
}
|
||||
const options = result;
|
||||
let t8;
|
||||
if ($[22] !== toolUseConfirm.tool.name) {
|
||||
t8 = sanitizeToolNameForAnalytics(toolUseConfirm.tool.name);
|
||||
$[22] = toolUseConfirm.tool.name;
|
||||
$[23] = t8;
|
||||
} else {
|
||||
t8 = $[23];
|
||||
}
|
||||
const t9 = toolUseConfirm.tool.isMcp ?? false;
|
||||
let t10;
|
||||
if ($[24] !== t8 || $[25] !== t9) {
|
||||
t10 = {
|
||||
toolName: t8,
|
||||
isMcp: t9
|
||||
};
|
||||
$[24] = t8;
|
||||
$[25] = t9;
|
||||
$[26] = t10;
|
||||
} else {
|
||||
t10 = $[26];
|
||||
}
|
||||
const toolAnalyticsContext = t10;
|
||||
let t11;
|
||||
if ($[27] !== theme || $[28] !== toolUseConfirm.input || $[29] !== toolUseConfirm.tool) {
|
||||
t11 = toolUseConfirm.tool.renderToolUseMessage(toolUseConfirm.input as never, {
|
||||
theme,
|
||||
verbose: true
|
||||
});
|
||||
$[27] = theme;
|
||||
$[28] = toolUseConfirm.input;
|
||||
$[29] = toolUseConfirm.tool;
|
||||
$[30] = t11;
|
||||
} else {
|
||||
t11 = $[30];
|
||||
}
|
||||
let t12;
|
||||
if ($[31] !== originalUserFacingName) {
|
||||
t12 = originalUserFacingName.endsWith(" (MCP)") ? <Text dimColor={true}> (MCP)</Text> : "";
|
||||
$[31] = originalUserFacingName;
|
||||
$[32] = t12;
|
||||
} else {
|
||||
t12 = $[32];
|
||||
}
|
||||
let t13;
|
||||
if ($[33] !== t11 || $[34] !== t12 || $[35] !== userFacingName) {
|
||||
t13 = <Text>{userFacingName}({t11}){t12}</Text>;
|
||||
$[33] = t11;
|
||||
$[34] = t12;
|
||||
$[35] = userFacingName;
|
||||
$[36] = t13;
|
||||
} else {
|
||||
t13 = $[36];
|
||||
}
|
||||
let t14;
|
||||
if ($[37] !== toolUseConfirm.description) {
|
||||
t14 = truncateToLines(toolUseConfirm.description, 3);
|
||||
$[37] = toolUseConfirm.description;
|
||||
$[38] = t14;
|
||||
} else {
|
||||
t14 = $[38];
|
||||
}
|
||||
let t15;
|
||||
if ($[39] !== t14) {
|
||||
t15 = <Text dimColor={true}>{t14}</Text>;
|
||||
$[39] = t14;
|
||||
$[40] = t15;
|
||||
} else {
|
||||
t15 = $[40];
|
||||
}
|
||||
let t16;
|
||||
if ($[41] !== t13 || $[42] !== t15) {
|
||||
t16 = <Box flexDirection="column" paddingX={2} paddingY={1}>{t13}{t15}</Box>;
|
||||
$[41] = t13;
|
||||
$[42] = t15;
|
||||
$[43] = t16;
|
||||
} else {
|
||||
t16 = $[43];
|
||||
}
|
||||
let t17;
|
||||
if ($[44] !== toolUseConfirm.permissionResult) {
|
||||
t17 = <PermissionRuleExplanation permissionResult={toolUseConfirm.permissionResult} toolType="tool" />;
|
||||
$[44] = toolUseConfirm.permissionResult;
|
||||
$[45] = t17;
|
||||
} else {
|
||||
t17 = $[45];
|
||||
}
|
||||
let t18;
|
||||
if ($[46] !== handleCancel || $[47] !== handleSelect || $[48] !== options || $[49] !== toolAnalyticsContext) {
|
||||
t18 = <PermissionPrompt options={options} onSelect={handleSelect} onCancel={handleCancel} toolAnalyticsContext={toolAnalyticsContext} />;
|
||||
$[46] = handleCancel;
|
||||
$[47] = handleSelect;
|
||||
$[48] = options;
|
||||
$[49] = toolAnalyticsContext;
|
||||
$[50] = t18;
|
||||
} else {
|
||||
t18 = $[50];
|
||||
}
|
||||
let t19;
|
||||
if ($[51] !== t17 || $[52] !== t18) {
|
||||
t19 = <Box flexDirection="column">{t17}{t18}</Box>;
|
||||
$[51] = t17;
|
||||
$[52] = t18;
|
||||
$[53] = t19;
|
||||
} else {
|
||||
t19 = $[53];
|
||||
}
|
||||
let t20;
|
||||
if ($[54] !== t16 || $[55] !== t19 || $[56] !== workerBadge) {
|
||||
t20 = <PermissionDialog title="Tool use" workerBadge={workerBadge}>{t16}{t19}</PermissionDialog>;
|
||||
$[54] = t16;
|
||||
$[55] = t19;
|
||||
$[56] = workerBadge;
|
||||
$[57] = t20;
|
||||
} else {
|
||||
t20 = $[57];
|
||||
}
|
||||
return t20;
|
||||
|
||||
result.push({
|
||||
label: 'No',
|
||||
value: 'no',
|
||||
feedbackConfig: { type: 'reject' },
|
||||
})
|
||||
|
||||
return result
|
||||
}, [userFacingName, originalCwd, showAlwaysAllowOptions])
|
||||
|
||||
const toolAnalyticsContext = useMemo(
|
||||
(): ToolAnalyticsContext => ({
|
||||
toolName: sanitizeToolNameForAnalytics(toolUseConfirm.tool.name),
|
||||
isMcp: toolUseConfirm.tool.isMcp ?? false,
|
||||
}),
|
||||
[toolUseConfirm.tool.name, toolUseConfirm.tool.isMcp],
|
||||
)
|
||||
|
||||
return (
|
||||
<PermissionDialog title="Tool use" workerBadge={workerBadge}>
|
||||
<Box flexDirection="column" paddingX={2} paddingY={1}>
|
||||
<Text>
|
||||
{userFacingName}(
|
||||
{toolUseConfirm.tool.renderToolUseMessage(
|
||||
toolUseConfirm.input as never,
|
||||
{ theme, verbose: true },
|
||||
)}
|
||||
)
|
||||
{originalUserFacingName.endsWith(' (MCP)') ? (
|
||||
<Text dimColor> (MCP)</Text>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</Text>
|
||||
<Text dimColor>{truncateToLines(toolUseConfirm.description, 3)}</Text>
|
||||
</Box>
|
||||
|
||||
<Box flexDirection="column">
|
||||
<PermissionRuleExplanation
|
||||
permissionResult={toolUseConfirm.permissionResult}
|
||||
toolType="tool"
|
||||
/>
|
||||
<PermissionPrompt
|
||||
options={options}
|
||||
onSelect={handleSelect}
|
||||
onCancel={handleCancel}
|
||||
toolAnalyticsContext={toolAnalyticsContext}
|
||||
/>
|
||||
</Box>
|
||||
</PermissionDialog>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,181 +1,79 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { basename, relative } from 'path';
|
||||
import React from 'react';
|
||||
import { FileEditToolDiff } from 'src/components/FileEditToolDiff.js';
|
||||
import { getCwd } from 'src/utils/cwd.js';
|
||||
import type { z } from 'zod/v4';
|
||||
import { Text } from '../../../ink.js';
|
||||
import { FileEditTool } from '../../../tools/FileEditTool/FileEditTool.js';
|
||||
import { FilePermissionDialog } from '../FilePermissionDialog/FilePermissionDialog.js';
|
||||
import { createSingleEditDiffConfig, type FileEdit, type IDEDiffSupport } from '../FilePermissionDialog/ideDiffConfig.js';
|
||||
import type { PermissionRequestProps } from '../PermissionRequest.js';
|
||||
type FileEditInput = z.infer<typeof FileEditTool.inputSchema>;
|
||||
import { basename, relative } from 'path'
|
||||
import React from 'react'
|
||||
import { FileEditToolDiff } from 'src/components/FileEditToolDiff.js'
|
||||
import { getCwd } from 'src/utils/cwd.js'
|
||||
import type { z } from 'zod/v4'
|
||||
import { Text } from '../../../ink.js'
|
||||
import { FileEditTool } from '../../../tools/FileEditTool/FileEditTool.js'
|
||||
import { FilePermissionDialog } from '../FilePermissionDialog/FilePermissionDialog.js'
|
||||
import {
|
||||
createSingleEditDiffConfig,
|
||||
type FileEdit,
|
||||
type IDEDiffSupport,
|
||||
} from '../FilePermissionDialog/ideDiffConfig.js'
|
||||
import type { PermissionRequestProps } from '../PermissionRequest.js'
|
||||
|
||||
type FileEditInput = z.infer<typeof FileEditTool.inputSchema>
|
||||
|
||||
const ideDiffSupport: IDEDiffSupport<FileEditInput> = {
|
||||
getConfig: (input: FileEditInput) => createSingleEditDiffConfig(input.file_path, input.old_string, input.new_string, input.replace_all),
|
||||
getConfig: (input: FileEditInput) =>
|
||||
createSingleEditDiffConfig(
|
||||
input.file_path,
|
||||
input.old_string,
|
||||
input.new_string,
|
||||
input.replace_all,
|
||||
),
|
||||
applyChanges: (input: FileEditInput, modifiedEdits: FileEdit[]) => {
|
||||
const firstEdit = modifiedEdits[0];
|
||||
const firstEdit = modifiedEdits[0]
|
||||
if (firstEdit) {
|
||||
return {
|
||||
...input,
|
||||
old_string: firstEdit.old_string,
|
||||
new_string: firstEdit.new_string,
|
||||
replace_all: firstEdit.replace_all
|
||||
};
|
||||
replace_all: firstEdit.replace_all,
|
||||
}
|
||||
return input;
|
||||
}
|
||||
};
|
||||
export function FileEditPermissionRequest(props) {
|
||||
const $ = _c(51);
|
||||
const parseInput = _temp;
|
||||
let T0;
|
||||
let T1;
|
||||
let T2;
|
||||
let file_path;
|
||||
let new_string;
|
||||
let old_string;
|
||||
let replace_all;
|
||||
let t0;
|
||||
let t1;
|
||||
let t10;
|
||||
let t2;
|
||||
let t3;
|
||||
let t4;
|
||||
let t5;
|
||||
let t6;
|
||||
let t7;
|
||||
let t8;
|
||||
let t9;
|
||||
if ($[0] !== props.onDone || $[1] !== props.onReject || $[2] !== props.toolUseConfirm || $[3] !== props.toolUseContext || $[4] !== props.workerBadge) {
|
||||
const parsed = parseInput(props.toolUseConfirm.input);
|
||||
({
|
||||
file_path,
|
||||
old_string,
|
||||
new_string,
|
||||
replace_all
|
||||
} = parsed);
|
||||
T2 = FilePermissionDialog;
|
||||
t4 = props.toolUseConfirm;
|
||||
t5 = props.toolUseContext;
|
||||
t6 = props.onDone;
|
||||
t7 = props.onReject;
|
||||
t8 = props.workerBadge;
|
||||
t9 = "Edit file";
|
||||
t10 = relative(getCwd(), file_path);
|
||||
T1 = Text;
|
||||
t2 = "Do you want to make this edit to";
|
||||
t3 = " ";
|
||||
T0 = Text;
|
||||
t0 = true;
|
||||
t1 = basename(file_path);
|
||||
$[0] = props.onDone;
|
||||
$[1] = props.onReject;
|
||||
$[2] = props.toolUseConfirm;
|
||||
$[3] = props.toolUseContext;
|
||||
$[4] = props.workerBadge;
|
||||
$[5] = T0;
|
||||
$[6] = T1;
|
||||
$[7] = T2;
|
||||
$[8] = file_path;
|
||||
$[9] = new_string;
|
||||
$[10] = old_string;
|
||||
$[11] = replace_all;
|
||||
$[12] = t0;
|
||||
$[13] = t1;
|
||||
$[14] = t10;
|
||||
$[15] = t2;
|
||||
$[16] = t3;
|
||||
$[17] = t4;
|
||||
$[18] = t5;
|
||||
$[19] = t6;
|
||||
$[20] = t7;
|
||||
$[21] = t8;
|
||||
$[22] = t9;
|
||||
} else {
|
||||
T0 = $[5];
|
||||
T1 = $[6];
|
||||
T2 = $[7];
|
||||
file_path = $[8];
|
||||
new_string = $[9];
|
||||
old_string = $[10];
|
||||
replace_all = $[11];
|
||||
t0 = $[12];
|
||||
t1 = $[13];
|
||||
t10 = $[14];
|
||||
t2 = $[15];
|
||||
t3 = $[16];
|
||||
t4 = $[17];
|
||||
t5 = $[18];
|
||||
t6 = $[19];
|
||||
t7 = $[20];
|
||||
t8 = $[21];
|
||||
t9 = $[22];
|
||||
}
|
||||
let t11;
|
||||
if ($[23] !== T0 || $[24] !== t0 || $[25] !== t1) {
|
||||
t11 = <T0 bold={t0}>{t1}</T0>;
|
||||
$[23] = T0;
|
||||
$[24] = t0;
|
||||
$[25] = t1;
|
||||
$[26] = t11;
|
||||
} else {
|
||||
t11 = $[26];
|
||||
}
|
||||
let t12;
|
||||
if ($[27] !== T1 || $[28] !== t11 || $[29] !== t2 || $[30] !== t3) {
|
||||
t12 = <T1>{t2}{t3}{t11}?</T1>;
|
||||
$[27] = T1;
|
||||
$[28] = t11;
|
||||
$[29] = t2;
|
||||
$[30] = t3;
|
||||
$[31] = t12;
|
||||
} else {
|
||||
t12 = $[31];
|
||||
}
|
||||
const t13 = replace_all || false;
|
||||
let t14;
|
||||
if ($[32] !== new_string || $[33] !== old_string || $[34] !== t13) {
|
||||
t14 = [{
|
||||
old_string,
|
||||
new_string,
|
||||
replace_all: t13
|
||||
}];
|
||||
$[32] = new_string;
|
||||
$[33] = old_string;
|
||||
$[34] = t13;
|
||||
$[35] = t14;
|
||||
} else {
|
||||
t14 = $[35];
|
||||
}
|
||||
let t15;
|
||||
if ($[36] !== file_path || $[37] !== t14) {
|
||||
t15 = <FileEditToolDiff file_path={file_path} edits={t14} />;
|
||||
$[36] = file_path;
|
||||
$[37] = t14;
|
||||
$[38] = t15;
|
||||
} else {
|
||||
t15 = $[38];
|
||||
}
|
||||
let t16;
|
||||
if ($[39] !== T2 || $[40] !== file_path || $[41] !== t10 || $[42] !== t12 || $[43] !== t15 || $[44] !== t4 || $[45] !== t5 || $[46] !== t6 || $[47] !== t7 || $[48] !== t8 || $[49] !== t9) {
|
||||
t16 = <T2 toolUseConfirm={t4} toolUseContext={t5} onDone={t6} onReject={t7} workerBadge={t8} title={t9} subtitle={t10} question={t12} content={t15} path={file_path} completionType="str_replace_single" parseInput={parseInput} ideDiffSupport={ideDiffSupport} />;
|
||||
$[39] = T2;
|
||||
$[40] = file_path;
|
||||
$[41] = t10;
|
||||
$[42] = t12;
|
||||
$[43] = t15;
|
||||
$[44] = t4;
|
||||
$[45] = t5;
|
||||
$[46] = t6;
|
||||
$[47] = t7;
|
||||
$[48] = t8;
|
||||
$[49] = t9;
|
||||
$[50] = t16;
|
||||
} else {
|
||||
t16 = $[50];
|
||||
}
|
||||
return t16;
|
||||
return input
|
||||
},
|
||||
}
|
||||
function _temp(input) {
|
||||
return FileEditTool.inputSchema.parse(input);
|
||||
|
||||
export function FileEditPermissionRequest(
|
||||
props: PermissionRequestProps,
|
||||
): React.ReactNode {
|
||||
const parseInput = (input: unknown): FileEditInput => {
|
||||
return FileEditTool.inputSchema.parse(input)
|
||||
}
|
||||
|
||||
const parsed = parseInput(props.toolUseConfirm.input)
|
||||
const { file_path, old_string, new_string, replace_all } = parsed
|
||||
|
||||
return (
|
||||
<FilePermissionDialog
|
||||
toolUseConfirm={props.toolUseConfirm}
|
||||
toolUseContext={props.toolUseContext}
|
||||
onDone={props.onDone}
|
||||
onReject={props.onReject}
|
||||
workerBadge={props.workerBadge}
|
||||
title="Edit file"
|
||||
subtitle={relative(getCwd(), file_path)}
|
||||
question={
|
||||
<Text>
|
||||
Do you want to make this edit to{' '}
|
||||
<Text bold>{basename(file_path)}</Text>?
|
||||
</Text>
|
||||
}
|
||||
content={
|
||||
<FileEditToolDiff
|
||||
file_path={file_path}
|
||||
edits={[
|
||||
{ old_string, new_string, replace_all: replace_all || false },
|
||||
]}
|
||||
/>
|
||||
}
|
||||
path={file_path}
|
||||
completionType="str_replace_single"
|
||||
parseInput={parseInput}
|
||||
ideDiffSupport={ideDiffSupport}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,50 +1,61 @@
|
||||
import { relative } from 'path';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useDiffInIDE } from '../../../hooks/useDiffInIDE.js';
|
||||
import { Box, Text } from '../../../ink.js';
|
||||
import type { ToolUseContext } from '../../../Tool.js';
|
||||
import { getLanguageName } from '../../../utils/cliHighlight.js';
|
||||
import { getCwd } from '../../../utils/cwd.js';
|
||||
import { getFsImplementation, safeResolvePath } from '../../../utils/fsOperations.js';
|
||||
import { expandPath } from '../../../utils/path.js';
|
||||
import type { CompletionType } from '../../../utils/unaryLogging.js';
|
||||
import { Select } from '../../CustomSelect/index.js';
|
||||
import { ShowInIDEPrompt } from '../../ShowInIDEPrompt.js';
|
||||
import { usePermissionRequestLogging } from '../hooks.js';
|
||||
import { PermissionDialog } from '../PermissionDialog.js';
|
||||
import type { ToolUseConfirm } from '../PermissionRequest.js';
|
||||
import type { WorkerBadgeProps } from '../WorkerBadge.js';
|
||||
import type { IDEDiffSupport } from './ideDiffConfig.js';
|
||||
import type { FileOperationType, PermissionOption } from './permissionOptions.js';
|
||||
import { type ToolInput, useFilePermissionDialog } from './useFilePermissionDialog.js';
|
||||
import { relative } from 'path'
|
||||
import React, { useMemo } from 'react'
|
||||
import { useDiffInIDE } from '../../../hooks/useDiffInIDE.js'
|
||||
import { Box, Text } from '../../../ink.js'
|
||||
import type { ToolUseContext } from '../../../Tool.js'
|
||||
import { getLanguageName } from '../../../utils/cliHighlight.js'
|
||||
import { getCwd } from '../../../utils/cwd.js'
|
||||
import {
|
||||
getFsImplementation,
|
||||
safeResolvePath,
|
||||
} from '../../../utils/fsOperations.js'
|
||||
import { expandPath } from '../../../utils/path.js'
|
||||
import type { CompletionType } from '../../../utils/unaryLogging.js'
|
||||
import { Select } from '../../CustomSelect/index.js'
|
||||
import { ShowInIDEPrompt } from '../../ShowInIDEPrompt.js'
|
||||
import { usePermissionRequestLogging } from '../hooks.js'
|
||||
import { PermissionDialog } from '../PermissionDialog.js'
|
||||
import type { ToolUseConfirm } from '../PermissionRequest.js'
|
||||
import type { WorkerBadgeProps } from '../WorkerBadge.js'
|
||||
import type { IDEDiffSupport } from './ideDiffConfig.js'
|
||||
import type {
|
||||
FileOperationType,
|
||||
PermissionOption,
|
||||
} from './permissionOptions.js'
|
||||
import {
|
||||
type ToolInput,
|
||||
useFilePermissionDialog,
|
||||
} from './useFilePermissionDialog.js'
|
||||
|
||||
export type FilePermissionDialogProps<T extends ToolInput = ToolInput> = {
|
||||
// Required props from PermissionRequestProps
|
||||
toolUseConfirm: ToolUseConfirm;
|
||||
toolUseContext: ToolUseContext;
|
||||
onDone: () => void;
|
||||
onReject: () => void;
|
||||
toolUseConfirm: ToolUseConfirm
|
||||
toolUseContext: ToolUseContext
|
||||
onDone: () => void
|
||||
onReject: () => void
|
||||
|
||||
// Dialog customization
|
||||
title: string;
|
||||
subtitle?: React.ReactNode;
|
||||
question?: string | React.ReactNode;
|
||||
content?: React.ReactNode; // Can be general content or diff component
|
||||
title: string
|
||||
subtitle?: React.ReactNode
|
||||
question?: string | React.ReactNode
|
||||
content?: React.ReactNode // Can be general content or diff component
|
||||
|
||||
// Logging
|
||||
completionType?: CompletionType;
|
||||
languageName?: string; // override — derived from path when omitted
|
||||
completionType?: CompletionType
|
||||
languageName?: string // override — derived from path when omitted
|
||||
|
||||
// File/directory operations
|
||||
path: string | null;
|
||||
parseInput: (input: unknown) => T;
|
||||
operationType?: FileOperationType;
|
||||
path: string | null
|
||||
parseInput: (input: unknown) => T
|
||||
operationType?: FileOperationType
|
||||
|
||||
// IDE diff support
|
||||
ideDiffSupport?: IDEDiffSupport<T>;
|
||||
ideDiffSupport?: IDEDiffSupport<T>
|
||||
|
||||
// Worker badge for teammate permission requests
|
||||
workerBadge: WorkerBadgeProps | undefined;
|
||||
};
|
||||
workerBadge: WorkerBadgeProps | undefined
|
||||
}
|
||||
|
||||
export function FilePermissionDialog<T extends ToolInput = ToolInput>({
|
||||
toolUseConfirm,
|
||||
toolUseContext,
|
||||
@@ -60,33 +71,38 @@ export function FilePermissionDialog<T extends ToolInput = ToolInput>({
|
||||
operationType = 'write',
|
||||
ideDiffSupport,
|
||||
workerBadge,
|
||||
languageName: languageNameOverride
|
||||
languageName: languageNameOverride,
|
||||
}: FilePermissionDialogProps<T>): React.ReactNode {
|
||||
// Derive from path unless caller provided an explicit override (NotebookEdit
|
||||
// passes 'python'/'markdown' from cell_type). getLanguageName is async;
|
||||
// downstream UnaryEvent.language_name and logPermissionEvent already accept
|
||||
// Promise<string>. useMemo keeps the promise stable across renders.
|
||||
const languageName = useMemo(() => languageNameOverride ?? (path ? getLanguageName(path) : 'none'), [languageNameOverride, path]);
|
||||
const unaryEvent = useMemo(() => ({
|
||||
const languageName = useMemo(
|
||||
() => languageNameOverride ?? (path ? getLanguageName(path) : 'none'),
|
||||
[languageNameOverride, path],
|
||||
)
|
||||
const unaryEvent = useMemo(
|
||||
() => ({
|
||||
completion_type: completionType,
|
||||
language_name: languageName
|
||||
}), [completionType, languageName]);
|
||||
usePermissionRequestLogging(toolUseConfirm, unaryEvent);
|
||||
language_name: languageName,
|
||||
}),
|
||||
[completionType, languageName],
|
||||
)
|
||||
usePermissionRequestLogging(toolUseConfirm, unaryEvent)
|
||||
|
||||
const symlinkTarget = useMemo(() => {
|
||||
if (!path || operationType === 'read') {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
const expandedPath = expandPath(path);
|
||||
const fs = getFsImplementation();
|
||||
const {
|
||||
resolvedPath,
|
||||
isSymlink
|
||||
} = safeResolvePath(fs, expandedPath);
|
||||
const expandedPath = expandPath(path)
|
||||
const fs = getFsImplementation()
|
||||
const { resolvedPath, isSymlink } = safeResolvePath(fs, expandedPath)
|
||||
if (isSymlink) {
|
||||
return resolvedPath;
|
||||
return resolvedPath
|
||||
}
|
||||
return null;
|
||||
}, [path, operationType]);
|
||||
return null
|
||||
}, [path, operationType])
|
||||
|
||||
const fileDialogResult = useFilePermissionDialog({
|
||||
filePath: path || '',
|
||||
completionType,
|
||||
@@ -95,8 +111,8 @@ export function FilePermissionDialog<T extends ToolInput = ToolInput>({
|
||||
onDone,
|
||||
onReject,
|
||||
parseInput,
|
||||
operationType
|
||||
});
|
||||
operationType,
|
||||
})
|
||||
|
||||
// Use file dialog results for options
|
||||
const {
|
||||
@@ -107,97 +123,150 @@ export function FilePermissionDialog<T extends ToolInput = ToolInput>({
|
||||
handleInputModeToggle,
|
||||
focusedOption,
|
||||
yesInputMode,
|
||||
noInputMode
|
||||
} = fileDialogResult;
|
||||
noInputMode,
|
||||
} = fileDialogResult
|
||||
|
||||
// Parse input using the provided parser
|
||||
const parsedInput = parseInput(toolUseConfirm.input);
|
||||
const parsedInput = parseInput(toolUseConfirm.input)
|
||||
|
||||
// Set up IDE diff support if enabled. Memoized: getConfig may do disk I/O
|
||||
// (FileWrite's getConfig calls readFileSync for the old-content diff).
|
||||
// Keyed on the raw input — parseInput is a pure Zod parse whose result
|
||||
// depends only on toolUseConfirm.input.
|
||||
const ideDiffConfig = useMemo(() => ideDiffSupport ? ideDiffSupport.getConfig(parseInput(toolUseConfirm.input)) : null, [ideDiffSupport, toolUseConfirm.input]);
|
||||
const ideDiffConfig = useMemo(
|
||||
() =>
|
||||
ideDiffSupport
|
||||
? ideDiffSupport.getConfig(parseInput(toolUseConfirm.input))
|
||||
: null,
|
||||
[ideDiffSupport, toolUseConfirm.input],
|
||||
)
|
||||
|
||||
// Create diff params based on whether IDE diff is available
|
||||
const diffParams = ideDiffConfig ? {
|
||||
onChange: (option: PermissionOption, input: {
|
||||
file_path: string;
|
||||
const diffParams = ideDiffConfig
|
||||
? {
|
||||
onChange: (
|
||||
option: PermissionOption,
|
||||
input: {
|
||||
file_path: string
|
||||
edits: Array<{
|
||||
old_string: string;
|
||||
new_string: string;
|
||||
replace_all?: boolean;
|
||||
}>;
|
||||
}) => {
|
||||
const transformedInput = ideDiffSupport!.applyChanges(parsedInput, input.edits);
|
||||
fileDialogResult.onChange(option, transformedInput);
|
||||
old_string: string
|
||||
new_string: string
|
||||
replace_all?: boolean
|
||||
}>
|
||||
},
|
||||
) => {
|
||||
const transformedInput = ideDiffSupport!.applyChanges(
|
||||
parsedInput,
|
||||
input.edits,
|
||||
)
|
||||
fileDialogResult.onChange(option, transformedInput)
|
||||
},
|
||||
toolUseContext,
|
||||
filePath: ideDiffConfig.filePath,
|
||||
edits: (ideDiffConfig.edits || []).map(e => ({
|
||||
old_string: e.old_string,
|
||||
new_string: e.new_string,
|
||||
replace_all: e.replace_all || false
|
||||
replace_all: e.replace_all || false,
|
||||
})),
|
||||
editMode: ideDiffConfig.editMode || 'single'
|
||||
} : {
|
||||
editMode: ideDiffConfig.editMode || 'single',
|
||||
}
|
||||
: {
|
||||
onChange: () => {},
|
||||
toolUseContext,
|
||||
filePath: '',
|
||||
edits: [],
|
||||
editMode: 'single' as const
|
||||
};
|
||||
const {
|
||||
closeTabInIDE,
|
||||
showingDiffInIDE,
|
||||
ideName
|
||||
} = useDiffInIDE(diffParams);
|
||||
const onChange = (option_0: PermissionOption, feedback?: string) => {
|
||||
closeTabInIDE?.();
|
||||
fileDialogResult.onChange(option_0, parsedInput, feedback?.trim());
|
||||
};
|
||||
if (showingDiffInIDE && ideDiffConfig && path) {
|
||||
return <ShowInIDEPrompt onChange={(option_1: PermissionOption, _input, feedback_0?: string) => onChange(option_1, feedback_0)} options={options} filePath={path} input={parsedInput} ideName={ideName} symlinkTarget={symlinkTarget} rejectFeedback={rejectFeedback} acceptFeedback={acceptFeedback} setFocusedOption={setFocusedOption} onInputModeToggle={handleInputModeToggle} focusedOption={focusedOption} yesInputMode={yesInputMode} noInputMode={noInputMode} />;
|
||||
editMode: 'single' as const,
|
||||
}
|
||||
const isSymlinkOutsideCwd = symlinkTarget != null && relative(getCwd(), symlinkTarget).startsWith('..');
|
||||
const symlinkWarning = symlinkTarget ? <Box paddingX={1} marginBottom={1}>
|
||||
|
||||
const { closeTabInIDE, showingDiffInIDE, ideName } = useDiffInIDE(diffParams)
|
||||
|
||||
const onChange = (option: PermissionOption, feedback?: string) => {
|
||||
closeTabInIDE?.()
|
||||
fileDialogResult.onChange(option, parsedInput, feedback?.trim())
|
||||
}
|
||||
|
||||
if (showingDiffInIDE && ideDiffConfig && path) {
|
||||
return (
|
||||
<ShowInIDEPrompt
|
||||
onChange={(option: PermissionOption, _input, feedback?: string) =>
|
||||
onChange(option, feedback)
|
||||
}
|
||||
options={options}
|
||||
filePath={path}
|
||||
input={parsedInput}
|
||||
ideName={ideName}
|
||||
symlinkTarget={symlinkTarget}
|
||||
rejectFeedback={rejectFeedback}
|
||||
acceptFeedback={acceptFeedback}
|
||||
setFocusedOption={setFocusedOption}
|
||||
onInputModeToggle={handleInputModeToggle}
|
||||
focusedOption={focusedOption}
|
||||
yesInputMode={yesInputMode}
|
||||
noInputMode={noInputMode}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const isSymlinkOutsideCwd =
|
||||
symlinkTarget != null && relative(getCwd(), symlinkTarget).startsWith('..')
|
||||
|
||||
const symlinkWarning = symlinkTarget ? (
|
||||
<Box paddingX={1} marginBottom={1}>
|
||||
<Text color="warning">
|
||||
{isSymlinkOutsideCwd ? `This will modify ${symlinkTarget} (outside working directory) via a symlink` : `Symlink target: ${symlinkTarget}`}
|
||||
{isSymlinkOutsideCwd
|
||||
? `This will modify ${symlinkTarget} (outside working directory) via a symlink`
|
||||
: `Symlink target: ${symlinkTarget}`}
|
||||
</Text>
|
||||
</Box> : null;
|
||||
return <>
|
||||
<PermissionDialog title={title} subtitle={subtitle} innerPaddingX={0} workerBadge={workerBadge}>
|
||||
</Box>
|
||||
) : null
|
||||
|
||||
return (
|
||||
<>
|
||||
<PermissionDialog
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
innerPaddingX={0}
|
||||
workerBadge={workerBadge}
|
||||
>
|
||||
{symlinkWarning}
|
||||
{content}
|
||||
<Box flexDirection="column" paddingX={1}>
|
||||
{typeof question === 'string' ? <Text>{question}</Text> : question}
|
||||
<Select options={options} inlineDescriptions onChange={value => {
|
||||
const selected = options.find(opt => opt.value === value);
|
||||
<Select
|
||||
options={options}
|
||||
inlineDescriptions
|
||||
onChange={value => {
|
||||
const selected = options.find(opt => opt.value === value)
|
||||
if (selected) {
|
||||
// For reject option
|
||||
if (selected.option.type === 'reject') {
|
||||
const trimmedFeedback = rejectFeedback.trim();
|
||||
onChange(selected.option, trimmedFeedback || undefined);
|
||||
return;
|
||||
const trimmedFeedback = rejectFeedback.trim()
|
||||
onChange(selected.option, trimmedFeedback || undefined)
|
||||
return
|
||||
}
|
||||
// For accept-once option, pass accept feedback if present
|
||||
if (selected.option.type === 'accept-once') {
|
||||
const trimmedFeedback_0 = acceptFeedback.trim();
|
||||
onChange(selected.option, trimmedFeedback_0 || undefined);
|
||||
return;
|
||||
const trimmedFeedback = acceptFeedback.trim()
|
||||
onChange(selected.option, trimmedFeedback || undefined)
|
||||
return
|
||||
}
|
||||
onChange(selected.option);
|
||||
onChange(selected.option)
|
||||
}
|
||||
}} onCancel={() => onChange({
|
||||
type: 'reject'
|
||||
})} onFocus={value_0 => setFocusedOption(value_0)} onInputModeToggle={handleInputModeToggle} />
|
||||
}}
|
||||
onCancel={() => onChange({ type: 'reject' })}
|
||||
onFocus={value => setFocusedOption(value)}
|
||||
onInputModeToggle={handleInputModeToggle}
|
||||
/>
|
||||
</Box>
|
||||
</PermissionDialog>
|
||||
<Box paddingX={1} marginTop={1}>
|
||||
<Text dimColor>
|
||||
Esc to cancel
|
||||
{(focusedOption === 'yes' && !yesInputMode || focusedOption === 'no' && !noInputMode) && ' · Tab to amend'}
|
||||
{((focusedOption === 'yes' && !yesInputMode) ||
|
||||
(focusedOption === 'no' && !noInputMode)) &&
|
||||
' · Tab to amend'}
|
||||
</Text>
|
||||
</Box>
|
||||
</>;
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,29 +1,37 @@
|
||||
import { homedir } from 'os';
|
||||
import { basename, join, sep } from 'path';
|
||||
import React, { type ReactNode } from 'react';
|
||||
import { getOriginalCwd } from '../../../bootstrap/state.js';
|
||||
import { Text } from '../../../ink.js';
|
||||
import { getShortcutDisplay } from '../../../keybindings/shortcutFormat.js';
|
||||
import type { ToolPermissionContext } from '../../../Tool.js';
|
||||
import { expandPath, getDirectoryForPath } from '../../../utils/path.js';
|
||||
import { normalizeCaseForComparison, pathInAllowedWorkingPath } from '../../../utils/permissions/filesystem.js';
|
||||
import type { OptionWithDescription } from '../../CustomSelect/select.js';
|
||||
import { homedir } from 'os'
|
||||
import { basename, join, sep } from 'path'
|
||||
import React, { type ReactNode } from 'react'
|
||||
import { getOriginalCwd } from '../../../bootstrap/state.js'
|
||||
import { Text } from '../../../ink.js'
|
||||
import { getShortcutDisplay } from '../../../keybindings/shortcutFormat.js'
|
||||
import type { ToolPermissionContext } from '../../../Tool.js'
|
||||
import { expandPath, getDirectoryForPath } from '../../../utils/path.js'
|
||||
import {
|
||||
normalizeCaseForComparison,
|
||||
pathInAllowedWorkingPath,
|
||||
} from '../../../utils/permissions/filesystem.js'
|
||||
import type { OptionWithDescription } from '../../CustomSelect/select.js'
|
||||
/**
|
||||
* Check if a path is within the project's .claude/ folder.
|
||||
* This is used to determine whether to show the special ".claude folder" permission option.
|
||||
*/
|
||||
export function isInClaudeFolder(filePath: string): boolean {
|
||||
const absolutePath = expandPath(filePath);
|
||||
const claudeFolderPath = expandPath(`${getOriginalCwd()}/.claude`);
|
||||
const absolutePath = expandPath(filePath)
|
||||
const claudeFolderPath = expandPath(`${getOriginalCwd()}/.claude`)
|
||||
|
||||
// Check if the path is within the project's .claude folder
|
||||
const normalizedAbsolutePath = normalizeCaseForComparison(absolutePath);
|
||||
const normalizedClaudeFolderPath = normalizeCaseForComparison(claudeFolderPath);
|
||||
const normalizedAbsolutePath = normalizeCaseForComparison(absolutePath)
|
||||
const normalizedClaudeFolderPath =
|
||||
normalizeCaseForComparison(claudeFolderPath)
|
||||
|
||||
// Path must start with the .claude folder path (and be inside it, not just the folder itself)
|
||||
return normalizedAbsolutePath.startsWith(normalizedClaudeFolderPath + sep.toLowerCase()) ||
|
||||
return (
|
||||
normalizedAbsolutePath.startsWith(
|
||||
normalizedClaudeFolderPath + sep.toLowerCase(),
|
||||
) ||
|
||||
// Also match case where sep is / on posix systems
|
||||
normalizedAbsolutePath.startsWith(normalizedClaudeFolderPath + '/');
|
||||
normalizedAbsolutePath.startsWith(normalizedClaudeFolderPath + '/')
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -32,24 +40,33 @@ export function isInClaudeFolder(filePath: string): boolean {
|
||||
* for files in the user's home directory.
|
||||
*/
|
||||
export function isInGlobalClaudeFolder(filePath: string): boolean {
|
||||
const absolutePath = expandPath(filePath);
|
||||
const globalClaudeFolderPath = join(homedir(), '.claude');
|
||||
const normalizedAbsolutePath = normalizeCaseForComparison(absolutePath);
|
||||
const normalizedGlobalClaudeFolderPath = normalizeCaseForComparison(globalClaudeFolderPath);
|
||||
return normalizedAbsolutePath.startsWith(normalizedGlobalClaudeFolderPath + sep.toLowerCase()) || normalizedAbsolutePath.startsWith(normalizedGlobalClaudeFolderPath + '/');
|
||||
const absolutePath = expandPath(filePath)
|
||||
const globalClaudeFolderPath = join(homedir(), '.claude')
|
||||
|
||||
const normalizedAbsolutePath = normalizeCaseForComparison(absolutePath)
|
||||
const normalizedGlobalClaudeFolderPath = normalizeCaseForComparison(
|
||||
globalClaudeFolderPath,
|
||||
)
|
||||
|
||||
return (
|
||||
normalizedAbsolutePath.startsWith(
|
||||
normalizedGlobalClaudeFolderPath + sep.toLowerCase(),
|
||||
) ||
|
||||
normalizedAbsolutePath.startsWith(normalizedGlobalClaudeFolderPath + '/')
|
||||
)
|
||||
}
|
||||
export type PermissionOption = {
|
||||
type: 'accept-once';
|
||||
} | {
|
||||
type: 'accept-session';
|
||||
scope?: 'claude-folder' | 'global-claude-folder';
|
||||
} | {
|
||||
type: 'reject';
|
||||
};
|
||||
|
||||
export type PermissionOption =
|
||||
| { type: 'accept-once' }
|
||||
| { type: 'accept-session'; scope?: 'claude-folder' | 'global-claude-folder' }
|
||||
| { type: 'reject' }
|
||||
|
||||
export type PermissionOptionWithLabel = OptionWithDescription<string> & {
|
||||
option: PermissionOption;
|
||||
};
|
||||
export type FileOperationType = 'read' | 'write' | 'create';
|
||||
option: PermissionOption
|
||||
}
|
||||
|
||||
export type FileOperationType = 'read' | 'write' | 'create'
|
||||
|
||||
export function getFilePermissionOptions({
|
||||
filePath,
|
||||
toolPermissionContext,
|
||||
@@ -57,18 +74,22 @@ export function getFilePermissionOptions({
|
||||
onRejectFeedbackChange,
|
||||
onAcceptFeedbackChange,
|
||||
yesInputMode = false,
|
||||
noInputMode = false
|
||||
noInputMode = false,
|
||||
}: {
|
||||
filePath: string;
|
||||
toolPermissionContext: ToolPermissionContext;
|
||||
operationType?: FileOperationType;
|
||||
onRejectFeedbackChange?: (value: string) => void;
|
||||
onAcceptFeedbackChange?: (value: string) => void;
|
||||
yesInputMode?: boolean;
|
||||
noInputMode?: boolean;
|
||||
filePath: string
|
||||
toolPermissionContext: ToolPermissionContext
|
||||
operationType?: FileOperationType
|
||||
onRejectFeedbackChange?: (value: string) => void
|
||||
onAcceptFeedbackChange?: (value: string) => void
|
||||
yesInputMode?: boolean
|
||||
noInputMode?: boolean
|
||||
}): PermissionOptionWithLabel[] {
|
||||
const options: PermissionOptionWithLabel[] = [];
|
||||
const modeCycleShortcut = getShortcutDisplay('chat:cycleMode', 'Chat', 'shift+tab');
|
||||
const options: PermissionOptionWithLabel[] = []
|
||||
const modeCycleShortcut = getShortcutDisplay(
|
||||
'chat:cycleMode',
|
||||
'Chat',
|
||||
'shift+tab',
|
||||
)
|
||||
|
||||
// When in input mode, show input field
|
||||
if (yesInputMode && onAcceptFeedbackChange) {
|
||||
@@ -79,24 +100,24 @@ export function getFilePermissionOptions({
|
||||
placeholder: 'and tell Claude what to do next',
|
||||
onChange: onAcceptFeedbackChange,
|
||||
allowEmptySubmitToCancel: true,
|
||||
option: {
|
||||
type: 'accept-once'
|
||||
}
|
||||
});
|
||||
option: { type: 'accept-once' },
|
||||
})
|
||||
} else {
|
||||
options.push({
|
||||
label: 'Yes',
|
||||
value: 'yes',
|
||||
option: {
|
||||
type: 'accept-once'
|
||||
option: { type: 'accept-once' },
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
const inAllowedPath = pathInAllowedWorkingPath(filePath, toolPermissionContext);
|
||||
|
||||
const inAllowedPath = pathInAllowedWorkingPath(
|
||||
filePath,
|
||||
toolPermissionContext,
|
||||
)
|
||||
|
||||
// Check if this is a .claude/ folder path (project or global)
|
||||
const inClaudeFolder = isInClaudeFolder(filePath);
|
||||
const inGlobalClaudeFolder = isInGlobalClaudeFolder(filePath);
|
||||
const inClaudeFolder = isInClaudeFolder(filePath)
|
||||
const inGlobalClaudeFolder = isInGlobalClaudeFolder(filePath)
|
||||
|
||||
// Option 2: For .claude/ folder, show special option instead of generic session option
|
||||
// Note: Session-level options are always shown since they only affect in-memory state,
|
||||
@@ -108,45 +129,52 @@ export function getFilePermissionOptions({
|
||||
value: 'yes-claude-folder',
|
||||
option: {
|
||||
type: 'accept-session',
|
||||
scope: inGlobalClaudeFolder ? 'global-claude-folder' : 'claude-folder'
|
||||
}
|
||||
});
|
||||
scope: inGlobalClaudeFolder ? 'global-claude-folder' : 'claude-folder',
|
||||
},
|
||||
})
|
||||
} else {
|
||||
// Option 2: Allow all changes/reads during session
|
||||
let sessionLabel: ReactNode;
|
||||
let sessionLabel: ReactNode
|
||||
|
||||
if (inAllowedPath) {
|
||||
// Inside working directory
|
||||
if (operationType === 'read') {
|
||||
sessionLabel = 'Yes, during this session';
|
||||
sessionLabel = 'Yes, during this session'
|
||||
} else {
|
||||
sessionLabel = <Text>
|
||||
sessionLabel = (
|
||||
<Text>
|
||||
Yes, allow all edits during this session{' '}
|
||||
<Text bold>({modeCycleShortcut})</Text>
|
||||
</Text>;
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Outside working directory - include directory name
|
||||
const dirPath = getDirectoryForPath(filePath);
|
||||
const dirName = basename(dirPath) || 'this directory';
|
||||
const dirPath = getDirectoryForPath(filePath)
|
||||
const dirName = basename(dirPath) || 'this directory'
|
||||
|
||||
if (operationType === 'read') {
|
||||
sessionLabel = <Text>
|
||||
sessionLabel = (
|
||||
<Text>
|
||||
Yes, allow reading from <Text bold>{dirName}/</Text> during this
|
||||
session
|
||||
</Text>;
|
||||
</Text>
|
||||
)
|
||||
} else {
|
||||
sessionLabel = <Text>
|
||||
sessionLabel = (
|
||||
<Text>
|
||||
Yes, allow all edits in <Text bold>{dirName}/</Text> during this
|
||||
session <Text bold>({modeCycleShortcut})</Text>
|
||||
</Text>;
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
options.push({
|
||||
label: sessionLabel,
|
||||
value: 'yes-session',
|
||||
option: {
|
||||
type: 'accept-session'
|
||||
}
|
||||
});
|
||||
option: { type: 'accept-session' },
|
||||
})
|
||||
}
|
||||
|
||||
// When in input mode, show input field for reject
|
||||
@@ -158,19 +186,16 @@ export function getFilePermissionOptions({
|
||||
placeholder: 'and tell Claude what to do differently',
|
||||
onChange: onRejectFeedbackChange,
|
||||
allowEmptySubmitToCancel: true,
|
||||
option: {
|
||||
type: 'reject'
|
||||
}
|
||||
});
|
||||
option: { type: 'reject' },
|
||||
})
|
||||
} else {
|
||||
// Not in input mode - simple option
|
||||
options.push({
|
||||
label: 'No',
|
||||
value: 'no',
|
||||
option: {
|
||||
type: 'reject'
|
||||
option: { type: 'reject' },
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
return options;
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
@@ -1,160 +1,101 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { basename, relative } from 'path';
|
||||
import React, { useMemo } from 'react';
|
||||
import type { z } from 'zod/v4';
|
||||
import { Text } from '../../../ink.js';
|
||||
import { FileWriteTool } from '../../../tools/FileWriteTool/FileWriteTool.js';
|
||||
import { getCwd } from '../../../utils/cwd.js';
|
||||
import { isENOENT } from '../../../utils/errors.js';
|
||||
import { readFileSync } from '../../../utils/fileRead.js';
|
||||
import { FilePermissionDialog } from '../FilePermissionDialog/FilePermissionDialog.js';
|
||||
import { createSingleEditDiffConfig, type FileEdit, type IDEDiffSupport } from '../FilePermissionDialog/ideDiffConfig.js';
|
||||
import type { PermissionRequestProps } from '../PermissionRequest.js';
|
||||
import { FileWriteToolDiff } from './FileWriteToolDiff.js';
|
||||
type FileWriteToolInput = z.infer<typeof FileWriteTool.inputSchema>;
|
||||
import { basename, relative } from 'path'
|
||||
import React, { useMemo } from 'react'
|
||||
import type { z } from 'zod/v4'
|
||||
import { Text } from '../../../ink.js'
|
||||
import { FileWriteTool } from '../../../tools/FileWriteTool/FileWriteTool.js'
|
||||
import { getCwd } from '../../../utils/cwd.js'
|
||||
import { isENOENT } from '../../../utils/errors.js'
|
||||
import { readFileSync } from '../../../utils/fileRead.js'
|
||||
import { FilePermissionDialog } from '../FilePermissionDialog/FilePermissionDialog.js'
|
||||
import {
|
||||
createSingleEditDiffConfig,
|
||||
type FileEdit,
|
||||
type IDEDiffSupport,
|
||||
} from '../FilePermissionDialog/ideDiffConfig.js'
|
||||
import type { PermissionRequestProps } from '../PermissionRequest.js'
|
||||
import { FileWriteToolDiff } from './FileWriteToolDiff.js'
|
||||
|
||||
type FileWriteToolInput = z.infer<typeof FileWriteTool.inputSchema>
|
||||
|
||||
const ideDiffSupport: IDEDiffSupport<FileWriteToolInput> = {
|
||||
getConfig: (input: FileWriteToolInput) => {
|
||||
let oldContent: string;
|
||||
let oldContent: string
|
||||
try {
|
||||
oldContent = readFileSync(input.file_path);
|
||||
oldContent = readFileSync(input.file_path)
|
||||
} catch (e) {
|
||||
if (!isENOENT(e)) throw e;
|
||||
oldContent = '';
|
||||
if (!isENOENT(e)) throw e
|
||||
oldContent = ''
|
||||
}
|
||||
return createSingleEditDiffConfig(input.file_path, oldContent, input.content, false // For file writes, we replace the entire content
|
||||
);
|
||||
|
||||
return createSingleEditDiffConfig(
|
||||
input.file_path,
|
||||
oldContent,
|
||||
input.content,
|
||||
false, // For file writes, we replace the entire content
|
||||
)
|
||||
},
|
||||
applyChanges: (input: FileWriteToolInput, modifiedEdits: FileEdit[]) => {
|
||||
const firstEdit = modifiedEdits[0];
|
||||
const firstEdit = modifiedEdits[0]
|
||||
if (firstEdit) {
|
||||
return {
|
||||
...input,
|
||||
content: firstEdit.new_string
|
||||
};
|
||||
content: firstEdit.new_string,
|
||||
}
|
||||
return input;
|
||||
}
|
||||
};
|
||||
export function FileWritePermissionRequest(props) {
|
||||
const $ = _c(30);
|
||||
const parseInput = _temp;
|
||||
let t0;
|
||||
if ($[0] !== props.toolUseConfirm.input) {
|
||||
t0 = parseInput(props.toolUseConfirm.input);
|
||||
$[0] = props.toolUseConfirm.input;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
return input
|
||||
},
|
||||
}
|
||||
|
||||
export function FileWritePermissionRequest(
|
||||
props: PermissionRequestProps,
|
||||
): React.ReactNode {
|
||||
const parseInput = (input: unknown): FileWriteToolInput => {
|
||||
return FileWriteTool.inputSchema.parse(input)
|
||||
}
|
||||
const parsed = t0;
|
||||
const {
|
||||
file_path,
|
||||
content
|
||||
} = parsed;
|
||||
let t1;
|
||||
if ($[2] !== file_path) {
|
||||
;
|
||||
|
||||
const parsed = parseInput(props.toolUseConfirm.input)
|
||||
const { file_path, content } = parsed
|
||||
|
||||
// Single read drives both UI text ("Create" vs "Overwrite") and the diff
|
||||
// shown by FileWriteToolDiff — avoids a redundant existsSync stat that would
|
||||
// block first-mount commit on slow/networked filesystems.
|
||||
const { fileExists, oldContent } = useMemo(() => {
|
||||
try {
|
||||
t1 = {
|
||||
fileExists: true,
|
||||
oldContent: readFileSync(file_path)
|
||||
};
|
||||
} catch (t2) {
|
||||
const e = t2;
|
||||
if (!isENOENT(e)) {
|
||||
throw e;
|
||||
return { fileExists: true, oldContent: readFileSync(file_path) }
|
||||
} catch (e) {
|
||||
if (!isENOENT(e)) throw e
|
||||
return { fileExists: false, oldContent: '' }
|
||||
}
|
||||
let t3;
|
||||
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = {
|
||||
fileExists: false,
|
||||
oldContent: ""
|
||||
};
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}, [file_path])
|
||||
|
||||
const actionText = fileExists ? 'overwrite' : 'create'
|
||||
|
||||
return (
|
||||
<FilePermissionDialog
|
||||
toolUseConfirm={props.toolUseConfirm}
|
||||
toolUseContext={props.toolUseContext}
|
||||
onDone={props.onDone}
|
||||
onReject={props.onReject}
|
||||
workerBadge={props.workerBadge}
|
||||
title={fileExists ? 'Overwrite file' : 'Create file'}
|
||||
subtitle={relative(getCwd(), file_path)}
|
||||
question={
|
||||
<Text>
|
||||
Do you want to {actionText} <Text bold>{basename(file_path)}</Text>?
|
||||
</Text>
|
||||
}
|
||||
t1 = t3;
|
||||
content={
|
||||
<FileWriteToolDiff
|
||||
file_path={file_path}
|
||||
content={content}
|
||||
fileExists={fileExists}
|
||||
oldContent={oldContent}
|
||||
/>
|
||||
}
|
||||
$[2] = file_path;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
const {
|
||||
fileExists,
|
||||
oldContent
|
||||
} = t1;
|
||||
const actionText = fileExists ? "overwrite" : "create";
|
||||
const t2 = props.toolUseConfirm;
|
||||
const t3 = props.toolUseContext;
|
||||
const t4 = props.onDone;
|
||||
const t5 = props.onReject;
|
||||
const t6 = props.workerBadge;
|
||||
const t7 = fileExists ? "Overwrite file" : "Create file";
|
||||
let t8;
|
||||
if ($[5] !== file_path) {
|
||||
t8 = relative(getCwd(), file_path);
|
||||
$[5] = file_path;
|
||||
$[6] = t8;
|
||||
} else {
|
||||
t8 = $[6];
|
||||
}
|
||||
let t9;
|
||||
if ($[7] !== file_path) {
|
||||
t9 = basename(file_path);
|
||||
$[7] = file_path;
|
||||
$[8] = t9;
|
||||
} else {
|
||||
t9 = $[8];
|
||||
}
|
||||
let t10;
|
||||
if ($[9] !== t9) {
|
||||
t10 = <Text bold={true}>{t9}</Text>;
|
||||
$[9] = t9;
|
||||
$[10] = t10;
|
||||
} else {
|
||||
t10 = $[10];
|
||||
}
|
||||
let t11;
|
||||
if ($[11] !== actionText || $[12] !== t10) {
|
||||
t11 = <Text>Do you want to {actionText} {t10}?</Text>;
|
||||
$[11] = actionText;
|
||||
$[12] = t10;
|
||||
$[13] = t11;
|
||||
} else {
|
||||
t11 = $[13];
|
||||
}
|
||||
let t12;
|
||||
if ($[14] !== content || $[15] !== fileExists || $[16] !== file_path || $[17] !== oldContent) {
|
||||
t12 = <FileWriteToolDiff file_path={file_path} content={content} fileExists={fileExists} oldContent={oldContent} />;
|
||||
$[14] = content;
|
||||
$[15] = fileExists;
|
||||
$[16] = file_path;
|
||||
$[17] = oldContent;
|
||||
$[18] = t12;
|
||||
} else {
|
||||
t12 = $[18];
|
||||
}
|
||||
let t13;
|
||||
if ($[19] !== file_path || $[20] !== props.onDone || $[21] !== props.onReject || $[22] !== props.toolUseConfirm || $[23] !== props.toolUseContext || $[24] !== props.workerBadge || $[25] !== t11 || $[26] !== t12 || $[27] !== t7 || $[28] !== t8) {
|
||||
t13 = <FilePermissionDialog toolUseConfirm={t2} toolUseContext={t3} onDone={t4} onReject={t5} workerBadge={t6} title={t7} subtitle={t8} question={t11} content={t12} path={file_path} completionType="write_file_single" parseInput={parseInput} ideDiffSupport={ideDiffSupport} />;
|
||||
$[19] = file_path;
|
||||
$[20] = props.onDone;
|
||||
$[21] = props.onReject;
|
||||
$[22] = props.toolUseConfirm;
|
||||
$[23] = props.toolUseContext;
|
||||
$[24] = props.workerBadge;
|
||||
$[25] = t11;
|
||||
$[26] = t12;
|
||||
$[27] = t7;
|
||||
$[28] = t8;
|
||||
$[29] = t13;
|
||||
} else {
|
||||
t13 = $[29];
|
||||
}
|
||||
return t13;
|
||||
}
|
||||
function _temp(input) {
|
||||
return FileWriteTool.inputSchema.parse(input);
|
||||
path={file_path}
|
||||
completionType="write_file_single"
|
||||
parseInput={parseInput}
|
||||
ideDiffSupport={ideDiffSupport}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,88 +1,82 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { useTerminalSize } from '../../../hooks/useTerminalSize.js';
|
||||
import { Box, NoSelect, Text } from '../../../ink.js';
|
||||
import { intersperse } from '../../../utils/array.js';
|
||||
import { getPatchForDisplay } from '../../../utils/diff.js';
|
||||
import { HighlightedCode } from '../../HighlightedCode.js';
|
||||
import { StructuredDiff } from '../../StructuredDiff.js';
|
||||
import * as React from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { useTerminalSize } from '../../../hooks/useTerminalSize.js'
|
||||
import { Box, NoSelect, Text } from '../../../ink.js'
|
||||
import { intersperse } from '../../../utils/array.js'
|
||||
import { getPatchForDisplay } from '../../../utils/diff.js'
|
||||
import { HighlightedCode } from '../../HighlightedCode.js'
|
||||
import { StructuredDiff } from '../../StructuredDiff.js'
|
||||
|
||||
type Props = {
|
||||
file_path: string;
|
||||
content: string;
|
||||
fileExists: boolean;
|
||||
oldContent: string;
|
||||
};
|
||||
export function FileWriteToolDiff(t0) {
|
||||
const $ = _c(15);
|
||||
const {
|
||||
file_path: string
|
||||
content: string
|
||||
fileExists: boolean
|
||||
oldContent: string
|
||||
}
|
||||
|
||||
export function FileWriteToolDiff({
|
||||
file_path,
|
||||
content,
|
||||
fileExists,
|
||||
oldContent
|
||||
} = t0;
|
||||
const {
|
||||
columns
|
||||
} = useTerminalSize();
|
||||
let t1;
|
||||
bb0: {
|
||||
oldContent,
|
||||
}: Props): React.ReactNode {
|
||||
const { columns } = useTerminalSize()
|
||||
const hunks = useMemo(() => {
|
||||
if (!fileExists) {
|
||||
t1 = null;
|
||||
break bb0;
|
||||
return null
|
||||
}
|
||||
let t2;
|
||||
if ($[0] !== content || $[1] !== file_path || $[2] !== oldContent) {
|
||||
t2 = getPatchForDisplay({
|
||||
return getPatchForDisplay({
|
||||
filePath: file_path,
|
||||
fileContents: oldContent,
|
||||
edits: [{
|
||||
edits: [
|
||||
{
|
||||
old_string: oldContent,
|
||||
new_string: content,
|
||||
replace_all: false
|
||||
}]
|
||||
});
|
||||
$[0] = content;
|
||||
$[1] = file_path;
|
||||
$[2] = oldContent;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
t1 = t2;
|
||||
}
|
||||
const hunks = t1;
|
||||
let t2;
|
||||
if ($[4] !== content) {
|
||||
t2 = content.split("\n")[0] ?? null;
|
||||
$[4] = content;
|
||||
$[5] = t2;
|
||||
} else {
|
||||
t2 = $[5];
|
||||
}
|
||||
const firstLine = t2;
|
||||
let t3;
|
||||
if ($[6] !== columns || $[7] !== content || $[8] !== file_path || $[9] !== firstLine || $[10] !== hunks || $[11] !== oldContent) {
|
||||
t3 = hunks ? intersperse(hunks.map(_ => <StructuredDiff key={_.newStart} patch={_} dim={false} filePath={file_path} firstLine={firstLine} fileContent={oldContent} width={columns - 2} />), _temp) : <HighlightedCode code={content || "(No content)"} filePath={file_path} />;
|
||||
$[6] = columns;
|
||||
$[7] = content;
|
||||
$[8] = file_path;
|
||||
$[9] = firstLine;
|
||||
$[10] = hunks;
|
||||
$[11] = oldContent;
|
||||
$[12] = t3;
|
||||
} else {
|
||||
t3 = $[12];
|
||||
}
|
||||
let t4;
|
||||
if ($[13] !== t3) {
|
||||
t4 = <Box flexDirection="column"><Box borderColor="subtle" borderStyle="dashed" flexDirection="column" borderLeft={false} borderRight={false} paddingX={1}>{t3}</Box></Box>;
|
||||
$[13] = t3;
|
||||
$[14] = t4;
|
||||
} else {
|
||||
t4 = $[14];
|
||||
}
|
||||
return t4;
|
||||
}
|
||||
function _temp(i) {
|
||||
return <NoSelect fromLeftEdge={true} key={`ellipsis-${i}`}><Text dimColor={true}>...</Text></NoSelect>;
|
||||
replace_all: false,
|
||||
},
|
||||
],
|
||||
})
|
||||
}, [fileExists, file_path, oldContent, content])
|
||||
|
||||
const firstLine = content.split('\n')[0] ?? null
|
||||
const paddingX = 1
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Box
|
||||
borderColor="subtle"
|
||||
borderStyle="dashed"
|
||||
flexDirection="column"
|
||||
borderLeft={false}
|
||||
borderRight={false}
|
||||
paddingX={paddingX}
|
||||
>
|
||||
{hunks ? (
|
||||
intersperse(
|
||||
hunks.map(_ => (
|
||||
<StructuredDiff
|
||||
key={_.newStart}
|
||||
patch={_}
|
||||
dim={false}
|
||||
filePath={file_path}
|
||||
firstLine={firstLine}
|
||||
fileContent={oldContent}
|
||||
width={columns - 2 * paddingX}
|
||||
/>
|
||||
)),
|
||||
i => (
|
||||
<NoSelect fromLeftEdge key={`ellipsis-${i}`}>
|
||||
<Text dimColor>...</Text>
|
||||
</NoSelect>
|
||||
),
|
||||
)
|
||||
) : (
|
||||
<HighlightedCode
|
||||
code={content || '(No content)'}
|
||||
filePath={file_path}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,114 +1,89 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React from 'react';
|
||||
import { Box, Text, useTheme } from '../../../ink.js';
|
||||
import { FallbackPermissionRequest } from '../FallbackPermissionRequest.js';
|
||||
import { FilePermissionDialog } from '../FilePermissionDialog/FilePermissionDialog.js';
|
||||
import type { ToolInput } from '../FilePermissionDialog/useFilePermissionDialog.js';
|
||||
import type { PermissionRequestProps, ToolUseConfirm } from '../PermissionRequest.js';
|
||||
import React from 'react'
|
||||
import { Box, Text, useTheme } from '../../../ink.js'
|
||||
import { FallbackPermissionRequest } from '../FallbackPermissionRequest.js'
|
||||
import { FilePermissionDialog } from '../FilePermissionDialog/FilePermissionDialog.js'
|
||||
import type { ToolInput } from '../FilePermissionDialog/useFilePermissionDialog.js'
|
||||
import type {
|
||||
PermissionRequestProps,
|
||||
ToolUseConfirm,
|
||||
} from '../PermissionRequest.js'
|
||||
|
||||
function pathFromToolUse(toolUseConfirm: ToolUseConfirm): string | null {
|
||||
const tool = toolUseConfirm.tool;
|
||||
const tool = toolUseConfirm.tool
|
||||
if ('getPath' in tool && typeof tool.getPath === 'function') {
|
||||
try {
|
||||
return tool.getPath(toolUseConfirm.input);
|
||||
return tool.getPath(toolUseConfirm.input)
|
||||
} catch {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
export function FilesystemPermissionRequest(t0) {
|
||||
const $ = _c(30);
|
||||
const {
|
||||
|
||||
export function FilesystemPermissionRequest({
|
||||
toolUseConfirm,
|
||||
onDone,
|
||||
onReject,
|
||||
verbose,
|
||||
toolUseContext,
|
||||
workerBadge
|
||||
} = t0;
|
||||
const [theme] = useTheme();
|
||||
let t1;
|
||||
if ($[0] !== toolUseConfirm) {
|
||||
t1 = pathFromToolUse(toolUseConfirm);
|
||||
$[0] = toolUseConfirm;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const path = t1;
|
||||
let t2;
|
||||
if ($[2] !== toolUseConfirm.input || $[3] !== toolUseConfirm.tool) {
|
||||
t2 = toolUseConfirm.tool.userFacingName(toolUseConfirm.input as never);
|
||||
$[2] = toolUseConfirm.input;
|
||||
$[3] = toolUseConfirm.tool;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
const userFacingName = t2;
|
||||
const isReadOnly = toolUseConfirm.tool.isReadOnly(toolUseConfirm.input);
|
||||
const userFacingReadOrEdit = isReadOnly ? "Read" : "Edit";
|
||||
const title = `${userFacingReadOrEdit} file`;
|
||||
const parseInput = _temp;
|
||||
workerBadge,
|
||||
}: PermissionRequestProps): React.ReactNode {
|
||||
const [theme] = useTheme()
|
||||
const path = pathFromToolUse(toolUseConfirm)
|
||||
const userFacingName = toolUseConfirm.tool.userFacingName(
|
||||
toolUseConfirm.input as never,
|
||||
)
|
||||
|
||||
const isReadOnly = toolUseConfirm.tool.isReadOnly(toolUseConfirm.input)
|
||||
const userFacingReadOrEdit = isReadOnly ? 'Read' : 'Edit'
|
||||
|
||||
// Use simple singular form - the actual operation details are shown in content
|
||||
const title = `${userFacingReadOrEdit} file`
|
||||
|
||||
// Simple pass-through parser since we don't need to transform the input
|
||||
const parseInput = (input: unknown): ToolInput => input as ToolInput
|
||||
|
||||
// Fall back to generic permission request if no path is found
|
||||
if (!path) {
|
||||
let t3;
|
||||
if ($[5] !== onDone || $[6] !== onReject || $[7] !== toolUseConfirm || $[8] !== toolUseContext || $[9] !== verbose || $[10] !== workerBadge) {
|
||||
t3 = <FallbackPermissionRequest toolUseConfirm={toolUseConfirm} toolUseContext={toolUseContext} onDone={onDone} onReject={onReject} verbose={verbose} workerBadge={workerBadge} />;
|
||||
$[5] = onDone;
|
||||
$[6] = onReject;
|
||||
$[7] = toolUseConfirm;
|
||||
$[8] = toolUseContext;
|
||||
$[9] = verbose;
|
||||
$[10] = workerBadge;
|
||||
$[11] = t3;
|
||||
} else {
|
||||
t3 = $[11];
|
||||
return (
|
||||
<FallbackPermissionRequest
|
||||
toolUseConfirm={toolUseConfirm}
|
||||
toolUseContext={toolUseContext}
|
||||
onDone={onDone}
|
||||
onReject={onReject}
|
||||
verbose={verbose}
|
||||
workerBadge={workerBadge}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
let t3;
|
||||
if ($[12] !== theme || $[13] !== toolUseConfirm.input || $[14] !== toolUseConfirm.tool || $[15] !== verbose) {
|
||||
t3 = toolUseConfirm.tool.renderToolUseMessage(toolUseConfirm.input as never, {
|
||||
theme,
|
||||
verbose
|
||||
});
|
||||
$[12] = theme;
|
||||
$[13] = toolUseConfirm.input;
|
||||
$[14] = toolUseConfirm.tool;
|
||||
$[15] = verbose;
|
||||
$[16] = t3;
|
||||
} else {
|
||||
t3 = $[16];
|
||||
}
|
||||
let t4;
|
||||
if ($[17] !== t3 || $[18] !== userFacingName) {
|
||||
t4 = <Box flexDirection="column" paddingX={2} paddingY={1}><Text>{userFacingName}({t3})</Text></Box>;
|
||||
$[17] = t3;
|
||||
$[18] = userFacingName;
|
||||
$[19] = t4;
|
||||
} else {
|
||||
t4 = $[19];
|
||||
}
|
||||
const content = t4;
|
||||
const t5 = isReadOnly ? "read" : "write";
|
||||
let t6;
|
||||
if ($[20] !== content || $[21] !== onDone || $[22] !== onReject || $[23] !== path || $[24] !== t5 || $[25] !== title || $[26] !== toolUseConfirm || $[27] !== toolUseContext || $[28] !== workerBadge) {
|
||||
t6 = <FilePermissionDialog toolUseConfirm={toolUseConfirm} toolUseContext={toolUseContext} onDone={onDone} onReject={onReject} workerBadge={workerBadge} title={title} content={content} path={path} parseInput={parseInput} operationType={t5} completionType="tool_use_single" />;
|
||||
$[20] = content;
|
||||
$[21] = onDone;
|
||||
$[22] = onReject;
|
||||
$[23] = path;
|
||||
$[24] = t5;
|
||||
$[25] = title;
|
||||
$[26] = toolUseConfirm;
|
||||
$[27] = toolUseContext;
|
||||
$[28] = workerBadge;
|
||||
$[29] = t6;
|
||||
} else {
|
||||
t6 = $[29];
|
||||
}
|
||||
return t6;
|
||||
}
|
||||
function _temp(input) {
|
||||
return input as ToolInput;
|
||||
|
||||
// Render tool use message content
|
||||
const content = (
|
||||
<Box flexDirection="column" paddingX={2} paddingY={1}>
|
||||
<Text>
|
||||
{userFacingName}(
|
||||
{toolUseConfirm.tool.renderToolUseMessage(
|
||||
toolUseConfirm.input as never,
|
||||
{ theme, verbose },
|
||||
)}
|
||||
)
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
|
||||
return (
|
||||
<FilePermissionDialog
|
||||
toolUseConfirm={toolUseConfirm}
|
||||
toolUseContext={toolUseContext}
|
||||
onDone={onDone}
|
||||
onReject={onReject}
|
||||
workerBadge={workerBadge}
|
||||
title={title}
|
||||
content={content}
|
||||
path={path}
|
||||
parseInput={parseInput}
|
||||
operationType={isReadOnly ? 'read' : 'write'}
|
||||
completionType="tool_use_single"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,165 +1,77 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { basename } from 'path';
|
||||
import React from 'react';
|
||||
import type { z } from 'zod/v4';
|
||||
import { Text } from '../../../ink.js';
|
||||
import { NotebookEditTool } from '../../../tools/NotebookEditTool/NotebookEditTool.js';
|
||||
import { logError } from '../../../utils/log.js';
|
||||
import { FilePermissionDialog } from '../FilePermissionDialog/FilePermissionDialog.js';
|
||||
import type { PermissionRequestProps } from '../PermissionRequest.js';
|
||||
import { NotebookEditToolDiff } from './NotebookEditToolDiff.js';
|
||||
type NotebookEditInput = z.infer<typeof NotebookEditTool.inputSchema>;
|
||||
export function NotebookEditPermissionRequest(props) {
|
||||
const $ = _c(52);
|
||||
const parseInput = _temp;
|
||||
let T0;
|
||||
let T1;
|
||||
let T2;
|
||||
let language;
|
||||
let notebook_path;
|
||||
let parsed;
|
||||
let t0;
|
||||
let t1;
|
||||
let t10;
|
||||
let t2;
|
||||
let t3;
|
||||
let t4;
|
||||
let t5;
|
||||
let t6;
|
||||
let t7;
|
||||
let t8;
|
||||
let t9;
|
||||
if ($[0] !== props.onDone || $[1] !== props.onReject || $[2] !== props.toolUseConfirm || $[3] !== props.toolUseContext || $[4] !== props.workerBadge) {
|
||||
parsed = parseInput(props.toolUseConfirm.input);
|
||||
const {
|
||||
notebook_path: t11,
|
||||
edit_mode,
|
||||
cell_type
|
||||
} = parsed;
|
||||
notebook_path = t11;
|
||||
language = cell_type === "markdown" ? "markdown" : "python";
|
||||
const editTypeText = edit_mode === "insert" ? "insert this cell into" : edit_mode === "delete" ? "delete this cell from" : "make this edit to";
|
||||
T2 = FilePermissionDialog;
|
||||
t5 = props.toolUseConfirm;
|
||||
t6 = props.toolUseContext;
|
||||
t7 = props.onDone;
|
||||
t8 = props.onReject;
|
||||
t9 = props.workerBadge;
|
||||
t10 = "Edit notebook";
|
||||
T1 = Text;
|
||||
t2 = "Do you want to ";
|
||||
t3 = editTypeText;
|
||||
t4 = " ";
|
||||
T0 = Text;
|
||||
t0 = true;
|
||||
t1 = basename(notebook_path);
|
||||
$[0] = props.onDone;
|
||||
$[1] = props.onReject;
|
||||
$[2] = props.toolUseConfirm;
|
||||
$[3] = props.toolUseContext;
|
||||
$[4] = props.workerBadge;
|
||||
$[5] = T0;
|
||||
$[6] = T1;
|
||||
$[7] = T2;
|
||||
$[8] = language;
|
||||
$[9] = notebook_path;
|
||||
$[10] = parsed;
|
||||
$[11] = t0;
|
||||
$[12] = t1;
|
||||
$[13] = t10;
|
||||
$[14] = t2;
|
||||
$[15] = t3;
|
||||
$[16] = t4;
|
||||
$[17] = t5;
|
||||
$[18] = t6;
|
||||
$[19] = t7;
|
||||
$[20] = t8;
|
||||
$[21] = t9;
|
||||
} else {
|
||||
T0 = $[5];
|
||||
T1 = $[6];
|
||||
T2 = $[7];
|
||||
language = $[8];
|
||||
notebook_path = $[9];
|
||||
parsed = $[10];
|
||||
t0 = $[11];
|
||||
t1 = $[12];
|
||||
t10 = $[13];
|
||||
t2 = $[14];
|
||||
t3 = $[15];
|
||||
t4 = $[16];
|
||||
t5 = $[17];
|
||||
t6 = $[18];
|
||||
t7 = $[19];
|
||||
t8 = $[20];
|
||||
t9 = $[21];
|
||||
}
|
||||
let t11;
|
||||
if ($[22] !== T0 || $[23] !== t0 || $[24] !== t1) {
|
||||
t11 = <T0 bold={t0}>{t1}</T0>;
|
||||
$[22] = T0;
|
||||
$[23] = t0;
|
||||
$[24] = t1;
|
||||
$[25] = t11;
|
||||
} else {
|
||||
t11 = $[25];
|
||||
}
|
||||
let t12;
|
||||
if ($[26] !== T1 || $[27] !== t11 || $[28] !== t2 || $[29] !== t3 || $[30] !== t4) {
|
||||
t12 = <T1>{t2}{t3}{t4}{t11}?</T1>;
|
||||
$[26] = T1;
|
||||
$[27] = t11;
|
||||
$[28] = t2;
|
||||
$[29] = t3;
|
||||
$[30] = t4;
|
||||
$[31] = t12;
|
||||
} else {
|
||||
t12 = $[31];
|
||||
}
|
||||
const t13 = props.verbose ? 120 : 80;
|
||||
let t14;
|
||||
if ($[32] !== parsed.cell_id || $[33] !== parsed.cell_type || $[34] !== parsed.edit_mode || $[35] !== parsed.new_source || $[36] !== parsed.notebook_path || $[37] !== props.verbose || $[38] !== t13) {
|
||||
t14 = <NotebookEditToolDiff notebook_path={parsed.notebook_path} cell_id={parsed.cell_id} new_source={parsed.new_source} cell_type={parsed.cell_type} edit_mode={parsed.edit_mode} verbose={props.verbose} width={t13} />;
|
||||
$[32] = parsed.cell_id;
|
||||
$[33] = parsed.cell_type;
|
||||
$[34] = parsed.edit_mode;
|
||||
$[35] = parsed.new_source;
|
||||
$[36] = parsed.notebook_path;
|
||||
$[37] = props.verbose;
|
||||
$[38] = t13;
|
||||
$[39] = t14;
|
||||
} else {
|
||||
t14 = $[39];
|
||||
}
|
||||
let t15;
|
||||
if ($[40] !== T2 || $[41] !== language || $[42] !== notebook_path || $[43] !== t10 || $[44] !== t12 || $[45] !== t14 || $[46] !== t5 || $[47] !== t6 || $[48] !== t7 || $[49] !== t8 || $[50] !== t9) {
|
||||
t15 = <T2 toolUseConfirm={t5} toolUseContext={t6} onDone={t7} onReject={t8} workerBadge={t9} title={t10} question={t12} content={t14} path={notebook_path} completionType="tool_use_single" languageName={language} parseInput={parseInput} />;
|
||||
$[40] = T2;
|
||||
$[41] = language;
|
||||
$[42] = notebook_path;
|
||||
$[43] = t10;
|
||||
$[44] = t12;
|
||||
$[45] = t14;
|
||||
$[46] = t5;
|
||||
$[47] = t6;
|
||||
$[48] = t7;
|
||||
$[49] = t8;
|
||||
$[50] = t9;
|
||||
$[51] = t15;
|
||||
} else {
|
||||
t15 = $[51];
|
||||
}
|
||||
return t15;
|
||||
}
|
||||
function _temp(input) {
|
||||
const result = NotebookEditTool.inputSchema.safeParse(input);
|
||||
import { basename } from 'path'
|
||||
import React from 'react'
|
||||
import type { z } from 'zod/v4'
|
||||
import { Text } from '../../../ink.js'
|
||||
import { NotebookEditTool } from '../../../tools/NotebookEditTool/NotebookEditTool.js'
|
||||
import { logError } from '../../../utils/log.js'
|
||||
import { FilePermissionDialog } from '../FilePermissionDialog/FilePermissionDialog.js'
|
||||
import type { PermissionRequestProps } from '../PermissionRequest.js'
|
||||
import { NotebookEditToolDiff } from './NotebookEditToolDiff.js'
|
||||
|
||||
type NotebookEditInput = z.infer<typeof NotebookEditTool.inputSchema>
|
||||
|
||||
export function NotebookEditPermissionRequest(
|
||||
props: PermissionRequestProps,
|
||||
): React.ReactNode {
|
||||
const parseInput = (input: unknown): NotebookEditInput => {
|
||||
const result = NotebookEditTool.inputSchema.safeParse(input)
|
||||
if (!result.success) {
|
||||
logError(new Error(`Failed to parse notebook edit input: ${result.error.message}`));
|
||||
logError(
|
||||
new Error(
|
||||
`Failed to parse notebook edit input: ${result.error.message}`,
|
||||
),
|
||||
)
|
||||
// Return a default value to avoid crashing
|
||||
return {
|
||||
notebook_path: "",
|
||||
new_source: "",
|
||||
cell_id: ""
|
||||
} as NotebookEditInput;
|
||||
notebook_path: '',
|
||||
new_source: '',
|
||||
cell_id: '',
|
||||
} as NotebookEditInput
|
||||
}
|
||||
return result.data;
|
||||
return result.data
|
||||
}
|
||||
|
||||
const parsed = parseInput(props.toolUseConfirm.input)
|
||||
const { notebook_path, edit_mode, cell_type } = parsed
|
||||
|
||||
const language = cell_type === 'markdown' ? 'markdown' : 'python'
|
||||
|
||||
const editTypeText =
|
||||
edit_mode === 'insert'
|
||||
? 'insert this cell into'
|
||||
: edit_mode === 'delete'
|
||||
? 'delete this cell from'
|
||||
: 'make this edit to'
|
||||
|
||||
return (
|
||||
<FilePermissionDialog
|
||||
toolUseConfirm={props.toolUseConfirm}
|
||||
toolUseContext={props.toolUseContext}
|
||||
onDone={props.onDone}
|
||||
onReject={props.onReject}
|
||||
workerBadge={props.workerBadge}
|
||||
title="Edit notebook"
|
||||
question={
|
||||
<Text>
|
||||
Do you want to {editTypeText}{' '}
|
||||
<Text bold>{basename(notebook_path)}</Text>?
|
||||
</Text>
|
||||
}
|
||||
content={
|
||||
<NotebookEditToolDiff
|
||||
notebook_path={parsed.notebook_path}
|
||||
cell_id={parsed.cell_id}
|
||||
new_source={parsed.new_source}
|
||||
cell_type={parsed.cell_type}
|
||||
edit_mode={parsed.edit_mode}
|
||||
verbose={props.verbose}
|
||||
width={props.verbose ? 120 : 80}
|
||||
/>
|
||||
}
|
||||
path={notebook_path}
|
||||
completionType="tool_use_single"
|
||||
languageName={language}
|
||||
parseInput={parseInput}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,234 +1,172 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { relative } from 'path';
|
||||
import * as React from 'react';
|
||||
import { Suspense, use, useMemo } from 'react';
|
||||
import { Box, NoSelect, Text } from '../../../ink.js';
|
||||
import type { NotebookCellType, NotebookContent } from '../../../types/notebook.js';
|
||||
import { intersperse } from '../../../utils/array.js';
|
||||
import { getCwd } from '../../../utils/cwd.js';
|
||||
import { getPatchForDisplay } from '../../../utils/diff.js';
|
||||
import { getFsImplementation } from '../../../utils/fsOperations.js';
|
||||
import { safeParseJSON } from '../../../utils/json.js';
|
||||
import { parseCellId } from '../../../utils/notebook.js';
|
||||
import { HighlightedCode } from '../../HighlightedCode.js';
|
||||
import { StructuredDiff } from '../../StructuredDiff.js';
|
||||
import { relative } from 'path'
|
||||
import * as React from 'react'
|
||||
import { Suspense, use, useMemo } from 'react'
|
||||
import { Box, NoSelect, Text } from '../../../ink.js'
|
||||
import type {
|
||||
NotebookCellType,
|
||||
NotebookContent,
|
||||
} from '../../../types/notebook.js'
|
||||
import { intersperse } from '../../../utils/array.js'
|
||||
import { getCwd } from '../../../utils/cwd.js'
|
||||
import { getPatchForDisplay } from '../../../utils/diff.js'
|
||||
import { getFsImplementation } from '../../../utils/fsOperations.js'
|
||||
import { safeParseJSON } from '../../../utils/json.js'
|
||||
import { parseCellId } from '../../../utils/notebook.js'
|
||||
import { HighlightedCode } from '../../HighlightedCode.js'
|
||||
import { StructuredDiff } from '../../StructuredDiff.js'
|
||||
|
||||
type Props = {
|
||||
notebook_path: string;
|
||||
cell_id: string | undefined;
|
||||
new_source: string;
|
||||
cell_type?: NotebookCellType;
|
||||
edit_mode?: string;
|
||||
verbose: boolean;
|
||||
width: number;
|
||||
};
|
||||
notebook_path: string
|
||||
cell_id: string | undefined
|
||||
new_source: string
|
||||
cell_type?: NotebookCellType
|
||||
edit_mode?: string
|
||||
verbose: boolean
|
||||
width: number
|
||||
}
|
||||
|
||||
type InnerProps = {
|
||||
notebook_path: string;
|
||||
cell_id: string | undefined;
|
||||
new_source: string;
|
||||
cell_type?: NotebookCellType;
|
||||
edit_mode?: string;
|
||||
verbose: boolean;
|
||||
width: number;
|
||||
promise: Promise<NotebookContent | null>;
|
||||
};
|
||||
export function NotebookEditToolDiff(props: Props) {
|
||||
const $ = _c(5);
|
||||
let t0;
|
||||
if ($[0] !== props.notebook_path) {
|
||||
t0 = getFsImplementation().readFile(props.notebook_path, {
|
||||
encoding: "utf-8"
|
||||
}).then(_temp).catch(_temp2);
|
||||
$[0] = props.notebook_path;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
const notebookDataPromise = t0;
|
||||
let t1;
|
||||
if ($[2] !== notebookDataPromise || $[3] !== props) {
|
||||
t1 = <Suspense fallback={null}><NotebookEditToolDiffInner {...props} promise={notebookDataPromise} /></Suspense>;
|
||||
$[2] = notebookDataPromise;
|
||||
$[3] = props;
|
||||
$[4] = t1;
|
||||
} else {
|
||||
t1 = $[4];
|
||||
}
|
||||
return t1;
|
||||
notebook_path: string
|
||||
cell_id: string | undefined
|
||||
new_source: string
|
||||
cell_type?: NotebookCellType
|
||||
edit_mode?: string
|
||||
verbose: boolean
|
||||
width: number
|
||||
promise: Promise<NotebookContent | null>
|
||||
}
|
||||
function _temp2() {
|
||||
return null;
|
||||
|
||||
export function NotebookEditToolDiff(props: Props): React.ReactNode {
|
||||
// Create a promise that never rejects so we can handle errors inline.
|
||||
// Memoized on notebook_path so we don't re-read on every render.
|
||||
const notebookDataPromise = useMemo(
|
||||
() =>
|
||||
getFsImplementation()
|
||||
.readFile(props.notebook_path, { encoding: 'utf-8' })
|
||||
.then(content => safeParseJSON(content) as NotebookContent | null)
|
||||
.catch(() => null),
|
||||
[props.notebook_path],
|
||||
)
|
||||
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<NotebookEditToolDiffInner {...props} promise={notebookDataPromise} />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
function _temp(content) {
|
||||
return safeParseJSON(content) as NotebookContent | null;
|
||||
}
|
||||
function NotebookEditToolDiffInner(t0: InnerProps) {
|
||||
const $ = _c(34);
|
||||
const {
|
||||
|
||||
function NotebookEditToolDiffInner({
|
||||
notebook_path,
|
||||
cell_id,
|
||||
new_source,
|
||||
cell_type,
|
||||
edit_mode: t1,
|
||||
edit_mode = 'replace',
|
||||
verbose,
|
||||
width,
|
||||
promise
|
||||
} = t0;
|
||||
const edit_mode = t1 === undefined ? "replace" : t1;
|
||||
const notebookData = use(promise);
|
||||
let t2;
|
||||
if ($[0] !== cell_id || $[1] !== notebookData) {
|
||||
bb0: {
|
||||
promise,
|
||||
}: InnerProps): React.ReactNode {
|
||||
const notebookData = use(promise)
|
||||
|
||||
const oldSource = useMemo(() => {
|
||||
if (!notebookData || !cell_id) {
|
||||
t2 = "";
|
||||
break bb0;
|
||||
return ''
|
||||
}
|
||||
const cellIndex = parseCellId(cell_id);
|
||||
const cellIndex = parseCellId(cell_id)
|
||||
if (cellIndex !== undefined) {
|
||||
if (notebookData.cells[cellIndex]) {
|
||||
const source = notebookData.cells[cellIndex].source;
|
||||
let t3;
|
||||
if ($[3] !== source) {
|
||||
t3 = Array.isArray(source) ? source.join("") : source;
|
||||
$[3] = source;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
const source = notebookData.cells[cellIndex].source
|
||||
return Array.isArray(source) ? source.join('') : source
|
||||
}
|
||||
t2 = t3;
|
||||
break bb0;
|
||||
return ''
|
||||
}
|
||||
t2 = "";
|
||||
break bb0;
|
||||
const cell = notebookData.cells.find(cell => cell.id === cell_id)
|
||||
if (!cell) {
|
||||
return ''
|
||||
}
|
||||
let t3;
|
||||
if ($[5] !== cell_id) {
|
||||
t3 = cell => cell.id === cell_id;
|
||||
$[5] = cell_id;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
return Array.isArray(cell.source) ? cell.source.join('') : cell.source
|
||||
}, [notebookData, cell_id])
|
||||
|
||||
const hunks = useMemo(() => {
|
||||
if (!notebookData || edit_mode === 'insert' || edit_mode === 'delete') {
|
||||
return null
|
||||
}
|
||||
const cell_0 = notebookData.cells.find(t3);
|
||||
if (!cell_0) {
|
||||
t2 = "";
|
||||
break bb0;
|
||||
}
|
||||
t2 = Array.isArray(cell_0.source) ? cell_0.source.join("") : cell_0.source;
|
||||
}
|
||||
$[0] = cell_id;
|
||||
$[1] = notebookData;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
const oldSource = t2;
|
||||
let t3;
|
||||
bb1: {
|
||||
if (!notebookData || edit_mode === "insert" || edit_mode === "delete") {
|
||||
t3 = null;
|
||||
break bb1;
|
||||
}
|
||||
let t4;
|
||||
if ($[7] !== new_source || $[8] !== notebook_path || $[9] !== oldSource) {
|
||||
t4 = getPatchForDisplay({
|
||||
// Create a "fake" file content with just the cell source
|
||||
// This allows us to use the regular diff mechanism
|
||||
return getPatchForDisplay({
|
||||
filePath: notebook_path,
|
||||
fileContents: oldSource,
|
||||
edits: [{
|
||||
edits: [
|
||||
{
|
||||
old_string: oldSource,
|
||||
new_string: new_source,
|
||||
replace_all: false
|
||||
}],
|
||||
ignoreWhitespace: false
|
||||
});
|
||||
$[7] = new_source;
|
||||
$[8] = notebook_path;
|
||||
$[9] = oldSource;
|
||||
$[10] = t4;
|
||||
} else {
|
||||
t4 = $[10];
|
||||
}
|
||||
t3 = t4;
|
||||
}
|
||||
const hunks = t3;
|
||||
let editTypeDescription;
|
||||
bb2: switch (edit_mode) {
|
||||
case "insert":
|
||||
{
|
||||
editTypeDescription = "Insert new cell";
|
||||
break bb2;
|
||||
}
|
||||
case "delete":
|
||||
{
|
||||
editTypeDescription = "Delete cell";
|
||||
break bb2;
|
||||
}
|
||||
replace_all: false,
|
||||
},
|
||||
],
|
||||
ignoreWhitespace: false,
|
||||
})
|
||||
}, [notebookData, notebook_path, oldSource, new_source, edit_mode])
|
||||
|
||||
let editTypeDescription: string
|
||||
switch (edit_mode) {
|
||||
case 'insert':
|
||||
editTypeDescription = 'Insert new cell'
|
||||
break
|
||||
case 'delete':
|
||||
editTypeDescription = 'Delete cell'
|
||||
break
|
||||
default:
|
||||
{
|
||||
editTypeDescription = "Replace cell contents";
|
||||
editTypeDescription = 'Replace cell contents'
|
||||
}
|
||||
}
|
||||
let t4;
|
||||
if ($[11] !== notebook_path || $[12] !== verbose) {
|
||||
t4 = verbose ? notebook_path : relative(getCwd(), notebook_path);
|
||||
$[11] = notebook_path;
|
||||
$[12] = verbose;
|
||||
$[13] = t4;
|
||||
} else {
|
||||
t4 = $[13];
|
||||
}
|
||||
let t5;
|
||||
if ($[14] !== t4) {
|
||||
t5 = <Text bold={true}>{t4}</Text>;
|
||||
$[14] = t4;
|
||||
$[15] = t5;
|
||||
} else {
|
||||
t5 = $[15];
|
||||
}
|
||||
const t6 = cell_type ? ` (${cell_type})` : "";
|
||||
let t7;
|
||||
if ($[16] !== cell_id || $[17] !== editTypeDescription || $[18] !== t6) {
|
||||
t7 = <Text dimColor={true}>{editTypeDescription} for cell {cell_id}{t6}</Text>;
|
||||
$[16] = cell_id;
|
||||
$[17] = editTypeDescription;
|
||||
$[18] = t6;
|
||||
$[19] = t7;
|
||||
} else {
|
||||
t7 = $[19];
|
||||
}
|
||||
let t8;
|
||||
if ($[20] !== t5 || $[21] !== t7) {
|
||||
t8 = <Box paddingBottom={1} flexDirection="column">{t5}{t7}</Box>;
|
||||
$[20] = t5;
|
||||
$[21] = t7;
|
||||
$[22] = t8;
|
||||
} else {
|
||||
t8 = $[22];
|
||||
}
|
||||
let t9;
|
||||
if ($[23] !== cell_type || $[24] !== edit_mode || $[25] !== hunks || $[26] !== new_source || $[27] !== notebook_path || $[28] !== oldSource || $[29] !== width) {
|
||||
t9 = edit_mode === "delete" ? <Box flexDirection="column" paddingLeft={2}><HighlightedCode code={oldSource} filePath={notebook_path} /></Box> : edit_mode === "insert" ? <Box flexDirection="column" paddingLeft={2}><HighlightedCode code={new_source} filePath={cell_type === "markdown" ? "file.md" : notebook_path} /></Box> : hunks ? intersperse(hunks.map(_ => <StructuredDiff key={_.newStart} patch={_} dim={false} width={width} filePath={notebook_path} firstLine={new_source.split("\n")[0] ?? null} fileContent={oldSource} />), _temp3) : <HighlightedCode code={new_source} filePath={cell_type === "markdown" ? "file.md" : notebook_path} />;
|
||||
$[23] = cell_type;
|
||||
$[24] = edit_mode;
|
||||
$[25] = hunks;
|
||||
$[26] = new_source;
|
||||
$[27] = notebook_path;
|
||||
$[28] = oldSource;
|
||||
$[29] = width;
|
||||
$[30] = t9;
|
||||
} else {
|
||||
t9 = $[30];
|
||||
}
|
||||
let t10;
|
||||
if ($[31] !== t8 || $[32] !== t9) {
|
||||
t10 = <Box flexDirection="column"><Box borderStyle="round" flexDirection="column" paddingX={1}>{t8}{t9}</Box></Box>;
|
||||
$[31] = t8;
|
||||
$[32] = t9;
|
||||
$[33] = t10;
|
||||
} else {
|
||||
t10 = $[33];
|
||||
}
|
||||
return t10;
|
||||
}
|
||||
function _temp3(i) {
|
||||
return <NoSelect fromLeftEdge={true} key={`ellipsis-${i}`}><Text dimColor={true}>...</Text></NoSelect>;
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Box borderStyle="round" flexDirection="column" paddingX={1}>
|
||||
<Box paddingBottom={1} flexDirection="column">
|
||||
<Text bold>
|
||||
{verbose ? notebook_path : relative(getCwd(), notebook_path)}
|
||||
</Text>
|
||||
<Text dimColor>
|
||||
{editTypeDescription} for cell {cell_id}
|
||||
{cell_type ? ` (${cell_type})` : ''}
|
||||
</Text>
|
||||
</Box>
|
||||
{edit_mode === 'delete' ? (
|
||||
<Box flexDirection="column" paddingLeft={2}>
|
||||
<HighlightedCode code={oldSource} filePath={notebook_path} />
|
||||
</Box>
|
||||
) : edit_mode === 'insert' ? (
|
||||
<Box flexDirection="column" paddingLeft={2}>
|
||||
<HighlightedCode
|
||||
code={new_source}
|
||||
filePath={cell_type === 'markdown' ? 'file.md' : notebook_path}
|
||||
/>
|
||||
</Box>
|
||||
) : hunks ? (
|
||||
intersperse(
|
||||
hunks.map(_ => (
|
||||
<StructuredDiff
|
||||
key={_.newStart}
|
||||
patch={_}
|
||||
dim={false}
|
||||
width={width}
|
||||
filePath={notebook_path}
|
||||
firstLine={new_source.split('\n')[0] ?? null}
|
||||
fileContent={oldSource}
|
||||
/>
|
||||
)),
|
||||
i => (
|
||||
<NoSelect fromLeftEdge key={`ellipsis-${i}`}>
|
||||
<Text dimColor>...</Text>
|
||||
</NoSelect>
|
||||
),
|
||||
)
|
||||
) : (
|
||||
<HighlightedCode
|
||||
code={new_source}
|
||||
filePath={cell_type === 'markdown' ? 'file.md' : notebook_path}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,459 +1,350 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { feature } from 'bun:bundle';
|
||||
import chalk from 'chalk';
|
||||
import figures from 'figures';
|
||||
import React, { useMemo } from 'react';
|
||||
import { Ansi, Box, color, Text, useTheme } from '../../ink.js';
|
||||
import { useAppState } from '../../state/AppState.js';
|
||||
import type { PermissionMode } from '../../utils/permissions/PermissionMode.js';
|
||||
import { permissionModeTitle } from '../../utils/permissions/PermissionMode.js';
|
||||
import type { PermissionDecision, PermissionDecisionReason } from '../../utils/permissions/PermissionResult.js';
|
||||
import { extractRules } from '../../utils/permissions/PermissionUpdate.js';
|
||||
import type { PermissionUpdate } from '../../utils/permissions/PermissionUpdateSchema.js';
|
||||
import { permissionRuleValueToString } from '../../utils/permissions/permissionRuleParser.js';
|
||||
import { detectUnreachableRules } from '../../utils/permissions/shadowedRuleDetection.js';
|
||||
import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js';
|
||||
import { getSettingSourceDisplayNameLowercase } from '../../utils/settings/constants.js';
|
||||
import { feature } from 'bun:bundle'
|
||||
import chalk from 'chalk'
|
||||
import figures from 'figures'
|
||||
import React, { useMemo } from 'react'
|
||||
import { Ansi, Box, color, Text, useTheme } from '../../ink.js'
|
||||
import { useAppState } from '../../state/AppState.js'
|
||||
import type { PermissionMode } from '../../utils/permissions/PermissionMode.js'
|
||||
import { permissionModeTitle } from '../../utils/permissions/PermissionMode.js'
|
||||
import type {
|
||||
PermissionDecision,
|
||||
PermissionDecisionReason,
|
||||
} from '../../utils/permissions/PermissionResult.js'
|
||||
import { extractRules } from '../../utils/permissions/PermissionUpdate.js'
|
||||
import type { PermissionUpdate } from '../../utils/permissions/PermissionUpdateSchema.js'
|
||||
import { permissionRuleValueToString } from '../../utils/permissions/permissionRuleParser.js'
|
||||
import { detectUnreachableRules } from '../../utils/permissions/shadowedRuleDetection.js'
|
||||
import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js'
|
||||
import { getSettingSourceDisplayNameLowercase } from '../../utils/settings/constants.js'
|
||||
|
||||
type PermissionDecisionInfoItemProps = {
|
||||
title?: string;
|
||||
decisionReason: PermissionDecisionReason;
|
||||
};
|
||||
function decisionReasonDisplayString(decisionReason: PermissionDecisionReason & {
|
||||
type: Exclude<PermissionDecisionReason['type'], 'subcommandResults'>;
|
||||
}): string {
|
||||
if ((feature('BASH_CLASSIFIER') || feature('TRANSCRIPT_CLASSIFIER')) && decisionReason.type === 'classifier') {
|
||||
return `${chalk.bold(decisionReason.classifier)} classifier: ${decisionReason.reason}`;
|
||||
title?: string
|
||||
decisionReason: PermissionDecisionReason
|
||||
}
|
||||
|
||||
function decisionReasonDisplayString(
|
||||
decisionReason: PermissionDecisionReason & {
|
||||
type: Exclude<PermissionDecisionReason['type'], 'subcommandResults'>
|
||||
},
|
||||
): string {
|
||||
if (
|
||||
(feature('BASH_CLASSIFIER') || feature('TRANSCRIPT_CLASSIFIER')) &&
|
||||
decisionReason.type === 'classifier'
|
||||
) {
|
||||
return `${chalk.bold(decisionReason.classifier)} classifier: ${decisionReason.reason}`
|
||||
}
|
||||
switch (decisionReason.type) {
|
||||
case 'rule':
|
||||
return `${chalk.bold(permissionRuleValueToString(decisionReason.rule.ruleValue))} rule from ${getSettingSourceDisplayNameLowercase(decisionReason.rule.source)}`;
|
||||
return `${chalk.bold(permissionRuleValueToString(decisionReason.rule.ruleValue))} rule from ${getSettingSourceDisplayNameLowercase(decisionReason.rule.source)}`
|
||||
case 'mode':
|
||||
return `${permissionModeTitle(decisionReason.mode)} mode`;
|
||||
return `${permissionModeTitle(decisionReason.mode)} mode`
|
||||
case 'sandboxOverride':
|
||||
return 'Requires permission to bypass sandbox';
|
||||
return 'Requires permission to bypass sandbox'
|
||||
case 'workingDir':
|
||||
return decisionReason.reason;
|
||||
return decisionReason.reason
|
||||
case 'safetyCheck':
|
||||
case 'other':
|
||||
return decisionReason.reason;
|
||||
return decisionReason.reason
|
||||
case 'permissionPromptTool':
|
||||
return `${chalk.bold(decisionReason.permissionPromptToolName)} permission prompt tool`;
|
||||
return `${chalk.bold(decisionReason.permissionPromptToolName)} permission prompt tool`
|
||||
case 'hook':
|
||||
return decisionReason.reason ? `${chalk.bold(decisionReason.hookName)} hook: ${decisionReason.reason}` : `${chalk.bold(decisionReason.hookName)} hook`;
|
||||
return decisionReason.reason
|
||||
? `${chalk.bold(decisionReason.hookName)} hook: ${decisionReason.reason}`
|
||||
: `${chalk.bold(decisionReason.hookName)} hook`
|
||||
case 'asyncAgent':
|
||||
return decisionReason.reason;
|
||||
return decisionReason.reason
|
||||
default:
|
||||
return '';
|
||||
return ''
|
||||
}
|
||||
}
|
||||
function PermissionDecisionInfoItem(t0) {
|
||||
const $ = _c(10);
|
||||
const {
|
||||
|
||||
function PermissionDecisionInfoItem({
|
||||
title,
|
||||
decisionReason
|
||||
} = t0;
|
||||
const [theme] = useTheme();
|
||||
let t1;
|
||||
if ($[0] !== decisionReason || $[1] !== theme) {
|
||||
t1 = function formatDecisionReason() {
|
||||
decisionReason,
|
||||
}: PermissionDecisionInfoItemProps): React.ReactNode {
|
||||
const [theme] = useTheme()
|
||||
|
||||
function formatDecisionReason(): React.ReactNode {
|
||||
switch (decisionReason.type) {
|
||||
case "subcommandResults":
|
||||
{
|
||||
return <Box flexDirection="column">{Array.from(decisionReason.reasons.entries()).map(t2 => {
|
||||
const [subcommand, result] = t2 as [string, { behavior: string; decisionReason?: { type: string }; suggestions?: unknown }];
|
||||
const icon = result.behavior === "allow" ? color("success", theme)(figures.tick) : color("error", theme)(figures.cross);
|
||||
return <Box flexDirection="column" key={subcommand}><Text>{icon} {subcommand}</Text>{result.decisionReason !== undefined && result.decisionReason.type !== "subcommandResults" && <Text><Text dimColor={true}>{" "}⎿{" "}</Text><Ansi>{decisionReasonDisplayString(result.decisionReason as any)}</Ansi></Text>}{result.behavior === "ask" && <SuggestedRules suggestions={result.suggestions} />}</Box>;
|
||||
})}</Box>;
|
||||
}
|
||||
case 'subcommandResults':
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
{Array.from(decisionReason.reasons.entries()).map(
|
||||
([subcommand, result]) => {
|
||||
const icon =
|
||||
result.behavior === 'allow'
|
||||
? color('success', theme)(figures.tick)
|
||||
: color('error', theme)(figures.cross)
|
||||
return (
|
||||
<Box flexDirection="column" key={subcommand}>
|
||||
<Text>
|
||||
{icon} {subcommand}
|
||||
</Text>
|
||||
{result.decisionReason !== undefined &&
|
||||
result.decisionReason.type !== 'subcommandResults' && (
|
||||
<Text>
|
||||
<Text dimColor>
|
||||
{' '}⎿{' '}
|
||||
</Text>
|
||||
<Ansi>
|
||||
{decisionReasonDisplayString(result.decisionReason)}
|
||||
</Ansi>
|
||||
</Text>
|
||||
)}
|
||||
{result.behavior === 'ask' && (
|
||||
<SuggestedRules suggestions={result.suggestions} />
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
default:
|
||||
{
|
||||
return <Text><Ansi>{decisionReasonDisplayString(decisionReason)}</Ansi></Text>;
|
||||
return (
|
||||
<Text>
|
||||
<Ansi>{decisionReasonDisplayString(decisionReason)}</Ansi>
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
$[0] = decisionReason;
|
||||
$[1] = theme;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
const formatDecisionReason = t1;
|
||||
let t2;
|
||||
if ($[3] !== title) {
|
||||
t2 = title && <Text>{title}</Text>;
|
||||
$[3] = title;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
let t3;
|
||||
if ($[5] !== formatDecisionReason) {
|
||||
t3 = formatDecisionReason();
|
||||
$[5] = formatDecisionReason;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
}
|
||||
let t4;
|
||||
if ($[7] !== t2 || $[8] !== t3) {
|
||||
t4 = <Box flexDirection="column">{t2}{t3}</Box>;
|
||||
$[7] = t2;
|
||||
$[8] = t3;
|
||||
$[9] = t4;
|
||||
} else {
|
||||
t4 = $[9];
|
||||
}
|
||||
return t4;
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
{title && <Text>{title}</Text>}
|
||||
{formatDecisionReason()}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
function SuggestedRules(t0) {
|
||||
const $ = _c(18);
|
||||
const {
|
||||
suggestions
|
||||
} = t0;
|
||||
let T0;
|
||||
let T1;
|
||||
let t1;
|
||||
let t2;
|
||||
let t3;
|
||||
let t4;
|
||||
let t5;
|
||||
if ($[0] !== suggestions) {
|
||||
t5 = Symbol.for("react.early_return_sentinel");
|
||||
bb0: {
|
||||
const rules = extractRules(suggestions);
|
||||
if (rules.length === 0) {
|
||||
t5 = null;
|
||||
break bb0;
|
||||
}
|
||||
T1 = Text;
|
||||
if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <Text dimColor={true}>{" "}⎿{" "}</Text>;
|
||||
$[8] = t2;
|
||||
} else {
|
||||
t2 = $[8];
|
||||
}
|
||||
t3 = "Suggested rules:";
|
||||
t4 = " ";
|
||||
T0 = Ansi;
|
||||
t1 = rules.map(_temp).join(", ");
|
||||
}
|
||||
$[0] = suggestions;
|
||||
$[1] = T0;
|
||||
$[2] = T1;
|
||||
$[3] = t1;
|
||||
$[4] = t2;
|
||||
$[5] = t3;
|
||||
$[6] = t4;
|
||||
$[7] = t5;
|
||||
} else {
|
||||
T0 = $[1];
|
||||
T1 = $[2];
|
||||
t1 = $[3];
|
||||
t2 = $[4];
|
||||
t3 = $[5];
|
||||
t4 = $[6];
|
||||
t5 = $[7];
|
||||
}
|
||||
if (t5 !== Symbol.for("react.early_return_sentinel")) {
|
||||
return t5;
|
||||
}
|
||||
let t6;
|
||||
if ($[9] !== T0 || $[10] !== t1) {
|
||||
t6 = <T0>{t1}</T0>;
|
||||
$[9] = T0;
|
||||
$[10] = t1;
|
||||
$[11] = t6;
|
||||
} else {
|
||||
t6 = $[11];
|
||||
}
|
||||
let t7;
|
||||
if ($[12] !== T1 || $[13] !== t2 || $[14] !== t3 || $[15] !== t4 || $[16] !== t6) {
|
||||
t7 = <T1>{t2}{t3}{t4}{t6}</T1>;
|
||||
$[12] = T1;
|
||||
$[13] = t2;
|
||||
$[14] = t3;
|
||||
$[15] = t4;
|
||||
$[16] = t6;
|
||||
$[17] = t7;
|
||||
} else {
|
||||
t7 = $[17];
|
||||
}
|
||||
return t7;
|
||||
}
|
||||
function _temp(rule) {
|
||||
return chalk.bold(permissionRuleValueToString(rule));
|
||||
|
||||
function SuggestedRules({
|
||||
suggestions,
|
||||
}: {
|
||||
suggestions: PermissionUpdate[] | undefined
|
||||
}): React.ReactNode {
|
||||
const rules = extractRules(suggestions)
|
||||
if (rules.length === 0) return null
|
||||
return (
|
||||
<Text>
|
||||
<Text dimColor>
|
||||
{' '}⎿{' '}
|
||||
</Text>
|
||||
Suggested rules:{' '}
|
||||
<Ansi>
|
||||
{rules
|
||||
.map(rule => chalk.bold(permissionRuleValueToString(rule)))
|
||||
.join(', ')}
|
||||
</Ansi>
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
type Props = {
|
||||
permissionResult: PermissionDecision;
|
||||
toolName?: string; // Filter unreachable rules to this tool
|
||||
};
|
||||
permissionResult: PermissionDecision
|
||||
toolName?: string // Filter unreachable rules to this tool
|
||||
}
|
||||
|
||||
// Helper function to extract directories from permission updates
|
||||
function extractDirectories(updates: PermissionUpdate[] | undefined): string[] {
|
||||
if (!updates) return [];
|
||||
if (!updates) return []
|
||||
|
||||
return updates.flatMap(update => {
|
||||
switch (update.type) {
|
||||
case 'addDirectories':
|
||||
return update.directories;
|
||||
return update.directories
|
||||
default:
|
||||
return [];
|
||||
return []
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
// Helper function to extract mode from permission updates
|
||||
function extractMode(updates: PermissionUpdate[] | undefined): PermissionMode | undefined {
|
||||
if (!updates) return undefined;
|
||||
const update = updates.findLast(u => u.type === 'setMode');
|
||||
return update?.type === 'setMode' ? update.mode : undefined;
|
||||
function extractMode(
|
||||
updates: PermissionUpdate[] | undefined,
|
||||
): PermissionMode | undefined {
|
||||
if (!updates) return undefined
|
||||
const update = updates.findLast(u => u.type === 'setMode')
|
||||
return update?.type === 'setMode' ? update.mode : undefined
|
||||
}
|
||||
function SuggestionDisplay(t0) {
|
||||
const $ = _c(22);
|
||||
const {
|
||||
|
||||
function SuggestionDisplay({
|
||||
suggestions,
|
||||
width
|
||||
} = t0;
|
||||
width,
|
||||
}: {
|
||||
suggestions: PermissionUpdate[] | undefined
|
||||
width: number
|
||||
}): React.ReactNode {
|
||||
if (!suggestions || suggestions.length === 0) {
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <Text dimColor={true}>Suggestions </Text>;
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
return (
|
||||
<Box flexDirection="row">
|
||||
<Box justifyContent="flex-end" minWidth={width}>
|
||||
<Text dimColor>Suggestions </Text>
|
||||
</Box>
|
||||
<Text>None</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
let t2;
|
||||
if ($[1] !== width) {
|
||||
t2 = <Box justifyContent="flex-end" minWidth={width}>{t1}</Box>;
|
||||
$[1] = width;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
let t3;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = <Text>None</Text>;
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t3 = $[3];
|
||||
}
|
||||
let t4;
|
||||
if ($[4] !== t2) {
|
||||
t4 = <Box flexDirection="row">{t2}{t3}</Box>;
|
||||
$[4] = t2;
|
||||
$[5] = t4;
|
||||
} else {
|
||||
t4 = $[5];
|
||||
}
|
||||
return t4;
|
||||
}
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[6] !== suggestions || $[7] !== width) {
|
||||
t2 = Symbol.for("react.early_return_sentinel");
|
||||
bb0: {
|
||||
const rules = extractRules(suggestions);
|
||||
const directories = extractDirectories(suggestions);
|
||||
const mode = extractMode(suggestions);
|
||||
|
||||
const rules = extractRules(suggestions)
|
||||
const directories = extractDirectories(suggestions)
|
||||
const mode = extractMode(suggestions)
|
||||
|
||||
// If nothing to display, show None
|
||||
if (rules.length === 0 && directories.length === 0 && !mode) {
|
||||
let t3;
|
||||
if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = <Text dimColor={true}>Suggestion </Text>;
|
||||
$[10] = t3;
|
||||
} else {
|
||||
t3 = $[10];
|
||||
return (
|
||||
<Box flexDirection="row">
|
||||
<Box justifyContent="flex-end" minWidth={width}>
|
||||
<Text dimColor>Suggestion </Text>
|
||||
</Box>
|
||||
<Text>None</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
let t4;
|
||||
if ($[11] !== width) {
|
||||
t4 = <Box justifyContent="flex-end" minWidth={width}>{t3}</Box>;
|
||||
$[11] = width;
|
||||
$[12] = t4;
|
||||
} else {
|
||||
t4 = $[12];
|
||||
}
|
||||
let t5;
|
||||
if ($[13] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t5 = <Text>None</Text>;
|
||||
$[13] = t5;
|
||||
} else {
|
||||
t5 = $[13];
|
||||
}
|
||||
let t6;
|
||||
if ($[14] !== t4) {
|
||||
t6 = <Box flexDirection="row">{t4}{t5}</Box>;
|
||||
$[14] = t4;
|
||||
$[15] = t6;
|
||||
} else {
|
||||
t6 = $[15];
|
||||
}
|
||||
t2 = t6;
|
||||
break bb0;
|
||||
}
|
||||
let t3;
|
||||
if ($[16] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = <Text dimColor={true}>Suggestions </Text>;
|
||||
$[16] = t3;
|
||||
} else {
|
||||
t3 = $[16];
|
||||
}
|
||||
let t4;
|
||||
if ($[17] !== width) {
|
||||
t4 = <Box justifyContent="flex-end" minWidth={width}>{t3}</Box>;
|
||||
$[17] = width;
|
||||
$[18] = t4;
|
||||
} else {
|
||||
t4 = $[18];
|
||||
}
|
||||
let t5;
|
||||
if ($[19] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t5 = <Text> </Text>;
|
||||
$[19] = t5;
|
||||
} else {
|
||||
t5 = $[19];
|
||||
}
|
||||
let t6;
|
||||
if ($[20] !== t4) {
|
||||
t6 = <Box flexDirection="row">{t4}{t5}</Box>;
|
||||
$[20] = t4;
|
||||
$[21] = t6;
|
||||
} else {
|
||||
t6 = $[21];
|
||||
}
|
||||
t1 = <Box flexDirection="column">{t6}{rules.length > 0 && <Box flexDirection="row"><Box justifyContent="flex-end" minWidth={width}><Text dimColor={true}> Rules </Text></Box><Box flexDirection="column">{rules.map(_temp2)}</Box></Box>}{directories.length > 0 && <Box flexDirection="row"><Box justifyContent="flex-end" minWidth={width}><Text dimColor={true}> Directories </Text></Box><Box flexDirection="column">{directories.map(_temp3)}</Box></Box>}{mode && <Box flexDirection="row"><Box justifyContent="flex-end" minWidth={width}><Text dimColor={true}> Mode </Text></Box><Text>{permissionModeTitle(mode)}</Text></Box>}</Box>;
|
||||
}
|
||||
$[6] = suggestions;
|
||||
$[7] = width;
|
||||
$[8] = t1;
|
||||
$[9] = t2;
|
||||
} else {
|
||||
t1 = $[8];
|
||||
t2 = $[9];
|
||||
}
|
||||
if (t2 !== Symbol.for("react.early_return_sentinel")) {
|
||||
return t2;
|
||||
}
|
||||
return t1;
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Box flexDirection="row">
|
||||
<Box justifyContent="flex-end" minWidth={width}>
|
||||
<Text dimColor>Suggestions </Text>
|
||||
</Box>
|
||||
<Text> </Text>
|
||||
</Box>
|
||||
|
||||
{/* Display rules */}
|
||||
{rules.length > 0 && (
|
||||
<Box flexDirection="row">
|
||||
<Box justifyContent="flex-end" minWidth={width}>
|
||||
<Text dimColor> Rules </Text>
|
||||
</Box>
|
||||
<Box flexDirection="column">
|
||||
{rules.map((rule, index) => (
|
||||
<Text key={index}>
|
||||
{figures.bullet} {permissionRuleValueToString(rule)}
|
||||
</Text>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Display directories */}
|
||||
{directories.length > 0 && (
|
||||
<Box flexDirection="row">
|
||||
<Box justifyContent="flex-end" minWidth={width}>
|
||||
<Text dimColor> Directories </Text>
|
||||
</Box>
|
||||
<Box flexDirection="column">
|
||||
{directories.map((dir, index) => (
|
||||
<Text key={index}>
|
||||
{figures.bullet} {dir}
|
||||
</Text>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Display mode change */}
|
||||
{mode && (
|
||||
<Box flexDirection="row">
|
||||
<Box justifyContent="flex-end" minWidth={width}>
|
||||
<Text dimColor> Mode </Text>
|
||||
</Box>
|
||||
<Text>{permissionModeTitle(mode)}</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
function _temp3(dir, index_0) {
|
||||
return <Text key={index_0}>{figures.bullet} {dir}</Text>;
|
||||
}
|
||||
function _temp2(rule, index) {
|
||||
return <Text key={index}>{figures.bullet} {permissionRuleValueToString(rule)}</Text>;
|
||||
}
|
||||
export function PermissionDecisionDebugInfo(t0) {
|
||||
const $ = _c(25);
|
||||
const {
|
||||
|
||||
export function PermissionDecisionDebugInfo({
|
||||
permissionResult,
|
||||
toolName
|
||||
} = t0;
|
||||
const toolPermissionContext = useAppState(_temp4);
|
||||
const decisionReason = permissionResult.decisionReason;
|
||||
const suggestions = "suggestions" in permissionResult ? permissionResult.suggestions : undefined;
|
||||
let t1;
|
||||
if ($[0] !== suggestions || $[1] !== toolName || $[2] !== toolPermissionContext) {
|
||||
bb0: {
|
||||
const sandboxAutoAllowEnabled = SandboxManager.isSandboxingEnabled() && SandboxManager.isAutoAllowBashIfSandboxedEnabled();
|
||||
toolName,
|
||||
}: Props): React.ReactNode {
|
||||
const toolPermissionContext = useAppState(s => s.toolPermissionContext)
|
||||
const decisionReason = permissionResult.decisionReason
|
||||
const suggestions =
|
||||
'suggestions' in permissionResult ? permissionResult.suggestions : undefined
|
||||
|
||||
const unreachableRules = useMemo(() => {
|
||||
const sandboxAutoAllowEnabled =
|
||||
SandboxManager.isSandboxingEnabled() &&
|
||||
SandboxManager.isAutoAllowBashIfSandboxedEnabled()
|
||||
const all = detectUnreachableRules(toolPermissionContext, {
|
||||
sandboxAutoAllowEnabled
|
||||
});
|
||||
const suggestedRules = extractRules(suggestions);
|
||||
sandboxAutoAllowEnabled,
|
||||
})
|
||||
|
||||
// Get the suggested rules from the permission result
|
||||
const suggestedRules = extractRules(suggestions)
|
||||
|
||||
// Filter to rules that match any of the suggested rules
|
||||
// A rule matches if it has the same toolName and ruleContent
|
||||
if (suggestedRules.length > 0) {
|
||||
t1 = all.filter(u => suggestedRules.some(suggested => suggested.toolName === u.rule.ruleValue.toolName && suggested.ruleContent === u.rule.ruleValue.ruleContent));
|
||||
break bb0;
|
||||
return all.filter(u =>
|
||||
suggestedRules.some(
|
||||
suggested =>
|
||||
suggested.toolName === u.rule.ruleValue.toolName &&
|
||||
suggested.ruleContent === u.rule.ruleValue.ruleContent,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// Fallback: filter by tool name if specified
|
||||
if (toolName) {
|
||||
let t2;
|
||||
if ($[4] !== toolName) {
|
||||
t2 = u_0 => u_0.rule.ruleValue.toolName === toolName;
|
||||
$[4] = toolName;
|
||||
$[5] = t2;
|
||||
} else {
|
||||
t2 = $[5];
|
||||
return all.filter(u => u.rule.ruleValue.toolName === toolName)
|
||||
}
|
||||
t1 = all.filter(t2);
|
||||
break bb0;
|
||||
}
|
||||
t1 = all;
|
||||
}
|
||||
$[0] = suggestions;
|
||||
$[1] = toolName;
|
||||
$[2] = toolPermissionContext;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
const unreachableRules = t1;
|
||||
let t2;
|
||||
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <Box justifyContent="flex-end" minWidth={10}><Text dimColor={true}>Behavior </Text></Box>;
|
||||
$[6] = t2;
|
||||
} else {
|
||||
t2 = $[6];
|
||||
}
|
||||
let t3;
|
||||
if ($[7] !== permissionResult.behavior) {
|
||||
t3 = <Box flexDirection="row">{t2}<Text>{permissionResult.behavior}</Text></Box>;
|
||||
$[7] = permissionResult.behavior;
|
||||
$[8] = t3;
|
||||
} else {
|
||||
t3 = $[8];
|
||||
}
|
||||
let t4;
|
||||
if ($[9] !== permissionResult.behavior || $[10] !== permissionResult.message) {
|
||||
t4 = permissionResult.behavior !== "allow" && <Box flexDirection="row"><Box justifyContent="flex-end" minWidth={10}><Text dimColor={true}>Message </Text></Box><Text>{permissionResult.message}</Text></Box>;
|
||||
$[9] = permissionResult.behavior;
|
||||
$[10] = permissionResult.message;
|
||||
$[11] = t4;
|
||||
} else {
|
||||
t4 = $[11];
|
||||
}
|
||||
let t5;
|
||||
if ($[12] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t5 = <Box justifyContent="flex-end" minWidth={10}><Text dimColor={true}>Reason </Text></Box>;
|
||||
$[12] = t5;
|
||||
} else {
|
||||
t5 = $[12];
|
||||
}
|
||||
let t6;
|
||||
if ($[13] !== decisionReason) {
|
||||
t6 = <Box flexDirection="row">{t5}{decisionReason === undefined ? <Text>undefined</Text> : <PermissionDecisionInfoItem decisionReason={decisionReason} />}</Box>;
|
||||
$[13] = decisionReason;
|
||||
$[14] = t6;
|
||||
} else {
|
||||
t6 = $[14];
|
||||
}
|
||||
let t7;
|
||||
if ($[15] !== suggestions) {
|
||||
t7 = <SuggestionDisplay suggestions={suggestions} width={10} />;
|
||||
$[15] = suggestions;
|
||||
$[16] = t7;
|
||||
} else {
|
||||
t7 = $[16];
|
||||
}
|
||||
let t8;
|
||||
if ($[17] !== unreachableRules) {
|
||||
t8 = unreachableRules.length > 0 && <Box flexDirection="column" marginTop={1}><Text color="warning">{figures.warning} Unreachable Rules ({unreachableRules.length})</Text>{unreachableRules.map(_temp5)}</Box>;
|
||||
$[17] = unreachableRules;
|
||||
$[18] = t8;
|
||||
} else {
|
||||
t8 = $[18];
|
||||
}
|
||||
let t9;
|
||||
if ($[19] !== t3 || $[20] !== t4 || $[21] !== t6 || $[22] !== t7 || $[23] !== t8) {
|
||||
t9 = <Box flexDirection="column">{t3}{t4}{t6}{t7}{t8}</Box>;
|
||||
$[19] = t3;
|
||||
$[20] = t4;
|
||||
$[21] = t6;
|
||||
$[22] = t7;
|
||||
$[23] = t8;
|
||||
$[24] = t9;
|
||||
} else {
|
||||
t9 = $[24];
|
||||
}
|
||||
return t9;
|
||||
}
|
||||
function _temp5(u_1, i) {
|
||||
return <Box key={i} flexDirection="column" marginLeft={2}><Text color="warning">{permissionRuleValueToString(u_1.rule.ruleValue)}</Text><Text dimColor={true}>{" "}{u_1.reason}</Text><Text dimColor={true}>{" "}Fix: {u_1.fix}</Text></Box>;
|
||||
}
|
||||
function _temp4(s) {
|
||||
return s.toolPermissionContext;
|
||||
|
||||
return all
|
||||
}, [toolPermissionContext, toolName, suggestions])
|
||||
|
||||
const WIDTH = 10
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Box flexDirection="row">
|
||||
<Box justifyContent="flex-end" minWidth={WIDTH}>
|
||||
<Text dimColor>Behavior </Text>
|
||||
</Box>
|
||||
<Text>{permissionResult.behavior}</Text>
|
||||
</Box>
|
||||
{permissionResult.behavior !== 'allow' && (
|
||||
<Box flexDirection="row">
|
||||
<Box justifyContent="flex-end" minWidth={WIDTH}>
|
||||
<Text dimColor>Message </Text>
|
||||
</Box>
|
||||
<Text>{permissionResult.message}</Text>
|
||||
</Box>
|
||||
)}
|
||||
<Box flexDirection="row">
|
||||
<Box justifyContent="flex-end" minWidth={WIDTH}>
|
||||
<Text dimColor>Reason </Text>
|
||||
</Box>
|
||||
{decisionReason === undefined ? (
|
||||
<Text>undefined</Text>
|
||||
) : (
|
||||
<PermissionDecisionInfoItem decisionReason={decisionReason} />
|
||||
)}
|
||||
</Box>
|
||||
<SuggestionDisplay suggestions={suggestions} width={WIDTH} />
|
||||
{unreachableRules.length > 0 && (
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<Text color="warning">
|
||||
{figures.warning} Unreachable Rules ({unreachableRules.length})
|
||||
</Text>
|
||||
{unreachableRules.map((u, i) => (
|
||||
<Box key={i} flexDirection="column" marginLeft={2}>
|
||||
<Text color="warning">
|
||||
{permissionRuleValueToString(u.rule.ruleValue)}
|
||||
</Text>
|
||||
<Text dimColor>
|
||||
{' '}
|
||||
{u.reason}
|
||||
</Text>
|
||||
<Text dimColor>
|
||||
{' '}Fix: {u.fix}
|
||||
</Text>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,71 +1,54 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { Box } from '../../ink.js';
|
||||
import type { Theme } from '../../utils/theme.js';
|
||||
import { PermissionRequestTitle } from './PermissionRequestTitle.js';
|
||||
import type { WorkerBadgeProps } from './WorkerBadge.js';
|
||||
import * as React from 'react'
|
||||
import { Box } from '../../ink.js'
|
||||
import type { Theme } from '../../utils/theme.js'
|
||||
import { PermissionRequestTitle } from './PermissionRequestTitle.js'
|
||||
import type { WorkerBadgeProps } from './WorkerBadge.js'
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
subtitle?: React.ReactNode;
|
||||
color?: keyof Theme;
|
||||
titleColor?: keyof Theme;
|
||||
innerPaddingX?: number;
|
||||
workerBadge?: WorkerBadgeProps;
|
||||
titleRight?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
export function PermissionDialog(t0) {
|
||||
const $ = _c(15);
|
||||
const {
|
||||
title: string
|
||||
subtitle?: React.ReactNode
|
||||
color?: keyof Theme
|
||||
titleColor?: keyof Theme
|
||||
innerPaddingX?: number
|
||||
workerBadge?: WorkerBadgeProps
|
||||
titleRight?: React.ReactNode
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function PermissionDialog({
|
||||
title,
|
||||
subtitle,
|
||||
color: t1,
|
||||
color = 'permission',
|
||||
titleColor,
|
||||
innerPaddingX: t2,
|
||||
innerPaddingX = 1,
|
||||
workerBadge,
|
||||
titleRight,
|
||||
children
|
||||
} = t0;
|
||||
const color = t1 === undefined ? "permission" : t1;
|
||||
const innerPaddingX = t2 === undefined ? 1 : t2;
|
||||
let t3;
|
||||
if ($[0] !== subtitle || $[1] !== title || $[2] !== titleColor || $[3] !== workerBadge) {
|
||||
t3 = <PermissionRequestTitle title={title} subtitle={subtitle} color={titleColor} workerBadge={workerBadge} />;
|
||||
$[0] = subtitle;
|
||||
$[1] = title;
|
||||
$[2] = titleColor;
|
||||
$[3] = workerBadge;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
let t4;
|
||||
if ($[5] !== t3 || $[6] !== titleRight) {
|
||||
t4 = <Box paddingX={1} flexDirection="column"><Box justifyContent="space-between">{t3}{titleRight}</Box></Box>;
|
||||
$[5] = t3;
|
||||
$[6] = titleRight;
|
||||
$[7] = t4;
|
||||
} else {
|
||||
t4 = $[7];
|
||||
}
|
||||
let t5;
|
||||
if ($[8] !== children || $[9] !== innerPaddingX) {
|
||||
t5 = <Box flexDirection="column" paddingX={innerPaddingX}>{children}</Box>;
|
||||
$[8] = children;
|
||||
$[9] = innerPaddingX;
|
||||
$[10] = t5;
|
||||
} else {
|
||||
t5 = $[10];
|
||||
}
|
||||
let t6;
|
||||
if ($[11] !== color || $[12] !== t4 || $[13] !== t5) {
|
||||
t6 = <Box flexDirection="column" borderStyle="round" borderColor={color} borderLeft={false} borderRight={false} borderBottom={false} marginTop={1}>{t4}{t5}</Box>;
|
||||
$[11] = color;
|
||||
$[12] = t4;
|
||||
$[13] = t5;
|
||||
$[14] = t6;
|
||||
} else {
|
||||
t6 = $[14];
|
||||
}
|
||||
return t6;
|
||||
children,
|
||||
}: Props): React.ReactNode {
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
borderStyle="round"
|
||||
borderColor={color}
|
||||
borderLeft={false}
|
||||
borderRight={false}
|
||||
borderBottom={false}
|
||||
marginTop={1}
|
||||
>
|
||||
<Box paddingX={1} flexDirection="column">
|
||||
<Box justifyContent="space-between">
|
||||
<PermissionRequestTitle
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
color={titleColor}
|
||||
workerBadge={workerBadge}
|
||||
/>
|
||||
{titleRight}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box flexDirection="column" paddingX={innerPaddingX}>
|
||||
{children}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,87 +1,93 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React, { Suspense, use, useState } from 'react';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { useKeybinding } from '../../keybindings/useKeybinding.js';
|
||||
import { logEvent } from '../../services/analytics/index.js';
|
||||
import type { Message } from '../../types/message.js';
|
||||
import { generatePermissionExplanation, isPermissionExplainerEnabled, type PermissionExplanation as PermissionExplanationType, type RiskLevel } from '../../utils/permissions/permissionExplainer.js';
|
||||
import { ShimmerChar } from '../Spinner/ShimmerChar.js';
|
||||
import { useShimmerAnimation } from '../Spinner/useShimmerAnimation.js';
|
||||
const LOADING_MESSAGE = 'Loading explanation…';
|
||||
function ShimmerLoadingText() {
|
||||
const $ = _c(7);
|
||||
const [ref, glimmerIndex] = useShimmerAnimation("responding", LOADING_MESSAGE, false);
|
||||
let t0;
|
||||
if ($[0] !== glimmerIndex) {
|
||||
t0 = LOADING_MESSAGE.split("").map((char, index) => <ShimmerChar key={index} char={char} index={index} glimmerIndex={glimmerIndex} messageColor="inactive" shimmerColor="text" />);
|
||||
$[0] = glimmerIndex;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
let t1;
|
||||
if ($[2] !== t0) {
|
||||
t1 = <Text>{t0}</Text>;
|
||||
$[2] = t0;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
let t2;
|
||||
if ($[4] !== ref || $[5] !== t1) {
|
||||
t2 = <Box ref={ref}>{t1}</Box>;
|
||||
$[4] = ref;
|
||||
$[5] = t1;
|
||||
$[6] = t2;
|
||||
} else {
|
||||
t2 = $[6];
|
||||
}
|
||||
return t2;
|
||||
import React, { Suspense, use, useState } from 'react'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { useKeybinding } from '../../keybindings/useKeybinding.js'
|
||||
import { logEvent } from '../../services/analytics/index.js'
|
||||
import type { Message } from '../../types/message.js'
|
||||
import {
|
||||
generatePermissionExplanation,
|
||||
isPermissionExplainerEnabled,
|
||||
type PermissionExplanation as PermissionExplanationType,
|
||||
type RiskLevel,
|
||||
} from '../../utils/permissions/permissionExplainer.js'
|
||||
import { ShimmerChar } from '../Spinner/ShimmerChar.js'
|
||||
import { useShimmerAnimation } from '../Spinner/useShimmerAnimation.js'
|
||||
|
||||
const LOADING_MESSAGE = 'Loading explanation…'
|
||||
|
||||
function ShimmerLoadingText(): React.ReactNode {
|
||||
const [ref, glimmerIndex] = useShimmerAnimation(
|
||||
'responding',
|
||||
LOADING_MESSAGE,
|
||||
false,
|
||||
)
|
||||
|
||||
return (
|
||||
<Box ref={ref}>
|
||||
<Text>
|
||||
{LOADING_MESSAGE.split('').map((char, index) => (
|
||||
<ShimmerChar
|
||||
key={index}
|
||||
char={char}
|
||||
index={index}
|
||||
glimmerIndex={glimmerIndex}
|
||||
messageColor="inactive"
|
||||
shimmerColor="text"
|
||||
/>
|
||||
))}
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
function getRiskColor(riskLevel: RiskLevel): 'success' | 'warning' | 'error' {
|
||||
switch (riskLevel) {
|
||||
case 'LOW':
|
||||
return 'success';
|
||||
return 'success'
|
||||
case 'MEDIUM':
|
||||
return 'warning';
|
||||
return 'warning'
|
||||
case 'HIGH':
|
||||
return 'error';
|
||||
return 'error'
|
||||
}
|
||||
}
|
||||
|
||||
function getRiskLabel(riskLevel: RiskLevel): string {
|
||||
switch (riskLevel) {
|
||||
case 'LOW':
|
||||
return 'Low risk';
|
||||
return 'Low risk'
|
||||
case 'MEDIUM':
|
||||
return 'Med risk';
|
||||
return 'Med risk'
|
||||
case 'HIGH':
|
||||
return 'High risk';
|
||||
return 'High risk'
|
||||
}
|
||||
}
|
||||
|
||||
type PermissionExplanationProps = {
|
||||
toolName: string;
|
||||
toolInput: unknown;
|
||||
toolDescription?: string;
|
||||
messages?: Message[];
|
||||
};
|
||||
toolName: string
|
||||
toolInput: unknown
|
||||
toolDescription?: string
|
||||
messages?: Message[]
|
||||
}
|
||||
|
||||
type ExplainerState = {
|
||||
visible: boolean;
|
||||
enabled: boolean;
|
||||
promise: Promise<PermissionExplanationType | null> | null;
|
||||
};
|
||||
visible: boolean
|
||||
enabled: boolean
|
||||
promise: Promise<PermissionExplanationType | null> | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an explanation promise that never rejects.
|
||||
* Errors are caught and returned as null.
|
||||
*/
|
||||
function createExplanationPromise(props: PermissionExplanationProps): Promise<PermissionExplanationType | null> {
|
||||
function createExplanationPromise(
|
||||
props: PermissionExplanationProps,
|
||||
): Promise<PermissionExplanationType | null> {
|
||||
return generatePermissionExplanation({
|
||||
toolName: props.toolName,
|
||||
toolInput: props.toolInput,
|
||||
toolDescription: props.toolDescription,
|
||||
messages: props.messages,
|
||||
signal: new AbortController().signal // Won't abort - request is fast enough
|
||||
}).catch(() => null);
|
||||
signal: new AbortController().signal, // Won't abort - request is fast enough
|
||||
}).catch(() => null)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -89,183 +95,93 @@ function createExplanationPromise(props: PermissionExplanationProps): Promise<Pe
|
||||
* Creates the fetch promise lazily (only when user hits Ctrl+E)
|
||||
* to avoid consuming tokens for explanations users never view.
|
||||
*/
|
||||
export function usePermissionExplainerUI(props) {
|
||||
const $ = _c(9);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = isPermissionExplainerEnabled();
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
const enabled = t0;
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [promise, setPromise] = useState(null);
|
||||
let t1;
|
||||
if ($[1] !== promise || $[2] !== props || $[3] !== visible) {
|
||||
t1 = () => {
|
||||
export function usePermissionExplainerUI(
|
||||
props: PermissionExplanationProps,
|
||||
): ExplainerState {
|
||||
const enabled = isPermissionExplainerEnabled()
|
||||
const [visible, setVisible] = useState(false)
|
||||
const [promise, setPromise] =
|
||||
useState<Promise<PermissionExplanationType | null> | null>(null)
|
||||
|
||||
// Use keybinding for ctrl+e toggle (configurable via keybindings.json)
|
||||
useKeybinding(
|
||||
'confirm:toggleExplanation',
|
||||
() => {
|
||||
if (!visible) {
|
||||
logEvent("tengu_permission_explainer_shortcut_used", {});
|
||||
logEvent('tengu_permission_explainer_shortcut_used', {})
|
||||
// Only create the promise on first toggle (lazy loading)
|
||||
if (!promise) {
|
||||
setPromise(createExplanationPromise(props));
|
||||
setPromise(createExplanationPromise(props))
|
||||
}
|
||||
}
|
||||
setVisible(_temp);
|
||||
};
|
||||
$[1] = promise;
|
||||
$[2] = props;
|
||||
$[3] = visible;
|
||||
$[4] = t1;
|
||||
} else {
|
||||
t1 = $[4];
|
||||
}
|
||||
let t2;
|
||||
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = {
|
||||
context: "Confirmation",
|
||||
isActive: enabled
|
||||
};
|
||||
$[5] = t2;
|
||||
} else {
|
||||
t2 = $[5];
|
||||
}
|
||||
useKeybinding("confirm:toggleExplanation", t1, t2);
|
||||
let t3;
|
||||
if ($[6] !== promise || $[7] !== visible) {
|
||||
t3 = {
|
||||
visible,
|
||||
enabled,
|
||||
promise
|
||||
};
|
||||
$[6] = promise;
|
||||
$[7] = visible;
|
||||
$[8] = t3;
|
||||
} else {
|
||||
t3 = $[8];
|
||||
}
|
||||
return t3;
|
||||
setVisible(v => !v)
|
||||
},
|
||||
{ context: 'Confirmation', isActive: enabled },
|
||||
)
|
||||
|
||||
return { visible, enabled, promise }
|
||||
}
|
||||
|
||||
/**
|
||||
* Inner component that uses React 19's use() to read the promise.
|
||||
* Suspends while loading, returns null on error.
|
||||
*/
|
||||
function _temp(v) {
|
||||
return !v;
|
||||
}
|
||||
function ExplanationResult(t0) {
|
||||
const $ = _c(21);
|
||||
const {
|
||||
promise
|
||||
} = t0;
|
||||
const explanation = use(promise) as PermissionExplanationType | null;
|
||||
function ExplanationResult({
|
||||
promise,
|
||||
}: {
|
||||
promise: Promise<PermissionExplanationType | null>
|
||||
}): React.ReactNode {
|
||||
const explanation = use(promise)
|
||||
|
||||
if (!explanation) {
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <Box marginTop={1}><Text dimColor={true}>Explanation unavailable</Text></Box>;
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
return (
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>Explanation unavailable</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
let t1;
|
||||
if ($[1] !== explanation.explanation) {
|
||||
t1 = <Text>{explanation.explanation}</Text>;
|
||||
$[1] = explanation.explanation;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
let t2;
|
||||
if ($[3] !== explanation.reasoning) {
|
||||
t2 = <Box marginTop={1}><Text>{explanation.reasoning}</Text></Box>;
|
||||
$[3] = explanation.reasoning;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
let t3;
|
||||
if ($[5] !== explanation.riskLevel) {
|
||||
t3 = getRiskColor(explanation.riskLevel);
|
||||
$[5] = explanation.riskLevel;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
}
|
||||
let t4;
|
||||
if ($[7] !== explanation.riskLevel) {
|
||||
t4 = getRiskLabel(explanation.riskLevel);
|
||||
$[7] = explanation.riskLevel;
|
||||
$[8] = t4;
|
||||
} else {
|
||||
t4 = $[8];
|
||||
}
|
||||
let t5;
|
||||
if ($[9] !== t3 || $[10] !== t4) {
|
||||
t5 = <Text color={t3}>{t4}:</Text>;
|
||||
$[9] = t3;
|
||||
$[10] = t4;
|
||||
$[11] = t5;
|
||||
} else {
|
||||
t5 = $[11];
|
||||
}
|
||||
let t6;
|
||||
if ($[12] !== explanation.risk) {
|
||||
t6 = <Text> {explanation.risk}</Text>;
|
||||
$[12] = explanation.risk;
|
||||
$[13] = t6;
|
||||
} else {
|
||||
t6 = $[13];
|
||||
}
|
||||
let t7;
|
||||
if ($[14] !== t5 || $[15] !== t6) {
|
||||
t7 = <Box marginTop={1}><Text>{t5}{t6}</Text></Box>;
|
||||
$[14] = t5;
|
||||
$[15] = t6;
|
||||
$[16] = t7;
|
||||
} else {
|
||||
t7 = $[16];
|
||||
}
|
||||
let t8;
|
||||
if ($[17] !== t1 || $[18] !== t2 || $[19] !== t7) {
|
||||
t8 = <Box flexDirection="column" marginTop={1}>{t1}{t2}{t7}</Box>;
|
||||
$[17] = t1;
|
||||
$[18] = t2;
|
||||
$[19] = t7;
|
||||
$[20] = t8;
|
||||
} else {
|
||||
t8 = $[20];
|
||||
}
|
||||
return t8;
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<Text>{explanation.explanation}</Text>
|
||||
<Box marginTop={1}>
|
||||
<Text>{explanation.reasoning}</Text>
|
||||
</Box>
|
||||
<Box marginTop={1}>
|
||||
<Text>
|
||||
<Text color={getRiskColor(explanation.riskLevel)}>
|
||||
{getRiskLabel(explanation.riskLevel)}:
|
||||
</Text>
|
||||
<Text> {explanation.risk}</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Content component - shows loading (via Suspense) or explanation when visible
|
||||
*/
|
||||
export function PermissionExplainerContent(t0) {
|
||||
const $ = _c(3);
|
||||
const {
|
||||
export function PermissionExplainerContent({
|
||||
visible,
|
||||
promise
|
||||
} = t0;
|
||||
promise,
|
||||
}: {
|
||||
visible: boolean
|
||||
promise: Promise<PermissionExplanationType | null> | null
|
||||
}): React.ReactNode {
|
||||
if (!visible || !promise) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <Box marginTop={1}><ShimmerLoadingText /></Box>;
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<Box marginTop={1}>
|
||||
<ShimmerLoadingText />
|
||||
</Box>
|
||||
}
|
||||
let t2;
|
||||
if ($[1] !== promise) {
|
||||
t2 = <Suspense fallback={t1}><ExplanationResult promise={promise} /></Suspense>;
|
||||
$[1] = promise;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
return t2;
|
||||
>
|
||||
<ExplanationResult promise={promise} />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,36 +1,43 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React, { type ReactNode, useCallback, useMemo, useState } from 'react';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import type { KeybindingAction } from '../../keybindings/types.js';
|
||||
import { useKeybindings } from '../../keybindings/useKeybinding.js';
|
||||
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics/index.js';
|
||||
import { useSetAppState } from '../../state/AppState.js';
|
||||
import { type OptionWithDescription, Select } from '../CustomSelect/select.js';
|
||||
export type FeedbackType = 'accept' | 'reject';
|
||||
import React, { type ReactNode, useCallback, useMemo, useState } from 'react'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import type { KeybindingAction } from '../../keybindings/types.js'
|
||||
import { useKeybindings } from '../../keybindings/useKeybinding.js'
|
||||
import {
|
||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
logEvent,
|
||||
} from '../../services/analytics/index.js'
|
||||
import { useSetAppState } from '../../state/AppState.js'
|
||||
import { type OptionWithDescription, Select } from '../CustomSelect/select.js'
|
||||
|
||||
export type FeedbackType = 'accept' | 'reject'
|
||||
|
||||
export type PermissionPromptOption<T extends string> = {
|
||||
value: T;
|
||||
label: ReactNode;
|
||||
value: T
|
||||
label: ReactNode
|
||||
feedbackConfig?: {
|
||||
type: FeedbackType;
|
||||
placeholder?: string;
|
||||
};
|
||||
keybinding?: KeybindingAction;
|
||||
};
|
||||
type: FeedbackType
|
||||
placeholder?: string
|
||||
}
|
||||
keybinding?: KeybindingAction
|
||||
}
|
||||
|
||||
export type ToolAnalyticsContext = {
|
||||
toolName: string;
|
||||
isMcp: boolean;
|
||||
};
|
||||
toolName: string
|
||||
isMcp: boolean
|
||||
}
|
||||
|
||||
export type PermissionPromptProps<T extends string> = {
|
||||
options: PermissionPromptOption<T>[];
|
||||
onSelect: (value: T, feedback?: string) => void;
|
||||
onCancel?: () => void;
|
||||
question?: string | ReactNode;
|
||||
toolAnalyticsContext?: ToolAnalyticsContext;
|
||||
};
|
||||
options: PermissionPromptOption<T>[]
|
||||
onSelect: (value: T, feedback?: string) => void
|
||||
onCancel?: () => void
|
||||
question?: string | ReactNode
|
||||
toolAnalyticsContext?: ToolAnalyticsContext
|
||||
}
|
||||
|
||||
const DEFAULT_PLACEHOLDERS: Record<FeedbackType, string> = {
|
||||
accept: 'tell Claude what to do next',
|
||||
reject: 'tell Claude what to do differently'
|
||||
};
|
||||
reject: 'tell Claude what to do differently',
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared component for permission prompts with optional feedback input.
|
||||
@@ -42,294 +49,219 @@ const DEFAULT_PLACEHOLDERS: Record<FeedbackType, string> = {
|
||||
* - Analytics events for feedback interactions
|
||||
* - Transforming options to Select-compatible format
|
||||
*/
|
||||
export function PermissionPrompt(t0) {
|
||||
const $ = _c(54);
|
||||
const {
|
||||
export function PermissionPrompt<T extends string>({
|
||||
options,
|
||||
onSelect,
|
||||
onCancel,
|
||||
question: t1,
|
||||
toolAnalyticsContext
|
||||
} = t0;
|
||||
const question = t1 === undefined ? "Do you want to proceed?" : t1;
|
||||
const setAppState = useSetAppState();
|
||||
const [acceptFeedback, setAcceptFeedback] = useState("");
|
||||
const [rejectFeedback, setRejectFeedback] = useState("");
|
||||
const [acceptInputMode, setAcceptInputMode] = useState(false);
|
||||
const [rejectInputMode, setRejectInputMode] = useState(false);
|
||||
const [focusedValue, setFocusedValue] = useState(null);
|
||||
const [acceptFeedbackModeEntered, setAcceptFeedbackModeEntered] = useState(false);
|
||||
const [rejectFeedbackModeEntered, setRejectFeedbackModeEntered] = useState(false);
|
||||
let t2;
|
||||
if ($[0] !== focusedValue || $[1] !== options) {
|
||||
let t3;
|
||||
if ($[3] !== focusedValue) {
|
||||
t3 = opt => opt.value === focusedValue;
|
||||
$[3] = focusedValue;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
t2 = options.find(t3);
|
||||
$[0] = focusedValue;
|
||||
$[1] = options;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
const focusedOption = t2;
|
||||
const focusedFeedbackType = focusedOption?.feedbackConfig?.type;
|
||||
const showTabHint = focusedFeedbackType === "accept" && !acceptInputMode || focusedFeedbackType === "reject" && !rejectInputMode;
|
||||
let t3;
|
||||
if ($[5] !== acceptInputMode || $[6] !== options || $[7] !== rejectInputMode) {
|
||||
let t4;
|
||||
if ($[9] !== acceptInputMode || $[10] !== rejectInputMode) {
|
||||
t4 = opt_0 => {
|
||||
const {
|
||||
value,
|
||||
label,
|
||||
feedbackConfig
|
||||
} = opt_0;
|
||||
question = 'Do you want to proceed?',
|
||||
toolAnalyticsContext,
|
||||
}: PermissionPromptProps<T>): React.ReactNode {
|
||||
const setAppState = useSetAppState()
|
||||
const [acceptFeedback, setAcceptFeedback] = useState('')
|
||||
const [rejectFeedback, setRejectFeedback] = useState('')
|
||||
const [acceptInputMode, setAcceptInputMode] = useState(false)
|
||||
const [rejectInputMode, setRejectInputMode] = useState(false)
|
||||
const [focusedValue, setFocusedValue] = useState<T | null>(null)
|
||||
// Track whether user ever entered feedback mode (persists after collapse)
|
||||
const [acceptFeedbackModeEntered, setAcceptFeedbackModeEntered] =
|
||||
useState(false)
|
||||
const [rejectFeedbackModeEntered, setRejectFeedbackModeEntered] =
|
||||
useState(false)
|
||||
|
||||
// Find which option is focused and whether it has feedback config
|
||||
const focusedOption = options.find(opt => opt.value === focusedValue)
|
||||
const focusedFeedbackType = focusedOption?.feedbackConfig?.type
|
||||
|
||||
// Show Tab hint when focused on a feedback-enabled option that's not already in input mode
|
||||
const showTabHint =
|
||||
(focusedFeedbackType === 'accept' && !acceptInputMode) ||
|
||||
(focusedFeedbackType === 'reject' && !rejectInputMode)
|
||||
|
||||
// Transform options to Select-compatible format
|
||||
const selectOptions = useMemo((): OptionWithDescription<T>[] => {
|
||||
return options.map(opt => {
|
||||
const { value, label, feedbackConfig } = opt
|
||||
|
||||
// No feedback config = simple option
|
||||
if (!feedbackConfig) {
|
||||
return {
|
||||
label,
|
||||
value
|
||||
};
|
||||
value,
|
||||
}
|
||||
const {
|
||||
type,
|
||||
placeholder
|
||||
} = feedbackConfig;
|
||||
const isInputMode = type === "accept" ? acceptInputMode : rejectInputMode;
|
||||
const onChange = type === "accept" ? setAcceptFeedback : setRejectFeedback;
|
||||
const defaultPlaceholder = DEFAULT_PLACEHOLDERS[type];
|
||||
}
|
||||
|
||||
const { type, placeholder } = feedbackConfig
|
||||
const isInputMode = type === 'accept' ? acceptInputMode : rejectInputMode
|
||||
const onChange = type === 'accept' ? setAcceptFeedback : setRejectFeedback
|
||||
const defaultPlaceholder = DEFAULT_PLACEHOLDERS[type]
|
||||
|
||||
// When in input mode, show input field
|
||||
if (isInputMode) {
|
||||
return {
|
||||
type: "input" as const,
|
||||
type: 'input' as const,
|
||||
label,
|
||||
value,
|
||||
placeholder: placeholder ?? defaultPlaceholder,
|
||||
onChange,
|
||||
allowEmptySubmitToCancel: true
|
||||
};
|
||||
allowEmptySubmitToCancel: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Not in input mode - show simple option
|
||||
return {
|
||||
label,
|
||||
value
|
||||
};
|
||||
};
|
||||
$[9] = acceptInputMode;
|
||||
$[10] = rejectInputMode;
|
||||
$[11] = t4;
|
||||
} else {
|
||||
t4 = $[11];
|
||||
value,
|
||||
}
|
||||
t3 = options.map(t4);
|
||||
$[5] = acceptInputMode;
|
||||
$[6] = options;
|
||||
$[7] = rejectInputMode;
|
||||
$[8] = t3;
|
||||
} else {
|
||||
t3 = $[8];
|
||||
}
|
||||
const selectOptions = t3;
|
||||
let t4;
|
||||
if ($[12] !== acceptInputMode || $[13] !== options || $[14] !== rejectInputMode || $[15] !== toolAnalyticsContext?.isMcp || $[16] !== toolAnalyticsContext?.toolName) {
|
||||
t4 = value_0 => {
|
||||
const option = options.find(opt_1 => opt_1.value === value_0);
|
||||
if (!option?.feedbackConfig) {
|
||||
return;
|
||||
}
|
||||
const {
|
||||
type: type_0
|
||||
} = option.feedbackConfig;
|
||||
})
|
||||
}, [options, acceptInputMode, rejectInputMode])
|
||||
|
||||
// Handle Tab key to toggle input mode
|
||||
const handleInputModeToggle = useCallback(
|
||||
(value: T) => {
|
||||
const option = options.find(opt => opt.value === value)
|
||||
if (!option?.feedbackConfig) return
|
||||
|
||||
const { type } = option.feedbackConfig
|
||||
const analyticsProps = {
|
||||
toolName: toolAnalyticsContext?.toolName as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
isMcp: toolAnalyticsContext?.isMcp ?? false
|
||||
};
|
||||
if (type_0 === "accept") {
|
||||
toolName:
|
||||
toolAnalyticsContext?.toolName as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
isMcp: toolAnalyticsContext?.isMcp ?? false,
|
||||
}
|
||||
|
||||
if (type === 'accept') {
|
||||
if (acceptInputMode) {
|
||||
setAcceptInputMode(false);
|
||||
logEvent("tengu_accept_feedback_mode_collapsed", analyticsProps);
|
||||
setAcceptInputMode(false)
|
||||
logEvent('tengu_accept_feedback_mode_collapsed', analyticsProps)
|
||||
} else {
|
||||
setAcceptInputMode(true);
|
||||
setAcceptFeedbackModeEntered(true);
|
||||
logEvent("tengu_accept_feedback_mode_entered", analyticsProps);
|
||||
setAcceptInputMode(true)
|
||||
setAcceptFeedbackModeEntered(true)
|
||||
logEvent('tengu_accept_feedback_mode_entered', analyticsProps)
|
||||
}
|
||||
} else {
|
||||
if (type_0 === "reject") {
|
||||
} else if (type === 'reject') {
|
||||
if (rejectInputMode) {
|
||||
setRejectInputMode(false);
|
||||
logEvent("tengu_reject_feedback_mode_collapsed", analyticsProps);
|
||||
setRejectInputMode(false)
|
||||
logEvent('tengu_reject_feedback_mode_collapsed', analyticsProps)
|
||||
} else {
|
||||
setRejectInputMode(true);
|
||||
setRejectFeedbackModeEntered(true);
|
||||
logEvent("tengu_reject_feedback_mode_entered", analyticsProps);
|
||||
setRejectInputMode(true)
|
||||
setRejectFeedbackModeEntered(true)
|
||||
logEvent('tengu_reject_feedback_mode_entered', analyticsProps)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
$[12] = acceptInputMode;
|
||||
$[13] = options;
|
||||
$[14] = rejectInputMode;
|
||||
$[15] = toolAnalyticsContext?.isMcp;
|
||||
$[16] = toolAnalyticsContext?.toolName;
|
||||
$[17] = t4;
|
||||
} else {
|
||||
t4 = $[17];
|
||||
}
|
||||
const handleInputModeToggle = t4;
|
||||
let t5;
|
||||
if ($[18] !== acceptFeedback || $[19] !== acceptFeedbackModeEntered || $[20] !== onSelect || $[21] !== options || $[22] !== rejectFeedback || $[23] !== rejectFeedbackModeEntered || $[24] !== toolAnalyticsContext?.isMcp || $[25] !== toolAnalyticsContext?.toolName) {
|
||||
t5 = value_1 => {
|
||||
const option_0 = options.find(opt_2 => opt_2.value === value_1);
|
||||
if (!option_0) {
|
||||
return;
|
||||
}
|
||||
let feedback;
|
||||
if (option_0.feedbackConfig) {
|
||||
const rawFeedback = option_0.feedbackConfig.type === "accept" ? acceptFeedback : rejectFeedback;
|
||||
const trimmedFeedback = rawFeedback.trim();
|
||||
},
|
||||
[options, acceptInputMode, rejectInputMode, toolAnalyticsContext],
|
||||
)
|
||||
|
||||
// Handle selection
|
||||
const handleSelect = useCallback(
|
||||
(value: T) => {
|
||||
const option = options.find(opt => opt.value === value)
|
||||
if (!option) return
|
||||
|
||||
// Get feedback if applicable
|
||||
let feedback: string | undefined
|
||||
if (option.feedbackConfig) {
|
||||
const rawFeedback =
|
||||
option.feedbackConfig.type === 'accept'
|
||||
? acceptFeedback
|
||||
: rejectFeedback
|
||||
const trimmedFeedback = rawFeedback.trim()
|
||||
|
||||
if (trimmedFeedback) {
|
||||
feedback = trimmedFeedback;
|
||||
feedback = trimmedFeedback
|
||||
}
|
||||
const analyticsProps_0 = {
|
||||
toolName: toolAnalyticsContext?.toolName as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
|
||||
// Log accept/reject submission with feedback context
|
||||
const analyticsProps = {
|
||||
toolName:
|
||||
toolAnalyticsContext?.toolName as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
isMcp: toolAnalyticsContext?.isMcp ?? false,
|
||||
has_instructions: !!trimmedFeedback,
|
||||
instructions_length: trimmedFeedback?.length ?? 0,
|
||||
entered_feedback_mode: option_0.feedbackConfig.type === "accept" ? acceptFeedbackModeEntered : rejectFeedbackModeEntered
|
||||
};
|
||||
if (option_0.feedbackConfig.type === "accept") {
|
||||
logEvent("tengu_accept_submitted", analyticsProps_0);
|
||||
} else {
|
||||
if (option_0.feedbackConfig.type === "reject") {
|
||||
logEvent("tengu_reject_submitted", analyticsProps_0);
|
||||
entered_feedback_mode:
|
||||
option.feedbackConfig.type === 'accept'
|
||||
? acceptFeedbackModeEntered
|
||||
: rejectFeedbackModeEntered,
|
||||
}
|
||||
|
||||
if (option.feedbackConfig.type === 'accept') {
|
||||
logEvent('tengu_accept_submitted', analyticsProps)
|
||||
} else if (option.feedbackConfig.type === 'reject') {
|
||||
logEvent('tengu_reject_submitted', analyticsProps)
|
||||
}
|
||||
}
|
||||
}
|
||||
onSelect(value_1, feedback);
|
||||
};
|
||||
$[18] = acceptFeedback;
|
||||
$[19] = acceptFeedbackModeEntered;
|
||||
$[20] = onSelect;
|
||||
$[21] = options;
|
||||
$[22] = rejectFeedback;
|
||||
$[23] = rejectFeedbackModeEntered;
|
||||
$[24] = toolAnalyticsContext?.isMcp;
|
||||
$[25] = toolAnalyticsContext?.toolName;
|
||||
$[26] = t5;
|
||||
} else {
|
||||
t5 = $[26];
|
||||
}
|
||||
const handleSelect = t5;
|
||||
let handlers;
|
||||
if ($[27] !== handleSelect || $[28] !== options) {
|
||||
handlers = {};
|
||||
for (const opt_3 of options) {
|
||||
if (opt_3.keybinding) {
|
||||
handlers[opt_3.keybinding] = () => handleSelect(opt_3.value);
|
||||
|
||||
onSelect(value, feedback)
|
||||
},
|
||||
[
|
||||
options,
|
||||
acceptFeedback,
|
||||
rejectFeedback,
|
||||
onSelect,
|
||||
toolAnalyticsContext,
|
||||
acceptFeedbackModeEntered,
|
||||
rejectFeedbackModeEntered,
|
||||
],
|
||||
)
|
||||
|
||||
// Register keybinding handlers for options that have a keybinding set
|
||||
const keybindingHandlers = useMemo(() => {
|
||||
const handlers: Record<string, () => void> = {}
|
||||
for (const opt of options) {
|
||||
if (opt.keybinding) {
|
||||
handlers[opt.keybinding] = () => handleSelect(opt.value)
|
||||
}
|
||||
}
|
||||
$[27] = handleSelect;
|
||||
$[28] = options;
|
||||
$[29] = handlers;
|
||||
} else {
|
||||
handlers = $[29];
|
||||
}
|
||||
const keybindingHandlers = handlers;
|
||||
let t6;
|
||||
if ($[30] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t6 = {
|
||||
context: "Confirmation"
|
||||
};
|
||||
$[30] = t6;
|
||||
} else {
|
||||
t6 = $[30];
|
||||
}
|
||||
useKeybindings(keybindingHandlers, t6);
|
||||
let t7;
|
||||
if ($[31] !== onCancel || $[32] !== setAppState) {
|
||||
t7 = () => {
|
||||
logEvent("tengu_permission_request_escape", {});
|
||||
setAppState(_temp);
|
||||
onCancel?.();
|
||||
};
|
||||
$[31] = onCancel;
|
||||
$[32] = setAppState;
|
||||
$[33] = t7;
|
||||
} else {
|
||||
t7 = $[33];
|
||||
}
|
||||
const handleCancel = t7;
|
||||
let t8;
|
||||
if ($[34] !== question) {
|
||||
t8 = typeof question === "string" ? <Text>{question}</Text> : question;
|
||||
$[34] = question;
|
||||
$[35] = t8;
|
||||
} else {
|
||||
t8 = $[35];
|
||||
}
|
||||
let t9;
|
||||
if ($[36] !== acceptFeedback || $[37] !== acceptInputMode || $[38] !== options || $[39] !== rejectFeedback || $[40] !== rejectInputMode) {
|
||||
t9 = value_2 => {
|
||||
const newOption = options.find(opt_4 => opt_4.value === value_2);
|
||||
if (newOption?.feedbackConfig?.type !== "accept" && acceptInputMode && !acceptFeedback.trim()) {
|
||||
setAcceptInputMode(false);
|
||||
}
|
||||
if (newOption?.feedbackConfig?.type !== "reject" && rejectInputMode && !rejectFeedback.trim()) {
|
||||
setRejectInputMode(false);
|
||||
}
|
||||
setFocusedValue(value_2);
|
||||
};
|
||||
$[36] = acceptFeedback;
|
||||
$[37] = acceptInputMode;
|
||||
$[38] = options;
|
||||
$[39] = rejectFeedback;
|
||||
$[40] = rejectInputMode;
|
||||
$[41] = t9;
|
||||
} else {
|
||||
t9 = $[41];
|
||||
}
|
||||
let t10;
|
||||
if ($[42] !== handleCancel || $[43] !== handleInputModeToggle || $[44] !== handleSelect || $[45] !== selectOptions || $[46] !== t9) {
|
||||
t10 = <Select options={selectOptions} inlineDescriptions={true} onChange={handleSelect} onCancel={handleCancel} onFocus={t9} onInputModeToggle={handleInputModeToggle} />;
|
||||
$[42] = handleCancel;
|
||||
$[43] = handleInputModeToggle;
|
||||
$[44] = handleSelect;
|
||||
$[45] = selectOptions;
|
||||
$[46] = t9;
|
||||
$[47] = t10;
|
||||
} else {
|
||||
t10 = $[47];
|
||||
}
|
||||
const t11 = showTabHint && " \xB7 Tab to amend";
|
||||
let t12;
|
||||
if ($[48] !== t11) {
|
||||
t12 = <Box marginTop={1}><Text dimColor={true}>Esc to cancel{t11}</Text></Box>;
|
||||
$[48] = t11;
|
||||
$[49] = t12;
|
||||
} else {
|
||||
t12 = $[49];
|
||||
}
|
||||
let t13;
|
||||
if ($[50] !== t10 || $[51] !== t12 || $[52] !== t8) {
|
||||
t13 = <Box flexDirection="column">{t8}{t10}{t12}</Box>;
|
||||
$[50] = t10;
|
||||
$[51] = t12;
|
||||
$[52] = t8;
|
||||
$[53] = t13;
|
||||
} else {
|
||||
t13 = $[53];
|
||||
}
|
||||
return t13;
|
||||
}
|
||||
function _temp(prev) {
|
||||
return {
|
||||
return handlers
|
||||
}, [options, handleSelect])
|
||||
|
||||
useKeybindings(keybindingHandlers, { context: 'Confirmation' })
|
||||
|
||||
// Handle cancel (Esc)
|
||||
const handleCancel = useCallback(() => {
|
||||
logEvent('tengu_permission_request_escape', {})
|
||||
// Increment escape count for attribution tracking
|
||||
setAppState(prev => ({
|
||||
...prev,
|
||||
attribution: {
|
||||
...prev.attribution,
|
||||
escapeCount: prev.attribution.escapeCount + 1
|
||||
escapeCount: prev.attribution.escapeCount + 1,
|
||||
},
|
||||
}))
|
||||
onCancel?.()
|
||||
}, [onCancel, setAppState])
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
{typeof question === 'string' ? <Text>{question}</Text> : question}
|
||||
<Select
|
||||
options={selectOptions}
|
||||
inlineDescriptions
|
||||
onChange={handleSelect}
|
||||
onCancel={handleCancel}
|
||||
onFocus={value => {
|
||||
// Reset input mode when navigating away, but only if no text typed
|
||||
const newOption = options.find(opt => opt.value === value)
|
||||
if (
|
||||
newOption?.feedbackConfig?.type !== 'accept' &&
|
||||
acceptInputMode &&
|
||||
!acceptFeedback.trim()
|
||||
) {
|
||||
setAcceptInputMode(false)
|
||||
}
|
||||
};
|
||||
if (
|
||||
newOption?.feedbackConfig?.type !== 'reject' &&
|
||||
rejectInputMode &&
|
||||
!rejectFeedback.trim()
|
||||
) {
|
||||
setRejectInputMode(false)
|
||||
}
|
||||
setFocusedValue(value)
|
||||
}}
|
||||
onInputModeToggle={handleInputModeToggle}
|
||||
/>
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>Esc to cancel{showTabHint && ' · Tab to amend'}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,92 +1,125 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { feature } from 'bun:bundle';
|
||||
import * as React from 'react';
|
||||
import { EnterPlanModeTool } from 'src/tools/EnterPlanModeTool/EnterPlanModeTool.js';
|
||||
import { ExitPlanModeV2Tool } from 'src/tools/ExitPlanModeTool/ExitPlanModeV2Tool.js';
|
||||
import { useNotifyAfterTimeout } from '../../hooks/useNotifyAfterTimeout.js';
|
||||
import { useKeybinding } from '../../keybindings/useKeybinding.js';
|
||||
import type { AnyObject, Tool, ToolUseContext } from '../../Tool.js';
|
||||
import { AskUserQuestionTool } from '../../tools/AskUserQuestionTool/AskUserQuestionTool.js';
|
||||
import { BashTool } from '../../tools/BashTool/BashTool.js';
|
||||
import { FileEditTool } from '../../tools/FileEditTool/FileEditTool.js';
|
||||
import { FileReadTool } from '../../tools/FileReadTool/FileReadTool.js';
|
||||
import { FileWriteTool } from '../../tools/FileWriteTool/FileWriteTool.js';
|
||||
import { GlobTool } from '../../tools/GlobTool/GlobTool.js';
|
||||
import { GrepTool } from '../../tools/GrepTool/GrepTool.js';
|
||||
import { NotebookEditTool } from '../../tools/NotebookEditTool/NotebookEditTool.js';
|
||||
import { PowerShellTool } from '../../tools/PowerShellTool/PowerShellTool.js';
|
||||
import { SkillTool } from '../../tools/SkillTool/SkillTool.js';
|
||||
import { WebFetchTool } from '../../tools/WebFetchTool/WebFetchTool.js';
|
||||
import type { AssistantMessage } from '../../types/message.js';
|
||||
import type { PermissionDecision } from '../../utils/permissions/PermissionResult.js';
|
||||
import { AskUserQuestionPermissionRequest } from './AskUserQuestionPermissionRequest/AskUserQuestionPermissionRequest.js';
|
||||
import { BashPermissionRequest } from './BashPermissionRequest/BashPermissionRequest.js';
|
||||
import { EnterPlanModePermissionRequest } from './EnterPlanModePermissionRequest/EnterPlanModePermissionRequest.js';
|
||||
import { ExitPlanModePermissionRequest } from './ExitPlanModePermissionRequest/ExitPlanModePermissionRequest.js';
|
||||
import { FallbackPermissionRequest } from './FallbackPermissionRequest.js';
|
||||
import { FileEditPermissionRequest } from './FileEditPermissionRequest/FileEditPermissionRequest.js';
|
||||
import { FilesystemPermissionRequest } from './FilesystemPermissionRequest/FilesystemPermissionRequest.js';
|
||||
import { FileWritePermissionRequest } from './FileWritePermissionRequest/FileWritePermissionRequest.js';
|
||||
import { NotebookEditPermissionRequest } from './NotebookEditPermissionRequest/NotebookEditPermissionRequest.js';
|
||||
import { PowerShellPermissionRequest } from './PowerShellPermissionRequest/PowerShellPermissionRequest.js';
|
||||
import { SkillPermissionRequest } from './SkillPermissionRequest/SkillPermissionRequest.js';
|
||||
import { WebFetchPermissionRequest } from './WebFetchPermissionRequest/WebFetchPermissionRequest.js';
|
||||
import { feature } from 'bun:bundle'
|
||||
import * as React from 'react'
|
||||
import { EnterPlanModeTool } from 'src/tools/EnterPlanModeTool/EnterPlanModeTool.js'
|
||||
import { ExitPlanModeV2Tool } from 'src/tools/ExitPlanModeTool/ExitPlanModeV2Tool.js'
|
||||
import { useNotifyAfterTimeout } from '../../hooks/useNotifyAfterTimeout.js'
|
||||
import { useKeybinding } from '../../keybindings/useKeybinding.js'
|
||||
import type { AnyObject, Tool, ToolUseContext } from '../../Tool.js'
|
||||
import { AskUserQuestionTool } from '../../tools/AskUserQuestionTool/AskUserQuestionTool.js'
|
||||
import { BashTool } from '../../tools/BashTool/BashTool.js'
|
||||
import { FileEditTool } from '../../tools/FileEditTool/FileEditTool.js'
|
||||
import { FileReadTool } from '../../tools/FileReadTool/FileReadTool.js'
|
||||
import { FileWriteTool } from '../../tools/FileWriteTool/FileWriteTool.js'
|
||||
import { GlobTool } from '../../tools/GlobTool/GlobTool.js'
|
||||
import { GrepTool } from '../../tools/GrepTool/GrepTool.js'
|
||||
import { NotebookEditTool } from '../../tools/NotebookEditTool/NotebookEditTool.js'
|
||||
import { PowerShellTool } from '../../tools/PowerShellTool/PowerShellTool.js'
|
||||
import { SkillTool } from '../../tools/SkillTool/SkillTool.js'
|
||||
import { WebFetchTool } from '../../tools/WebFetchTool/WebFetchTool.js'
|
||||
import type { AssistantMessage } from '../../types/message.js'
|
||||
import type { PermissionDecision } from '../../utils/permissions/PermissionResult.js'
|
||||
import { AskUserQuestionPermissionRequest } from './AskUserQuestionPermissionRequest/AskUserQuestionPermissionRequest.js'
|
||||
import { BashPermissionRequest } from './BashPermissionRequest/BashPermissionRequest.js'
|
||||
import { EnterPlanModePermissionRequest } from './EnterPlanModePermissionRequest/EnterPlanModePermissionRequest.js'
|
||||
import { ExitPlanModePermissionRequest } from './ExitPlanModePermissionRequest/ExitPlanModePermissionRequest.js'
|
||||
import { FallbackPermissionRequest } from './FallbackPermissionRequest.js'
|
||||
import { FileEditPermissionRequest } from './FileEditPermissionRequest/FileEditPermissionRequest.js'
|
||||
import { FilesystemPermissionRequest } from './FilesystemPermissionRequest/FilesystemPermissionRequest.js'
|
||||
import { FileWritePermissionRequest } from './FileWritePermissionRequest/FileWritePermissionRequest.js'
|
||||
import { NotebookEditPermissionRequest } from './NotebookEditPermissionRequest/NotebookEditPermissionRequest.js'
|
||||
import { PowerShellPermissionRequest } from './PowerShellPermissionRequest/PowerShellPermissionRequest.js'
|
||||
import { SkillPermissionRequest } from './SkillPermissionRequest/SkillPermissionRequest.js'
|
||||
import { WebFetchPermissionRequest } from './WebFetchPermissionRequest/WebFetchPermissionRequest.js'
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
const ReviewArtifactTool = feature('REVIEW_ARTIFACT') ? (require('../../tools/ReviewArtifactTool/ReviewArtifactTool.js') as typeof import('../../tools/ReviewArtifactTool/ReviewArtifactTool.js')).ReviewArtifactTool : null;
|
||||
const ReviewArtifactPermissionRequest = feature('REVIEW_ARTIFACT') ? (require('./ReviewArtifactPermissionRequest/ReviewArtifactPermissionRequest.js') as typeof import('./ReviewArtifactPermissionRequest/ReviewArtifactPermissionRequest.js')).ReviewArtifactPermissionRequest : null;
|
||||
const WorkflowTool = feature('WORKFLOW_SCRIPTS') ? (require('../../tools/WorkflowTool/WorkflowTool.js') as typeof import('../../tools/WorkflowTool/WorkflowTool.js')).WorkflowTool : null;
|
||||
const WorkflowPermissionRequest = feature('WORKFLOW_SCRIPTS') ? (require('../../tools/WorkflowTool/WorkflowPermissionRequest.js') as typeof import('../../tools/WorkflowTool/WorkflowPermissionRequest.js')).WorkflowPermissionRequest : null;
|
||||
const MonitorTool = feature('MONITOR_TOOL') ? (require('../../tools/MonitorTool/MonitorTool.js') as typeof import('../../tools/MonitorTool/MonitorTool.js')).MonitorTool : null;
|
||||
const MonitorPermissionRequest = feature('MONITOR_TOOL') ? (require('./MonitorPermissionRequest/MonitorPermissionRequest.js') as typeof import('./MonitorPermissionRequest/MonitorPermissionRequest.js')).MonitorPermissionRequest : null;
|
||||
import type { ContentBlockParam } from '@anthropic-ai/sdk/resources/messages.mjs';
|
||||
const ReviewArtifactTool = feature('REVIEW_ARTIFACT')
|
||||
? (
|
||||
require('../../tools/ReviewArtifactTool/ReviewArtifactTool.js') as typeof import('../../tools/ReviewArtifactTool/ReviewArtifactTool.js')
|
||||
).ReviewArtifactTool
|
||||
: null
|
||||
|
||||
const ReviewArtifactPermissionRequest = feature('REVIEW_ARTIFACT')
|
||||
? (
|
||||
require('./ReviewArtifactPermissionRequest/ReviewArtifactPermissionRequest.js') as typeof import('./ReviewArtifactPermissionRequest/ReviewArtifactPermissionRequest.js')
|
||||
).ReviewArtifactPermissionRequest
|
||||
: null
|
||||
|
||||
const WorkflowTool = feature('WORKFLOW_SCRIPTS')
|
||||
? (
|
||||
require('../../tools/WorkflowTool/WorkflowTool.js') as typeof import('../../tools/WorkflowTool/WorkflowTool.js')
|
||||
).WorkflowTool
|
||||
: null
|
||||
|
||||
const WorkflowPermissionRequest = feature('WORKFLOW_SCRIPTS')
|
||||
? (
|
||||
require('../../tools/WorkflowTool/WorkflowPermissionRequest.js') as typeof import('../../tools/WorkflowTool/WorkflowPermissionRequest.js')
|
||||
).WorkflowPermissionRequest
|
||||
: null
|
||||
|
||||
const MonitorTool = feature('MONITOR_TOOL')
|
||||
? (
|
||||
require('../../tools/MonitorTool/MonitorTool.js') as typeof import('../../tools/MonitorTool/MonitorTool.js')
|
||||
).MonitorTool
|
||||
: null
|
||||
|
||||
const MonitorPermissionRequest = feature('MONITOR_TOOL')
|
||||
? (
|
||||
require('./MonitorPermissionRequest/MonitorPermissionRequest.js') as typeof import('./MonitorPermissionRequest/MonitorPermissionRequest.js')
|
||||
).MonitorPermissionRequest
|
||||
: null
|
||||
|
||||
import type { ContentBlockParam } from '@anthropic-ai/sdk/resources/messages.mjs'
|
||||
/* eslint-enable @typescript-eslint/no-require-imports */
|
||||
import type { z } from 'zod/v4';
|
||||
import type { PermissionUpdate } from '../../utils/permissions/PermissionUpdateSchema.js';
|
||||
import type { WorkerBadgeProps } from './WorkerBadge.js';
|
||||
function permissionComponentForTool(tool: Tool): React.ComponentType<PermissionRequestProps> {
|
||||
import type { z } from 'zod/v4'
|
||||
import type { PermissionUpdate } from '../../utils/permissions/PermissionUpdateSchema.js'
|
||||
import type { WorkerBadgeProps } from './WorkerBadge.js'
|
||||
|
||||
function permissionComponentForTool(
|
||||
tool: Tool,
|
||||
): React.ComponentType<PermissionRequestProps> {
|
||||
switch (tool) {
|
||||
case FileEditTool:
|
||||
return FileEditPermissionRequest;
|
||||
return FileEditPermissionRequest
|
||||
case FileWriteTool:
|
||||
return FileWritePermissionRequest;
|
||||
return FileWritePermissionRequest
|
||||
case BashTool:
|
||||
return BashPermissionRequest;
|
||||
return BashPermissionRequest
|
||||
case PowerShellTool:
|
||||
return PowerShellPermissionRequest;
|
||||
return PowerShellPermissionRequest
|
||||
case ReviewArtifactTool:
|
||||
return ReviewArtifactPermissionRequest ?? FallbackPermissionRequest;
|
||||
return ReviewArtifactPermissionRequest ?? FallbackPermissionRequest
|
||||
case WebFetchTool:
|
||||
return WebFetchPermissionRequest;
|
||||
return WebFetchPermissionRequest
|
||||
case NotebookEditTool:
|
||||
return NotebookEditPermissionRequest;
|
||||
return NotebookEditPermissionRequest
|
||||
case ExitPlanModeV2Tool:
|
||||
return ExitPlanModePermissionRequest;
|
||||
return ExitPlanModePermissionRequest
|
||||
case EnterPlanModeTool:
|
||||
return EnterPlanModePermissionRequest;
|
||||
return EnterPlanModePermissionRequest
|
||||
case SkillTool:
|
||||
return SkillPermissionRequest;
|
||||
return SkillPermissionRequest
|
||||
case AskUserQuestionTool:
|
||||
return AskUserQuestionPermissionRequest;
|
||||
return AskUserQuestionPermissionRequest
|
||||
case WorkflowTool:
|
||||
return WorkflowPermissionRequest ?? FallbackPermissionRequest;
|
||||
return WorkflowPermissionRequest ?? FallbackPermissionRequest
|
||||
case MonitorTool:
|
||||
return MonitorPermissionRequest ?? FallbackPermissionRequest;
|
||||
return MonitorPermissionRequest ?? FallbackPermissionRequest
|
||||
case GlobTool:
|
||||
case GrepTool:
|
||||
case FileReadTool:
|
||||
return FilesystemPermissionRequest;
|
||||
return FilesystemPermissionRequest
|
||||
default:
|
||||
return FallbackPermissionRequest;
|
||||
return FallbackPermissionRequest
|
||||
}
|
||||
}
|
||||
|
||||
export type PermissionRequestProps<Input extends AnyObject = AnyObject> = {
|
||||
toolUseConfirm: ToolUseConfirm<Input>;
|
||||
toolUseContext: ToolUseContext;
|
||||
onDone(): void;
|
||||
onReject(): void;
|
||||
verbose: boolean;
|
||||
workerBadge: WorkerBadgeProps | undefined;
|
||||
toolUseConfirm: ToolUseConfirm<Input>
|
||||
toolUseContext: ToolUseContext
|
||||
onDone(): void
|
||||
onReject(): void
|
||||
verbose: boolean
|
||||
workerBadge: WorkerBadgeProps | undefined
|
||||
/**
|
||||
* Register JSX to render in a sticky footer below the scrollable area.
|
||||
* Fullscreen mode only (non-fullscreen has no sticky area — terminal
|
||||
@@ -98,119 +131,102 @@ export type PermissionRequestProps<Input extends AnyObject = AnyObject> = {
|
||||
* to avoid stale closures (React reconciles the JSX, preserving Select's
|
||||
* internal focus/input state).
|
||||
*/
|
||||
setStickyFooter?: (jsx: React.ReactNode | null) => void;
|
||||
};
|
||||
setStickyFooter?: (jsx: React.ReactNode | null) => void
|
||||
}
|
||||
|
||||
export type ToolUseConfirm<Input extends AnyObject = AnyObject> = {
|
||||
assistantMessage: AssistantMessage;
|
||||
tool: Tool<Input>;
|
||||
description: string;
|
||||
input: z.infer<Input>;
|
||||
toolUseContext: ToolUseContext;
|
||||
toolUseID: string;
|
||||
permissionResult: PermissionDecision;
|
||||
permissionPromptStartTimeMs: number;
|
||||
assistantMessage: AssistantMessage
|
||||
tool: Tool<Input>
|
||||
description: string
|
||||
input: z.infer<Input>
|
||||
toolUseContext: ToolUseContext
|
||||
toolUseID: string
|
||||
permissionResult: PermissionDecision
|
||||
permissionPromptStartTimeMs: number
|
||||
/**
|
||||
* Called when user interacts with the permission dialog (e.g., arrow keys, tab, typing).
|
||||
* This prevents async auto-approval mechanisms (like the bash classifier) from
|
||||
* dismissing the dialog while the user is actively engaging with it.
|
||||
*/
|
||||
classifierCheckInProgress?: boolean;
|
||||
classifierAutoApproved?: boolean;
|
||||
classifierMatchedRule?: string;
|
||||
workerBadge?: WorkerBadgeProps;
|
||||
onUserInteraction(): void;
|
||||
onAbort(): void;
|
||||
onDismissCheckmark?(): void;
|
||||
onAllow(updatedInput: z.infer<Input>, permissionUpdates: PermissionUpdate[], feedback?: string, contentBlocks?: ContentBlockParam[]): void;
|
||||
onReject(feedback?: string, contentBlocks?: ContentBlockParam[]): void;
|
||||
recheckPermission(): Promise<void>;
|
||||
};
|
||||
classifierCheckInProgress?: boolean
|
||||
classifierAutoApproved?: boolean
|
||||
classifierMatchedRule?: string
|
||||
workerBadge?: WorkerBadgeProps
|
||||
onUserInteraction(): void
|
||||
onAbort(): void
|
||||
onDismissCheckmark?(): void
|
||||
onAllow(
|
||||
updatedInput: z.infer<Input>,
|
||||
permissionUpdates: PermissionUpdate[],
|
||||
feedback?: string,
|
||||
contentBlocks?: ContentBlockParam[],
|
||||
): void
|
||||
onReject(feedback?: string, contentBlocks?: ContentBlockParam[]): void
|
||||
recheckPermission(): Promise<void>
|
||||
}
|
||||
|
||||
function getNotificationMessage(toolUseConfirm: ToolUseConfirm): string {
|
||||
const toolName = toolUseConfirm.tool.userFacingName(toolUseConfirm.input as never);
|
||||
const toolName = toolUseConfirm.tool.userFacingName(
|
||||
toolUseConfirm.input as never,
|
||||
)
|
||||
|
||||
if (toolUseConfirm.tool === ExitPlanModeV2Tool) {
|
||||
return 'Claude Code needs your approval for the plan';
|
||||
return 'Claude Code needs your approval for the plan'
|
||||
}
|
||||
|
||||
if (toolUseConfirm.tool === EnterPlanModeTool) {
|
||||
return 'Claude Code wants to enter plan mode';
|
||||
return 'Claude Code wants to enter plan mode'
|
||||
}
|
||||
if (feature('REVIEW_ARTIFACT') && toolUseConfirm.tool === ReviewArtifactTool) {
|
||||
return 'Claude needs your approval for a review artifact';
|
||||
|
||||
if (
|
||||
feature('REVIEW_ARTIFACT') &&
|
||||
toolUseConfirm.tool === ReviewArtifactTool
|
||||
) {
|
||||
return 'Claude needs your approval for a review artifact'
|
||||
}
|
||||
|
||||
if (!toolName || toolName.trim() === '') {
|
||||
return 'Claude Code needs your attention';
|
||||
return 'Claude Code needs your attention'
|
||||
}
|
||||
return `Claude needs your permission to use ${toolName}`;
|
||||
|
||||
return `Claude needs your permission to use ${toolName}`
|
||||
}
|
||||
|
||||
// TODO: Move this to Tool.renderPermissionRequest
|
||||
export function PermissionRequest(t0) {
|
||||
const $ = _c(18);
|
||||
const {
|
||||
export function PermissionRequest({
|
||||
toolUseConfirm,
|
||||
toolUseContext,
|
||||
onDone,
|
||||
onReject,
|
||||
verbose,
|
||||
workerBadge,
|
||||
setStickyFooter
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] !== onDone || $[1] !== onReject || $[2] !== toolUseConfirm) {
|
||||
t1 = () => {
|
||||
onDone();
|
||||
onReject();
|
||||
toolUseConfirm.onReject();
|
||||
};
|
||||
$[0] = onDone;
|
||||
$[1] = onReject;
|
||||
$[2] = toolUseConfirm;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
let t2;
|
||||
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = {
|
||||
context: "Confirmation"
|
||||
};
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
useKeybinding("app:interrupt", t1, t2);
|
||||
let t3;
|
||||
if ($[5] !== toolUseConfirm) {
|
||||
t3 = getNotificationMessage(toolUseConfirm);
|
||||
$[5] = toolUseConfirm;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
}
|
||||
const notificationMessage = t3;
|
||||
useNotifyAfterTimeout(notificationMessage, "permission_prompt");
|
||||
let t4;
|
||||
if ($[7] !== toolUseConfirm.tool) {
|
||||
t4 = permissionComponentForTool(toolUseConfirm.tool);
|
||||
$[7] = toolUseConfirm.tool;
|
||||
$[8] = t4;
|
||||
} else {
|
||||
t4 = $[8];
|
||||
}
|
||||
const PermissionComponent = t4;
|
||||
let t5;
|
||||
if ($[9] !== PermissionComponent || $[10] !== onDone || $[11] !== onReject || $[12] !== setStickyFooter || $[13] !== toolUseConfirm || $[14] !== toolUseContext || $[15] !== verbose || $[16] !== workerBadge) {
|
||||
t5 = <PermissionComponent toolUseContext={toolUseContext} toolUseConfirm={toolUseConfirm} onDone={onDone} onReject={onReject} verbose={verbose} workerBadge={workerBadge} setStickyFooter={setStickyFooter} />;
|
||||
$[9] = PermissionComponent;
|
||||
$[10] = onDone;
|
||||
$[11] = onReject;
|
||||
$[12] = setStickyFooter;
|
||||
$[13] = toolUseConfirm;
|
||||
$[14] = toolUseContext;
|
||||
$[15] = verbose;
|
||||
$[16] = workerBadge;
|
||||
$[17] = t5;
|
||||
} else {
|
||||
t5 = $[17];
|
||||
}
|
||||
return t5;
|
||||
setStickyFooter,
|
||||
}: PermissionRequestProps): React.ReactNode {
|
||||
// Handle Ctrl+C (app:interrupt) to reject
|
||||
useKeybinding(
|
||||
'app:interrupt',
|
||||
() => {
|
||||
onDone()
|
||||
onReject()
|
||||
toolUseConfirm.onReject()
|
||||
},
|
||||
{ context: 'Confirmation' },
|
||||
)
|
||||
|
||||
const notificationMessage = getNotificationMessage(toolUseConfirm)
|
||||
useNotifyAfterTimeout(notificationMessage, 'permission_prompt')
|
||||
|
||||
const PermissionComponent = permissionComponentForTool(toolUseConfirm.tool)
|
||||
|
||||
return (
|
||||
<PermissionComponent
|
||||
toolUseContext={toolUseContext}
|
||||
toolUseConfirm={toolUseConfirm}
|
||||
onDone={onDone}
|
||||
onReject={onReject}
|
||||
verbose={verbose}
|
||||
workerBadge={workerBadge}
|
||||
setStickyFooter={setStickyFooter}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,65 +1,41 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import type { Theme } from '../../utils/theme.js';
|
||||
import type { WorkerBadgeProps } from './WorkerBadge.js';
|
||||
import * as React from 'react'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import type { Theme } from '../../utils/theme.js'
|
||||
import type { WorkerBadgeProps } from './WorkerBadge.js'
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
subtitle?: React.ReactNode;
|
||||
color?: keyof Theme;
|
||||
workerBadge?: WorkerBadgeProps;
|
||||
};
|
||||
export function PermissionRequestTitle(t0) {
|
||||
const $ = _c(13);
|
||||
const {
|
||||
title: string
|
||||
subtitle?: React.ReactNode
|
||||
color?: keyof Theme
|
||||
workerBadge?: WorkerBadgeProps
|
||||
}
|
||||
|
||||
export function PermissionRequestTitle({
|
||||
title,
|
||||
subtitle,
|
||||
color: t1,
|
||||
workerBadge
|
||||
} = t0;
|
||||
const color = t1 === undefined ? "permission" : t1;
|
||||
let t2;
|
||||
if ($[0] !== color || $[1] !== title) {
|
||||
t2 = <Text bold={true} color={color}>{title}</Text>;
|
||||
$[0] = color;
|
||||
$[1] = title;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
let t3;
|
||||
if ($[3] !== workerBadge) {
|
||||
t3 = workerBadge && <Text dimColor={true}>{"\xB7 "}@{workerBadge.name}</Text>;
|
||||
$[3] = workerBadge;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
let t4;
|
||||
if ($[5] !== t2 || $[6] !== t3) {
|
||||
t4 = <Box flexDirection="row" gap={1}>{t2}{t3}</Box>;
|
||||
$[5] = t2;
|
||||
$[6] = t3;
|
||||
$[7] = t4;
|
||||
} else {
|
||||
t4 = $[7];
|
||||
}
|
||||
let t5;
|
||||
if ($[8] !== subtitle) {
|
||||
t5 = subtitle != null && (typeof subtitle === "string" ? <Text dimColor={true} wrap="truncate-start">{subtitle}</Text> : subtitle);
|
||||
$[8] = subtitle;
|
||||
$[9] = t5;
|
||||
} else {
|
||||
t5 = $[9];
|
||||
}
|
||||
let t6;
|
||||
if ($[10] !== t4 || $[11] !== t5) {
|
||||
t6 = <Box flexDirection="column">{t4}{t5}</Box>;
|
||||
$[10] = t4;
|
||||
$[11] = t5;
|
||||
$[12] = t6;
|
||||
} else {
|
||||
t6 = $[12];
|
||||
}
|
||||
return t6;
|
||||
color = 'permission',
|
||||
workerBadge,
|
||||
}: Props): React.ReactNode {
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Box flexDirection="row" gap={1}>
|
||||
<Text bold color={color}>
|
||||
{title}
|
||||
</Text>
|
||||
{workerBadge && (
|
||||
<Text dimColor>
|
||||
{'· '}@{workerBadge.name}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
{subtitle != null &&
|
||||
(typeof subtitle === 'string' ? (
|
||||
<Text dimColor wrap="truncate-start">
|
||||
{subtitle}
|
||||
</Text>
|
||||
) : (
|
||||
subtitle
|
||||
))}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,120 +1,118 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { feature } from 'bun:bundle';
|
||||
import chalk from 'chalk';
|
||||
import React from 'react';
|
||||
import { Ansi, Box, Text } from '../../ink.js';
|
||||
import { useAppState } from '../../state/AppState.js';
|
||||
import type { PermissionDecision, PermissionDecisionReason } from '../../utils/permissions/PermissionResult.js';
|
||||
import { permissionRuleValueToString } from '../../utils/permissions/permissionRuleParser.js';
|
||||
import type { Theme } from '../../utils/theme.js';
|
||||
import ThemedText from '../design-system/ThemedText.js';
|
||||
import { feature } from 'bun:bundle'
|
||||
import chalk from 'chalk'
|
||||
import React from 'react'
|
||||
import { Ansi, Box, Text } from '../../ink.js'
|
||||
import { useAppState } from '../../state/AppState.js'
|
||||
import type {
|
||||
PermissionDecision,
|
||||
PermissionDecisionReason,
|
||||
} from '../../utils/permissions/PermissionResult.js'
|
||||
import { permissionRuleValueToString } from '../../utils/permissions/permissionRuleParser.js'
|
||||
import type { Theme } from '../../utils/theme.js'
|
||||
import ThemedText from '../design-system/ThemedText.js'
|
||||
|
||||
export type PermissionRuleExplanationProps = {
|
||||
permissionResult: PermissionDecision;
|
||||
toolType: 'tool' | 'command' | 'edit' | 'read';
|
||||
};
|
||||
permissionResult: PermissionDecision
|
||||
toolType: 'tool' | 'command' | 'edit' | 'read'
|
||||
}
|
||||
|
||||
type DecisionReasonStrings = {
|
||||
reasonString: string;
|
||||
configString?: string;
|
||||
reasonString: string
|
||||
configString?: string
|
||||
/** When set, reasonString is plain text rendered with this theme color instead of <Ansi>. */
|
||||
themeColor?: keyof Theme;
|
||||
};
|
||||
function stringsForDecisionReason(reason: PermissionDecisionReason | undefined, toolType: 'tool' | 'command' | 'edit' | 'read'): DecisionReasonStrings | null {
|
||||
themeColor?: keyof Theme
|
||||
}
|
||||
|
||||
function stringsForDecisionReason(
|
||||
reason: PermissionDecisionReason | undefined,
|
||||
toolType: 'tool' | 'command' | 'edit' | 'read',
|
||||
): DecisionReasonStrings | null {
|
||||
if (!reason) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
if ((feature('BASH_CLASSIFIER') || feature('TRANSCRIPT_CLASSIFIER')) && reason.type === 'classifier') {
|
||||
if (
|
||||
(feature('BASH_CLASSIFIER') || feature('TRANSCRIPT_CLASSIFIER')) &&
|
||||
reason.type === 'classifier'
|
||||
) {
|
||||
if (reason.classifier === 'auto-mode') {
|
||||
return {
|
||||
reasonString: `Auto mode classifier requires confirmation for this ${toolType}.\n${reason.reason}`,
|
||||
configString: undefined,
|
||||
themeColor: 'error'
|
||||
};
|
||||
themeColor: 'error',
|
||||
}
|
||||
}
|
||||
return {
|
||||
reasonString: `Classifier ${chalk.bold(reason.classifier)} requires confirmation for this ${toolType}.\n${reason.reason}`,
|
||||
configString: undefined
|
||||
};
|
||||
configString: undefined,
|
||||
}
|
||||
}
|
||||
switch (reason.type) {
|
||||
case 'rule':
|
||||
return {
|
||||
reasonString: `Permission rule ${chalk.bold(permissionRuleValueToString(reason.rule.ruleValue))} requires confirmation for this ${toolType}.`,
|
||||
configString: reason.rule.source === 'policySettings' ? undefined : '/permissions to update rules'
|
||||
};
|
||||
case 'hook':
|
||||
{
|
||||
const hookReasonString = reason.reason ? `:\n${reason.reason}` : '.';
|
||||
const sourceLabel = reason.hookSource ? ` ${chalk.dim(`[${reason.hookSource}]`)}` : '';
|
||||
reasonString: `Permission rule ${chalk.bold(
|
||||
permissionRuleValueToString(reason.rule.ruleValue),
|
||||
)} requires confirmation for this ${toolType}.`,
|
||||
configString:
|
||||
reason.rule.source === 'policySettings'
|
||||
? undefined
|
||||
: '/permissions to update rules',
|
||||
}
|
||||
case 'hook': {
|
||||
const hookReasonString = reason.reason ? `:\n${reason.reason}` : '.'
|
||||
const sourceLabel = reason.hookSource
|
||||
? ` ${chalk.dim(`[${reason.hookSource}]`)}`
|
||||
: ''
|
||||
return {
|
||||
reasonString: `Hook ${chalk.bold(reason.hookName)} requires confirmation for this ${toolType}${hookReasonString}${sourceLabel}`,
|
||||
configString: '/hooks to update'
|
||||
};
|
||||
configString: '/hooks to update',
|
||||
}
|
||||
}
|
||||
case 'safetyCheck':
|
||||
case 'other':
|
||||
return {
|
||||
reasonString: reason.reason,
|
||||
configString: undefined
|
||||
};
|
||||
configString: undefined,
|
||||
}
|
||||
case 'workingDir':
|
||||
return {
|
||||
reasonString: reason.reason,
|
||||
configString: '/permissions to update rules'
|
||||
};
|
||||
configString: '/permissions to update rules',
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
}
|
||||
export function PermissionRuleExplanation(t0) {
|
||||
const $ = _c(11);
|
||||
const {
|
||||
|
||||
export function PermissionRuleExplanation({
|
||||
permissionResult,
|
||||
toolType
|
||||
} = t0;
|
||||
const permissionMode = useAppState(_temp);
|
||||
const t1 = permissionResult?.decisionReason;
|
||||
let t2;
|
||||
if ($[0] !== t1 || $[1] !== toolType) {
|
||||
t2 = stringsForDecisionReason(t1, toolType);
|
||||
$[0] = t1;
|
||||
$[1] = toolType;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
const strings = t2;
|
||||
toolType,
|
||||
}: PermissionRuleExplanationProps): React.ReactNode {
|
||||
const permissionMode = useAppState(s => s.toolPermissionContext.mode)
|
||||
const strings = stringsForDecisionReason(
|
||||
permissionResult?.decisionReason,
|
||||
toolType,
|
||||
)
|
||||
if (!strings) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
const themeColor = strings.themeColor ?? (permissionResult?.decisionReason?.type === "hook" && permissionMode === "auto" ? "warning" : undefined);
|
||||
let t3;
|
||||
if ($[3] !== strings.reasonString || $[4] !== themeColor) {
|
||||
t3 = themeColor ? <ThemedText color={themeColor}>{strings.reasonString}</ThemedText> : <Text><Ansi>{strings.reasonString}</Ansi></Text>;
|
||||
$[3] = strings.reasonString;
|
||||
$[4] = themeColor;
|
||||
$[5] = t3;
|
||||
} else {
|
||||
t3 = $[5];
|
||||
}
|
||||
let t4;
|
||||
if ($[6] !== strings.configString) {
|
||||
t4 = strings.configString && <Text dimColor={true}>{strings.configString}</Text>;
|
||||
$[6] = strings.configString;
|
||||
$[7] = t4;
|
||||
} else {
|
||||
t4 = $[7];
|
||||
}
|
||||
let t5;
|
||||
if ($[8] !== t3 || $[9] !== t4) {
|
||||
t5 = <Box marginBottom={1} flexDirection="column">{t3}{t4}</Box>;
|
||||
$[8] = t3;
|
||||
$[9] = t4;
|
||||
$[10] = t5;
|
||||
} else {
|
||||
t5 = $[10];
|
||||
}
|
||||
return t5;
|
||||
}
|
||||
function _temp(s) {
|
||||
return s.toolPermissionContext.mode;
|
||||
|
||||
const themeColor =
|
||||
strings.themeColor ??
|
||||
(permissionResult?.decisionReason?.type === 'hook' &&
|
||||
permissionMode === 'auto'
|
||||
? 'warning'
|
||||
: undefined)
|
||||
|
||||
return (
|
||||
<Box marginBottom={1} flexDirection="column">
|
||||
{themeColor ? (
|
||||
<ThemedText color={themeColor}>{strings.reasonString}</ThemedText>
|
||||
) : (
|
||||
<Text>
|
||||
<Ansi>{strings.reasonString}</Ansi>
|
||||
</Text>
|
||||
)}
|
||||
{strings.configString && <Text dimColor>{strings.configString}</Text>}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,43 +1,48 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Box, Text, useTheme } from '../../../ink.js';
|
||||
import { useKeybinding } from '../../../keybindings/useKeybinding.js';
|
||||
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../../services/analytics/growthbook.js';
|
||||
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../../services/analytics/index.js';
|
||||
import { sanitizeToolNameForAnalytics } from '../../../services/analytics/metadata.js';
|
||||
import { getDestructiveCommandWarning } from '../../../tools/PowerShellTool/destructiveCommandWarning.js';
|
||||
import { PowerShellTool } from '../../../tools/PowerShellTool/PowerShellTool.js';
|
||||
import { isAllowlistedCommand } from '../../../tools/PowerShellTool/readOnlyValidation.js';
|
||||
import type { PermissionUpdate } from '../../../utils/permissions/PermissionUpdateSchema.js';
|
||||
import { getCompoundCommandPrefixesStatic } from '../../../utils/powershell/staticPrefix.js';
|
||||
import { Select } from '../../CustomSelect/select.js';
|
||||
import { type UnaryEvent, usePermissionRequestLogging } from '../hooks.js';
|
||||
import { PermissionDecisionDebugInfo } from '../PermissionDecisionDebugInfo.js';
|
||||
import { PermissionDialog } from '../PermissionDialog.js';
|
||||
import { PermissionExplainerContent, usePermissionExplainerUI } from '../PermissionExplanation.js';
|
||||
import type { PermissionRequestProps } from '../PermissionRequest.js';
|
||||
import { PermissionRuleExplanation } from '../PermissionRuleExplanation.js';
|
||||
import { useShellPermissionFeedback } from '../useShellPermissionFeedback.js';
|
||||
import { logUnaryPermissionEvent } from '../utils.js';
|
||||
import { powershellToolUseOptions } from './powershellToolUseOptions.js';
|
||||
export function PowerShellPermissionRequest(props: PermissionRequestProps): React.ReactNode {
|
||||
const {
|
||||
toolUseConfirm,
|
||||
toolUseContext,
|
||||
onDone,
|
||||
onReject,
|
||||
workerBadge
|
||||
} = props;
|
||||
const {
|
||||
command,
|
||||
description
|
||||
} = PowerShellTool.inputSchema.parse(toolUseConfirm.input);
|
||||
const [theme] = useTheme();
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Box, Text, useTheme } from '../../../ink.js'
|
||||
import { useKeybinding } from '../../../keybindings/useKeybinding.js'
|
||||
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../../services/analytics/growthbook.js'
|
||||
import {
|
||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
logEvent,
|
||||
} from '../../../services/analytics/index.js'
|
||||
import { sanitizeToolNameForAnalytics } from '../../../services/analytics/metadata.js'
|
||||
import { getDestructiveCommandWarning } from '../../../tools/PowerShellTool/destructiveCommandWarning.js'
|
||||
import { PowerShellTool } from '../../../tools/PowerShellTool/PowerShellTool.js'
|
||||
import { isAllowlistedCommand } from '../../../tools/PowerShellTool/readOnlyValidation.js'
|
||||
import type { PermissionUpdate } from '../../../utils/permissions/PermissionUpdateSchema.js'
|
||||
import { getCompoundCommandPrefixesStatic } from '../../../utils/powershell/staticPrefix.js'
|
||||
import { Select } from '../../CustomSelect/select.js'
|
||||
import { type UnaryEvent, usePermissionRequestLogging } from '../hooks.js'
|
||||
import { PermissionDecisionDebugInfo } from '../PermissionDecisionDebugInfo.js'
|
||||
import { PermissionDialog } from '../PermissionDialog.js'
|
||||
import {
|
||||
PermissionExplainerContent,
|
||||
usePermissionExplainerUI,
|
||||
} from '../PermissionExplanation.js'
|
||||
import type { PermissionRequestProps } from '../PermissionRequest.js'
|
||||
import { PermissionRuleExplanation } from '../PermissionRuleExplanation.js'
|
||||
import { useShellPermissionFeedback } from '../useShellPermissionFeedback.js'
|
||||
import { logUnaryPermissionEvent } from '../utils.js'
|
||||
import { powershellToolUseOptions } from './powershellToolUseOptions.js'
|
||||
|
||||
export function PowerShellPermissionRequest(
|
||||
props: PermissionRequestProps,
|
||||
): React.ReactNode {
|
||||
const { toolUseConfirm, toolUseContext, onDone, onReject, workerBadge } =
|
||||
props
|
||||
|
||||
const { command, description } = PowerShellTool.inputSchema.parse(
|
||||
toolUseConfirm.input,
|
||||
)
|
||||
|
||||
const [theme] = useTheme()
|
||||
const explainerState = usePermissionExplainerUI({
|
||||
toolName: toolUseConfirm.tool.name,
|
||||
toolInput: toolUseConfirm.input,
|
||||
toolDescription: toolUseConfirm.description,
|
||||
messages: toolUseContext.messages
|
||||
});
|
||||
messages: toolUseContext.messages,
|
||||
})
|
||||
const {
|
||||
yesInputMode,
|
||||
noInputMode,
|
||||
@@ -50,15 +55,21 @@ export function PowerShellPermissionRequest(props: PermissionRequestProps): Reac
|
||||
focusedOption,
|
||||
handleInputModeToggle,
|
||||
handleReject,
|
||||
handleFocus
|
||||
handleFocus,
|
||||
} = useShellPermissionFeedback({
|
||||
toolUseConfirm,
|
||||
onDone,
|
||||
onReject,
|
||||
explainerVisible: explainerState.visible
|
||||
});
|
||||
const destructiveWarning = getFeatureValue_CACHED_MAY_BE_STALE('tengu_destructive_command_warning', false) ? getDestructiveCommandWarning(command) : null;
|
||||
const [showPermissionDebug, setShowPermissionDebug] = useState(false);
|
||||
explainerVisible: explainerState.visible,
|
||||
})
|
||||
const destructiveWarning = getFeatureValue_CACHED_MAY_BE_STALE(
|
||||
'tengu_destructive_command_warning',
|
||||
false,
|
||||
)
|
||||
? getDestructiveCommandWarning(command)
|
||||
: null
|
||||
|
||||
const [showPermissionDebug, setShowPermissionDebug] = useState(false)
|
||||
|
||||
// Editable prefix — compute static prefix locally (no LLM call).
|
||||
// Initialize synchronously to the raw command for single-line commands so
|
||||
@@ -69,113 +80,150 @@ export function PowerShellPermissionRequest(props: PermissionRequestProps): Reac
|
||||
// corpus shows 14 multiline rules, zero match twice). For compound commands,
|
||||
// computes a prefix per subcommand, excluding subcommands that are already
|
||||
// auto-allowed (read-only).
|
||||
const [editablePrefix, setEditablePrefix] = useState<string | undefined>(command.includes('\n') ? undefined : command);
|
||||
const hasUserEditedPrefix = useRef(false);
|
||||
const [editablePrefix, setEditablePrefix] = useState<string | undefined>(
|
||||
command.includes('\n') ? undefined : command,
|
||||
)
|
||||
const hasUserEditedPrefix = useRef(false)
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
let cancelled = false
|
||||
// Filter receives ParsedCommandElement — isAllowlistedCommand works from
|
||||
// element.name/nameType/args directly. isReadOnlyCommand(text) would need
|
||||
// to reparse (pwsh.exe spawn per subcommand) and returns false without the
|
||||
// full parsed AST, making the filter a no-op.
|
||||
getCompoundCommandPrefixesStatic(command, element => isAllowlistedCommand(element, element.text)).then(prefixes => {
|
||||
if (cancelled || hasUserEditedPrefix.current) return;
|
||||
getCompoundCommandPrefixesStatic(command, element =>
|
||||
isAllowlistedCommand(element, element.text),
|
||||
)
|
||||
.then(prefixes => {
|
||||
if (cancelled || hasUserEditedPrefix.current) return
|
||||
if (prefixes.length > 0) {
|
||||
setEditablePrefix(`${prefixes[0]}:*`);
|
||||
setEditablePrefix(`${prefixes[0]}:*`)
|
||||
}
|
||||
}).catch(() => {});
|
||||
})
|
||||
.catch(() => {})
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
cancelled = true
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [command]);
|
||||
}, [command])
|
||||
|
||||
const onEditablePrefixChange = useCallback((value: string) => {
|
||||
hasUserEditedPrefix.current = true;
|
||||
setEditablePrefix(value);
|
||||
}, []);
|
||||
const unaryEvent = useMemo<UnaryEvent>(() => ({
|
||||
completion_type: 'tool_use_single',
|
||||
language_name: 'none'
|
||||
}), []);
|
||||
usePermissionRequestLogging(toolUseConfirm, unaryEvent);
|
||||
const options = useMemo(() => powershellToolUseOptions({
|
||||
suggestions: toolUseConfirm.permissionResult.behavior === 'ask' ? toolUseConfirm.permissionResult.suggestions : undefined,
|
||||
hasUserEditedPrefix.current = true
|
||||
setEditablePrefix(value)
|
||||
}, [])
|
||||
|
||||
const unaryEvent = useMemo<UnaryEvent>(
|
||||
() => ({ completion_type: 'tool_use_single', language_name: 'none' }),
|
||||
[],
|
||||
)
|
||||
|
||||
usePermissionRequestLogging(toolUseConfirm, unaryEvent)
|
||||
|
||||
const options = useMemo(
|
||||
() =>
|
||||
powershellToolUseOptions({
|
||||
suggestions:
|
||||
toolUseConfirm.permissionResult.behavior === 'ask'
|
||||
? toolUseConfirm.permissionResult.suggestions
|
||||
: undefined,
|
||||
onRejectFeedbackChange: setRejectFeedback,
|
||||
onAcceptFeedbackChange: setAcceptFeedback,
|
||||
yesInputMode,
|
||||
noInputMode,
|
||||
editablePrefix,
|
||||
onEditablePrefixChange
|
||||
}), [toolUseConfirm, yesInputMode, noInputMode, editablePrefix, onEditablePrefixChange]);
|
||||
onEditablePrefixChange,
|
||||
}),
|
||||
[
|
||||
toolUseConfirm,
|
||||
yesInputMode,
|
||||
noInputMode,
|
||||
editablePrefix,
|
||||
onEditablePrefixChange,
|
||||
],
|
||||
)
|
||||
|
||||
// Toggle permission debug info with keybinding
|
||||
const handleToggleDebug = useCallback(() => {
|
||||
setShowPermissionDebug(prev => !prev);
|
||||
}, []);
|
||||
setShowPermissionDebug(prev => !prev)
|
||||
}, [])
|
||||
useKeybinding('permission:toggleDebug', handleToggleDebug, {
|
||||
context: 'Confirmation'
|
||||
});
|
||||
context: 'Confirmation',
|
||||
})
|
||||
|
||||
function onSelect(value: string) {
|
||||
// Map options to numeric values for analytics (strings not allowed in logEvent)
|
||||
const optionIndex: Record<string, number> = {
|
||||
yes: 1,
|
||||
'yes-apply-suggestions': 2,
|
||||
'yes-prefix-edited': 2,
|
||||
no: 3
|
||||
};
|
||||
no: 3,
|
||||
}
|
||||
logEvent('tengu_permission_request_option_selected', {
|
||||
option_index: optionIndex[value],
|
||||
explainer_visible: explainerState.visible
|
||||
});
|
||||
const toolNameForAnalytics = sanitizeToolNameForAnalytics(toolUseConfirm.tool.name) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS;
|
||||
explainer_visible: explainerState.visible,
|
||||
})
|
||||
|
||||
const toolNameForAnalytics = sanitizeToolNameForAnalytics(
|
||||
toolUseConfirm.tool.name,
|
||||
) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
|
||||
if (value === 'yes-prefix-edited') {
|
||||
const trimmedPrefix = (editablePrefix ?? '').trim();
|
||||
logUnaryPermissionEvent('tool_use_single', toolUseConfirm, 'accept');
|
||||
const trimmedPrefix = (editablePrefix ?? '').trim()
|
||||
logUnaryPermissionEvent('tool_use_single', toolUseConfirm, 'accept')
|
||||
if (!trimmedPrefix) {
|
||||
toolUseConfirm.onAllow(toolUseConfirm.input, []);
|
||||
toolUseConfirm.onAllow(toolUseConfirm.input, [])
|
||||
} else {
|
||||
const prefixUpdates: PermissionUpdate[] = [{
|
||||
type: 'addRules',
|
||||
rules: [{
|
||||
toolName: PowerShellTool.name,
|
||||
ruleContent: trimmedPrefix
|
||||
}],
|
||||
behavior: 'allow',
|
||||
destination: 'localSettings'
|
||||
}];
|
||||
toolUseConfirm.onAllow(toolUseConfirm.input, prefixUpdates);
|
||||
}
|
||||
onDone();
|
||||
return;
|
||||
}
|
||||
switch (value) {
|
||||
case 'yes':
|
||||
const prefixUpdates: PermissionUpdate[] = [
|
||||
{
|
||||
const trimmedFeedback = acceptFeedback.trim();
|
||||
logUnaryPermissionEvent('tool_use_single', toolUseConfirm, 'accept');
|
||||
type: 'addRules',
|
||||
rules: [
|
||||
{
|
||||
toolName: PowerShellTool.name,
|
||||
ruleContent: trimmedPrefix,
|
||||
},
|
||||
],
|
||||
behavior: 'allow',
|
||||
destination: 'localSettings',
|
||||
},
|
||||
]
|
||||
toolUseConfirm.onAllow(toolUseConfirm.input, prefixUpdates)
|
||||
}
|
||||
onDone()
|
||||
return
|
||||
}
|
||||
|
||||
switch (value) {
|
||||
case 'yes': {
|
||||
const trimmedFeedback = acceptFeedback.trim()
|
||||
logUnaryPermissionEvent('tool_use_single', toolUseConfirm, 'accept')
|
||||
// Log accept submission with feedback context
|
||||
logEvent('tengu_accept_submitted', {
|
||||
toolName: toolNameForAnalytics,
|
||||
isMcp: toolUseConfirm.tool.isMcp ?? false,
|
||||
has_instructions: !!trimmedFeedback,
|
||||
instructions_length: trimmedFeedback.length,
|
||||
entered_feedback_mode: yesFeedbackModeEntered
|
||||
});
|
||||
toolUseConfirm.onAllow(toolUseConfirm.input, [], trimmedFeedback || undefined);
|
||||
onDone();
|
||||
break;
|
||||
entered_feedback_mode: yesFeedbackModeEntered,
|
||||
})
|
||||
toolUseConfirm.onAllow(
|
||||
toolUseConfirm.input,
|
||||
[],
|
||||
trimmedFeedback || undefined,
|
||||
)
|
||||
onDone()
|
||||
break
|
||||
}
|
||||
case 'yes-apply-suggestions':
|
||||
{
|
||||
logUnaryPermissionEvent('tool_use_single', toolUseConfirm, 'accept');
|
||||
case 'yes-apply-suggestions': {
|
||||
logUnaryPermissionEvent('tool_use_single', toolUseConfirm, 'accept')
|
||||
// Extract suggestions if present (works for both 'ask' and 'passthrough' behaviors)
|
||||
const permissionUpdates = 'suggestions' in toolUseConfirm.permissionResult ? toolUseConfirm.permissionResult.suggestions || [] : [];
|
||||
toolUseConfirm.onAllow(toolUseConfirm.input, permissionUpdates);
|
||||
onDone();
|
||||
break;
|
||||
const permissionUpdates =
|
||||
'suggestions' in toolUseConfirm.permissionResult
|
||||
? toolUseConfirm.permissionResult.suggestions || []
|
||||
: []
|
||||
toolUseConfirm.onAllow(toolUseConfirm.input, permissionUpdates)
|
||||
onDone()
|
||||
break
|
||||
}
|
||||
case 'no':
|
||||
{
|
||||
const trimmedFeedback = rejectFeedback.trim();
|
||||
case 'no': {
|
||||
const trimmedFeedback = rejectFeedback.trim()
|
||||
|
||||
// Log reject submission with feedback context
|
||||
logEvent('tengu_reject_submitted', {
|
||||
@@ -183,52 +231,82 @@ export function PowerShellPermissionRequest(props: PermissionRequestProps): Reac
|
||||
isMcp: toolUseConfirm.tool.isMcp ?? false,
|
||||
has_instructions: !!trimmedFeedback,
|
||||
instructions_length: trimmedFeedback.length,
|
||||
entered_feedback_mode: noFeedbackModeEntered
|
||||
});
|
||||
entered_feedback_mode: noFeedbackModeEntered,
|
||||
})
|
||||
|
||||
// Process rejection (with or without feedback)
|
||||
handleReject(trimmedFeedback || undefined);
|
||||
break;
|
||||
handleReject(trimmedFeedback || undefined)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return <PermissionDialog workerBadge={workerBadge} title="PowerShell command">
|
||||
|
||||
return (
|
||||
<PermissionDialog workerBadge={workerBadge} title="PowerShell command">
|
||||
<Box flexDirection="column" paddingX={2} paddingY={1}>
|
||||
<Text dimColor={explainerState.visible}>
|
||||
{PowerShellTool.renderToolUseMessage({
|
||||
command,
|
||||
description
|
||||
}, {
|
||||
theme,
|
||||
verbose: true
|
||||
} // always show the full command
|
||||
{PowerShellTool.renderToolUseMessage(
|
||||
{ command, description },
|
||||
{ theme, verbose: true }, // always show the full command
|
||||
)}
|
||||
</Text>
|
||||
{!explainerState.visible && <Text dimColor>{toolUseConfirm.description}</Text>}
|
||||
<PermissionExplainerContent visible={explainerState.visible} promise={explainerState.promise} />
|
||||
{!explainerState.visible && (
|
||||
<Text dimColor>{toolUseConfirm.description}</Text>
|
||||
)}
|
||||
<PermissionExplainerContent
|
||||
visible={explainerState.visible}
|
||||
promise={explainerState.promise}
|
||||
/>
|
||||
</Box>
|
||||
{showPermissionDebug ? <>
|
||||
<PermissionDecisionDebugInfo permissionResult={toolUseConfirm.permissionResult} toolName="PowerShell" />
|
||||
{toolUseContext.options.debug && <Box justifyContent="flex-end" marginTop={1}>
|
||||
{showPermissionDebug ? (
|
||||
<>
|
||||
<PermissionDecisionDebugInfo
|
||||
permissionResult={toolUseConfirm.permissionResult}
|
||||
toolName="PowerShell"
|
||||
/>
|
||||
{toolUseContext.options.debug && (
|
||||
<Box justifyContent="flex-end" marginTop={1}>
|
||||
<Text dimColor>Ctrl-D to hide debug info</Text>
|
||||
</Box>}
|
||||
</> : <>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Box flexDirection="column">
|
||||
<PermissionRuleExplanation permissionResult={toolUseConfirm.permissionResult} toolType="command" />
|
||||
{destructiveWarning && <Box marginBottom={1}>
|
||||
<PermissionRuleExplanation
|
||||
permissionResult={toolUseConfirm.permissionResult}
|
||||
toolType="command"
|
||||
/>
|
||||
{destructiveWarning && (
|
||||
<Box marginBottom={1}>
|
||||
<Text color="warning">{destructiveWarning}</Text>
|
||||
</Box>}
|
||||
</Box>
|
||||
)}
|
||||
<Text>Do you want to proceed?</Text>
|
||||
<Select options={options} inlineDescriptions onChange={onSelect} onCancel={() => handleReject()} onFocus={handleFocus} onInputModeToggle={handleInputModeToggle} />
|
||||
<Select
|
||||
options={options}
|
||||
inlineDescriptions
|
||||
onChange={onSelect}
|
||||
onCancel={() => handleReject()}
|
||||
onFocus={handleFocus}
|
||||
onInputModeToggle={handleInputModeToggle}
|
||||
/>
|
||||
</Box>
|
||||
<Box justifyContent="space-between" marginTop={1}>
|
||||
<Text dimColor>
|
||||
Esc to cancel
|
||||
{(focusedOption === 'yes' && !yesInputMode || focusedOption === 'no' && !noInputMode) && ' · Tab to amend'}
|
||||
{explainerState.enabled && ` · ctrl+e to ${explainerState.visible ? 'hide' : 'explain'}`}
|
||||
{((focusedOption === 'yes' && !yesInputMode) ||
|
||||
(focusedOption === 'no' && !noInputMode)) &&
|
||||
' · Tab to amend'}
|
||||
{explainerState.enabled &&
|
||||
` · ctrl+e to ${explainerState.visible ? 'hide' : 'explain'}`}
|
||||
</Text>
|
||||
{toolUseContext.options.debug && <Text dimColor>Ctrl+d to show debug info</Text>}
|
||||
{toolUseContext.options.debug && (
|
||||
<Text dimColor>Ctrl+d to show debug info</Text>
|
||||
)}
|
||||
</Box>
|
||||
</>}
|
||||
</PermissionDialog>;
|
||||
</>
|
||||
)}
|
||||
</PermissionDialog>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import { POWERSHELL_TOOL_NAME } from '../../../tools/PowerShellTool/toolName.js';
|
||||
import type { PermissionUpdate } from '../../../utils/permissions/PermissionUpdateSchema.js';
|
||||
import { shouldShowAlwaysAllowOptions } from '../../../utils/permissions/permissionsLoader.js';
|
||||
import type { OptionWithDescription } from '../../CustomSelect/select.js';
|
||||
import { generateShellSuggestionsLabel } from '../shellPermissionHelpers.js';
|
||||
export type PowerShellToolUseOption = 'yes' | 'yes-apply-suggestions' | 'yes-prefix-edited' | 'no';
|
||||
import { POWERSHELL_TOOL_NAME } from '../../../tools/PowerShellTool/toolName.js'
|
||||
import type { PermissionUpdate } from '../../../utils/permissions/PermissionUpdateSchema.js'
|
||||
import { shouldShowAlwaysAllowOptions } from '../../../utils/permissions/permissionsLoader.js'
|
||||
import type { OptionWithDescription } from '../../CustomSelect/select.js'
|
||||
import { generateShellSuggestionsLabel } from '../shellPermissionHelpers.js'
|
||||
|
||||
export type PowerShellToolUseOption =
|
||||
| 'yes'
|
||||
| 'yes-apply-suggestions'
|
||||
| 'yes-prefix-edited'
|
||||
| 'no'
|
||||
|
||||
export function powershellToolUseOptions({
|
||||
suggestions = [],
|
||||
onRejectFeedbackChange,
|
||||
@@ -11,17 +17,18 @@ export function powershellToolUseOptions({
|
||||
yesInputMode = false,
|
||||
noInputMode = false,
|
||||
editablePrefix,
|
||||
onEditablePrefixChange
|
||||
onEditablePrefixChange,
|
||||
}: {
|
||||
suggestions?: PermissionUpdate[];
|
||||
onRejectFeedbackChange: (value: string) => void;
|
||||
onAcceptFeedbackChange: (value: string) => void;
|
||||
yesInputMode?: boolean;
|
||||
noInputMode?: boolean;
|
||||
editablePrefix?: string;
|
||||
onEditablePrefixChange?: (value: string) => void;
|
||||
suggestions?: PermissionUpdate[]
|
||||
onRejectFeedbackChange: (value: string) => void
|
||||
onAcceptFeedbackChange: (value: string) => void
|
||||
yesInputMode?: boolean
|
||||
noInputMode?: boolean
|
||||
editablePrefix?: string
|
||||
onEditablePrefixChange?: (value: string) => void
|
||||
}): OptionWithDescription<PowerShellToolUseOption>[] {
|
||||
const options: OptionWithDescription<PowerShellToolUseOption>[] = [];
|
||||
const options: OptionWithDescription<PowerShellToolUseOption>[] = []
|
||||
|
||||
if (yesInputMode) {
|
||||
options.push({
|
||||
type: 'input',
|
||||
@@ -29,13 +36,13 @@ export function powershellToolUseOptions({
|
||||
value: 'yes',
|
||||
placeholder: 'and tell Claude what to do next',
|
||||
onChange: onAcceptFeedbackChange,
|
||||
allowEmptySubmitToCancel: true
|
||||
});
|
||||
allowEmptySubmitToCancel: true,
|
||||
})
|
||||
} else {
|
||||
options.push({
|
||||
label: 'Yes',
|
||||
value: 'yes'
|
||||
});
|
||||
value: 'yes',
|
||||
})
|
||||
}
|
||||
|
||||
// Note: No sandbox toggle for PowerShell - sandbox is not supported on Windows
|
||||
@@ -47,8 +54,17 @@ export function powershellToolUseOptions({
|
||||
// directory permissions or Read-tool rules, so fall back to the label when
|
||||
// those are present.
|
||||
if (shouldShowAlwaysAllowOptions() && suggestions.length > 0) {
|
||||
const hasNonPowerShellSuggestions = suggestions.some(s => s.type === 'addDirectories' || s.type === 'addRules' && s.rules?.some(r => r.toolName !== POWERSHELL_TOOL_NAME));
|
||||
if (editablePrefix !== undefined && onEditablePrefixChange && !hasNonPowerShellSuggestions) {
|
||||
const hasNonPowerShellSuggestions = suggestions.some(
|
||||
s =>
|
||||
s.type === 'addDirectories' ||
|
||||
(s.type === 'addRules' &&
|
||||
s.rules?.some(r => r.toolName !== POWERSHELL_TOOL_NAME)),
|
||||
)
|
||||
if (
|
||||
editablePrefix !== undefined &&
|
||||
onEditablePrefixChange &&
|
||||
!hasNonPowerShellSuggestions
|
||||
) {
|
||||
options.push({
|
||||
type: 'input',
|
||||
label: 'Yes, and don\u2019t ask again for',
|
||||
@@ -59,18 +75,22 @@ export function powershellToolUseOptions({
|
||||
allowEmptySubmitToCancel: true,
|
||||
showLabelWithValue: true,
|
||||
labelValueSeparator: ': ',
|
||||
resetCursorOnUpdate: true
|
||||
});
|
||||
resetCursorOnUpdate: true,
|
||||
})
|
||||
} else {
|
||||
const label = generateShellSuggestionsLabel(suggestions, POWERSHELL_TOOL_NAME);
|
||||
const label = generateShellSuggestionsLabel(
|
||||
suggestions,
|
||||
POWERSHELL_TOOL_NAME,
|
||||
)
|
||||
if (label) {
|
||||
options.push({
|
||||
label,
|
||||
value: 'yes-apply-suggestions'
|
||||
});
|
||||
value: 'yes-apply-suggestions',
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (noInputMode) {
|
||||
options.push({
|
||||
type: 'input',
|
||||
@@ -78,13 +98,14 @@ export function powershellToolUseOptions({
|
||||
value: 'no',
|
||||
placeholder: 'and tell Claude what to do differently',
|
||||
onChange: onRejectFeedbackChange,
|
||||
allowEmptySubmitToCancel: true
|
||||
});
|
||||
allowEmptySubmitToCancel: true,
|
||||
})
|
||||
} else {
|
||||
options.push({
|
||||
label: 'No',
|
||||
value: 'no'
|
||||
});
|
||||
value: 'no',
|
||||
})
|
||||
}
|
||||
return options;
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
@@ -1,162 +1,106 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { Box, Text } from 'src/ink.js';
|
||||
import { type NetworkHostPattern, shouldAllowManagedSandboxDomainsOnly } from 'src/utils/sandbox/sandbox-adapter.js';
|
||||
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics/index.js';
|
||||
import { Select } from '../CustomSelect/select.js';
|
||||
import { PermissionDialog } from './PermissionDialog.js';
|
||||
import * as React from 'react'
|
||||
import { Box, Text } from 'src/ink.js'
|
||||
import {
|
||||
type NetworkHostPattern,
|
||||
shouldAllowManagedSandboxDomainsOnly,
|
||||
} from 'src/utils/sandbox/sandbox-adapter.js'
|
||||
import {
|
||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
logEvent,
|
||||
} from '../../services/analytics/index.js'
|
||||
import { Select } from '../CustomSelect/select.js'
|
||||
import { PermissionDialog } from './PermissionDialog.js'
|
||||
|
||||
export type SandboxPermissionRequestProps = {
|
||||
hostPattern: NetworkHostPattern;
|
||||
hostPattern: NetworkHostPattern
|
||||
onUserResponse: (response: {
|
||||
allow: boolean;
|
||||
persistToSettings: boolean;
|
||||
}) => void;
|
||||
};
|
||||
export function SandboxPermissionRequest(t0) {
|
||||
const $ = _c(22);
|
||||
const {
|
||||
hostPattern: t1,
|
||||
onUserResponse
|
||||
} = t0;
|
||||
const {
|
||||
host
|
||||
} = t1;
|
||||
let t2;
|
||||
if ($[0] !== onUserResponse) {
|
||||
t2 = function onSelect(value) {
|
||||
bb4: switch (value) {
|
||||
case "yes":
|
||||
{
|
||||
onUserResponse({
|
||||
allow: true,
|
||||
persistToSettings: false
|
||||
});
|
||||
break bb4;
|
||||
}
|
||||
case "yes-dont-ask-again":
|
||||
{
|
||||
onUserResponse({
|
||||
allow: true,
|
||||
persistToSettings: true
|
||||
});
|
||||
break bb4;
|
||||
}
|
||||
case "no":
|
||||
{
|
||||
onUserResponse({
|
||||
allow: false,
|
||||
persistToSettings: false
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
$[0] = onUserResponse;
|
||||
$[1] = t2;
|
||||
} else {
|
||||
t2 = $[1];
|
||||
}
|
||||
const onSelect = t2;
|
||||
let t3;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = shouldAllowManagedSandboxDomainsOnly();
|
||||
$[2] = t3;
|
||||
} else {
|
||||
t3 = $[2];
|
||||
}
|
||||
const managedDomainsOnly = t3;
|
||||
let t4;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = {
|
||||
label: "Yes",
|
||||
value: "yes"
|
||||
};
|
||||
$[3] = t4;
|
||||
} else {
|
||||
t4 = $[3];
|
||||
}
|
||||
let t5;
|
||||
if ($[4] !== host) {
|
||||
t5 = !managedDomainsOnly ? [{
|
||||
label: <Text>Yes, and don't ask again for <Text bold={true}>{host}</Text></Text>,
|
||||
value: "yes-dont-ask-again"
|
||||
}] : [];
|
||||
$[4] = host;
|
||||
$[5] = t5;
|
||||
} else {
|
||||
t5 = $[5];
|
||||
}
|
||||
let t6;
|
||||
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t6 = {
|
||||
label: <Text>No, and tell Claude what to do differently <Text bold={true}>(esc)</Text></Text>,
|
||||
value: "no"
|
||||
};
|
||||
$[6] = t6;
|
||||
} else {
|
||||
t6 = $[6];
|
||||
}
|
||||
let t7;
|
||||
if ($[7] !== t5) {
|
||||
t7 = [t4, ...t5, t6];
|
||||
$[7] = t5;
|
||||
$[8] = t7;
|
||||
} else {
|
||||
t7 = $[8];
|
||||
}
|
||||
const options = t7;
|
||||
let t8;
|
||||
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t8 = <Text dimColor={true}>Host:</Text>;
|
||||
$[9] = t8;
|
||||
} else {
|
||||
t8 = $[9];
|
||||
}
|
||||
let t9;
|
||||
if ($[10] !== host) {
|
||||
t9 = <Box>{t8}<Text> {host}</Text></Box>;
|
||||
$[10] = host;
|
||||
$[11] = t9;
|
||||
} else {
|
||||
t9 = $[11];
|
||||
}
|
||||
let t10;
|
||||
if ($[12] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t10 = <Box marginTop={1}><Text>Do you want to allow this connection?</Text></Box>;
|
||||
$[12] = t10;
|
||||
} else {
|
||||
t10 = $[12];
|
||||
}
|
||||
let t11;
|
||||
if ($[13] !== onUserResponse) {
|
||||
t11 = () => {
|
||||
onUserResponse({
|
||||
allow: false,
|
||||
persistToSettings: false
|
||||
});
|
||||
};
|
||||
$[13] = onUserResponse;
|
||||
$[14] = t11;
|
||||
} else {
|
||||
t11 = $[14];
|
||||
}
|
||||
let t12;
|
||||
if ($[15] !== onSelect || $[16] !== options || $[17] !== t11) {
|
||||
t12 = <Box><Select options={options} onChange={onSelect} onCancel={t11} /></Box>;
|
||||
$[15] = onSelect;
|
||||
$[16] = options;
|
||||
$[17] = t11;
|
||||
$[18] = t12;
|
||||
} else {
|
||||
t12 = $[18];
|
||||
}
|
||||
let t13;
|
||||
if ($[19] !== t12 || $[20] !== t9) {
|
||||
t13 = <PermissionDialog title="Network request outside of sandbox"><Box flexDirection="column" paddingX={2} paddingY={1}>{t9}{t10}{t12}</Box></PermissionDialog>;
|
||||
$[19] = t12;
|
||||
$[20] = t9;
|
||||
$[21] = t13;
|
||||
} else {
|
||||
t13 = $[21];
|
||||
}
|
||||
return t13;
|
||||
allow: boolean
|
||||
persistToSettings: boolean
|
||||
}) => void
|
||||
}
|
||||
|
||||
export function SandboxPermissionRequest({
|
||||
hostPattern: { host },
|
||||
onUserResponse,
|
||||
}: SandboxPermissionRequestProps): React.ReactNode {
|
||||
function onSelect(value: string) {
|
||||
// We may want to better unify this dialog with other permission dialogs
|
||||
// and use their logging, but this is slightly different and we don't have
|
||||
// the tool context here. For now, just use basic logging for basic data.
|
||||
if (process.env.USER_TYPE === 'ant') {
|
||||
logEvent('tengu_sandbox_network_dialog_result', {
|
||||
host: host as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
result:
|
||||
value as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
}
|
||||
|
||||
switch (value) {
|
||||
case 'yes':
|
||||
onUserResponse({ allow: true, persistToSettings: false })
|
||||
break
|
||||
case 'yes-dont-ask-again':
|
||||
onUserResponse({ allow: true, persistToSettings: true })
|
||||
break
|
||||
case 'no':
|
||||
onUserResponse({ allow: false, persistToSettings: false })
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const managedDomainsOnly = shouldAllowManagedSandboxDomainsOnly()
|
||||
|
||||
const options = [
|
||||
{ label: 'Yes', value: 'yes' },
|
||||
...(!managedDomainsOnly
|
||||
? [
|
||||
{
|
||||
label: (
|
||||
<Text>
|
||||
Yes, and don't ask again for <Text bold>{host}</Text>
|
||||
</Text>
|
||||
),
|
||||
value: 'yes-dont-ask-again',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: (
|
||||
<Text>
|
||||
No, and tell Claude what to do differently <Text bold>(esc)</Text>
|
||||
</Text>
|
||||
),
|
||||
value: 'no',
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<PermissionDialog title="Network request outside of sandbox">
|
||||
<Box flexDirection="column" paddingX={2} paddingY={1}>
|
||||
<Box>
|
||||
<Text dimColor>Host:</Text>
|
||||
<Text> {host}</Text>
|
||||
</Box>
|
||||
<Box marginTop={1}>
|
||||
<Text>Do you want to allow this connection?</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Select
|
||||
options={options}
|
||||
onChange={onSelect}
|
||||
onCancel={() => {
|
||||
if (process.env.USER_TYPE === 'ant') {
|
||||
logEvent('tengu_sandbox_network_dialog_result', {
|
||||
host: host as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
result:
|
||||
'cancel' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
}
|
||||
onUserResponse({ allow: false, persistToSettings: false })
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</PermissionDialog>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,229 +1,139 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { basename, relative } from 'path';
|
||||
import React, { Suspense, use, useMemo } from 'react';
|
||||
import { FileEditToolDiff } from 'src/components/FileEditToolDiff.js';
|
||||
import { getCwd } from 'src/utils/cwd.js';
|
||||
import { isENOENT } from 'src/utils/errors.js';
|
||||
import { detectEncodingForResolvedPath } from 'src/utils/fileRead.js';
|
||||
import { getFsImplementation } from 'src/utils/fsOperations.js';
|
||||
import { Text } from '../../../ink.js';
|
||||
import { BashTool } from '../../../tools/BashTool/BashTool.js';
|
||||
import { applySedSubstitution, type SedEditInfo } from '../../../tools/BashTool/sedEditParser.js';
|
||||
import { FilePermissionDialog } from '../FilePermissionDialog/FilePermissionDialog.js';
|
||||
import type { PermissionRequestProps } from '../PermissionRequest.js';
|
||||
import { basename, relative } from 'path'
|
||||
import React, { Suspense, use, useMemo } from 'react'
|
||||
import { FileEditToolDiff } from 'src/components/FileEditToolDiff.js'
|
||||
import { getCwd } from 'src/utils/cwd.js'
|
||||
import { isENOENT } from 'src/utils/errors.js'
|
||||
import { detectEncodingForResolvedPath } from 'src/utils/fileRead.js'
|
||||
import { getFsImplementation } from 'src/utils/fsOperations.js'
|
||||
import { Text } from '../../../ink.js'
|
||||
import { BashTool } from '../../../tools/BashTool/BashTool.js'
|
||||
import {
|
||||
applySedSubstitution,
|
||||
type SedEditInfo,
|
||||
} from '../../../tools/BashTool/sedEditParser.js'
|
||||
import { FilePermissionDialog } from '../FilePermissionDialog/FilePermissionDialog.js'
|
||||
import type { PermissionRequestProps } from '../PermissionRequest.js'
|
||||
|
||||
type SedEditPermissionRequestProps = PermissionRequestProps & {
|
||||
sedInfo: SedEditInfo;
|
||||
};
|
||||
type FileReadResult = {
|
||||
oldContent: string;
|
||||
fileExists: boolean;
|
||||
};
|
||||
export function SedEditPermissionRequest(t0) {
|
||||
const $ = _c(9);
|
||||
let props;
|
||||
let sedInfo;
|
||||
if ($[0] !== t0) {
|
||||
({
|
||||
sedInfo: SedEditInfo
|
||||
}
|
||||
|
||||
type FileReadResult = { oldContent: string; fileExists: boolean }
|
||||
|
||||
export function SedEditPermissionRequest({
|
||||
sedInfo,
|
||||
...props
|
||||
} = t0);
|
||||
$[0] = t0;
|
||||
$[1] = props;
|
||||
$[2] = sedInfo;
|
||||
} else {
|
||||
props = $[1];
|
||||
sedInfo = $[2];
|
||||
}
|
||||
const {
|
||||
filePath
|
||||
} = sedInfo;
|
||||
let t1;
|
||||
if ($[3] !== filePath) {
|
||||
t1 = (async () => {
|
||||
const encoding = detectEncodingForResolvedPath(filePath);
|
||||
const raw = await getFsImplementation().readFile(filePath, {
|
||||
encoding
|
||||
});
|
||||
}: SedEditPermissionRequestProps): React.ReactNode {
|
||||
const { filePath } = sedInfo
|
||||
|
||||
// Read file content async so mount doesn't block React commit on disk I/O.
|
||||
// Large files would otherwise hang the dialog before it renders.
|
||||
// Memoized on filePath so we don't re-read on every render.
|
||||
const contentPromise = useMemo(
|
||||
() =>
|
||||
(async (): Promise<FileReadResult> => {
|
||||
// Detect encoding first (sync 4KB read — negligible) so UTF-16LE BOMs
|
||||
// render correctly. This matches what readFileSync did before the
|
||||
// async conversion.
|
||||
const encoding = detectEncodingForResolvedPath(filePath)
|
||||
const raw = await getFsImplementation().readFile(filePath, { encoding })
|
||||
return {
|
||||
oldContent: raw.replaceAll("\r\n", "\n"),
|
||||
fileExists: true
|
||||
};
|
||||
})().catch(_temp);
|
||||
$[3] = filePath;
|
||||
$[4] = t1;
|
||||
} else {
|
||||
t1 = $[4];
|
||||
oldContent: raw.replaceAll('\r\n', '\n'),
|
||||
fileExists: true,
|
||||
}
|
||||
const contentPromise = t1;
|
||||
let t2;
|
||||
if ($[5] !== contentPromise || $[6] !== props || $[7] !== sedInfo) {
|
||||
t2 = <Suspense fallback={null}><SedEditPermissionRequestInner sedInfo={sedInfo} contentPromise={contentPromise} {...props} /></Suspense>;
|
||||
$[5] = contentPromise;
|
||||
$[6] = props;
|
||||
$[7] = sedInfo;
|
||||
$[8] = t2;
|
||||
} else {
|
||||
t2 = $[8];
|
||||
}
|
||||
return t2;
|
||||
})().catch((e: unknown): FileReadResult => {
|
||||
if (!isENOENT(e)) throw e
|
||||
return { oldContent: '', fileExists: false }
|
||||
}),
|
||||
[filePath],
|
||||
)
|
||||
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<SedEditPermissionRequestInner
|
||||
sedInfo={sedInfo}
|
||||
contentPromise={contentPromise}
|
||||
{...props}
|
||||
/>
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
function _temp(e) {
|
||||
if (!isENOENT(e)) {
|
||||
throw e;
|
||||
}
|
||||
return {
|
||||
oldContent: "",
|
||||
fileExists: false
|
||||
};
|
||||
}
|
||||
function SedEditPermissionRequestInner(t0) {
|
||||
const $ = _c(35);
|
||||
let contentPromise;
|
||||
let props;
|
||||
let sedInfo;
|
||||
if ($[0] !== t0) {
|
||||
({
|
||||
|
||||
function SedEditPermissionRequestInner({
|
||||
sedInfo,
|
||||
contentPromise,
|
||||
...props
|
||||
} = t0);
|
||||
$[0] = t0;
|
||||
$[1] = contentPromise;
|
||||
$[2] = props;
|
||||
$[3] = sedInfo;
|
||||
} else {
|
||||
contentPromise = $[1];
|
||||
props = $[2];
|
||||
sedInfo = $[3];
|
||||
}
|
||||
const {
|
||||
filePath
|
||||
} = sedInfo;
|
||||
const {
|
||||
oldContent,
|
||||
fileExists
|
||||
} = use(contentPromise) as any;
|
||||
let t1;
|
||||
if ($[4] !== oldContent || $[5] !== sedInfo) {
|
||||
t1 = applySedSubstitution(oldContent, sedInfo);
|
||||
$[4] = oldContent;
|
||||
$[5] = sedInfo;
|
||||
$[6] = t1;
|
||||
} else {
|
||||
t1 = $[6];
|
||||
}
|
||||
const newContent = t1;
|
||||
let t2;
|
||||
bb0: {
|
||||
}: SedEditPermissionRequestProps & {
|
||||
contentPromise: Promise<FileReadResult>
|
||||
}): React.ReactNode {
|
||||
const { filePath } = sedInfo
|
||||
const { oldContent, fileExists } = use(contentPromise)
|
||||
|
||||
// Compute the new content by applying the sed substitution
|
||||
const newContent = useMemo(() => {
|
||||
return applySedSubstitution(oldContent, sedInfo)
|
||||
}, [oldContent, sedInfo])
|
||||
|
||||
// Create the edit representation for the diff
|
||||
const edits = useMemo(() => {
|
||||
if (oldContent === newContent) {
|
||||
let t3;
|
||||
if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = [];
|
||||
$[7] = t3;
|
||||
} else {
|
||||
t3 = $[7];
|
||||
return []
|
||||
}
|
||||
t2 = t3;
|
||||
break bb0;
|
||||
}
|
||||
let t3;
|
||||
if ($[8] !== newContent || $[9] !== oldContent) {
|
||||
t3 = [{
|
||||
return [
|
||||
{
|
||||
old_string: oldContent,
|
||||
new_string: newContent,
|
||||
replace_all: false
|
||||
}];
|
||||
$[8] = newContent;
|
||||
$[9] = oldContent;
|
||||
$[10] = t3;
|
||||
} else {
|
||||
t3 = $[10];
|
||||
}
|
||||
t2 = t3;
|
||||
}
|
||||
const edits = t2;
|
||||
let t3;
|
||||
bb1: {
|
||||
replace_all: false,
|
||||
},
|
||||
]
|
||||
}, [oldContent, newContent])
|
||||
|
||||
// Determine appropriate message when no changes
|
||||
const noChangesMessage = useMemo(() => {
|
||||
if (!fileExists) {
|
||||
t3 = "File does not exist";
|
||||
break bb1;
|
||||
return 'File does not exist'
|
||||
}
|
||||
t3 = "Pattern did not match any content";
|
||||
}
|
||||
const noChangesMessage = t3;
|
||||
let t4;
|
||||
if ($[11] !== filePath || $[12] !== newContent) {
|
||||
t4 = input => {
|
||||
const parsed = BashTool.inputSchema.parse(input);
|
||||
return 'Pattern did not match any content'
|
||||
}, [fileExists])
|
||||
|
||||
// Parse input and add _simulatedSedEdit to ensure what user previewed
|
||||
// is exactly what gets written (prevents sed/JS regex differences)
|
||||
const parseInput = (input: unknown) => {
|
||||
const parsed = BashTool.inputSchema.parse(input)
|
||||
return {
|
||||
...parsed,
|
||||
_simulatedSedEdit: {
|
||||
filePath,
|
||||
newContent
|
||||
newContent,
|
||||
},
|
||||
}
|
||||
};
|
||||
};
|
||||
$[11] = filePath;
|
||||
$[12] = newContent;
|
||||
$[13] = t4;
|
||||
} else {
|
||||
t4 = $[13];
|
||||
}
|
||||
const parseInput = t4;
|
||||
const t5 = props.toolUseConfirm;
|
||||
const t6 = props.toolUseContext;
|
||||
const t7 = props.onDone;
|
||||
const t8 = props.onReject;
|
||||
let t9;
|
||||
if ($[14] !== filePath) {
|
||||
t9 = relative(getCwd(), filePath);
|
||||
$[14] = filePath;
|
||||
$[15] = t9;
|
||||
} else {
|
||||
t9 = $[15];
|
||||
|
||||
return (
|
||||
<FilePermissionDialog
|
||||
toolUseConfirm={props.toolUseConfirm}
|
||||
toolUseContext={props.toolUseContext}
|
||||
onDone={props.onDone}
|
||||
onReject={props.onReject}
|
||||
title="Edit file"
|
||||
subtitle={relative(getCwd(), filePath)}
|
||||
question={
|
||||
<Text>
|
||||
Do you want to make this edit to{' '}
|
||||
<Text bold>{basename(filePath)}</Text>?
|
||||
</Text>
|
||||
}
|
||||
let t10;
|
||||
if ($[16] !== filePath) {
|
||||
t10 = basename(filePath);
|
||||
$[16] = filePath;
|
||||
$[17] = t10;
|
||||
} else {
|
||||
t10 = $[17];
|
||||
content={
|
||||
edits.length > 0 ? (
|
||||
<FileEditToolDiff file_path={filePath} edits={edits} />
|
||||
) : (
|
||||
<Text dimColor>{noChangesMessage}</Text>
|
||||
)
|
||||
}
|
||||
let t11;
|
||||
if ($[18] !== t10) {
|
||||
t11 = <Text>Do you want to make this edit to{" "}<Text bold={true}>{t10}</Text>?</Text>;
|
||||
$[18] = t10;
|
||||
$[19] = t11;
|
||||
} else {
|
||||
t11 = $[19];
|
||||
}
|
||||
let t12;
|
||||
if ($[20] !== edits || $[21] !== filePath || $[22] !== noChangesMessage) {
|
||||
t12 = edits.length > 0 ? <FileEditToolDiff file_path={filePath} edits={edits} /> : <Text dimColor={true}>{noChangesMessage}</Text>;
|
||||
$[20] = edits;
|
||||
$[21] = filePath;
|
||||
$[22] = noChangesMessage;
|
||||
$[23] = t12;
|
||||
} else {
|
||||
t12 = $[23];
|
||||
}
|
||||
let t13;
|
||||
if ($[24] !== filePath || $[25] !== parseInput || $[26] !== props.onDone || $[27] !== props.onReject || $[28] !== props.toolUseConfirm || $[29] !== props.toolUseContext || $[30] !== props.workerBadge || $[31] !== t11 || $[32] !== t12 || $[33] !== t9) {
|
||||
t13 = <FilePermissionDialog toolUseConfirm={t5} toolUseContext={t6} onDone={t7} onReject={t8} title="Edit file" subtitle={t9} question={t11} content={t12} path={filePath} completionType="str_replace_single" parseInput={parseInput} workerBadge={props.workerBadge} />;
|
||||
$[24] = filePath;
|
||||
$[25] = parseInput;
|
||||
$[26] = props.onDone;
|
||||
$[27] = props.onReject;
|
||||
$[28] = props.toolUseConfirm;
|
||||
$[29] = props.toolUseContext;
|
||||
$[30] = props.workerBadge;
|
||||
$[31] = t11;
|
||||
$[32] = t12;
|
||||
$[33] = t9;
|
||||
$[34] = t13;
|
||||
} else {
|
||||
t13 = $[34];
|
||||
}
|
||||
return t13;
|
||||
path={filePath}
|
||||
completionType="str_replace_single"
|
||||
parseInput={parseInput}
|
||||
workerBadge={props.workerBadge}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,368 +1,253 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { logError } from 'src/utils/log.js';
|
||||
import { getOriginalCwd } from '../../../bootstrap/state.js';
|
||||
import { Box, Text } from '../../../ink.js';
|
||||
import { sanitizeToolNameForAnalytics } from '../../../services/analytics/metadata.js';
|
||||
import { SKILL_TOOL_NAME } from '../../../tools/SkillTool/constants.js';
|
||||
import { SkillTool } from '../../../tools/SkillTool/SkillTool.js';
|
||||
import { env } from '../../../utils/env.js';
|
||||
import { shouldShowAlwaysAllowOptions } from '../../../utils/permissions/permissionsLoader.js';
|
||||
import { logUnaryEvent } from '../../../utils/unaryLogging.js';
|
||||
import { type UnaryEvent, usePermissionRequestLogging } from '../hooks.js';
|
||||
import { PermissionDialog } from '../PermissionDialog.js';
|
||||
import { PermissionPrompt, type PermissionPromptOption, type ToolAnalyticsContext } from '../PermissionPrompt.js';
|
||||
import type { PermissionRequestProps } from '../PermissionRequest.js';
|
||||
import { PermissionRuleExplanation } from '../PermissionRuleExplanation.js';
|
||||
type SkillOptionValue = 'yes' | 'yes-exact' | 'yes-prefix' | 'no';
|
||||
export function SkillPermissionRequest(props) {
|
||||
const $ = _c(51);
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import { logError } from 'src/utils/log.js'
|
||||
import { getOriginalCwd } from '../../../bootstrap/state.js'
|
||||
import { Box, Text } from '../../../ink.js'
|
||||
import { sanitizeToolNameForAnalytics } from '../../../services/analytics/metadata.js'
|
||||
import { SKILL_TOOL_NAME } from '../../../tools/SkillTool/constants.js'
|
||||
import { SkillTool } from '../../../tools/SkillTool/SkillTool.js'
|
||||
import { env } from '../../../utils/env.js'
|
||||
import { shouldShowAlwaysAllowOptions } from '../../../utils/permissions/permissionsLoader.js'
|
||||
import { logUnaryEvent } from '../../../utils/unaryLogging.js'
|
||||
import { type UnaryEvent, usePermissionRequestLogging } from '../hooks.js'
|
||||
import { PermissionDialog } from '../PermissionDialog.js'
|
||||
import {
|
||||
PermissionPrompt,
|
||||
type PermissionPromptOption,
|
||||
type ToolAnalyticsContext,
|
||||
} from '../PermissionPrompt.js'
|
||||
import type { PermissionRequestProps } from '../PermissionRequest.js'
|
||||
import { PermissionRuleExplanation } from '../PermissionRuleExplanation.js'
|
||||
|
||||
type SkillOptionValue = 'yes' | 'yes-exact' | 'yes-prefix' | 'no'
|
||||
|
||||
export function SkillPermissionRequest(
|
||||
props: PermissionRequestProps,
|
||||
): React.ReactNode {
|
||||
const {
|
||||
toolUseConfirm,
|
||||
onDone,
|
||||
onReject,
|
||||
workerBadge
|
||||
} = props;
|
||||
const parseInput = _temp;
|
||||
let t0;
|
||||
if ($[0] !== toolUseConfirm.input) {
|
||||
t0 = parseInput(toolUseConfirm.input);
|
||||
$[0] = toolUseConfirm.input;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
const skill = t0;
|
||||
const commandObj = toolUseConfirm.permissionResult.behavior === "ask" && toolUseConfirm.permissionResult.metadata && "command" in toolUseConfirm.permissionResult.metadata ? toolUseConfirm.permissionResult.metadata.command : undefined;
|
||||
let t1;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = {
|
||||
completion_type: "tool_use_single",
|
||||
language_name: "none"
|
||||
};
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
const unaryEvent = t1;
|
||||
usePermissionRequestLogging(toolUseConfirm, unaryEvent);
|
||||
let t2;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = getOriginalCwd();
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
const originalCwd = t2;
|
||||
let t3;
|
||||
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = shouldShowAlwaysAllowOptions();
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
const showAlwaysAllowOptions = t3;
|
||||
let t4;
|
||||
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = [{
|
||||
label: "Yes",
|
||||
value: "yes",
|
||||
feedbackConfig: {
|
||||
type: "accept"
|
||||
}
|
||||
}];
|
||||
$[5] = t4;
|
||||
} else {
|
||||
t4 = $[5];
|
||||
}
|
||||
const baseOptions = t4;
|
||||
let alwaysAllowOptions;
|
||||
if ($[6] !== skill) {
|
||||
alwaysAllowOptions = [];
|
||||
if (showAlwaysAllowOptions) {
|
||||
const t5 = <Text bold={true}>{skill}</Text>;
|
||||
let t6;
|
||||
if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t6 = <Text bold={true}>{originalCwd}</Text>;
|
||||
$[8] = t6;
|
||||
} else {
|
||||
t6 = $[8];
|
||||
}
|
||||
let t7;
|
||||
if ($[9] !== t5) {
|
||||
t7 = {
|
||||
label: <Text>Yes, and don't ask again for {t5} in{" "}{t6}</Text>,
|
||||
value: "yes-exact"
|
||||
};
|
||||
$[9] = t5;
|
||||
$[10] = t7;
|
||||
} else {
|
||||
t7 = $[10];
|
||||
}
|
||||
alwaysAllowOptions.push(t7);
|
||||
const spaceIndex = skill.indexOf(" ");
|
||||
if (spaceIndex > 0) {
|
||||
const commandPrefix = skill.substring(0, spaceIndex);
|
||||
const t8 = commandPrefix + ":*";
|
||||
let t9;
|
||||
if ($[11] !== t8) {
|
||||
t9 = <Text bold={true}>{t8}</Text>;
|
||||
$[11] = t8;
|
||||
$[12] = t9;
|
||||
} else {
|
||||
t9 = $[12];
|
||||
}
|
||||
let t10;
|
||||
if ($[13] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t10 = <Text bold={true}>{originalCwd}</Text>;
|
||||
$[13] = t10;
|
||||
} else {
|
||||
t10 = $[13];
|
||||
}
|
||||
let t11;
|
||||
if ($[14] !== t9) {
|
||||
t11 = {
|
||||
label: <Text>Yes, and don't ask again for{" "}{t9} commands in{" "}{t10}</Text>,
|
||||
value: "yes-prefix"
|
||||
};
|
||||
$[14] = t9;
|
||||
$[15] = t11;
|
||||
} else {
|
||||
t11 = $[15];
|
||||
}
|
||||
alwaysAllowOptions.push(t11);
|
||||
}
|
||||
}
|
||||
$[6] = skill;
|
||||
$[7] = alwaysAllowOptions;
|
||||
} else {
|
||||
alwaysAllowOptions = $[7];
|
||||
}
|
||||
let t5;
|
||||
if ($[16] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t5 = {
|
||||
label: "No",
|
||||
value: "no",
|
||||
feedbackConfig: {
|
||||
type: "reject"
|
||||
}
|
||||
};
|
||||
$[16] = t5;
|
||||
} else {
|
||||
t5 = $[16];
|
||||
}
|
||||
const noOption = t5;
|
||||
let t6;
|
||||
if ($[17] !== alwaysAllowOptions) {
|
||||
t6 = [...baseOptions, ...alwaysAllowOptions, noOption];
|
||||
$[17] = alwaysAllowOptions;
|
||||
$[18] = t6;
|
||||
} else {
|
||||
t6 = $[18];
|
||||
}
|
||||
const options = t6;
|
||||
let t7;
|
||||
if ($[19] !== toolUseConfirm.tool.name) {
|
||||
t7 = sanitizeToolNameForAnalytics(toolUseConfirm.tool.name);
|
||||
$[19] = toolUseConfirm.tool.name;
|
||||
$[20] = t7;
|
||||
} else {
|
||||
t7 = $[20];
|
||||
}
|
||||
const t8 = toolUseConfirm.tool.isMcp ?? false;
|
||||
let t9;
|
||||
if ($[21] !== t7 || $[22] !== t8) {
|
||||
t9 = {
|
||||
toolName: t7,
|
||||
isMcp: t8
|
||||
};
|
||||
$[21] = t7;
|
||||
$[22] = t8;
|
||||
$[23] = t9;
|
||||
} else {
|
||||
t9 = $[23];
|
||||
}
|
||||
const toolAnalyticsContext = t9;
|
||||
let t10;
|
||||
if ($[24] !== onDone || $[25] !== onReject || $[26] !== skill || $[27] !== toolUseConfirm) {
|
||||
t10 = (value, feedback) => {
|
||||
bb33: switch (value) {
|
||||
case "yes":
|
||||
{
|
||||
logUnaryEvent({
|
||||
completion_type: "tool_use_single",
|
||||
event: "accept",
|
||||
metadata: {
|
||||
language_name: "none",
|
||||
message_id: toolUseConfirm.assistantMessage.message.id,
|
||||
platform: env.platform
|
||||
}
|
||||
});
|
||||
toolUseConfirm.onAllow(toolUseConfirm.input, [], feedback);
|
||||
onDone();
|
||||
break bb33;
|
||||
}
|
||||
case "yes-exact":
|
||||
{
|
||||
logUnaryEvent({
|
||||
completion_type: "tool_use_single",
|
||||
event: "accept",
|
||||
metadata: {
|
||||
language_name: "none",
|
||||
message_id: toolUseConfirm.assistantMessage.message.id,
|
||||
platform: env.platform
|
||||
}
|
||||
});
|
||||
toolUseConfirm.onAllow(toolUseConfirm.input, [{
|
||||
type: "addRules",
|
||||
rules: [{
|
||||
toolName: SKILL_TOOL_NAME,
|
||||
ruleContent: skill
|
||||
}],
|
||||
behavior: "allow",
|
||||
destination: "localSettings"
|
||||
}]);
|
||||
onDone();
|
||||
break bb33;
|
||||
}
|
||||
case "yes-prefix":
|
||||
{
|
||||
logUnaryEvent({
|
||||
completion_type: "tool_use_single",
|
||||
event: "accept",
|
||||
metadata: {
|
||||
language_name: "none",
|
||||
message_id: toolUseConfirm.assistantMessage.message.id,
|
||||
platform: env.platform
|
||||
}
|
||||
});
|
||||
const spaceIndex_0 = skill.indexOf(" ");
|
||||
const commandPrefix_0 = spaceIndex_0 > 0 ? skill.substring(0, spaceIndex_0) : skill;
|
||||
toolUseConfirm.onAllow(toolUseConfirm.input, [{
|
||||
type: "addRules",
|
||||
rules: [{
|
||||
toolName: SKILL_TOOL_NAME,
|
||||
ruleContent: `${commandPrefix_0}:*`
|
||||
}],
|
||||
behavior: "allow",
|
||||
destination: "localSettings"
|
||||
}]);
|
||||
onDone();
|
||||
break bb33;
|
||||
}
|
||||
case "no":
|
||||
{
|
||||
logUnaryEvent({
|
||||
completion_type: "tool_use_single",
|
||||
event: "reject",
|
||||
metadata: {
|
||||
language_name: "none",
|
||||
message_id: toolUseConfirm.assistantMessage.message.id,
|
||||
platform: env.platform
|
||||
}
|
||||
});
|
||||
toolUseConfirm.onReject(feedback);
|
||||
onReject();
|
||||
onDone();
|
||||
}
|
||||
}
|
||||
};
|
||||
$[24] = onDone;
|
||||
$[25] = onReject;
|
||||
$[26] = skill;
|
||||
$[27] = toolUseConfirm;
|
||||
$[28] = t10;
|
||||
} else {
|
||||
t10 = $[28];
|
||||
}
|
||||
const handleSelect = t10;
|
||||
let t11;
|
||||
if ($[29] !== onDone || $[30] !== onReject || $[31] !== toolUseConfirm) {
|
||||
t11 = () => {
|
||||
logUnaryEvent({
|
||||
completion_type: "tool_use_single",
|
||||
event: "reject",
|
||||
metadata: {
|
||||
language_name: "none",
|
||||
message_id: toolUseConfirm.assistantMessage.message.id,
|
||||
platform: env.platform
|
||||
}
|
||||
});
|
||||
toolUseConfirm.onReject();
|
||||
onReject();
|
||||
onDone();
|
||||
};
|
||||
$[29] = onDone;
|
||||
$[30] = onReject;
|
||||
$[31] = toolUseConfirm;
|
||||
$[32] = t11;
|
||||
} else {
|
||||
t11 = $[32];
|
||||
}
|
||||
const handleCancel = t11;
|
||||
const t12 = `Use skill "${skill}"?`;
|
||||
let t13;
|
||||
if ($[33] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t13 = <Text>Claude may use instructions, code, or files from this Skill.</Text>;
|
||||
$[33] = t13;
|
||||
} else {
|
||||
t13 = $[33];
|
||||
}
|
||||
const t14 = commandObj?.description;
|
||||
let t15;
|
||||
if ($[34] !== t14) {
|
||||
t15 = <Box flexDirection="column" paddingX={2} paddingY={1}><Text dimColor={true}>{t14}</Text></Box>;
|
||||
$[34] = t14;
|
||||
$[35] = t15;
|
||||
} else {
|
||||
t15 = $[35];
|
||||
}
|
||||
let t16;
|
||||
if ($[36] !== toolUseConfirm.permissionResult) {
|
||||
t16 = <PermissionRuleExplanation permissionResult={toolUseConfirm.permissionResult} toolType="tool" />;
|
||||
$[36] = toolUseConfirm.permissionResult;
|
||||
$[37] = t16;
|
||||
} else {
|
||||
t16 = $[37];
|
||||
}
|
||||
let t17;
|
||||
if ($[38] !== handleCancel || $[39] !== handleSelect || $[40] !== options || $[41] !== toolAnalyticsContext) {
|
||||
t17 = <PermissionPrompt options={options} onSelect={handleSelect} onCancel={handleCancel} toolAnalyticsContext={toolAnalyticsContext} />;
|
||||
$[38] = handleCancel;
|
||||
$[39] = handleSelect;
|
||||
$[40] = options;
|
||||
$[41] = toolAnalyticsContext;
|
||||
$[42] = t17;
|
||||
} else {
|
||||
t17 = $[42];
|
||||
}
|
||||
let t18;
|
||||
if ($[43] !== t16 || $[44] !== t17) {
|
||||
t18 = <Box flexDirection="column">{t16}{t17}</Box>;
|
||||
$[43] = t16;
|
||||
$[44] = t17;
|
||||
$[45] = t18;
|
||||
} else {
|
||||
t18 = $[45];
|
||||
}
|
||||
let t19;
|
||||
if ($[46] !== t12 || $[47] !== t15 || $[48] !== t18 || $[49] !== workerBadge) {
|
||||
t19 = <PermissionDialog title={t12} workerBadge={workerBadge}>{t13}{t15}{t18}</PermissionDialog>;
|
||||
$[46] = t12;
|
||||
$[47] = t15;
|
||||
$[48] = t18;
|
||||
$[49] = workerBadge;
|
||||
$[50] = t19;
|
||||
} else {
|
||||
t19 = $[50];
|
||||
}
|
||||
return t19;
|
||||
}
|
||||
function _temp(input) {
|
||||
const result = SkillTool.inputSchema.safeParse(input);
|
||||
verbose: _verbose,
|
||||
workerBadge,
|
||||
} = props
|
||||
const parseInput = (input: unknown): string => {
|
||||
const result = SkillTool.inputSchema.safeParse(input)
|
||||
if (!result.success) {
|
||||
logError(new Error(`Failed to parse skill tool input: ${result.error.message}`));
|
||||
return "";
|
||||
logError(
|
||||
new Error(`Failed to parse skill tool input: ${result.error.message}`),
|
||||
)
|
||||
return ''
|
||||
}
|
||||
return result.data.skill;
|
||||
return result.data.skill
|
||||
}
|
||||
|
||||
const skill = parseInput(toolUseConfirm.input)
|
||||
|
||||
// Check if this is a command using metadata from checkPermissions
|
||||
const commandObj =
|
||||
toolUseConfirm.permissionResult.behavior === 'ask' &&
|
||||
toolUseConfirm.permissionResult.metadata &&
|
||||
'command' in toolUseConfirm.permissionResult.metadata
|
||||
? toolUseConfirm.permissionResult.metadata.command
|
||||
: undefined
|
||||
|
||||
const unaryEvent = useMemo<UnaryEvent>(
|
||||
() => ({
|
||||
completion_type: 'tool_use_single',
|
||||
language_name: 'none',
|
||||
}),
|
||||
[],
|
||||
)
|
||||
|
||||
usePermissionRequestLogging(toolUseConfirm, unaryEvent)
|
||||
|
||||
const originalCwd = getOriginalCwd()
|
||||
const showAlwaysAllowOptions = shouldShowAlwaysAllowOptions()
|
||||
const options = useMemo((): PermissionPromptOption<SkillOptionValue>[] => {
|
||||
const baseOptions: PermissionPromptOption<SkillOptionValue>[] = [
|
||||
{
|
||||
label: 'Yes',
|
||||
value: 'yes',
|
||||
feedbackConfig: { type: 'accept' },
|
||||
},
|
||||
]
|
||||
|
||||
// Only add "always allow" options when not restricted by allowManagedPermissionRulesOnly
|
||||
const alwaysAllowOptions: PermissionPromptOption<SkillOptionValue>[] = []
|
||||
if (showAlwaysAllowOptions) {
|
||||
// Add exact match option
|
||||
alwaysAllowOptions.push({
|
||||
label: (
|
||||
<Text>
|
||||
Yes, and don't ask again for <Text bold>{skill}</Text> in{' '}
|
||||
<Text bold>{originalCwd}</Text>
|
||||
</Text>
|
||||
),
|
||||
value: 'yes-exact',
|
||||
})
|
||||
|
||||
// Add prefix option if the skill has arguments
|
||||
const spaceIndex = skill.indexOf(' ')
|
||||
if (spaceIndex > 0) {
|
||||
const commandPrefix = skill.substring(0, spaceIndex)
|
||||
alwaysAllowOptions.push({
|
||||
label: (
|
||||
<Text>
|
||||
Yes, and don't ask again for{' '}
|
||||
<Text bold>{commandPrefix + ':*'}</Text> commands in{' '}
|
||||
<Text bold>{originalCwd}</Text>
|
||||
</Text>
|
||||
),
|
||||
value: 'yes-prefix',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const noOption: PermissionPromptOption<SkillOptionValue> = {
|
||||
label: 'No',
|
||||
value: 'no',
|
||||
feedbackConfig: { type: 'reject' },
|
||||
}
|
||||
|
||||
return [...baseOptions, ...alwaysAllowOptions, noOption]
|
||||
}, [skill, originalCwd, showAlwaysAllowOptions])
|
||||
|
||||
const toolAnalyticsContext = useMemo(
|
||||
(): ToolAnalyticsContext => ({
|
||||
toolName: sanitizeToolNameForAnalytics(toolUseConfirm.tool.name),
|
||||
isMcp: toolUseConfirm.tool.isMcp ?? false,
|
||||
}),
|
||||
[toolUseConfirm.tool.name, toolUseConfirm.tool.isMcp],
|
||||
)
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(value: SkillOptionValue, feedback?: string) => {
|
||||
switch (value) {
|
||||
case 'yes':
|
||||
void logUnaryEvent({
|
||||
completion_type: 'tool_use_single',
|
||||
event: 'accept',
|
||||
metadata: {
|
||||
language_name: 'none',
|
||||
message_id: toolUseConfirm.assistantMessage.message.id,
|
||||
platform: env.platform,
|
||||
},
|
||||
})
|
||||
toolUseConfirm.onAllow(toolUseConfirm.input, [], feedback)
|
||||
onDone()
|
||||
break
|
||||
case 'yes-exact': {
|
||||
void logUnaryEvent({
|
||||
completion_type: 'tool_use_single',
|
||||
event: 'accept',
|
||||
metadata: {
|
||||
language_name: 'none',
|
||||
message_id: toolUseConfirm.assistantMessage.message.id,
|
||||
platform: env.platform,
|
||||
},
|
||||
})
|
||||
|
||||
toolUseConfirm.onAllow(toolUseConfirm.input, [
|
||||
{
|
||||
type: 'addRules',
|
||||
rules: [
|
||||
{
|
||||
toolName: SKILL_TOOL_NAME,
|
||||
ruleContent: skill,
|
||||
},
|
||||
],
|
||||
behavior: 'allow',
|
||||
destination: 'localSettings',
|
||||
},
|
||||
])
|
||||
onDone()
|
||||
break
|
||||
}
|
||||
case 'yes-prefix': {
|
||||
void logUnaryEvent({
|
||||
completion_type: 'tool_use_single',
|
||||
event: 'accept',
|
||||
metadata: {
|
||||
language_name: 'none',
|
||||
message_id: toolUseConfirm.assistantMessage.message.id,
|
||||
platform: env.platform,
|
||||
},
|
||||
})
|
||||
|
||||
// Extract the skill prefix (everything before the first space)
|
||||
const spaceIndex = skill.indexOf(' ')
|
||||
const commandPrefix =
|
||||
spaceIndex > 0 ? skill.substring(0, spaceIndex) : skill
|
||||
|
||||
toolUseConfirm.onAllow(toolUseConfirm.input, [
|
||||
{
|
||||
type: 'addRules',
|
||||
rules: [
|
||||
{
|
||||
toolName: SKILL_TOOL_NAME,
|
||||
ruleContent: `${commandPrefix}:*`,
|
||||
},
|
||||
],
|
||||
behavior: 'allow',
|
||||
destination: 'localSettings',
|
||||
},
|
||||
])
|
||||
onDone()
|
||||
break
|
||||
}
|
||||
case 'no':
|
||||
void logUnaryEvent({
|
||||
completion_type: 'tool_use_single',
|
||||
event: 'reject',
|
||||
metadata: {
|
||||
language_name: 'none',
|
||||
message_id: toolUseConfirm.assistantMessage.message.id,
|
||||
platform: env.platform,
|
||||
},
|
||||
})
|
||||
toolUseConfirm.onReject(feedback)
|
||||
onReject()
|
||||
onDone()
|
||||
break
|
||||
}
|
||||
},
|
||||
[toolUseConfirm, onDone, onReject, skill],
|
||||
)
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
void logUnaryEvent({
|
||||
completion_type: 'tool_use_single',
|
||||
event: 'reject',
|
||||
metadata: {
|
||||
language_name: 'none',
|
||||
message_id: toolUseConfirm.assistantMessage.message.id,
|
||||
platform: env.platform,
|
||||
},
|
||||
})
|
||||
toolUseConfirm.onReject()
|
||||
onReject()
|
||||
onDone()
|
||||
}, [toolUseConfirm, onDone, onReject])
|
||||
|
||||
return (
|
||||
<PermissionDialog title={`Use skill "${skill}"?`} workerBadge={workerBadge}>
|
||||
<Text>Claude may use instructions, code, or files from this Skill.</Text>
|
||||
<Box flexDirection="column" paddingX={2} paddingY={1}>
|
||||
<Text dimColor>{commandObj?.description}</Text>
|
||||
</Box>
|
||||
|
||||
<Box flexDirection="column">
|
||||
<PermissionRuleExplanation
|
||||
permissionResult={toolUseConfirm.permissionResult}
|
||||
toolType="tool"
|
||||
/>
|
||||
<PermissionPrompt
|
||||
options={options}
|
||||
onSelect={handleSelect}
|
||||
onCancel={handleCancel}
|
||||
toolAnalyticsContext={toolAnalyticsContext}
|
||||
/>
|
||||
</Box>
|
||||
</PermissionDialog>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,257 +1,148 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React, { useMemo } from 'react';
|
||||
import { Box, Text, useTheme } from '../../../ink.js';
|
||||
import { WebFetchTool } from '../../../tools/WebFetchTool/WebFetchTool.js';
|
||||
import { shouldShowAlwaysAllowOptions } from '../../../utils/permissions/permissionsLoader.js';
|
||||
import { type OptionWithDescription, Select } from '../../CustomSelect/select.js';
|
||||
import { type UnaryEvent, usePermissionRequestLogging } from '../hooks.js';
|
||||
import { PermissionDialog } from '../PermissionDialog.js';
|
||||
import type { PermissionRequestProps } from '../PermissionRequest.js';
|
||||
import { PermissionRuleExplanation } from '../PermissionRuleExplanation.js';
|
||||
import { logUnaryPermissionEvent } from '../utils.js';
|
||||
function inputToPermissionRuleContent(input: {
|
||||
[k: string]: unknown;
|
||||
}): string {
|
||||
import React, { useMemo } from 'react'
|
||||
import { Box, Text, useTheme } from '../../../ink.js'
|
||||
import { WebFetchTool } from '../../../tools/WebFetchTool/WebFetchTool.js'
|
||||
import { shouldShowAlwaysAllowOptions } from '../../../utils/permissions/permissionsLoader.js'
|
||||
import {
|
||||
type OptionWithDescription,
|
||||
Select,
|
||||
} from '../../CustomSelect/select.js'
|
||||
import { type UnaryEvent, usePermissionRequestLogging } from '../hooks.js'
|
||||
import { PermissionDialog } from '../PermissionDialog.js'
|
||||
import type { PermissionRequestProps } from '../PermissionRequest.js'
|
||||
import { PermissionRuleExplanation } from '../PermissionRuleExplanation.js'
|
||||
import { logUnaryPermissionEvent } from '../utils.js'
|
||||
|
||||
function inputToPermissionRuleContent(input: { [k: string]: unknown }): string {
|
||||
try {
|
||||
const parsedInput = WebFetchTool.inputSchema.safeParse(input);
|
||||
const parsedInput = WebFetchTool.inputSchema.safeParse(input)
|
||||
if (!parsedInput.success) {
|
||||
return `input:${input.toString()}`;
|
||||
return `input:${input.toString()}`
|
||||
}
|
||||
const {
|
||||
url
|
||||
} = parsedInput.data;
|
||||
const hostname = new URL(url).hostname;
|
||||
return `domain:${hostname}`;
|
||||
const { url } = parsedInput.data
|
||||
const hostname = new URL(url).hostname
|
||||
return `domain:${hostname}`
|
||||
} catch {
|
||||
return `input:${input.toString()}`;
|
||||
return `input:${input.toString()}`
|
||||
}
|
||||
}
|
||||
export function WebFetchPermissionRequest(t0) {
|
||||
const $ = _c(41);
|
||||
const {
|
||||
|
||||
export function WebFetchPermissionRequest({
|
||||
toolUseConfirm,
|
||||
onDone,
|
||||
onReject,
|
||||
verbose,
|
||||
workerBadge
|
||||
} = t0;
|
||||
const [theme] = useTheme();
|
||||
const {
|
||||
url
|
||||
} = toolUseConfirm.input as {
|
||||
url: string;
|
||||
};
|
||||
let t1;
|
||||
if ($[0] !== url) {
|
||||
t1 = new URL(url);
|
||||
$[0] = url;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const hostname = t1.hostname;
|
||||
let t2;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = {
|
||||
completion_type: "tool_use_single",
|
||||
language_name: "none"
|
||||
};
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
const unaryEvent = t2;
|
||||
usePermissionRequestLogging(toolUseConfirm, unaryEvent);
|
||||
let t3;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = shouldShowAlwaysAllowOptions();
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t3 = $[3];
|
||||
}
|
||||
const showAlwaysAllowOptions = t3;
|
||||
let t4;
|
||||
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = {
|
||||
label: "Yes",
|
||||
value: "yes"
|
||||
};
|
||||
$[4] = t4;
|
||||
} else {
|
||||
t4 = $[4];
|
||||
}
|
||||
let result;
|
||||
if ($[5] !== hostname) {
|
||||
result = [t4];
|
||||
workerBadge,
|
||||
}: PermissionRequestProps): React.ReactNode {
|
||||
const [theme] = useTheme()
|
||||
// url is already validated by the input schema
|
||||
const { url } = toolUseConfirm.input as { url: string }
|
||||
|
||||
// Extract hostname from URL
|
||||
const hostname = new URL(url).hostname
|
||||
|
||||
const unaryEvent = useMemo<UnaryEvent>(
|
||||
() => ({ completion_type: 'tool_use_single', language_name: 'none' }),
|
||||
[],
|
||||
)
|
||||
|
||||
usePermissionRequestLogging(toolUseConfirm, unaryEvent)
|
||||
|
||||
// Generate permission options specific to domains
|
||||
const showAlwaysAllowOptions = shouldShowAlwaysAllowOptions()
|
||||
const options = useMemo((): OptionWithDescription<string>[] => {
|
||||
const result: OptionWithDescription<string>[] = [
|
||||
{
|
||||
label: 'Yes',
|
||||
value: 'yes',
|
||||
},
|
||||
]
|
||||
|
||||
if (showAlwaysAllowOptions) {
|
||||
const t5 = <Text bold={true}>{hostname}</Text>;
|
||||
let t6;
|
||||
if ($[7] !== t5) {
|
||||
t6 = {
|
||||
label: <Text>Yes, and don't ask again for {t5}</Text>,
|
||||
value: "yes-dont-ask-again-domain"
|
||||
};
|
||||
$[7] = t5;
|
||||
$[8] = t6;
|
||||
} else {
|
||||
t6 = $[8];
|
||||
result.push({
|
||||
label: (
|
||||
<Text>
|
||||
Yes, and don't ask again for <Text bold>{hostname}</Text>
|
||||
</Text>
|
||||
),
|
||||
value: 'yes-dont-ask-again-domain',
|
||||
})
|
||||
}
|
||||
result.push(t6);
|
||||
}
|
||||
let t5;
|
||||
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t5 = {
|
||||
label: <Text>No, and tell Claude what to do differently <Text bold={true}>(esc)</Text></Text>,
|
||||
value: "no"
|
||||
};
|
||||
$[9] = t5;
|
||||
} else {
|
||||
t5 = $[9];
|
||||
}
|
||||
result.push(t5);
|
||||
$[5] = hostname;
|
||||
$[6] = result;
|
||||
} else {
|
||||
result = $[6];
|
||||
}
|
||||
const options = result;
|
||||
let t5;
|
||||
if ($[10] !== onDone || $[11] !== onReject || $[12] !== toolUseConfirm) {
|
||||
t5 = function onChange(newValue) {
|
||||
bb8: switch (newValue) {
|
||||
case "yes":
|
||||
{
|
||||
logUnaryPermissionEvent("tool_use_single", toolUseConfirm, "accept");
|
||||
toolUseConfirm.onAllow(toolUseConfirm.input, []);
|
||||
onDone();
|
||||
break bb8;
|
||||
}
|
||||
case "yes-dont-ask-again-domain":
|
||||
{
|
||||
logUnaryPermissionEvent("tool_use_single", toolUseConfirm, "accept");
|
||||
const ruleContent = inputToPermissionRuleContent(toolUseConfirm.input);
|
||||
|
||||
result.push({
|
||||
label: (
|
||||
<Text>
|
||||
No, and tell Claude what to do differently <Text bold>(esc)</Text>
|
||||
</Text>
|
||||
),
|
||||
value: 'no',
|
||||
})
|
||||
|
||||
return result
|
||||
}, [hostname, showAlwaysAllowOptions])
|
||||
|
||||
function onChange(newValue: string) {
|
||||
switch (newValue) {
|
||||
case 'yes':
|
||||
logUnaryPermissionEvent('tool_use_single', toolUseConfirm, 'accept')
|
||||
toolUseConfirm.onAllow(toolUseConfirm.input, [])
|
||||
onDone()
|
||||
break
|
||||
case 'yes-dont-ask-again-domain': {
|
||||
logUnaryPermissionEvent('tool_use_single', toolUseConfirm, 'accept')
|
||||
const ruleContent = inputToPermissionRuleContent(toolUseConfirm.input)
|
||||
const ruleValue = {
|
||||
toolName: toolUseConfirm.tool.name,
|
||||
ruleContent
|
||||
};
|
||||
toolUseConfirm.onAllow(toolUseConfirm.input, [{
|
||||
type: "addRules",
|
||||
rules: [ruleValue],
|
||||
behavior: "allow",
|
||||
destination: "localSettings"
|
||||
}]);
|
||||
onDone();
|
||||
break bb8;
|
||||
ruleContent,
|
||||
}
|
||||
case "no":
|
||||
|
||||
// Pass permission update directly to onAllow
|
||||
toolUseConfirm.onAllow(toolUseConfirm.input, [
|
||||
{
|
||||
logUnaryPermissionEvent("tool_use_single", toolUseConfirm, "reject");
|
||||
toolUseConfirm.onReject();
|
||||
onReject();
|
||||
onDone();
|
||||
type: 'addRules',
|
||||
rules: [ruleValue],
|
||||
behavior: 'allow',
|
||||
destination: 'localSettings',
|
||||
},
|
||||
])
|
||||
onDone()
|
||||
break
|
||||
}
|
||||
case 'no':
|
||||
logUnaryPermissionEvent('tool_use_single', toolUseConfirm, 'reject')
|
||||
toolUseConfirm.onReject()
|
||||
onReject()
|
||||
onDone()
|
||||
break
|
||||
}
|
||||
}
|
||||
};
|
||||
$[10] = onDone;
|
||||
$[11] = onReject;
|
||||
$[12] = toolUseConfirm;
|
||||
$[13] = t5;
|
||||
} else {
|
||||
t5 = $[13];
|
||||
}
|
||||
const onChange = t5;
|
||||
let t6;
|
||||
if ($[14] !== theme || $[15] !== toolUseConfirm.input || $[16] !== verbose) {
|
||||
t6 = WebFetchTool.renderToolUseMessage(toolUseConfirm.input as {
|
||||
url: string;
|
||||
prompt: string;
|
||||
}, {
|
||||
|
||||
return (
|
||||
<PermissionDialog title="Fetch" workerBadge={workerBadge}>
|
||||
<Box flexDirection="column" paddingX={2} paddingY={1}>
|
||||
<Text>
|
||||
{WebFetchTool.renderToolUseMessage(
|
||||
toolUseConfirm.input as { url: string; prompt: string },
|
||||
{
|
||||
theme,
|
||||
verbose
|
||||
});
|
||||
$[14] = theme;
|
||||
$[15] = toolUseConfirm.input;
|
||||
$[16] = verbose;
|
||||
$[17] = t6;
|
||||
} else {
|
||||
t6 = $[17];
|
||||
}
|
||||
let t7;
|
||||
if ($[18] !== t6) {
|
||||
t7 = <Text>{t6}</Text>;
|
||||
$[18] = t6;
|
||||
$[19] = t7;
|
||||
} else {
|
||||
t7 = $[19];
|
||||
}
|
||||
let t8;
|
||||
if ($[20] !== toolUseConfirm.description) {
|
||||
t8 = <Text dimColor={true}>{toolUseConfirm.description}</Text>;
|
||||
$[20] = toolUseConfirm.description;
|
||||
$[21] = t8;
|
||||
} else {
|
||||
t8 = $[21];
|
||||
}
|
||||
let t9;
|
||||
if ($[22] !== t7 || $[23] !== t8) {
|
||||
t9 = <Box flexDirection="column" paddingX={2} paddingY={1}>{t7}{t8}</Box>;
|
||||
$[22] = t7;
|
||||
$[23] = t8;
|
||||
$[24] = t9;
|
||||
} else {
|
||||
t9 = $[24];
|
||||
}
|
||||
let t10;
|
||||
if ($[25] !== toolUseConfirm.permissionResult) {
|
||||
t10 = <PermissionRuleExplanation permissionResult={toolUseConfirm.permissionResult} toolType="tool" />;
|
||||
$[25] = toolUseConfirm.permissionResult;
|
||||
$[26] = t10;
|
||||
} else {
|
||||
t10 = $[26];
|
||||
}
|
||||
let t11;
|
||||
if ($[27] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t11 = <Text>Do you want to allow Claude to fetch this content?</Text>;
|
||||
$[27] = t11;
|
||||
} else {
|
||||
t11 = $[27];
|
||||
}
|
||||
let t12;
|
||||
if ($[28] !== onChange) {
|
||||
t12 = () => onChange("no");
|
||||
$[28] = onChange;
|
||||
$[29] = t12;
|
||||
} else {
|
||||
t12 = $[29];
|
||||
}
|
||||
let t13;
|
||||
if ($[30] !== onChange || $[31] !== options || $[32] !== t12) {
|
||||
t13 = <Select options={options} onChange={onChange} onCancel={t12} />;
|
||||
$[30] = onChange;
|
||||
$[31] = options;
|
||||
$[32] = t12;
|
||||
$[33] = t13;
|
||||
} else {
|
||||
t13 = $[33];
|
||||
}
|
||||
let t14;
|
||||
if ($[34] !== t10 || $[35] !== t13) {
|
||||
t14 = <Box flexDirection="column">{t10}{t11}{t13}</Box>;
|
||||
$[34] = t10;
|
||||
$[35] = t13;
|
||||
$[36] = t14;
|
||||
} else {
|
||||
t14 = $[36];
|
||||
}
|
||||
let t15;
|
||||
if ($[37] !== t14 || $[38] !== t9 || $[39] !== workerBadge) {
|
||||
t15 = <PermissionDialog title="Fetch" workerBadge={workerBadge}>{t9}{t14}</PermissionDialog>;
|
||||
$[37] = t14;
|
||||
$[38] = t9;
|
||||
$[39] = workerBadge;
|
||||
$[40] = t15;
|
||||
} else {
|
||||
t15 = $[40];
|
||||
}
|
||||
return t15;
|
||||
verbose,
|
||||
},
|
||||
)}
|
||||
</Text>
|
||||
<Text dimColor>{toolUseConfirm.description}</Text>
|
||||
</Box>
|
||||
|
||||
<Box flexDirection="column">
|
||||
<PermissionRuleExplanation
|
||||
permissionResult={toolUseConfirm.permissionResult}
|
||||
toolType="tool"
|
||||
/>
|
||||
<Text>Do you want to allow Claude to fetch this content?</Text>
|
||||
<Select
|
||||
options={options}
|
||||
onChange={onChange}
|
||||
onCancel={() => onChange('no')}
|
||||
/>
|
||||
</Box>
|
||||
</PermissionDialog>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,48 +1,27 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { BLACK_CIRCLE } from '../../constants/figures.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { toInkColor } from '../../utils/ink.js';
|
||||
import * as React from 'react'
|
||||
import { BLACK_CIRCLE } from '../../constants/figures.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { toInkColor } from '../../utils/ink.js'
|
||||
|
||||
export type WorkerBadgeProps = {
|
||||
name: string;
|
||||
color: string;
|
||||
};
|
||||
name: string
|
||||
color: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a colored badge showing the worker's name for permission prompts.
|
||||
* Used to indicate which swarm worker is requesting the permission.
|
||||
*/
|
||||
export function WorkerBadge(t0) {
|
||||
const $ = _c(7);
|
||||
const {
|
||||
export function WorkerBadge({
|
||||
name,
|
||||
color
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] !== color) {
|
||||
t1 = toInkColor(color);
|
||||
$[0] = color;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const inkColor = t1;
|
||||
let t2;
|
||||
if ($[2] !== name) {
|
||||
t2 = <Text bold={true}>@{name}</Text>;
|
||||
$[2] = name;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
let t3;
|
||||
if ($[4] !== inkColor || $[5] !== t2) {
|
||||
t3 = <Box flexDirection="row" gap={1}><Text color={inkColor}>{BLACK_CIRCLE} {t2}</Text></Box>;
|
||||
$[4] = inkColor;
|
||||
$[5] = t2;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
}
|
||||
return t3;
|
||||
color,
|
||||
}: WorkerBadgeProps): React.ReactNode {
|
||||
const inkColor = toInkColor(color)
|
||||
return (
|
||||
<Box flexDirection="row" gap={1}>
|
||||
<Text color={inkColor}>
|
||||
{BLACK_CIRCLE} <Text bold>@{name}</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,104 +1,70 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { getAgentName, getTeammateColor, getTeamName } from '../../utils/teammate.js';
|
||||
import { Spinner } from '../Spinner.js';
|
||||
import { WorkerBadge } from './WorkerBadge.js';
|
||||
import * as React from 'react'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import {
|
||||
getAgentName,
|
||||
getTeammateColor,
|
||||
getTeamName,
|
||||
} from '../../utils/teammate.js'
|
||||
import { Spinner } from '../Spinner.js'
|
||||
import { WorkerBadge } from './WorkerBadge.js'
|
||||
|
||||
type Props = {
|
||||
toolName: string;
|
||||
description: string;
|
||||
};
|
||||
toolName: string
|
||||
description: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Visual indicator shown on workers while waiting for leader to approve a permission request.
|
||||
* Displays the pending tool with a spinner and information about what's being requested.
|
||||
*/
|
||||
export function WorkerPendingPermission(t0) {
|
||||
const $ = _c(15);
|
||||
const {
|
||||
export function WorkerPendingPermission({
|
||||
toolName,
|
||||
description
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = getTeamName();
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
const teamName = t1;
|
||||
let t2;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = getAgentName();
|
||||
$[1] = t2;
|
||||
} else {
|
||||
t2 = $[1];
|
||||
}
|
||||
const agentName = t2;
|
||||
let t3;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = getTeammateColor();
|
||||
$[2] = t3;
|
||||
} else {
|
||||
t3 = $[2];
|
||||
}
|
||||
const agentColor = t3;
|
||||
let t4;
|
||||
let t5;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = <Box marginBottom={1}><Spinner /><Text color="warning" bold={true}>{" "}Waiting for team lead approval</Text></Box>;
|
||||
t5 = agentName && agentColor && <Box marginBottom={1}><WorkerBadge name={agentName} color={agentColor} /></Box>;
|
||||
$[3] = t4;
|
||||
$[4] = t5;
|
||||
} else {
|
||||
t4 = $[3];
|
||||
t5 = $[4];
|
||||
}
|
||||
let t6;
|
||||
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t6 = <Text dimColor={true}>Tool: </Text>;
|
||||
$[5] = t6;
|
||||
} else {
|
||||
t6 = $[5];
|
||||
}
|
||||
let t7;
|
||||
if ($[6] !== toolName) {
|
||||
t7 = <Box>{t6}<Text>{toolName}</Text></Box>;
|
||||
$[6] = toolName;
|
||||
$[7] = t7;
|
||||
} else {
|
||||
t7 = $[7];
|
||||
}
|
||||
let t8;
|
||||
if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t8 = <Text dimColor={true}>Action: </Text>;
|
||||
$[8] = t8;
|
||||
} else {
|
||||
t8 = $[8];
|
||||
}
|
||||
let t9;
|
||||
if ($[9] !== description) {
|
||||
t9 = <Box>{t8}<Text>{description}</Text></Box>;
|
||||
$[9] = description;
|
||||
$[10] = t9;
|
||||
} else {
|
||||
t9 = $[10];
|
||||
}
|
||||
let t10;
|
||||
if ($[11] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t10 = teamName && <Box marginTop={1}><Text dimColor={true}>Permission request sent to team {"\""}{teamName}{"\""} leader</Text></Box>;
|
||||
$[11] = t10;
|
||||
} else {
|
||||
t10 = $[11];
|
||||
}
|
||||
let t11;
|
||||
if ($[12] !== t7 || $[13] !== t9) {
|
||||
t11 = <Box flexDirection="column" borderStyle="round" borderColor="warning" paddingX={1}>{t4}{t5}{t7}{t9}{t10}</Box>;
|
||||
$[12] = t7;
|
||||
$[13] = t9;
|
||||
$[14] = t11;
|
||||
} else {
|
||||
t11 = $[14];
|
||||
}
|
||||
return t11;
|
||||
description,
|
||||
}: Props): React.ReactNode {
|
||||
const teamName = getTeamName()
|
||||
const agentName = getAgentName()
|
||||
const agentColor = getTeammateColor()
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
borderStyle="round"
|
||||
borderColor="warning"
|
||||
paddingX={1}
|
||||
>
|
||||
<Box marginBottom={1}>
|
||||
<Spinner />
|
||||
<Text color="warning" bold>
|
||||
{' '}
|
||||
Waiting for team lead approval
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{agentName && agentColor && (
|
||||
<Box marginBottom={1}>
|
||||
<WorkerBadge name={agentName} color={agentColor} />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box>
|
||||
<Text dimColor>Tool: </Text>
|
||||
<Text>{toolName}</Text>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text dimColor>Action: </Text>
|
||||
<Text>{description}</Text>
|
||||
</Box>
|
||||
|
||||
{teamName && (
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>
|
||||
Permission request sent to team {'"'}
|
||||
{teamName}
|
||||
{'"'} leader
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,179 +1,165 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { Select } from '../../../components/CustomSelect/select.js';
|
||||
import { Box, Text } from '../../../ink.js';
|
||||
import type { ToolPermissionContext } from '../../../Tool.js';
|
||||
import type { PermissionBehavior, PermissionRule, PermissionRuleValue } from '../../../utils/permissions/PermissionRule.js';
|
||||
import { applyPermissionUpdate, persistPermissionUpdate } from '../../../utils/permissions/PermissionUpdate.js';
|
||||
import { permissionRuleValueToString } from '../../../utils/permissions/permissionRuleParser.js';
|
||||
import { detectUnreachableRules, type UnreachableRule } from '../../../utils/permissions/shadowedRuleDetection.js';
|
||||
import { SandboxManager } from '../../../utils/sandbox/sandbox-adapter.js';
|
||||
import { type EditableSettingSource, SOURCES } from '../../../utils/settings/constants.js';
|
||||
import { getRelativeSettingsFilePathForSource } from '../../../utils/settings/settings.js';
|
||||
import { plural } from '../../../utils/stringUtils.js';
|
||||
import type { OptionWithDescription } from '../../CustomSelect/select.js';
|
||||
import { Dialog } from '../../design-system/Dialog.js';
|
||||
import { PermissionRuleDescription } from './PermissionRuleDescription.js';
|
||||
export function optionForPermissionSaveDestination(saveDestination: EditableSettingSource): OptionWithDescription {
|
||||
import * as React from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { Select } from '../../../components/CustomSelect/select.js'
|
||||
import { Box, Text } from '../../../ink.js'
|
||||
import type { ToolPermissionContext } from '../../../Tool.js'
|
||||
import type {
|
||||
PermissionBehavior,
|
||||
PermissionRule,
|
||||
PermissionRuleValue,
|
||||
} from '../../../utils/permissions/PermissionRule.js'
|
||||
import {
|
||||
applyPermissionUpdate,
|
||||
persistPermissionUpdate,
|
||||
} from '../../../utils/permissions/PermissionUpdate.js'
|
||||
import { permissionRuleValueToString } from '../../../utils/permissions/permissionRuleParser.js'
|
||||
import {
|
||||
detectUnreachableRules,
|
||||
type UnreachableRule,
|
||||
} from '../../../utils/permissions/shadowedRuleDetection.js'
|
||||
import { SandboxManager } from '../../../utils/sandbox/sandbox-adapter.js'
|
||||
import {
|
||||
type EditableSettingSource,
|
||||
SOURCES,
|
||||
} from '../../../utils/settings/constants.js'
|
||||
import { getRelativeSettingsFilePathForSource } from '../../../utils/settings/settings.js'
|
||||
import { plural } from '../../../utils/stringUtils.js'
|
||||
import type { OptionWithDescription } from '../../CustomSelect/select.js'
|
||||
import { Dialog } from '../../design-system/Dialog.js'
|
||||
import { PermissionRuleDescription } from './PermissionRuleDescription.js'
|
||||
|
||||
export function optionForPermissionSaveDestination(
|
||||
saveDestination: EditableSettingSource,
|
||||
): OptionWithDescription {
|
||||
switch (saveDestination) {
|
||||
case 'localSettings':
|
||||
return {
|
||||
label: 'Project settings (local)',
|
||||
description: `Saved in ${getRelativeSettingsFilePathForSource('localSettings')}`,
|
||||
value: saveDestination
|
||||
};
|
||||
value: saveDestination,
|
||||
}
|
||||
case 'projectSettings':
|
||||
return {
|
||||
label: 'Project settings',
|
||||
description: `Checked in at ${getRelativeSettingsFilePathForSource('projectSettings')}`,
|
||||
value: saveDestination
|
||||
};
|
||||
value: saveDestination,
|
||||
}
|
||||
case 'userSettings':
|
||||
return {
|
||||
label: 'User settings',
|
||||
description: `Saved in at ~/.claude/settings.json`,
|
||||
value: saveDestination
|
||||
};
|
||||
value: saveDestination,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Props = {
|
||||
onAddRules: (rules: PermissionRule[], unreachable?: UnreachableRule[]) => void;
|
||||
onCancel: () => void;
|
||||
ruleValues: PermissionRuleValue[];
|
||||
ruleBehavior: PermissionBehavior;
|
||||
initialContext: ToolPermissionContext;
|
||||
setToolPermissionContext: (newContext: ToolPermissionContext) => void;
|
||||
};
|
||||
export function AddPermissionRules(t0) {
|
||||
const $ = _c(26);
|
||||
const {
|
||||
onAddRules: (rules: PermissionRule[], unreachable?: UnreachableRule[]) => void
|
||||
onCancel: () => void
|
||||
ruleValues: PermissionRuleValue[]
|
||||
ruleBehavior: PermissionBehavior
|
||||
initialContext: ToolPermissionContext
|
||||
setToolPermissionContext: (newContext: ToolPermissionContext) => void
|
||||
}
|
||||
|
||||
export function AddPermissionRules({
|
||||
onAddRules,
|
||||
onCancel,
|
||||
ruleValues,
|
||||
ruleBehavior,
|
||||
initialContext,
|
||||
setToolPermissionContext
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = SOURCES.map(optionForPermissionSaveDestination);
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
const allOptions = t1;
|
||||
let t2;
|
||||
if ($[1] !== initialContext || $[2] !== onAddRules || $[3] !== onCancel || $[4] !== ruleBehavior || $[5] !== ruleValues || $[6] !== setToolPermissionContext) {
|
||||
t2 = selectedValue => {
|
||||
if (selectedValue === "cancel") {
|
||||
onCancel();
|
||||
return;
|
||||
} else {
|
||||
if ((SOURCES as readonly string[]).includes(selectedValue)) {
|
||||
const destination = selectedValue as EditableSettingSource;
|
||||
setToolPermissionContext,
|
||||
}: Props): React.ReactNode {
|
||||
const allOptions = SOURCES.map(optionForPermissionSaveDestination)
|
||||
|
||||
const onSelect = useCallback(
|
||||
(selectedValue: string) => {
|
||||
if (selectedValue === 'cancel') {
|
||||
onCancel()
|
||||
return
|
||||
} else if ((SOURCES as readonly string[]).includes(selectedValue)) {
|
||||
const destination = selectedValue as EditableSettingSource
|
||||
|
||||
const updatedContext = applyPermissionUpdate(initialContext, {
|
||||
type: "addRules",
|
||||
type: 'addRules',
|
||||
rules: ruleValues,
|
||||
behavior: ruleBehavior,
|
||||
destination
|
||||
});
|
||||
destination,
|
||||
})
|
||||
|
||||
// Persist to settings
|
||||
persistPermissionUpdate({
|
||||
type: "addRules",
|
||||
type: 'addRules',
|
||||
rules: ruleValues,
|
||||
behavior: ruleBehavior,
|
||||
destination
|
||||
});
|
||||
setToolPermissionContext(updatedContext);
|
||||
const rules = ruleValues.map(ruleValue => ({
|
||||
destination,
|
||||
})
|
||||
|
||||
setToolPermissionContext(updatedContext)
|
||||
|
||||
const rules: PermissionRule[] = ruleValues.map(ruleValue => ({
|
||||
ruleValue,
|
||||
ruleBehavior,
|
||||
source: destination
|
||||
}));
|
||||
const sandboxAutoAllowEnabled = SandboxManager.isSandboxingEnabled() && SandboxManager.isAutoAllowBashIfSandboxedEnabled();
|
||||
source: destination,
|
||||
}))
|
||||
|
||||
// Check for unreachable rules among the ones we just added
|
||||
const sandboxAutoAllowEnabled =
|
||||
SandboxManager.isSandboxingEnabled() &&
|
||||
SandboxManager.isAutoAllowBashIfSandboxedEnabled()
|
||||
const allUnreachable = detectUnreachableRules(updatedContext, {
|
||||
sandboxAutoAllowEnabled
|
||||
});
|
||||
const newUnreachable = allUnreachable.filter(u => ruleValues.some(rv => rv.toolName === u.rule.ruleValue.toolName && rv.ruleContent === u.rule.ruleValue.ruleContent));
|
||||
onAddRules(rules, newUnreachable.length > 0 ? newUnreachable : undefined);
|
||||
sandboxAutoAllowEnabled,
|
||||
})
|
||||
|
||||
// Filter to only rules we just added
|
||||
const newUnreachable = allUnreachable.filter(u =>
|
||||
ruleValues.some(
|
||||
rv =>
|
||||
rv.toolName === u.rule.ruleValue.toolName &&
|
||||
rv.ruleContent === u.rule.ruleValue.ruleContent,
|
||||
),
|
||||
)
|
||||
|
||||
onAddRules(
|
||||
rules,
|
||||
newUnreachable.length > 0 ? newUnreachable : undefined,
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
$[1] = initialContext;
|
||||
$[2] = onAddRules;
|
||||
$[3] = onCancel;
|
||||
$[4] = ruleBehavior;
|
||||
$[5] = ruleValues;
|
||||
$[6] = setToolPermissionContext;
|
||||
$[7] = t2;
|
||||
} else {
|
||||
t2 = $[7];
|
||||
}
|
||||
const onSelect = t2;
|
||||
let t3;
|
||||
if ($[8] !== ruleValues.length) {
|
||||
t3 = plural(ruleValues.length, "rule");
|
||||
$[8] = ruleValues.length;
|
||||
$[9] = t3;
|
||||
} else {
|
||||
t3 = $[9];
|
||||
}
|
||||
const title = `Add ${ruleBehavior} permission ${t3}`;
|
||||
let t4;
|
||||
if ($[10] !== ruleValues) {
|
||||
t4 = ruleValues.map(_temp);
|
||||
$[10] = ruleValues;
|
||||
$[11] = t4;
|
||||
} else {
|
||||
t4 = $[11];
|
||||
}
|
||||
let t5;
|
||||
if ($[12] !== t4) {
|
||||
t5 = <Box flexDirection="column" paddingX={2}>{t4}</Box>;
|
||||
$[12] = t4;
|
||||
$[13] = t5;
|
||||
} else {
|
||||
t5 = $[13];
|
||||
}
|
||||
const t6 = ruleValues.length === 1 ? "Where should this rule be saved?" : "Where should these rules be saved?";
|
||||
let t7;
|
||||
if ($[14] !== t6) {
|
||||
t7 = <Text>{t6}</Text>;
|
||||
$[14] = t6;
|
||||
$[15] = t7;
|
||||
} else {
|
||||
t7 = $[15];
|
||||
}
|
||||
let t8;
|
||||
if ($[16] !== onSelect) {
|
||||
t8 = <Select options={allOptions} onChange={onSelect} />;
|
||||
$[16] = onSelect;
|
||||
$[17] = t8;
|
||||
} else {
|
||||
t8 = $[17];
|
||||
}
|
||||
let t9;
|
||||
if ($[18] !== t7 || $[19] !== t8) {
|
||||
t9 = <Box flexDirection="column" marginY={1}>{t7}{t8}</Box>;
|
||||
$[18] = t7;
|
||||
$[19] = t8;
|
||||
$[20] = t9;
|
||||
} else {
|
||||
t9 = $[20];
|
||||
}
|
||||
let t10;
|
||||
if ($[21] !== onCancel || $[22] !== t5 || $[23] !== t9 || $[24] !== title) {
|
||||
t10 = <Dialog title={title} onCancel={onCancel} color="permission">{t5}{t9}</Dialog>;
|
||||
$[21] = onCancel;
|
||||
$[22] = t5;
|
||||
$[23] = t9;
|
||||
$[24] = title;
|
||||
$[25] = t10;
|
||||
} else {
|
||||
t10 = $[25];
|
||||
}
|
||||
return t10;
|
||||
}
|
||||
function _temp(ruleValue_0) {
|
||||
return <Box flexDirection="column" key={permissionRuleValueToString(ruleValue_0)}><Text bold={true}>{permissionRuleValueToString(ruleValue_0)}</Text><PermissionRuleDescription ruleValue={ruleValue_0} /></Box>;
|
||||
},
|
||||
[
|
||||
onAddRules,
|
||||
onCancel,
|
||||
ruleValues,
|
||||
ruleBehavior,
|
||||
initialContext,
|
||||
setToolPermissionContext,
|
||||
],
|
||||
)
|
||||
|
||||
const title = `Add ${ruleBehavior} permission ${plural(ruleValues.length, 'rule')}`
|
||||
|
||||
return (
|
||||
<Dialog title={title} onCancel={onCancel} color="permission">
|
||||
<Box flexDirection="column" paddingX={2}>
|
||||
{ruleValues.map(ruleValue => (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
key={permissionRuleValueToString(ruleValue)}
|
||||
>
|
||||
<Text bold>{permissionRuleValueToString(ruleValue)}</Text>
|
||||
<PermissionRuleDescription ruleValue={ruleValue} />
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
<Box flexDirection="column" marginY={1}>
|
||||
<Text>
|
||||
{ruleValues.length === 1
|
||||
? 'Where should this rule be saved?'
|
||||
: 'Where should these rules be saved?'}
|
||||
</Text>
|
||||
<Select options={allOptions} onChange={onSelect} />
|
||||
</Box>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,339 +1,292 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import figures from 'figures';
|
||||
import * as React from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useDebounceCallback } from 'usehooks-ts';
|
||||
import { addDirHelpMessage, validateDirectoryForWorkspace } from '../../../commands/add-dir/validation.js';
|
||||
import TextInput from '../../../components/TextInput.js';
|
||||
import type { KeyboardEvent } from '../../../ink/events/keyboard-event.js';
|
||||
import { Box, Text } from '../../../ink.js';
|
||||
import { useKeybinding } from '../../../keybindings/useKeybinding.js';
|
||||
import type { ToolPermissionContext } from '../../../Tool.js';
|
||||
import { getDirectoryCompletions } from '../../../utils/suggestions/directoryCompletion.js';
|
||||
import { ConfigurableShortcutHint } from '../../ConfigurableShortcutHint.js';
|
||||
import { Select } from '../../CustomSelect/select.js';
|
||||
import { Byline } from '../../design-system/Byline.js';
|
||||
import { Dialog } from '../../design-system/Dialog.js';
|
||||
import { KeyboardShortcutHint } from '../../design-system/KeyboardShortcutHint.js';
|
||||
import { PromptInputFooterSuggestions, type SuggestionItem } from '../../PromptInput/PromptInputFooterSuggestions.js';
|
||||
import figures from 'figures'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useDebounceCallback } from 'usehooks-ts'
|
||||
import {
|
||||
addDirHelpMessage,
|
||||
validateDirectoryForWorkspace,
|
||||
} from '../../../commands/add-dir/validation.js'
|
||||
import TextInput from '../../../components/TextInput.js'
|
||||
import type { KeyboardEvent } from '../../../ink/events/keyboard-event.js'
|
||||
import { Box, Text } from '../../../ink.js'
|
||||
import { useKeybinding } from '../../../keybindings/useKeybinding.js'
|
||||
import type { ToolPermissionContext } from '../../../Tool.js'
|
||||
import { getDirectoryCompletions } from '../../../utils/suggestions/directoryCompletion.js'
|
||||
import { ConfigurableShortcutHint } from '../../ConfigurableShortcutHint.js'
|
||||
import { Select } from '../../CustomSelect/select.js'
|
||||
import { Byline } from '../../design-system/Byline.js'
|
||||
import { Dialog } from '../../design-system/Dialog.js'
|
||||
import { KeyboardShortcutHint } from '../../design-system/KeyboardShortcutHint.js'
|
||||
import {
|
||||
PromptInputFooterSuggestions,
|
||||
type SuggestionItem,
|
||||
} from '../../PromptInput/PromptInputFooterSuggestions.js'
|
||||
|
||||
type Props = {
|
||||
onAddDirectory: (path: string, remember?: boolean) => void;
|
||||
onCancel: () => void;
|
||||
permissionContext: ToolPermissionContext;
|
||||
directoryPath?: string; // When directoryPath is provided, show selection options instead of input
|
||||
};
|
||||
type RememberDirectoryOption = 'yes-session' | 'yes-remember' | 'no';
|
||||
onAddDirectory: (path: string, remember?: boolean) => void
|
||||
onCancel: () => void
|
||||
permissionContext: ToolPermissionContext
|
||||
directoryPath?: string // When directoryPath is provided, show selection options instead of input
|
||||
}
|
||||
|
||||
type RememberDirectoryOption = 'yes-session' | 'yes-remember' | 'no'
|
||||
|
||||
const REMEMBER_DIRECTORY_OPTIONS: Array<{
|
||||
value: RememberDirectoryOption;
|
||||
label: string;
|
||||
}> = [{
|
||||
value: RememberDirectoryOption
|
||||
label: string
|
||||
}> = [
|
||||
{
|
||||
value: 'yes-session',
|
||||
label: 'Yes, for this session'
|
||||
}, {
|
||||
label: 'Yes, for this session',
|
||||
},
|
||||
{
|
||||
value: 'yes-remember',
|
||||
label: 'Yes, and remember this directory'
|
||||
}, {
|
||||
label: 'Yes, and remember this directory',
|
||||
},
|
||||
{
|
||||
value: 'no',
|
||||
label: 'No'
|
||||
}];
|
||||
function PermissionDescription() {
|
||||
const $ = _c(1);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = <Text dimColor={true}>Claude Code will be able to read files in this directory and make edits when auto-accept edits is on.</Text>;
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
label: 'No',
|
||||
},
|
||||
]
|
||||
|
||||
function PermissionDescription(): React.ReactNode {
|
||||
return (
|
||||
<Text dimColor>
|
||||
Claude Code will be able to read files in this directory and make edits
|
||||
when auto-accept edits is on.
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
function DirectoryDisplay(t0) {
|
||||
const $ = _c(5);
|
||||
const {
|
||||
path
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] !== path) {
|
||||
t1 = <Text color="permission">{path}</Text>;
|
||||
$[0] = path;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
let t2;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <PermissionDescription />;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
let t3;
|
||||
if ($[3] !== t1) {
|
||||
t3 = <Box flexDirection="column" paddingX={2} gap={1}>{t1}{t2}</Box>;
|
||||
$[3] = t1;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
return t3;
|
||||
|
||||
function DirectoryDisplay({ path }: { path: string }): React.ReactNode {
|
||||
return (
|
||||
<Box flexDirection="column" paddingX={2} gap={1}>
|
||||
<Text color="permission">{path}</Text>
|
||||
<PermissionDescription />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
function DirectoryInput(t0) {
|
||||
const $ = _c(14);
|
||||
const {
|
||||
|
||||
function DirectoryInput({
|
||||
value,
|
||||
onChange,
|
||||
onSubmit,
|
||||
error,
|
||||
suggestions,
|
||||
selectedSuggestion
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <Text>Enter the path to the directory:</Text>;
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
let t2;
|
||||
if ($[1] !== onChange || $[2] !== onSubmit || $[3] !== value) {
|
||||
t2 = <Box borderDimColor={true} borderStyle="round" marginY={1} paddingLeft={1}><TextInput showCursor={true} placeholder={`Directory path${figures.ellipsis}`} value={value} onChange={onChange} onSubmit={onSubmit} columns={80} cursorOffset={value.length} onChangeCursorOffset={_temp} /></Box>;
|
||||
$[1] = onChange;
|
||||
$[2] = onSubmit;
|
||||
$[3] = value;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
let t3;
|
||||
if ($[5] !== selectedSuggestion || $[6] !== suggestions) {
|
||||
t3 = suggestions.length > 0 && <Box marginBottom={1}><PromptInputFooterSuggestions suggestions={suggestions} selectedSuggestion={selectedSuggestion} /></Box>;
|
||||
$[5] = selectedSuggestion;
|
||||
$[6] = suggestions;
|
||||
$[7] = t3;
|
||||
} else {
|
||||
t3 = $[7];
|
||||
}
|
||||
let t4;
|
||||
if ($[8] !== error) {
|
||||
t4 = error && <Text color="error">{error}</Text>;
|
||||
$[8] = error;
|
||||
$[9] = t4;
|
||||
} else {
|
||||
t4 = $[9];
|
||||
}
|
||||
let t5;
|
||||
if ($[10] !== t2 || $[11] !== t3 || $[12] !== t4) {
|
||||
t5 = <Box flexDirection="column">{t1}{t2}{t3}{t4}</Box>;
|
||||
$[10] = t2;
|
||||
$[11] = t3;
|
||||
$[12] = t4;
|
||||
$[13] = t5;
|
||||
} else {
|
||||
t5 = $[13];
|
||||
}
|
||||
return t5;
|
||||
selectedSuggestion,
|
||||
}: {
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
onSubmit: (value: string) => void
|
||||
error: string | null
|
||||
suggestions: SuggestionItem[]
|
||||
selectedSuggestion: number
|
||||
}): React.ReactNode {
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text>Enter the path to the directory:</Text>
|
||||
<Box borderDimColor borderStyle="round" marginY={1} paddingLeft={1}>
|
||||
<TextInput
|
||||
showCursor
|
||||
placeholder={`Directory path${figures.ellipsis}`}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onSubmit={onSubmit}
|
||||
columns={80}
|
||||
cursorOffset={value.length}
|
||||
onChangeCursorOffset={() => {}}
|
||||
/>
|
||||
</Box>
|
||||
{suggestions.length > 0 && (
|
||||
<Box marginBottom={1}>
|
||||
<PromptInputFooterSuggestions
|
||||
suggestions={suggestions}
|
||||
selectedSuggestion={selectedSuggestion}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
{error && <Text color="error">{error}</Text>}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
function _temp() {}
|
||||
export function AddWorkspaceDirectory(t0) {
|
||||
const $ = _c(34);
|
||||
const {
|
||||
|
||||
export function AddWorkspaceDirectory({
|
||||
onAddDirectory,
|
||||
onCancel,
|
||||
permissionContext,
|
||||
directoryPath
|
||||
} = t0;
|
||||
const [directoryInput, setDirectoryInput] = useState("");
|
||||
const [error, setError] = useState(null);
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = [];
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
const [suggestions, setSuggestions] = useState(t1);
|
||||
const [selectedSuggestion, setSelectedSuggestion] = useState(0);
|
||||
let t2;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = async path => {
|
||||
directoryPath,
|
||||
}: Props): React.ReactNode {
|
||||
const [directoryInput, setDirectoryInput] = useState('')
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [suggestions, setSuggestions] = useState<SuggestionItem[]>([])
|
||||
const [selectedSuggestion, setSelectedSuggestion] = useState(0)
|
||||
const options = useMemo(() => REMEMBER_DIRECTORY_OPTIONS, [])
|
||||
|
||||
// Fetch directory completions
|
||||
const fetchSuggestions = useCallback(async (path: string) => {
|
||||
if (!path) {
|
||||
setSuggestions([]);
|
||||
setSelectedSuggestion(0);
|
||||
return;
|
||||
setSuggestions([])
|
||||
setSelectedSuggestion(0)
|
||||
return
|
||||
}
|
||||
const completions = await getDirectoryCompletions(path);
|
||||
setSuggestions(completions);
|
||||
setSelectedSuggestion(0);
|
||||
};
|
||||
$[1] = t2;
|
||||
const completions = await getDirectoryCompletions(path)
|
||||
setSuggestions(completions)
|
||||
setSelectedSuggestion(0)
|
||||
}, [])
|
||||
|
||||
const debouncedFetchSuggestions = useDebounceCallback(fetchSuggestions, 100)
|
||||
|
||||
useEffect(() => {
|
||||
void debouncedFetchSuggestions(directoryInput)
|
||||
}, [directoryInput, debouncedFetchSuggestions])
|
||||
|
||||
const applySuggestion = useCallback((suggestion: SuggestionItem) => {
|
||||
const newPath = suggestion.id + '/'
|
||||
setDirectoryInput(newPath)
|
||||
setError(null)
|
||||
// Suggestions will update via the useEffect
|
||||
}, [])
|
||||
|
||||
// Handle directory submission from input
|
||||
const handleSubmit = useCallback(
|
||||
async (newPath: string) => {
|
||||
const result = await validateDirectoryForWorkspace(
|
||||
newPath,
|
||||
permissionContext,
|
||||
)
|
||||
|
||||
if (result.resultType === 'success') {
|
||||
onAddDirectory(result.absolutePath, false)
|
||||
} else {
|
||||
t2 = $[1];
|
||||
setError(addDirHelpMessage(result))
|
||||
}
|
||||
const fetchSuggestions = t2;
|
||||
const debouncedFetchSuggestions = useDebounceCallback(fetchSuggestions, 100);
|
||||
let t3;
|
||||
let t4;
|
||||
if ($[2] !== debouncedFetchSuggestions || $[3] !== directoryInput) {
|
||||
t3 = () => {
|
||||
debouncedFetchSuggestions(directoryInput);
|
||||
};
|
||||
t4 = [directoryInput, debouncedFetchSuggestions];
|
||||
$[2] = debouncedFetchSuggestions;
|
||||
$[3] = directoryInput;
|
||||
$[4] = t3;
|
||||
$[5] = t4;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
t4 = $[5];
|
||||
}
|
||||
useEffect(t3, t4);
|
||||
let t5;
|
||||
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t5 = suggestion => {
|
||||
const newPath = suggestion.id + "/";
|
||||
setDirectoryInput(newPath);
|
||||
setError(null);
|
||||
};
|
||||
$[6] = t5;
|
||||
} else {
|
||||
t5 = $[6];
|
||||
}
|
||||
const applySuggestion = t5;
|
||||
let t6;
|
||||
if ($[7] !== onAddDirectory || $[8] !== permissionContext) {
|
||||
t6 = async newPath_0 => {
|
||||
const result = await validateDirectoryForWorkspace(newPath_0, permissionContext);
|
||||
if (result.resultType === "success") {
|
||||
onAddDirectory(result.absolutePath, false);
|
||||
} else {
|
||||
setError(addDirHelpMessage(result));
|
||||
}
|
||||
};
|
||||
$[7] = onAddDirectory;
|
||||
$[8] = permissionContext;
|
||||
$[9] = t6;
|
||||
} else {
|
||||
t6 = $[9];
|
||||
}
|
||||
const handleSubmit = t6;
|
||||
let t7;
|
||||
if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t7 = {
|
||||
context: "Settings"
|
||||
};
|
||||
$[10] = t7;
|
||||
} else {
|
||||
t7 = $[10];
|
||||
}
|
||||
useKeybinding("confirm:no", onCancel, t7);
|
||||
let t8;
|
||||
if ($[11] !== handleSubmit || $[12] !== selectedSuggestion || $[13] !== suggestions) {
|
||||
t8 = e => {
|
||||
},
|
||||
[permissionContext, onAddDirectory],
|
||||
)
|
||||
|
||||
// Handle Esc to cancel (Ctrl+C handled by global keybindings)
|
||||
// Use Settings context so 'n' key doesn't cancel (allows typing 'n' in input)
|
||||
useKeybinding('confirm:no', onCancel, { context: 'Settings' })
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (suggestions.length > 0) {
|
||||
if (e.key === "tab") {
|
||||
e.preventDefault();
|
||||
const suggestion_0 = suggestions[selectedSuggestion];
|
||||
if (suggestion_0) {
|
||||
applySuggestion(suggestion_0);
|
||||
// Tab: accept selected suggestion and continue (for drilling into subdirs)
|
||||
if (e.key === 'tab') {
|
||||
e.preventDefault()
|
||||
const suggestion = suggestions[selectedSuggestion]
|
||||
if (suggestion) {
|
||||
applySuggestion(suggestion)
|
||||
}
|
||||
return;
|
||||
return
|
||||
}
|
||||
if (e.key === "return") {
|
||||
e.preventDefault();
|
||||
const suggestion_1 = suggestions[selectedSuggestion];
|
||||
if (suggestion_1) {
|
||||
handleSubmit(suggestion_1.id + "/");
|
||||
|
||||
// Enter: apply selected suggestion and submit
|
||||
if (e.key === 'return') {
|
||||
e.preventDefault()
|
||||
const suggestion = suggestions[selectedSuggestion]
|
||||
if (suggestion) {
|
||||
void handleSubmit(suggestion.id + '/')
|
||||
}
|
||||
return;
|
||||
return
|
||||
}
|
||||
if (e.key === "up" || e.ctrl && e.key === "p") {
|
||||
e.preventDefault();
|
||||
setSelectedSuggestion(prev => prev <= 0 ? suggestions.length - 1 : prev - 1);
|
||||
return;
|
||||
|
||||
if (e.key === 'up' || (e.ctrl && e.key === 'p')) {
|
||||
e.preventDefault()
|
||||
setSelectedSuggestion(prev =>
|
||||
prev <= 0 ? suggestions.length - 1 : prev - 1,
|
||||
)
|
||||
return
|
||||
}
|
||||
if (e.key === "down" || e.ctrl && e.key === "n") {
|
||||
e.preventDefault();
|
||||
setSelectedSuggestion(prev_0 => prev_0 >= suggestions.length - 1 ? 0 : prev_0 + 1);
|
||||
return;
|
||||
|
||||
if (e.key === 'down' || (e.ctrl && e.key === 'n')) {
|
||||
e.preventDefault()
|
||||
setSelectedSuggestion(prev =>
|
||||
prev >= suggestions.length - 1 ? 0 : prev + 1,
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
};
|
||||
$[11] = handleSubmit;
|
||||
$[12] = selectedSuggestion;
|
||||
$[13] = suggestions;
|
||||
$[14] = t8;
|
||||
} else {
|
||||
t8 = $[14];
|
||||
},
|
||||
[suggestions, selectedSuggestion, applySuggestion, handleSubmit],
|
||||
)
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(value: string) => {
|
||||
if (!directoryPath) return
|
||||
|
||||
const selectionValue = value as RememberDirectoryOption
|
||||
|
||||
switch (selectionValue) {
|
||||
case 'yes-session':
|
||||
onAddDirectory(directoryPath, false)
|
||||
break
|
||||
case 'yes-remember':
|
||||
onAddDirectory(directoryPath, true)
|
||||
break
|
||||
case 'no':
|
||||
onCancel()
|
||||
break
|
||||
}
|
||||
const handleKeyDown = t8;
|
||||
let t9;
|
||||
if ($[15] !== directoryPath || $[16] !== onAddDirectory || $[17] !== onCancel) {
|
||||
t9 = value => {
|
||||
if (!directoryPath) {
|
||||
return;
|
||||
},
|
||||
[directoryPath, onAddDirectory, onCancel],
|
||||
)
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
tabIndex={0}
|
||||
autoFocus
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<Dialog
|
||||
title="Add directory to workspace"
|
||||
onCancel={onCancel}
|
||||
color="permission"
|
||||
isCancelActive={false}
|
||||
inputGuide={
|
||||
directoryPath
|
||||
? undefined
|
||||
: exitState =>
|
||||
exitState.pending ? (
|
||||
<Text>Press {exitState.keyName} again to exit</Text>
|
||||
) : (
|
||||
<Byline>
|
||||
<KeyboardShortcutHint shortcut="Tab" action="complete" />
|
||||
<KeyboardShortcutHint shortcut="Enter" action="add" />
|
||||
<ConfigurableShortcutHint
|
||||
action="confirm:no"
|
||||
context="Settings"
|
||||
fallback="Esc"
|
||||
description="cancel"
|
||||
/>
|
||||
</Byline>
|
||||
)
|
||||
}
|
||||
const selectionValue = value as RememberDirectoryOption;
|
||||
bb64: switch (selectionValue) {
|
||||
case "yes-session":
|
||||
{
|
||||
onAddDirectory(directoryPath, false);
|
||||
break bb64;
|
||||
}
|
||||
case "yes-remember":
|
||||
{
|
||||
onAddDirectory(directoryPath, true);
|
||||
break bb64;
|
||||
}
|
||||
case "no":
|
||||
{
|
||||
onCancel();
|
||||
}
|
||||
}
|
||||
};
|
||||
$[15] = directoryPath;
|
||||
$[16] = onAddDirectory;
|
||||
$[17] = onCancel;
|
||||
$[18] = t9;
|
||||
} else {
|
||||
t9 = $[18];
|
||||
}
|
||||
const handleSelect = t9;
|
||||
const t10 = directoryPath ? undefined : _temp2;
|
||||
let t11;
|
||||
if ($[19] !== directoryInput || $[20] !== directoryPath || $[21] !== error || $[22] !== handleSelect || $[23] !== handleSubmit || $[24] !== selectedSuggestion || $[25] !== suggestions) {
|
||||
t11 = directoryPath ? <Box flexDirection="column" gap={1}><DirectoryDisplay path={directoryPath} /><Select options={REMEMBER_DIRECTORY_OPTIONS} onChange={handleSelect} onCancel={() => handleSelect("no")} /></Box> : <Box flexDirection="column" gap={1} marginX={2}><PermissionDescription /><DirectoryInput value={directoryInput} onChange={setDirectoryInput} onSubmit={handleSubmit} error={error} suggestions={suggestions} selectedSuggestion={selectedSuggestion} /></Box>;
|
||||
$[19] = directoryInput;
|
||||
$[20] = directoryPath;
|
||||
$[21] = error;
|
||||
$[22] = handleSelect;
|
||||
$[23] = handleSubmit;
|
||||
$[24] = selectedSuggestion;
|
||||
$[25] = suggestions;
|
||||
$[26] = t11;
|
||||
} else {
|
||||
t11 = $[26];
|
||||
}
|
||||
let t12;
|
||||
if ($[27] !== onCancel || $[28] !== t10 || $[29] !== t11) {
|
||||
t12 = <Dialog title="Add directory to workspace" onCancel={onCancel} color="permission" isCancelActive={false} inputGuide={t10}>{t11}</Dialog>;
|
||||
$[27] = onCancel;
|
||||
$[28] = t10;
|
||||
$[29] = t11;
|
||||
$[30] = t12;
|
||||
} else {
|
||||
t12 = $[30];
|
||||
}
|
||||
let t13;
|
||||
if ($[31] !== handleKeyDown || $[32] !== t12) {
|
||||
t13 = <Box flexDirection="column" tabIndex={0} autoFocus={true} onKeyDown={handleKeyDown}>{t12}</Box>;
|
||||
$[31] = handleKeyDown;
|
||||
$[32] = t12;
|
||||
$[33] = t13;
|
||||
} else {
|
||||
t13 = $[33];
|
||||
}
|
||||
return t13;
|
||||
}
|
||||
function _temp2(exitState) {
|
||||
return exitState.pending ? <Text>Press {exitState.keyName} again to exit</Text> : <Byline><KeyboardShortcutHint shortcut="Tab" action="complete" /><KeyboardShortcutHint shortcut="Enter" action="add" /><ConfigurableShortcutHint action="confirm:no" context="Settings" fallback="Esc" description="cancel" /></Byline>;
|
||||
>
|
||||
{directoryPath ? (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<DirectoryDisplay path={directoryPath} />
|
||||
<Select
|
||||
options={options}
|
||||
onChange={handleSelect}
|
||||
onCancel={() => handleSelect('no')}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<Box flexDirection="column" gap={1} marginX={2}>
|
||||
<PermissionDescription />
|
||||
<DirectoryInput
|
||||
value={directoryInput}
|
||||
onChange={setDirectoryInput}
|
||||
onSubmit={handleSubmit}
|
||||
error={error}
|
||||
suggestions={suggestions}
|
||||
selectedSuggestion={selectedSuggestion}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Dialog>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,74 +1,45 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { Text } from '../../../ink.js';
|
||||
import { BashTool } from '../../../tools/BashTool/BashTool.js';
|
||||
import type { PermissionRuleValue } from '../../../utils/permissions/PermissionRule.js';
|
||||
import * as React from 'react'
|
||||
import { Text } from '../../../ink.js'
|
||||
import { BashTool } from '../../../tools/BashTool/BashTool.js'
|
||||
import type { PermissionRuleValue } from '../../../utils/permissions/PermissionRule.js'
|
||||
|
||||
type RuleSubtitleProps = {
|
||||
ruleValue: PermissionRuleValue;
|
||||
};
|
||||
export function PermissionRuleDescription(t0) {
|
||||
const $ = _c(9);
|
||||
const {
|
||||
ruleValue
|
||||
} = t0;
|
||||
ruleValue: PermissionRuleValue
|
||||
}
|
||||
|
||||
export function PermissionRuleDescription({
|
||||
ruleValue,
|
||||
}: RuleSubtitleProps): React.ReactNode {
|
||||
switch (ruleValue.toolName) {
|
||||
case BashTool.name:
|
||||
{
|
||||
case BashTool.name: {
|
||||
if (ruleValue.ruleContent) {
|
||||
if (ruleValue.ruleContent.endsWith(":*")) {
|
||||
let t1;
|
||||
if ($[0] !== ruleValue.ruleContent) {
|
||||
t1 = ruleValue.ruleContent.slice(0, -2);
|
||||
$[0] = ruleValue.ruleContent;
|
||||
$[1] = t1;
|
||||
if (ruleValue.ruleContent.endsWith(':*')) {
|
||||
return (
|
||||
<Text dimColor>
|
||||
Any Bash command starting with{' '}
|
||||
<Text bold>{ruleValue.ruleContent.slice(0, -2)}</Text>
|
||||
</Text>
|
||||
)
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
let t2;
|
||||
if ($[2] !== t1) {
|
||||
t2 = <Text dimColor={true}>Any Bash command starting with{" "}<Text bold={true}>{t1}</Text></Text>;
|
||||
$[2] = t1;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
return t2;
|
||||
} else {
|
||||
let t1;
|
||||
if ($[4] !== ruleValue.ruleContent) {
|
||||
t1 = <Text dimColor={true}>The Bash command <Text bold={true}>{ruleValue.ruleContent}</Text></Text>;
|
||||
$[4] = ruleValue.ruleContent;
|
||||
$[5] = t1;
|
||||
} else {
|
||||
t1 = $[5];
|
||||
}
|
||||
return t1;
|
||||
return (
|
||||
<Text dimColor>
|
||||
The Bash command <Text bold>{ruleValue.ruleContent}</Text>
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
} else {
|
||||
let t1;
|
||||
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <Text dimColor={true}>Any Bash command</Text>;
|
||||
$[6] = t1;
|
||||
} else {
|
||||
t1 = $[6];
|
||||
}
|
||||
return t1;
|
||||
return <Text dimColor>Any Bash command</Text>
|
||||
}
|
||||
}
|
||||
default:
|
||||
{
|
||||
default: {
|
||||
if (!ruleValue.ruleContent) {
|
||||
let t1;
|
||||
if ($[7] !== ruleValue.toolName) {
|
||||
t1 = <Text dimColor={true}>Any use of the <Text bold={true}>{ruleValue.toolName}</Text> tool</Text>;
|
||||
$[7] = ruleValue.toolName;
|
||||
$[8] = t1;
|
||||
return (
|
||||
<Text dimColor>
|
||||
Any use of the <Text bold>{ruleValue.toolName}</Text> tool
|
||||
</Text>
|
||||
)
|
||||
} else {
|
||||
t1 = $[8];
|
||||
}
|
||||
return t1;
|
||||
} else {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,137 +1,107 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import figures from 'figures';
|
||||
import * as React from 'react';
|
||||
import { useState } from 'react';
|
||||
import TextInput from '../../../components/TextInput.js';
|
||||
import { useExitOnCtrlCDWithKeybindings } from '../../../hooks/useExitOnCtrlCDWithKeybindings.js';
|
||||
import { useTerminalSize } from '../../../hooks/useTerminalSize.js';
|
||||
import { Box, Newline, Text } from '../../../ink.js';
|
||||
import { useKeybinding } from '../../../keybindings/useKeybinding.js';
|
||||
import { BashTool } from '../../../tools/BashTool/BashTool.js';
|
||||
import { WebFetchTool } from '../../../tools/WebFetchTool/WebFetchTool.js';
|
||||
import type { PermissionBehavior, PermissionRuleValue } from '../../../utils/permissions/PermissionRule.js';
|
||||
import { permissionRuleValueFromString, permissionRuleValueToString } from '../../../utils/permissions/permissionRuleParser.js';
|
||||
import figures from 'figures'
|
||||
import * as React from 'react'
|
||||
import { useState } from 'react'
|
||||
import TextInput from '../../../components/TextInput.js'
|
||||
import { useExitOnCtrlCDWithKeybindings } from '../../../hooks/useExitOnCtrlCDWithKeybindings.js'
|
||||
import { useTerminalSize } from '../../../hooks/useTerminalSize.js'
|
||||
import { Box, Newline, Text } from '../../../ink.js'
|
||||
import { useKeybinding } from '../../../keybindings/useKeybinding.js'
|
||||
import { BashTool } from '../../../tools/BashTool/BashTool.js'
|
||||
import { WebFetchTool } from '../../../tools/WebFetchTool/WebFetchTool.js'
|
||||
import type {
|
||||
PermissionBehavior,
|
||||
PermissionRuleValue,
|
||||
} from '../../../utils/permissions/PermissionRule.js'
|
||||
import {
|
||||
permissionRuleValueFromString,
|
||||
permissionRuleValueToString,
|
||||
} from '../../../utils/permissions/permissionRuleParser.js'
|
||||
|
||||
export type PermissionRuleInputProps = {
|
||||
onCancel: () => void;
|
||||
onSubmit: (ruleValue: PermissionRuleValue, ruleBehavior: PermissionBehavior) => void;
|
||||
ruleBehavior: PermissionBehavior;
|
||||
};
|
||||
export function PermissionRuleInput(t0) {
|
||||
const $ = _c(24);
|
||||
const {
|
||||
onCancel: () => void
|
||||
onSubmit: (
|
||||
ruleValue: PermissionRuleValue,
|
||||
ruleBehavior: PermissionBehavior,
|
||||
) => void
|
||||
ruleBehavior: PermissionBehavior
|
||||
}
|
||||
|
||||
export function PermissionRuleInput({
|
||||
onCancel,
|
||||
onSubmit,
|
||||
ruleBehavior
|
||||
} = t0;
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const [cursorOffset, setCursorOffset] = useState(0);
|
||||
const exitState = useExitOnCtrlCDWithKeybindings();
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = {
|
||||
context: "Settings"
|
||||
};
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
useKeybinding("confirm:no", onCancel, t1);
|
||||
const {
|
||||
columns
|
||||
} = useTerminalSize();
|
||||
const textInputColumns = columns - 6;
|
||||
let t2;
|
||||
if ($[1] !== onSubmit || $[2] !== ruleBehavior) {
|
||||
t2 = value => {
|
||||
const trimmedValue = value.trim();
|
||||
ruleBehavior,
|
||||
}: PermissionRuleInputProps): React.ReactNode {
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
const [cursorOffset, setCursorOffset] = useState(0)
|
||||
const exitState = useExitOnCtrlCDWithKeybindings()
|
||||
|
||||
// Use configurable keybinding for ESC to cancel
|
||||
// Use Settings context so 'n' key doesn't cancel (allows typing 'n' in input)
|
||||
useKeybinding('confirm:no', onCancel, { context: 'Settings' })
|
||||
|
||||
const { columns } = useTerminalSize()
|
||||
const textInputColumns = columns - 6
|
||||
|
||||
const handleSubmit = (value: string) => {
|
||||
const trimmedValue = value.trim()
|
||||
if (trimmedValue.length === 0) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
const ruleValue = permissionRuleValueFromString(trimmedValue);
|
||||
onSubmit(ruleValue, ruleBehavior);
|
||||
};
|
||||
$[1] = onSubmit;
|
||||
$[2] = ruleBehavior;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
const ruleValue = permissionRuleValueFromString(trimmedValue)
|
||||
onSubmit(ruleValue, ruleBehavior)
|
||||
}
|
||||
const handleSubmit = t2;
|
||||
let t3;
|
||||
if ($[4] !== ruleBehavior) {
|
||||
t3 = <Text bold={true} color="permission">Add {ruleBehavior} permission rule</Text>;
|
||||
$[4] = ruleBehavior;
|
||||
$[5] = t3;
|
||||
} else {
|
||||
t3 = $[5];
|
||||
}
|
||||
let t4;
|
||||
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = <Newline />;
|
||||
$[6] = t4;
|
||||
} else {
|
||||
t4 = $[6];
|
||||
}
|
||||
let t5;
|
||||
let t6;
|
||||
if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t5 = <Text bold={true}>{permissionRuleValueToString({
|
||||
toolName: WebFetchTool.name
|
||||
})}</Text>;
|
||||
t6 = <Text bold={false}> or </Text>;
|
||||
$[7] = t5;
|
||||
$[8] = t6;
|
||||
} else {
|
||||
t5 = $[7];
|
||||
t6 = $[8];
|
||||
}
|
||||
let t7;
|
||||
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t7 = <Text>Permission rules are a tool name, optionally followed by a specifier in parentheses.{t4}e.g.,{" "}{t5}{t6}<Text bold={true}>{permissionRuleValueToString({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
flexDirection="column"
|
||||
gap={1}
|
||||
borderStyle="round"
|
||||
paddingLeft={1}
|
||||
paddingRight={1}
|
||||
borderColor="permission"
|
||||
>
|
||||
<Text bold color="permission">
|
||||
Add {ruleBehavior} permission rule
|
||||
</Text>
|
||||
<Box flexDirection="column">
|
||||
<Text>
|
||||
Permission rules are a tool name, optionally followed by a specifier
|
||||
in parentheses.
|
||||
<Newline />
|
||||
e.g.,{' '}
|
||||
<Text bold>
|
||||
{permissionRuleValueToString({ toolName: WebFetchTool.name })}
|
||||
</Text>
|
||||
<Text bold={false}> or </Text>
|
||||
<Text bold>
|
||||
{permissionRuleValueToString({
|
||||
toolName: BashTool.name,
|
||||
ruleContent: "ls:*"
|
||||
})}</Text></Text>;
|
||||
$[9] = t7;
|
||||
} else {
|
||||
t7 = $[9];
|
||||
}
|
||||
let t8;
|
||||
if ($[10] !== cursorOffset || $[11] !== handleSubmit || $[12] !== inputValue || $[13] !== textInputColumns) {
|
||||
t8 = <Box flexDirection="column">{t7}<Box borderDimColor={true} borderStyle="round" marginY={1} paddingLeft={1}><TextInput showCursor={true} value={inputValue} onChange={setInputValue} onSubmit={handleSubmit} placeholder={`Enter permission rule${figures.ellipsis}`} columns={textInputColumns} cursorOffset={cursorOffset} onChangeCursorOffset={setCursorOffset} /></Box></Box>;
|
||||
$[10] = cursorOffset;
|
||||
$[11] = handleSubmit;
|
||||
$[12] = inputValue;
|
||||
$[13] = textInputColumns;
|
||||
$[14] = t8;
|
||||
} else {
|
||||
t8 = $[14];
|
||||
}
|
||||
let t9;
|
||||
if ($[15] !== t3 || $[16] !== t8) {
|
||||
t9 = <Box flexDirection="column" gap={1} borderStyle="round" paddingLeft={1} paddingRight={1} borderColor="permission">{t3}{t8}</Box>;
|
||||
$[15] = t3;
|
||||
$[16] = t8;
|
||||
$[17] = t9;
|
||||
} else {
|
||||
t9 = $[17];
|
||||
}
|
||||
let t10;
|
||||
if ($[18] !== exitState.keyName || $[19] !== exitState.pending) {
|
||||
t10 = <Box marginLeft={3}>{exitState.pending ? <Text dimColor={true}>Press {exitState.keyName} again to exit</Text> : <Text dimColor={true}>Enter to submit · Esc to cancel</Text>}</Box>;
|
||||
$[18] = exitState.keyName;
|
||||
$[19] = exitState.pending;
|
||||
$[20] = t10;
|
||||
} else {
|
||||
t10 = $[20];
|
||||
}
|
||||
let t11;
|
||||
if ($[21] !== t10 || $[22] !== t9) {
|
||||
t11 = <>{t9}{t10}</>;
|
||||
$[21] = t10;
|
||||
$[22] = t9;
|
||||
$[23] = t11;
|
||||
} else {
|
||||
t11 = $[23];
|
||||
}
|
||||
return t11;
|
||||
ruleContent: 'ls:*',
|
||||
})}
|
||||
</Text>
|
||||
</Text>
|
||||
<Box borderDimColor borderStyle="round" marginY={1} paddingLeft={1}>
|
||||
<TextInput
|
||||
showCursor
|
||||
value={inputValue}
|
||||
onChange={setInputValue}
|
||||
onSubmit={handleSubmit}
|
||||
placeholder={`Enter permission rule${figures.ellipsis}`}
|
||||
columns={textInputColumns}
|
||||
cursorOffset={cursorOffset}
|
||||
onChangeCursorOffset={setCursorOffset}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box marginLeft={3}>
|
||||
{exitState.pending ? (
|
||||
<Text dimColor>Press {exitState.keyName} again to exit</Text>
|
||||
) : (
|
||||
<Text dimColor>Enter to submit · Esc to cancel</Text>
|
||||
)}
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,206 +1,118 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
// eslint-disable-next-line custom-rules/prefer-use-keybindings -- 'r' is a view-specific key, not a global keybinding
|
||||
import { Box, Text, useInput } from '../../../ink.js';
|
||||
import { type AutoModeDenial, getAutoModeDenials } from '../../../utils/autoModeDenials.js';
|
||||
import { Select } from '../../CustomSelect/select.js';
|
||||
import { StatusIcon } from '../../design-system/StatusIcon.js';
|
||||
import { useTabHeaderFocus } from '../../design-system/Tabs.js';
|
||||
import { Box, Text, useInput } from '../../../ink.js'
|
||||
import {
|
||||
type AutoModeDenial,
|
||||
getAutoModeDenials,
|
||||
} from '../../../utils/autoModeDenials.js'
|
||||
import { Select } from '../../CustomSelect/select.js'
|
||||
import { StatusIcon } from '../../design-system/StatusIcon.js'
|
||||
import { useTabHeaderFocus } from '../../design-system/Tabs.js'
|
||||
|
||||
type Props = {
|
||||
onHeaderFocusChange?: (focused: boolean) => void;
|
||||
onHeaderFocusChange?: (focused: boolean) => void
|
||||
/** Called when approved/retry state changes so parent can act on exit */
|
||||
onStateChange: (state: {
|
||||
approved: Set<number>;
|
||||
retry: Set<number>;
|
||||
denials: readonly AutoModeDenial[];
|
||||
}) => void;
|
||||
};
|
||||
export function RecentDenialsTab(t0) {
|
||||
const $ = _c(30);
|
||||
const {
|
||||
approved: Set<number>
|
||||
retry: Set<number>
|
||||
denials: readonly AutoModeDenial[]
|
||||
}) => void
|
||||
}
|
||||
|
||||
export function RecentDenialsTab({
|
||||
onHeaderFocusChange,
|
||||
onStateChange
|
||||
} = t0;
|
||||
const {
|
||||
headerFocused,
|
||||
focusHeader
|
||||
} = useTabHeaderFocus();
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== headerFocused || $[1] !== onHeaderFocusChange) {
|
||||
t1 = () => {
|
||||
onHeaderFocusChange?.(headerFocused);
|
||||
};
|
||||
t2 = [headerFocused, onHeaderFocusChange];
|
||||
$[0] = headerFocused;
|
||||
$[1] = onHeaderFocusChange;
|
||||
$[2] = t1;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
t2 = $[3];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
const [denials] = useState(_temp);
|
||||
const [approved, setApproved] = useState(_temp2);
|
||||
const [retry, setRetry] = useState(_temp3);
|
||||
const [focusedIdx, setFocusedIdx] = useState(0);
|
||||
let t3;
|
||||
let t4;
|
||||
if ($[4] !== approved || $[5] !== denials || $[6] !== onStateChange || $[7] !== retry) {
|
||||
t3 = () => {
|
||||
onStateChange({
|
||||
approved,
|
||||
retry,
|
||||
denials
|
||||
});
|
||||
};
|
||||
t4 = [approved, retry, denials, onStateChange];
|
||||
$[4] = approved;
|
||||
$[5] = denials;
|
||||
$[6] = onStateChange;
|
||||
$[7] = retry;
|
||||
$[8] = t3;
|
||||
$[9] = t4;
|
||||
} else {
|
||||
t3 = $[8];
|
||||
t4 = $[9];
|
||||
}
|
||||
useEffect(t3, t4);
|
||||
let t5;
|
||||
if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t5 = value => {
|
||||
const idx = Number(value);
|
||||
onStateChange,
|
||||
}: Props): React.ReactNode {
|
||||
const { headerFocused, focusHeader } = useTabHeaderFocus()
|
||||
useEffect(() => {
|
||||
onHeaderFocusChange?.(headerFocused)
|
||||
}, [headerFocused, onHeaderFocusChange])
|
||||
|
||||
// Snapshot on mount — approved/retry Sets key by index, and the live store
|
||||
// prepends. A concurrent denial would shift all indices mid-edit.
|
||||
const [denials] = useState(() => getAutoModeDenials())
|
||||
|
||||
const [approved, setApproved] = useState<Set<number>>(() => new Set())
|
||||
const [retry, setRetry] = useState<Set<number>>(() => new Set())
|
||||
const [focusedIdx, setFocusedIdx] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
onStateChange({ approved, retry, denials })
|
||||
}, [approved, retry, denials, onStateChange])
|
||||
|
||||
const handleSelect = useCallback((value: string) => {
|
||||
const idx = Number(value)
|
||||
setApproved(prev => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(idx)) {
|
||||
next.delete(idx);
|
||||
} else {
|
||||
next.add(idx);
|
||||
const next = new Set(prev)
|
||||
if (next.has(idx)) next.delete(idx)
|
||||
else next.add(idx)
|
||||
return next
|
||||
})
|
||||
}, [])
|
||||
|
||||
const handleFocus = useCallback((value: string) => {
|
||||
setFocusedIdx(Number(value))
|
||||
}, [])
|
||||
|
||||
useInput(
|
||||
(input, _key) => {
|
||||
if (input === 'r') {
|
||||
setRetry(prev => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(focusedIdx)) next.delete(focusedIdx)
|
||||
else next.add(focusedIdx)
|
||||
return next
|
||||
})
|
||||
// Retry implies approve
|
||||
setApproved(prev => {
|
||||
if (prev.has(focusedIdx)) return prev
|
||||
const next = new Set(prev)
|
||||
next.add(focusedIdx)
|
||||
return next
|
||||
})
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
$[10] = t5;
|
||||
} else {
|
||||
t5 = $[10];
|
||||
}
|
||||
const handleSelect = t5;
|
||||
let t6;
|
||||
if ($[11] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t6 = value_0 => {
|
||||
setFocusedIdx(Number(value_0));
|
||||
};
|
||||
$[11] = t6;
|
||||
} else {
|
||||
t6 = $[11];
|
||||
}
|
||||
const handleFocus = t6;
|
||||
let t7;
|
||||
if ($[12] !== focusedIdx) {
|
||||
t7 = (input, _key) => {
|
||||
if (input === "r") {
|
||||
setRetry(prev_0 => {
|
||||
const next_0 = new Set(prev_0);
|
||||
if (next_0.has(focusedIdx)) {
|
||||
next_0.delete(focusedIdx);
|
||||
} else {
|
||||
next_0.add(focusedIdx);
|
||||
}
|
||||
return next_0;
|
||||
});
|
||||
setApproved(prev_1 => {
|
||||
if (prev_1.has(focusedIdx)) {
|
||||
return prev_1;
|
||||
}
|
||||
const next_1 = new Set(prev_1);
|
||||
next_1.add(focusedIdx);
|
||||
return next_1;
|
||||
});
|
||||
}
|
||||
};
|
||||
$[12] = focusedIdx;
|
||||
$[13] = t7;
|
||||
} else {
|
||||
t7 = $[13];
|
||||
}
|
||||
const t8 = denials.length > 0;
|
||||
let t9;
|
||||
if ($[14] !== t8) {
|
||||
t9 = {
|
||||
isActive: t8
|
||||
};
|
||||
$[14] = t8;
|
||||
$[15] = t9;
|
||||
} else {
|
||||
t9 = $[15];
|
||||
}
|
||||
useInput(t7, t9);
|
||||
},
|
||||
{ isActive: denials.length > 0 },
|
||||
)
|
||||
|
||||
if (denials.length === 0) {
|
||||
let t10;
|
||||
if ($[16] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t10 = <Text dimColor={true}>No recent denials. Commands denied by the auto mode classifier will appear here.</Text>;
|
||||
$[16] = t10;
|
||||
} else {
|
||||
t10 = $[16];
|
||||
return (
|
||||
<Text dimColor>
|
||||
No recent denials. Commands denied by the auto mode classifier will
|
||||
appear here.
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
return t10;
|
||||
}
|
||||
let t10;
|
||||
if ($[17] !== approved || $[18] !== denials || $[19] !== retry) {
|
||||
let t11;
|
||||
if ($[21] !== approved || $[22] !== retry) {
|
||||
t11 = (d, idx_0) => {
|
||||
const isApproved = approved.has(idx_0);
|
||||
const suffix = retry.has(idx_0) ? " (retry)" : "";
|
||||
|
||||
const options = denials.map((d, idx) => {
|
||||
const isApproved = approved.has(idx)
|
||||
const suffix = retry.has(idx) ? ' (retry)' : ''
|
||||
return {
|
||||
label: <Text><StatusIcon status={isApproved ? "success" : "error"} withSpace={true} />{d.display}<Text dimColor={true}>{suffix}</Text></Text>,
|
||||
value: String(idx_0)
|
||||
};
|
||||
};
|
||||
$[21] = approved;
|
||||
$[22] = retry;
|
||||
$[23] = t11;
|
||||
} else {
|
||||
t11 = $[23];
|
||||
label: (
|
||||
<Text>
|
||||
<StatusIcon status={isApproved ? 'success' : 'error'} withSpace />
|
||||
{d.display}
|
||||
<Text dimColor>{suffix}</Text>
|
||||
</Text>
|
||||
),
|
||||
value: String(idx),
|
||||
}
|
||||
t10 = denials.map(t11);
|
||||
$[17] = approved;
|
||||
$[18] = denials;
|
||||
$[19] = retry;
|
||||
$[20] = t10;
|
||||
} else {
|
||||
t10 = $[20];
|
||||
}
|
||||
const options = t10;
|
||||
let t11;
|
||||
if ($[24] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t11 = <Text>Commands recently denied by the auto mode classifier.</Text>;
|
||||
$[24] = t11;
|
||||
} else {
|
||||
t11 = $[24];
|
||||
}
|
||||
const t12 = Math.min(10, options.length);
|
||||
let t13;
|
||||
if ($[25] !== focusHeader || $[26] !== headerFocused || $[27] !== options || $[28] !== t12) {
|
||||
t13 = <Box flexDirection="column">{t11}<Box marginTop={1}><Select options={options} onChange={handleSelect} onFocus={handleFocus} visibleOptionCount={t12} isDisabled={headerFocused} onUpFromFirstItem={focusHeader} /></Box></Box>;
|
||||
$[25] = focusHeader;
|
||||
$[26] = headerFocused;
|
||||
$[27] = options;
|
||||
$[28] = t12;
|
||||
$[29] = t13;
|
||||
} else {
|
||||
t13 = $[29];
|
||||
}
|
||||
return t13;
|
||||
}
|
||||
function _temp3() {
|
||||
return new Set();
|
||||
}
|
||||
function _temp2() {
|
||||
return new Set();
|
||||
}
|
||||
function _temp() {
|
||||
return getAutoModeDenials();
|
||||
})
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text>Commands recently denied by the auto mode classifier.</Text>
|
||||
<Box marginTop={1}>
|
||||
<Select
|
||||
options={options}
|
||||
onChange={handleSelect}
|
||||
onFocus={handleFocus}
|
||||
visibleOptionCount={Math.min(10, options.length)}
|
||||
isDisabled={headerFocused}
|
||||
onUpFromFirstItem={focusHeader}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,109 +1,68 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { Select } from '../../../components/CustomSelect/select.js';
|
||||
import { Box, Text } from '../../../ink.js';
|
||||
import type { ToolPermissionContext } from '../../../Tool.js';
|
||||
import { applyPermissionUpdate } from '../../../utils/permissions/PermissionUpdate.js';
|
||||
import { Dialog } from '../../design-system/Dialog.js';
|
||||
import * as React from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { Select } from '../../../components/CustomSelect/select.js'
|
||||
import { Box, Text } from '../../../ink.js'
|
||||
import type { ToolPermissionContext } from '../../../Tool.js'
|
||||
import { applyPermissionUpdate } from '../../../utils/permissions/PermissionUpdate.js'
|
||||
import { Dialog } from '../../design-system/Dialog.js'
|
||||
|
||||
type Props = {
|
||||
directoryPath: string;
|
||||
onRemove: () => void;
|
||||
onCancel: () => void;
|
||||
permissionContext: ToolPermissionContext;
|
||||
setPermissionContext: (context: ToolPermissionContext) => void;
|
||||
};
|
||||
export function RemoveWorkspaceDirectory(t0) {
|
||||
const $ = _c(19);
|
||||
const {
|
||||
directoryPath: string
|
||||
onRemove: () => void
|
||||
onCancel: () => void
|
||||
permissionContext: ToolPermissionContext
|
||||
setPermissionContext: (context: ToolPermissionContext) => void
|
||||
}
|
||||
|
||||
export function RemoveWorkspaceDirectory({
|
||||
directoryPath,
|
||||
onRemove,
|
||||
onCancel,
|
||||
permissionContext,
|
||||
setPermissionContext
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] !== directoryPath || $[1] !== onRemove || $[2] !== permissionContext || $[3] !== setPermissionContext) {
|
||||
t1 = () => {
|
||||
setPermissionContext,
|
||||
}: Props): React.ReactNode {
|
||||
const handleRemove = useCallback(() => {
|
||||
const updatedContext = applyPermissionUpdate(permissionContext, {
|
||||
type: "removeDirectories",
|
||||
type: 'removeDirectories',
|
||||
directories: [directoryPath],
|
||||
destination: "session"
|
||||
});
|
||||
setPermissionContext(updatedContext);
|
||||
onRemove();
|
||||
};
|
||||
$[0] = directoryPath;
|
||||
$[1] = onRemove;
|
||||
$[2] = permissionContext;
|
||||
$[3] = setPermissionContext;
|
||||
$[4] = t1;
|
||||
destination: 'session',
|
||||
})
|
||||
|
||||
setPermissionContext(updatedContext)
|
||||
onRemove()
|
||||
}, [directoryPath, permissionContext, setPermissionContext, onRemove])
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(value: string) => {
|
||||
if (value === 'yes') {
|
||||
handleRemove()
|
||||
} else {
|
||||
t1 = $[4];
|
||||
onCancel()
|
||||
}
|
||||
const handleRemove = t1;
|
||||
let t2;
|
||||
if ($[5] !== handleRemove || $[6] !== onCancel) {
|
||||
t2 = value => {
|
||||
if (value === "yes") {
|
||||
handleRemove();
|
||||
} else {
|
||||
onCancel();
|
||||
}
|
||||
};
|
||||
$[5] = handleRemove;
|
||||
$[6] = onCancel;
|
||||
$[7] = t2;
|
||||
} else {
|
||||
t2 = $[7];
|
||||
}
|
||||
const handleSelect = t2;
|
||||
let t3;
|
||||
if ($[8] !== directoryPath) {
|
||||
t3 = <Box marginX={2} flexDirection="column"><Text bold={true}>{directoryPath}</Text></Box>;
|
||||
$[8] = directoryPath;
|
||||
$[9] = t3;
|
||||
} else {
|
||||
t3 = $[9];
|
||||
}
|
||||
let t4;
|
||||
if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = <Text>Claude Code will no longer have access to files in this directory.</Text>;
|
||||
$[10] = t4;
|
||||
} else {
|
||||
t4 = $[10];
|
||||
}
|
||||
let t5;
|
||||
if ($[11] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t5 = [{
|
||||
label: "Yes",
|
||||
value: "yes"
|
||||
}, {
|
||||
label: "No",
|
||||
value: "no"
|
||||
}];
|
||||
$[11] = t5;
|
||||
} else {
|
||||
t5 = $[11];
|
||||
}
|
||||
let t6;
|
||||
if ($[12] !== handleSelect || $[13] !== onCancel) {
|
||||
t6 = <Select onChange={handleSelect} onCancel={onCancel} options={t5} />;
|
||||
$[12] = handleSelect;
|
||||
$[13] = onCancel;
|
||||
$[14] = t6;
|
||||
} else {
|
||||
t6 = $[14];
|
||||
}
|
||||
let t7;
|
||||
if ($[15] !== onCancel || $[16] !== t3 || $[17] !== t6) {
|
||||
t7 = <Dialog title="Remove directory from workspace?" onCancel={onCancel} color="error">{t3}{t4}{t6}</Dialog>;
|
||||
$[15] = onCancel;
|
||||
$[16] = t3;
|
||||
$[17] = t6;
|
||||
$[18] = t7;
|
||||
} else {
|
||||
t7 = $[18];
|
||||
}
|
||||
return t7;
|
||||
},
|
||||
[handleRemove, onCancel],
|
||||
)
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
title="Remove directory from workspace?"
|
||||
onCancel={onCancel}
|
||||
color="error"
|
||||
>
|
||||
<Box marginX={2} flexDirection="column">
|
||||
<Text bold>{directoryPath}</Text>
|
||||
</Box>
|
||||
<Text>
|
||||
Claude Code will no longer have access to files in this directory.
|
||||
</Text>
|
||||
<Select
|
||||
onChange={handleSelect}
|
||||
onCancel={onCancel}
|
||||
options={[
|
||||
{ label: 'Yes', value: 'yes' },
|
||||
{ label: 'No', value: 'no' },
|
||||
]}
|
||||
/>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,149 +1,105 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import figures from 'figures';
|
||||
import * as React from 'react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { getOriginalCwd } from '../../../bootstrap/state.js';
|
||||
import type { CommandResultDisplay } from '../../../commands.js';
|
||||
import { Select } from '../../../components/CustomSelect/select.js';
|
||||
import { Box, Text } from '../../../ink.js';
|
||||
import type { ToolPermissionContext } from '../../../Tool.js';
|
||||
import { useTabHeaderFocus } from '../../design-system/Tabs.js';
|
||||
import figures from 'figures'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import { getOriginalCwd } from '../../../bootstrap/state.js'
|
||||
import type { CommandResultDisplay } from '../../../commands.js'
|
||||
import { Select } from '../../../components/CustomSelect/select.js'
|
||||
import { Box, Text } from '../../../ink.js'
|
||||
import type { ToolPermissionContext } from '../../../Tool.js'
|
||||
import { useTabHeaderFocus } from '../../design-system/Tabs.js'
|
||||
|
||||
type Props = {
|
||||
onExit: (result?: string, options?: {
|
||||
display?: CommandResultDisplay;
|
||||
}) => void;
|
||||
toolPermissionContext: ToolPermissionContext;
|
||||
onRequestAddDirectory: () => void;
|
||||
onRequestRemoveDirectory: (path: string) => void;
|
||||
onHeaderFocusChange?: (focused: boolean) => void;
|
||||
};
|
||||
onExit: (
|
||||
result?: string,
|
||||
options?: { display?: CommandResultDisplay },
|
||||
) => void
|
||||
toolPermissionContext: ToolPermissionContext
|
||||
onRequestAddDirectory: () => void
|
||||
onRequestRemoveDirectory: (path: string) => void
|
||||
onHeaderFocusChange?: (focused: boolean) => void
|
||||
}
|
||||
|
||||
type DirectoryItem = {
|
||||
path: string;
|
||||
isCurrent: boolean;
|
||||
isDeletable: boolean;
|
||||
};
|
||||
export function WorkspaceTab(t0) {
|
||||
const $ = _c(23);
|
||||
const {
|
||||
path: string
|
||||
isCurrent: boolean
|
||||
isDeletable: boolean
|
||||
}
|
||||
|
||||
export function WorkspaceTab({
|
||||
onExit,
|
||||
toolPermissionContext,
|
||||
onRequestAddDirectory,
|
||||
onRequestRemoveDirectory,
|
||||
onHeaderFocusChange
|
||||
} = t0;
|
||||
const {
|
||||
headerFocused,
|
||||
focusHeader
|
||||
} = useTabHeaderFocus();
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== headerFocused || $[1] !== onHeaderFocusChange) {
|
||||
t1 = () => {
|
||||
onHeaderFocusChange?.(headerFocused);
|
||||
};
|
||||
t2 = [headerFocused, onHeaderFocusChange];
|
||||
$[0] = headerFocused;
|
||||
$[1] = onHeaderFocusChange;
|
||||
$[2] = t1;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
t2 = $[3];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[4] !== toolPermissionContext.additionalWorkingDirectories) {
|
||||
t3 = Array.from(toolPermissionContext.additionalWorkingDirectories.keys()).map(_temp);
|
||||
$[4] = toolPermissionContext.additionalWorkingDirectories;
|
||||
$[5] = t3;
|
||||
} else {
|
||||
t3 = $[5];
|
||||
}
|
||||
const additionalDirectories = t3;
|
||||
let t4;
|
||||
if ($[6] !== additionalDirectories || $[7] !== onRequestAddDirectory || $[8] !== onRequestRemoveDirectory) {
|
||||
t4 = selectedValue => {
|
||||
if (selectedValue === "add-directory") {
|
||||
onRequestAddDirectory();
|
||||
return;
|
||||
}
|
||||
const directory = additionalDirectories.find(d => d.path === selectedValue);
|
||||
if (directory && directory.isDeletable) {
|
||||
onRequestRemoveDirectory(directory.path);
|
||||
}
|
||||
};
|
||||
$[6] = additionalDirectories;
|
||||
$[7] = onRequestAddDirectory;
|
||||
$[8] = onRequestRemoveDirectory;
|
||||
$[9] = t4;
|
||||
} else {
|
||||
t4 = $[9];
|
||||
}
|
||||
const handleDirectorySelect = t4;
|
||||
let t5;
|
||||
if ($[10] !== onExit) {
|
||||
t5 = () => onExit("Workspace dialog dismissed", {
|
||||
display: "system"
|
||||
});
|
||||
$[10] = onExit;
|
||||
$[11] = t5;
|
||||
} else {
|
||||
t5 = $[11];
|
||||
}
|
||||
const handleCancel = t5;
|
||||
let opts;
|
||||
if ($[12] !== additionalDirectories) {
|
||||
opts = additionalDirectories.map(_temp2);
|
||||
let t6;
|
||||
if ($[14] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t6 = {
|
||||
label: `Add directory${figures.ellipsis}`,
|
||||
value: "add-directory"
|
||||
};
|
||||
$[14] = t6;
|
||||
} else {
|
||||
t6 = $[14];
|
||||
}
|
||||
opts.push(t6);
|
||||
$[12] = additionalDirectories;
|
||||
$[13] = opts;
|
||||
} else {
|
||||
opts = $[13];
|
||||
}
|
||||
const options = opts;
|
||||
let t6;
|
||||
if ($[15] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t6 = <Box flexDirection="row" marginTop={1} marginLeft={2} gap={1}><Text>{`- ${getOriginalCwd()}`}</Text><Text dimColor={true}>(Original working directory)</Text></Box>;
|
||||
$[15] = t6;
|
||||
} else {
|
||||
t6 = $[15];
|
||||
}
|
||||
const t7 = Math.min(10, options.length);
|
||||
let t8;
|
||||
if ($[16] !== focusHeader || $[17] !== handleCancel || $[18] !== handleDirectorySelect || $[19] !== headerFocused || $[20] !== options || $[21] !== t7) {
|
||||
t8 = <Box flexDirection="column" marginBottom={1}>{t6}<Select options={options} onChange={handleDirectorySelect} onCancel={handleCancel} visibleOptionCount={t7} onUpFromFirstItem={focusHeader} isDisabled={headerFocused} /></Box>;
|
||||
$[16] = focusHeader;
|
||||
$[17] = handleCancel;
|
||||
$[18] = handleDirectorySelect;
|
||||
$[19] = headerFocused;
|
||||
$[20] = options;
|
||||
$[21] = t7;
|
||||
$[22] = t8;
|
||||
} else {
|
||||
t8 = $[22];
|
||||
}
|
||||
return t8;
|
||||
}
|
||||
function _temp2(dir) {
|
||||
return {
|
||||
label: dir.path,
|
||||
value: dir.path
|
||||
};
|
||||
}
|
||||
function _temp(path) {
|
||||
return {
|
||||
onHeaderFocusChange,
|
||||
}: Props): React.ReactNode {
|
||||
const { headerFocused, focusHeader } = useTabHeaderFocus()
|
||||
useEffect(() => {
|
||||
onHeaderFocusChange?.(headerFocused)
|
||||
}, [headerFocused, onHeaderFocusChange])
|
||||
// Get only additional workspace directories (not the current working directory)
|
||||
const additionalDirectories = React.useMemo((): DirectoryItem[] => {
|
||||
return Array.from(
|
||||
toolPermissionContext.additionalWorkingDirectories.keys(),
|
||||
).map(path => ({
|
||||
path,
|
||||
isCurrent: false,
|
||||
isDeletable: true
|
||||
};
|
||||
isDeletable: true,
|
||||
}))
|
||||
}, [toolPermissionContext.additionalWorkingDirectories])
|
||||
|
||||
const handleDirectorySelect = useCallback(
|
||||
(selectedValue: string) => {
|
||||
if (selectedValue === 'add-directory') {
|
||||
onRequestAddDirectory()
|
||||
return
|
||||
}
|
||||
|
||||
const directory = additionalDirectories.find(
|
||||
d => d.path === selectedValue,
|
||||
)
|
||||
if (directory && directory.isDeletable) {
|
||||
onRequestRemoveDirectory(directory.path)
|
||||
}
|
||||
},
|
||||
[additionalDirectories, onRequestAddDirectory, onRequestRemoveDirectory],
|
||||
)
|
||||
|
||||
const handleCancel = useCallback(
|
||||
() => onExit('Workspace dialog dismissed', { display: 'system' }),
|
||||
[onExit],
|
||||
)
|
||||
|
||||
// Main list view options
|
||||
const options = React.useMemo(() => {
|
||||
const opts = additionalDirectories.map(dir => ({
|
||||
label: dir.path,
|
||||
value: dir.path,
|
||||
}))
|
||||
|
||||
opts.push({
|
||||
label: `Add directory${figures.ellipsis}`,
|
||||
value: 'add-directory',
|
||||
})
|
||||
|
||||
return opts
|
||||
}, [additionalDirectories])
|
||||
|
||||
// Main list view
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
{/* Current working directory section */}
|
||||
<Box flexDirection="row" marginTop={1} marginLeft={2} gap={1}>
|
||||
<Text>{`- ${getOriginalCwd()}`}</Text>
|
||||
<Text dimColor>(Original working directory)</Text>
|
||||
</Box>
|
||||
<Select
|
||||
options={options}
|
||||
onChange={handleDirectorySelect}
|
||||
onCancel={handleCancel}
|
||||
visibleOptionCount={Math.min(10, options.length)}
|
||||
onUpFromFirstItem={focusHeader}
|
||||
isDisabled={headerFocused}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,59 +1,73 @@
|
||||
import { basename, sep } from 'path';
|
||||
import React, { type ReactNode } from 'react';
|
||||
import { getOriginalCwd } from '../../bootstrap/state.js';
|
||||
import { Text } from '../../ink.js';
|
||||
import type { PermissionUpdate } from '../../utils/permissions/PermissionUpdateSchema.js';
|
||||
import { permissionRuleExtractPrefix } from '../../utils/permissions/shellRuleMatching.js';
|
||||
import { basename, sep } from 'path'
|
||||
import React, { type ReactNode } from 'react'
|
||||
import { getOriginalCwd } from '../../bootstrap/state.js'
|
||||
import { Text } from '../../ink.js'
|
||||
import type { PermissionUpdate } from '../../utils/permissions/PermissionUpdateSchema.js'
|
||||
import { permissionRuleExtractPrefix } from '../../utils/permissions/shellRuleMatching.js'
|
||||
|
||||
function commandListDisplay(commands: string[]): ReactNode {
|
||||
switch (commands.length) {
|
||||
case 0:
|
||||
return '';
|
||||
return ''
|
||||
case 1:
|
||||
return <Text bold>{commands[0]}</Text>;
|
||||
return <Text bold>{commands[0]}</Text>
|
||||
case 2:
|
||||
return <Text>
|
||||
return (
|
||||
<Text>
|
||||
<Text bold>{commands[0]}</Text> and <Text bold>{commands[1]}</Text>
|
||||
</Text>;
|
||||
</Text>
|
||||
)
|
||||
default:
|
||||
return <Text>
|
||||
return (
|
||||
<Text>
|
||||
<Text bold>{commands.slice(0, -1).join(', ')}</Text>, and{' '}
|
||||
<Text bold>{commands.slice(-1)[0]}</Text>
|
||||
</Text>;
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function commandListDisplayTruncated(commands: string[]): ReactNode {
|
||||
// Check if the plain text representation would be too long
|
||||
const plainText = commands.join(', ');
|
||||
const plainText = commands.join(', ')
|
||||
if (plainText.length > 50) {
|
||||
return 'similar';
|
||||
return 'similar'
|
||||
}
|
||||
return commandListDisplay(commands);
|
||||
return commandListDisplay(commands)
|
||||
}
|
||||
|
||||
function formatPathList(paths: string[]): ReactNode {
|
||||
if (paths.length === 0) return '';
|
||||
if (paths.length === 0) return ''
|
||||
|
||||
// Extract directory names from paths
|
||||
const names = paths.map(p => basename(p) || p);
|
||||
const names = paths.map(p => basename(p) || p)
|
||||
|
||||
if (names.length === 1) {
|
||||
return <Text>
|
||||
return (
|
||||
<Text>
|
||||
<Text bold>{names[0]}</Text>
|
||||
{sep}
|
||||
</Text>;
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
if (names.length === 2) {
|
||||
return <Text>
|
||||
return (
|
||||
<Text>
|
||||
<Text bold>{names[0]}</Text>
|
||||
{sep} and <Text bold>{names[1]}</Text>
|
||||
{sep}
|
||||
</Text>;
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
// For 3+, show first two with "and N more"
|
||||
return <Text>
|
||||
return (
|
||||
<Text>
|
||||
<Text bold>{names[0]}</Text>
|
||||
{sep}, <Text bold>{names[1]}</Text>
|
||||
{sep} and {paths.length - 2} more
|
||||
</Text>;
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -62,102 +76,138 @@ function formatPathList(paths: string[]): ReactNode {
|
||||
* and an optional command transform (e.g., Bash strips output redirections so
|
||||
* filenames don't show as commands).
|
||||
*/
|
||||
export function generateShellSuggestionsLabel(suggestions: PermissionUpdate[], shellToolName: string, commandTransform?: (command: string) => string): ReactNode | null {
|
||||
export function generateShellSuggestionsLabel(
|
||||
suggestions: PermissionUpdate[],
|
||||
shellToolName: string,
|
||||
commandTransform?: (command: string) => string,
|
||||
): ReactNode | null {
|
||||
// Collect all rules for display
|
||||
const allRules = suggestions.filter(s => s.type === 'addRules').flatMap(s => s.rules || []);
|
||||
const allRules = suggestions
|
||||
.filter(s => s.type === 'addRules')
|
||||
.flatMap(s => s.rules || [])
|
||||
|
||||
// Separate Read rules from shell rules
|
||||
const readRules = allRules.filter(r => r.toolName === 'Read');
|
||||
const shellRules = allRules.filter(r => r.toolName === shellToolName);
|
||||
const readRules = allRules.filter(r => r.toolName === 'Read')
|
||||
const shellRules = allRules.filter(r => r.toolName === shellToolName)
|
||||
|
||||
// Get directory info
|
||||
const directories = suggestions.filter(s => s.type === 'addDirectories').flatMap(s => s.directories || []);
|
||||
const directories = suggestions
|
||||
.filter(s => s.type === 'addDirectories')
|
||||
.flatMap(s => s.directories || [])
|
||||
|
||||
// Extract paths from Read rules (keep separate from directories)
|
||||
const readPaths = readRules.map(r => r.ruleContent?.replace('/**', '') || '').filter(p => p);
|
||||
const readPaths = readRules
|
||||
.map(r => r.ruleContent?.replace('/**', '') || '')
|
||||
.filter(p => p)
|
||||
|
||||
// Extract shell command prefixes, optionally transforming for display
|
||||
const shellCommands = [...new Set(shellRules.flatMap(rule => {
|
||||
if (!rule.ruleContent) return [];
|
||||
const command = permissionRuleExtractPrefix(rule.ruleContent) ?? rule.ruleContent;
|
||||
return commandTransform ? commandTransform(command) : command;
|
||||
}))];
|
||||
const shellCommands = [
|
||||
...new Set(
|
||||
shellRules.flatMap(rule => {
|
||||
if (!rule.ruleContent) return []
|
||||
const command =
|
||||
permissionRuleExtractPrefix(rule.ruleContent) ?? rule.ruleContent
|
||||
return commandTransform ? commandTransform(command) : command
|
||||
}),
|
||||
),
|
||||
]
|
||||
|
||||
// Check what we have
|
||||
const hasDirectories = directories.length > 0;
|
||||
const hasReadPaths = readPaths.length > 0;
|
||||
const hasCommands = shellCommands.length > 0;
|
||||
const hasDirectories = directories.length > 0
|
||||
const hasReadPaths = readPaths.length > 0
|
||||
const hasCommands = shellCommands.length > 0
|
||||
|
||||
// Handle single type cases
|
||||
if (hasReadPaths && !hasDirectories && !hasCommands) {
|
||||
// Only Read rules - use "reading from" language
|
||||
if (readPaths.length === 1) {
|
||||
const firstPath = readPaths[0]!;
|
||||
const dirName = basename(firstPath) || firstPath;
|
||||
return <Text>
|
||||
const firstPath = readPaths[0]!
|
||||
const dirName = basename(firstPath) || firstPath
|
||||
return (
|
||||
<Text>
|
||||
Yes, allow reading from <Text bold>{dirName}</Text>
|
||||
{sep} from this project
|
||||
</Text>;
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
// Multiple read paths
|
||||
return <Text>
|
||||
return (
|
||||
<Text>
|
||||
Yes, allow reading from {formatPathList(readPaths)} from this project
|
||||
</Text>;
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
if (hasDirectories && !hasReadPaths && !hasCommands) {
|
||||
// Only directory permissions - use "access to" language
|
||||
if (directories.length === 1) {
|
||||
const firstDir = directories[0]!;
|
||||
const dirName = basename(firstDir) || firstDir;
|
||||
return <Text>
|
||||
const firstDir = directories[0]!
|
||||
const dirName = basename(firstDir) || firstDir
|
||||
return (
|
||||
<Text>
|
||||
Yes, and always allow access to <Text bold>{dirName}</Text>
|
||||
{sep} from this project
|
||||
</Text>;
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
// Multiple directories
|
||||
return <Text>
|
||||
return (
|
||||
<Text>
|
||||
Yes, and always allow access to {formatPathList(directories)} from this
|
||||
project
|
||||
</Text>;
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
if (hasCommands && !hasDirectories && !hasReadPaths) {
|
||||
// Only shell command permissions
|
||||
return <Text>
|
||||
return (
|
||||
<Text>
|
||||
{"Yes, and don't ask again for "}
|
||||
{commandListDisplayTruncated(shellCommands)} commands in{' '}
|
||||
<Text bold>{getOriginalCwd()}</Text>
|
||||
</Text>;
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
// Handle mixed cases
|
||||
if ((hasDirectories || hasReadPaths) && !hasCommands) {
|
||||
// Combine directories and read paths since they're both path access
|
||||
const allPaths = [...directories, ...readPaths];
|
||||
const allPaths = [...directories, ...readPaths]
|
||||
if (hasDirectories && hasReadPaths) {
|
||||
// Mixed - use generic "access to"
|
||||
return <Text>
|
||||
return (
|
||||
<Text>
|
||||
Yes, and always allow access to {formatPathList(allPaths)} from this
|
||||
project
|
||||
</Text>;
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if ((hasDirectories || hasReadPaths) && hasCommands) {
|
||||
// Build descriptive message for both types
|
||||
const allPaths = [...directories, ...readPaths];
|
||||
const allPaths = [...directories, ...readPaths]
|
||||
|
||||
// Keep it concise but informative
|
||||
if (allPaths.length === 1 && shellCommands.length === 1) {
|
||||
return <Text>
|
||||
return (
|
||||
<Text>
|
||||
Yes, and allow access to {formatPathList(allPaths)} and{' '}
|
||||
{commandListDisplayTruncated(shellCommands)} commands
|
||||
</Text>;
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
return <Text>
|
||||
|
||||
return (
|
||||
<Text>
|
||||
Yes, and allow {formatPathList(allPaths)} access and{' '}
|
||||
{commandListDisplayTruncated(shellCommands)} commands
|
||||
</Text>;
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
return null;
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,44 +1,135 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { SandboxManager, shouldAllowManagedSandboxDomainsOnly } from '../../utils/sandbox/sandbox-adapter.js';
|
||||
export function SandboxConfigTab() {
|
||||
const $ = _c(3);
|
||||
const isEnabled = SandboxManager.isSandboxingEnabled();
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
const depCheck = SandboxManager.checkDependencies();
|
||||
t0 = depCheck.warnings.length > 0 ? <Box marginTop={1} flexDirection="column">{depCheck.warnings.map(_temp)}</Box> : null;
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
const warningsNote = t0;
|
||||
import * as React from 'react'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import {
|
||||
SandboxManager,
|
||||
shouldAllowManagedSandboxDomainsOnly,
|
||||
} from '../../utils/sandbox/sandbox-adapter.js'
|
||||
|
||||
export function SandboxConfigTab(): React.ReactNode {
|
||||
const isEnabled = SandboxManager.isSandboxingEnabled()
|
||||
|
||||
// Show warnings (e.g., seccomp not available on Linux)
|
||||
const depCheck = SandboxManager.checkDependencies()
|
||||
const warningsNote =
|
||||
depCheck.warnings.length > 0 ? (
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
{depCheck.warnings.map((w, i) => (
|
||||
<Text key={i} dimColor>
|
||||
{w}
|
||||
</Text>
|
||||
))}
|
||||
</Box>
|
||||
) : null
|
||||
|
||||
if (!isEnabled) {
|
||||
let t1;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <Box flexDirection="column" paddingY={1}><Text color="subtle">Sandbox is not enabled</Text>{warningsNote}</Box>;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
return (
|
||||
<Box flexDirection="column" paddingY={1}>
|
||||
<Text color="subtle">Sandbox is not enabled</Text>
|
||||
{warningsNote}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
let t1;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
const fsReadConfig = SandboxManager.getFsReadConfig();
|
||||
const fsWriteConfig = SandboxManager.getFsWriteConfig();
|
||||
const networkConfig = SandboxManager.getNetworkRestrictionConfig();
|
||||
const allowUnixSockets = SandboxManager.getAllowUnixSockets();
|
||||
const excludedCommands = SandboxManager.getExcludedCommands();
|
||||
const globPatternWarnings = SandboxManager.getLinuxGlobPatternWarnings();
|
||||
t1 = <Box flexDirection="column" paddingY={1}><Box flexDirection="column"><Text bold={true} color="permission">Excluded Commands:</Text><Text dimColor={true}>{excludedCommands.length > 0 ? excludedCommands.join(", ") : "None"}</Text></Box>{fsReadConfig.denyOnly.length > 0 && <Box marginTop={1} flexDirection="column"><Text bold={true} color="permission">Filesystem Read Restrictions:</Text><Text dimColor={true}>Denied: {fsReadConfig.denyOnly.join(", ")}</Text>{fsReadConfig.allowWithinDeny && fsReadConfig.allowWithinDeny.length > 0 && <Text dimColor={true}>Allowed within denied: {fsReadConfig.allowWithinDeny.join(", ")}</Text>}</Box>}{fsWriteConfig.allowOnly.length > 0 && <Box marginTop={1} flexDirection="column"><Text bold={true} color="permission">Filesystem Write Restrictions:</Text><Text dimColor={true}>Allowed: {fsWriteConfig.allowOnly.join(", ")}</Text>{fsWriteConfig.denyWithinAllow.length > 0 && <Text dimColor={true}>Denied within allowed: {fsWriteConfig.denyWithinAllow.join(", ")}</Text>}</Box>}{(networkConfig.allowedHosts && networkConfig.allowedHosts.length > 0 || networkConfig.deniedHosts && networkConfig.deniedHosts.length > 0) && <Box marginTop={1} flexDirection="column"><Text bold={true} color="permission">Network Restrictions{shouldAllowManagedSandboxDomainsOnly() ? " (Managed)" : ""}:</Text>{networkConfig.allowedHosts && networkConfig.allowedHosts.length > 0 && <Text dimColor={true}>Allowed: {networkConfig.allowedHosts.join(", ")}</Text>}{networkConfig.deniedHosts && networkConfig.deniedHosts.length > 0 && <Text dimColor={true}>Denied: {networkConfig.deniedHosts.join(", ")}</Text>}</Box>}{allowUnixSockets && allowUnixSockets.length > 0 && <Box marginTop={1} flexDirection="column"><Text bold={true} color="permission">Allowed Unix Sockets:</Text><Text dimColor={true}>{allowUnixSockets.join(", ")}</Text></Box>}{globPatternWarnings.length > 0 && <Box marginTop={1} flexDirection="column"><Text bold={true} color="warning">⚠ Warning: Glob patterns not fully supported on Linux</Text><Text dimColor={true}>The following patterns will be ignored:{" "}{globPatternWarnings.slice(0, 3).join(", ")}{globPatternWarnings.length > 3 && ` (${globPatternWarnings.length - 3} more)`}</Text></Box>}{warningsNote}</Box>;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
function _temp(w, i) {
|
||||
return <Text key={i} dimColor={true}>{w}</Text>;
|
||||
|
||||
const fsReadConfig = SandboxManager.getFsReadConfig()
|
||||
const fsWriteConfig = SandboxManager.getFsWriteConfig()
|
||||
const networkConfig = SandboxManager.getNetworkRestrictionConfig()
|
||||
const allowUnixSockets = SandboxManager.getAllowUnixSockets()
|
||||
const excludedCommands = SandboxManager.getExcludedCommands()
|
||||
const globPatternWarnings = SandboxManager.getLinuxGlobPatternWarnings()
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" paddingY={1}>
|
||||
{/* Excluded Commands */}
|
||||
<Box flexDirection="column">
|
||||
<Text bold color="permission">
|
||||
Excluded Commands:
|
||||
</Text>
|
||||
<Text dimColor>
|
||||
{excludedCommands.length > 0 ? excludedCommands.join(', ') : 'None'}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{/* Filesystem Read Restrictions */}
|
||||
{fsReadConfig.denyOnly.length > 0 && (
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
<Text bold color="permission">
|
||||
Filesystem Read Restrictions:
|
||||
</Text>
|
||||
<Text dimColor>Denied: {fsReadConfig.denyOnly.join(', ')}</Text>
|
||||
{fsReadConfig.allowWithinDeny &&
|
||||
fsReadConfig.allowWithinDeny.length > 0 && (
|
||||
<Text dimColor>
|
||||
Allowed within denied: {fsReadConfig.allowWithinDeny.join(', ')}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Filesystem Write Restrictions */}
|
||||
{fsWriteConfig.allowOnly.length > 0 && (
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
<Text bold color="permission">
|
||||
Filesystem Write Restrictions:
|
||||
</Text>
|
||||
<Text dimColor>Allowed: {fsWriteConfig.allowOnly.join(', ')}</Text>
|
||||
{fsWriteConfig.denyWithinAllow.length > 0 && (
|
||||
<Text dimColor>
|
||||
Denied within allowed: {fsWriteConfig.denyWithinAllow.join(', ')}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Network Restrictions */}
|
||||
{((networkConfig.allowedHosts && networkConfig.allowedHosts.length > 0) ||
|
||||
(networkConfig.deniedHosts &&
|
||||
networkConfig.deniedHosts.length > 0)) && (
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
<Text bold color="permission">
|
||||
Network Restrictions
|
||||
{shouldAllowManagedSandboxDomainsOnly() ? ' (Managed)' : ''}:
|
||||
</Text>
|
||||
{networkConfig.allowedHosts &&
|
||||
networkConfig.allowedHosts.length > 0 && (
|
||||
<Text dimColor>
|
||||
Allowed: {networkConfig.allowedHosts.join(', ')}
|
||||
</Text>
|
||||
)}
|
||||
{networkConfig.deniedHosts &&
|
||||
networkConfig.deniedHosts.length > 0 && (
|
||||
<Text dimColor>
|
||||
Denied: {networkConfig.deniedHosts.join(', ')}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Unix Sockets */}
|
||||
{allowUnixSockets && allowUnixSockets.length > 0 && (
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
<Text bold color="permission">
|
||||
Allowed Unix Sockets:
|
||||
</Text>
|
||||
<Text dimColor>{allowUnixSockets.join(', ')}</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Linux Glob Pattern Warning */}
|
||||
{globPatternWarnings.length > 0 && (
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
<Text bold color="warning">
|
||||
⚠ Warning: Glob patterns not fully supported on Linux
|
||||
</Text>
|
||||
<Text dimColor>
|
||||
The following patterns will be ignored:{' '}
|
||||
{globPatternWarnings.slice(0, 3).join(', ')}
|
||||
{globPatternWarnings.length > 3 &&
|
||||
` (${globPatternWarnings.length - 3} more)`}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{warningsNote}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,119 +1,124 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React from 'react';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { getPlatform } from '../../utils/platform.js';
|
||||
import type { SandboxDependencyCheck } from '../../utils/sandbox/sandbox-adapter.js';
|
||||
import React from 'react'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { getPlatform } from '../../utils/platform.js'
|
||||
import type { SandboxDependencyCheck } from '../../utils/sandbox/sandbox-adapter.js'
|
||||
|
||||
type Props = {
|
||||
depCheck: SandboxDependencyCheck;
|
||||
};
|
||||
export function SandboxDependenciesTab(t0) {
|
||||
const $ = _c(24);
|
||||
const {
|
||||
depCheck
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = getPlatform();
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
const platform = t1;
|
||||
const isMac = platform === "macos";
|
||||
let t2;
|
||||
if ($[1] !== depCheck.errors) {
|
||||
t2 = depCheck.errors.some(_temp);
|
||||
$[1] = depCheck.errors;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
const rgMissing = t2;
|
||||
let t3;
|
||||
if ($[3] !== depCheck.errors) {
|
||||
t3 = depCheck.errors.some(_temp2);
|
||||
$[3] = depCheck.errors;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
const bwrapMissing = t3;
|
||||
let t4;
|
||||
if ($[5] !== depCheck.errors) {
|
||||
t4 = depCheck.errors.some(_temp3);
|
||||
$[5] = depCheck.errors;
|
||||
$[6] = t4;
|
||||
} else {
|
||||
t4 = $[6];
|
||||
}
|
||||
const socatMissing = t4;
|
||||
const seccompMissing = depCheck.warnings.length > 0;
|
||||
let t5;
|
||||
if ($[7] !== bwrapMissing || $[8] !== depCheck.errors || $[9] !== rgMissing || $[10] !== seccompMissing || $[11] !== socatMissing) {
|
||||
const otherErrors = depCheck.errors.filter(_temp4);
|
||||
const rgInstallHint = isMac ? "brew install ripgrep" : "apt install ripgrep";
|
||||
let t6;
|
||||
if ($[13] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t6 = isMac && <Box flexDirection="column"><Text>seatbelt: <Text color="success">built-in (macOS)</Text></Text></Box>;
|
||||
$[13] = t6;
|
||||
} else {
|
||||
t6 = $[13];
|
||||
}
|
||||
let t7;
|
||||
let t8;
|
||||
if ($[14] !== rgMissing) {
|
||||
t7 = <Text>ripgrep (rg):{" "}{rgMissing ? <Text color="error">not found</Text> : <Text color="success">found</Text>}</Text>;
|
||||
t8 = rgMissing && <Text dimColor={true}>{" "}· {rgInstallHint}</Text>;
|
||||
$[14] = rgMissing;
|
||||
$[15] = t7;
|
||||
$[16] = t8;
|
||||
} else {
|
||||
t7 = $[15];
|
||||
t8 = $[16];
|
||||
}
|
||||
let t9;
|
||||
if ($[17] !== t7 || $[18] !== t8) {
|
||||
t9 = <Box flexDirection="column">{t7}{t8}</Box>;
|
||||
$[17] = t7;
|
||||
$[18] = t8;
|
||||
$[19] = t9;
|
||||
} else {
|
||||
t9 = $[19];
|
||||
}
|
||||
let t10;
|
||||
if ($[20] !== bwrapMissing || $[21] !== seccompMissing || $[22] !== socatMissing) {
|
||||
t10 = !isMac && <><Box flexDirection="column"><Text>bubblewrap (bwrap):{" "}{bwrapMissing ? <Text color="error">not installed</Text> : <Text color="success">installed</Text>}</Text>{bwrapMissing && <Text dimColor={true}>{" "}· apt install bubblewrap</Text>}</Box><Box flexDirection="column"><Text>socat:{" "}{socatMissing ? <Text color="error">not installed</Text> : <Text color="success">installed</Text>}</Text>{socatMissing && <Text dimColor={true}>{" "}· apt install socat</Text>}</Box><Box flexDirection="column"><Text>seccomp filter:{" "}{seccompMissing ? <Text color="warning">not installed</Text> : <Text color="success">installed</Text>}{seccompMissing && <Text dimColor={true}> (required to block unix domain sockets)</Text>}</Text>{seccompMissing && <Box flexDirection="column"><Text dimColor={true}>{" "}· npm install -g @anthropic-ai/sandbox-runtime</Text><Text dimColor={true}>{" "}· or copy vendor/seccomp/* from sandbox-runtime and set</Text><Text dimColor={true}>{" "}sandbox.seccomp.bpfPath and applyPath in settings.json</Text></Box>}</Box></>;
|
||||
$[20] = bwrapMissing;
|
||||
$[21] = seccompMissing;
|
||||
$[22] = socatMissing;
|
||||
$[23] = t10;
|
||||
} else {
|
||||
t10 = $[23];
|
||||
}
|
||||
t5 = <Box flexDirection="column" paddingY={1} gap={1}>{t6}{t9}{t10}{otherErrors.map(_temp5)}</Box>;
|
||||
$[7] = bwrapMissing;
|
||||
$[8] = depCheck.errors;
|
||||
$[9] = rgMissing;
|
||||
$[10] = seccompMissing;
|
||||
$[11] = socatMissing;
|
||||
$[12] = t5;
|
||||
} else {
|
||||
t5 = $[12];
|
||||
}
|
||||
return t5;
|
||||
depCheck: SandboxDependencyCheck
|
||||
}
|
||||
function _temp5(err) {
|
||||
return <Text key={err} color="error">{err}</Text>;
|
||||
}
|
||||
function _temp4(e_2) {
|
||||
return !e_2.includes("ripgrep") && !e_2.includes("bwrap") && !e_2.includes("socat");
|
||||
}
|
||||
function _temp3(e_1) {
|
||||
return e_1.includes("socat");
|
||||
}
|
||||
function _temp2(e_0) {
|
||||
return e_0.includes("bwrap");
|
||||
}
|
||||
function _temp(e) {
|
||||
return e.includes("ripgrep");
|
||||
|
||||
export function SandboxDependenciesTab({ depCheck }: Props): React.ReactNode {
|
||||
const platform = getPlatform()
|
||||
const isMac = platform === 'macos'
|
||||
|
||||
// ripgrep is required on all platforms (used to scan for dangerous dirs).
|
||||
// On macOS, seatbelt is built into the OS — ripgrep is the only runtime dep.
|
||||
// On Linux/WSL, bwrap + socat are required, seccomp is optional.
|
||||
//
|
||||
// #31804: previously this tab unconditionally rendered Linux deps (bwrap,
|
||||
// socat, seccomp). When ripgrep was missing on macOS, users saw confusing
|
||||
// Linux install instructions and no mention of the actual problem.
|
||||
const rgMissing = depCheck.errors.some(e => e.includes('ripgrep'))
|
||||
const bwrapMissing = depCheck.errors.some(e => e.includes('bwrap'))
|
||||
const socatMissing = depCheck.errors.some(e => e.includes('socat'))
|
||||
const seccompMissing = depCheck.warnings.length > 0
|
||||
|
||||
// Any errors we don't have a dedicated row for — render verbatim so they
|
||||
// aren't silently swallowed (e.g. "Unsupported platform" or future deps).
|
||||
const otherErrors = depCheck.errors.filter(
|
||||
e => !e.includes('ripgrep') && !e.includes('bwrap') && !e.includes('socat'),
|
||||
)
|
||||
|
||||
const rgInstallHint = isMac ? 'brew install ripgrep' : 'apt install ripgrep'
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" paddingY={1} gap={1}>
|
||||
{isMac && (
|
||||
<Box flexDirection="column">
|
||||
<Text>
|
||||
seatbelt: <Text color="success">built-in (macOS)</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box flexDirection="column">
|
||||
<Text>
|
||||
ripgrep (rg):{' '}
|
||||
{rgMissing ? (
|
||||
<Text color="error">not found</Text>
|
||||
) : (
|
||||
<Text color="success">found</Text>
|
||||
)}
|
||||
</Text>
|
||||
{rgMissing && (
|
||||
<Text dimColor>
|
||||
{' '}· {rgInstallHint}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{!isMac && (
|
||||
<>
|
||||
<Box flexDirection="column">
|
||||
<Text>
|
||||
bubblewrap (bwrap):{' '}
|
||||
{bwrapMissing ? (
|
||||
<Text color="error">not installed</Text>
|
||||
) : (
|
||||
<Text color="success">installed</Text>
|
||||
)}
|
||||
</Text>
|
||||
{bwrapMissing && (
|
||||
<Text dimColor>{' '}· apt install bubblewrap</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box flexDirection="column">
|
||||
<Text>
|
||||
socat:{' '}
|
||||
{socatMissing ? (
|
||||
<Text color="error">not installed</Text>
|
||||
) : (
|
||||
<Text color="success">installed</Text>
|
||||
)}
|
||||
</Text>
|
||||
{socatMissing && <Text dimColor>{' '}· apt install socat</Text>}
|
||||
</Box>
|
||||
|
||||
<Box flexDirection="column">
|
||||
<Text>
|
||||
seccomp filter:{' '}
|
||||
{seccompMissing ? (
|
||||
<Text color="warning">not installed</Text>
|
||||
) : (
|
||||
<Text color="success">installed</Text>
|
||||
)}
|
||||
{seccompMissing && (
|
||||
<Text dimColor> (required to block unix domain sockets)</Text>
|
||||
)}
|
||||
</Text>
|
||||
{seccompMissing && (
|
||||
<Box flexDirection="column">
|
||||
<Text dimColor>
|
||||
{' '}· npm install -g @anthropic-ai/sandbox-runtime
|
||||
</Text>
|
||||
<Text dimColor>
|
||||
{' '}· or copy vendor/seccomp/* from sandbox-runtime and set
|
||||
</Text>
|
||||
<Text dimColor>
|
||||
{' '}sandbox.seccomp.bpfPath and applyPath in settings.json
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
|
||||
{otherErrors.map(err => (
|
||||
<Text key={err} color="error">
|
||||
{err}
|
||||
</Text>
|
||||
))}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,45 +1,48 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React from 'react';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js';
|
||||
export function SandboxDoctorSection() {
|
||||
const $ = _c(2);
|
||||
import React from 'react'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js'
|
||||
|
||||
export function SandboxDoctorSection(): React.ReactNode {
|
||||
if (!SandboxManager.isSupportedPlatform()) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
if (!SandboxManager.isSandboxEnabledInSettings()) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
let t0;
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = Symbol.for("react.early_return_sentinel");
|
||||
bb0: {
|
||||
const depCheck = SandboxManager.checkDependencies();
|
||||
const hasErrors = depCheck.errors.length > 0;
|
||||
const hasWarnings = depCheck.warnings.length > 0;
|
||||
|
||||
const depCheck = SandboxManager.checkDependencies()
|
||||
const hasErrors = depCheck.errors.length > 0
|
||||
const hasWarnings = depCheck.warnings.length > 0
|
||||
|
||||
if (!hasErrors && !hasWarnings) {
|
||||
t1 = null;
|
||||
break bb0;
|
||||
return null
|
||||
}
|
||||
const statusColor = hasErrors ? "error" as const : "warning" as const;
|
||||
const statusText = hasErrors ? "Missing dependencies" : "Available (with warnings)";
|
||||
t0 = <Box flexDirection="column"><Text bold={true}>Sandbox</Text><Text>└ Status: <Text color={statusColor}>{statusText}</Text></Text>{depCheck.errors.map(_temp)}{depCheck.warnings.map(_temp2)}{hasErrors && <Text dimColor={true}>└ Run /sandbox for install instructions</Text>}</Box>;
|
||||
}
|
||||
$[0] = t0;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
t1 = $[1];
|
||||
}
|
||||
if (t1 !== Symbol.for("react.early_return_sentinel")) {
|
||||
return t1;
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
function _temp2(w, i_0) {
|
||||
return <Text key={i_0} color="warning">└ {w}</Text>;
|
||||
}
|
||||
function _temp(e, i) {
|
||||
return <Text key={i} color="error">└ {e}</Text>;
|
||||
|
||||
const statusColor = hasErrors ? ('error' as const) : ('warning' as const)
|
||||
const statusText = hasErrors
|
||||
? 'Missing dependencies'
|
||||
: 'Available (with warnings)'
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text bold>Sandbox</Text>
|
||||
<Text>
|
||||
└ Status: <Text color={statusColor}>{statusText}</Text>
|
||||
</Text>
|
||||
{depCheck.errors.map((e, i) => (
|
||||
<Text key={i} color="error">
|
||||
└ {e}
|
||||
</Text>
|
||||
))}
|
||||
{depCheck.warnings.map((w, i) => (
|
||||
<Text key={i} color="warning">
|
||||
└ {w}
|
||||
</Text>
|
||||
))}
|
||||
{hasErrors && (
|
||||
<Text dimColor>└ Run /sandbox for install instructions</Text>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,192 +1,139 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React from 'react';
|
||||
import { Box, color, Link, Text, useTheme } from '../../ink.js';
|
||||
import type { CommandResultDisplay } from '../../types/command.js';
|
||||
import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js';
|
||||
import { Select } from '../CustomSelect/select.js';
|
||||
import { useTabHeaderFocus } from '../design-system/Tabs.js';
|
||||
import React from 'react'
|
||||
import { Box, color, Link, Text, useTheme } from '../../ink.js'
|
||||
import type { CommandResultDisplay } from '../../types/command.js'
|
||||
import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js'
|
||||
import { Select } from '../CustomSelect/select.js'
|
||||
import { useTabHeaderFocus } from '../design-system/Tabs.js'
|
||||
|
||||
type Props = {
|
||||
onComplete: (result?: string, options?: {
|
||||
display?: CommandResultDisplay;
|
||||
}) => void;
|
||||
};
|
||||
type OverrideMode = 'open' | 'closed';
|
||||
export function SandboxOverridesTab(t0) {
|
||||
const $ = _c(5);
|
||||
const {
|
||||
onComplete
|
||||
} = t0;
|
||||
const isEnabled = SandboxManager.isSandboxingEnabled();
|
||||
const isLocked = SandboxManager.areSandboxSettingsLockedByPolicy();
|
||||
const currentAllowUnsandboxed = SandboxManager.areUnsandboxedCommandsAllowed();
|
||||
onComplete: (
|
||||
result?: string,
|
||||
options?: { display?: CommandResultDisplay },
|
||||
) => void
|
||||
}
|
||||
|
||||
type OverrideMode = 'open' | 'closed'
|
||||
|
||||
export function SandboxOverridesTab({ onComplete }: Props): React.ReactNode {
|
||||
const isEnabled = SandboxManager.isSandboxingEnabled()
|
||||
const isLocked = SandboxManager.areSandboxSettingsLockedByPolicy()
|
||||
const currentAllowUnsandboxed = SandboxManager.areUnsandboxedCommandsAllowed()
|
||||
|
||||
if (!isEnabled) {
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <Box flexDirection="column" paddingY={1}><Text color="subtle">Sandbox is not enabled. Enable sandbox to configure override settings.</Text></Box>;
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
return t1;
|
||||
return (
|
||||
<Box flexDirection="column" paddingY={1}>
|
||||
<Text color="subtle">
|
||||
Sandbox is not enabled. Enable sandbox to configure override settings.
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
if (isLocked) {
|
||||
let t1;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <Text color="subtle">Override settings are managed by a higher-priority configuration and cannot be changed locally.</Text>;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
return (
|
||||
<Box flexDirection="column" paddingY={1}>
|
||||
<Text color="subtle">
|
||||
Override settings are managed by a higher-priority configuration and
|
||||
cannot be changed locally.
|
||||
</Text>
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>
|
||||
Current setting:{' '}
|
||||
{currentAllowUnsandboxed
|
||||
? 'Allow unsandboxed fallback'
|
||||
: 'Strict sandbox mode'}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
let t2;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <Box flexDirection="column" paddingY={1}>{t1}<Box marginTop={1}><Text dimColor={true}>Current setting:{" "}{currentAllowUnsandboxed ? "Allow unsandboxed fallback" : "Strict sandbox mode"}</Text></Box></Box>;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
let t1;
|
||||
if ($[3] !== onComplete) {
|
||||
t1 = <OverridesSelect onComplete={onComplete} currentMode={currentAllowUnsandboxed ? "open" : "closed"} />;
|
||||
$[3] = onComplete;
|
||||
$[4] = t1;
|
||||
} else {
|
||||
t1 = $[4];
|
||||
}
|
||||
return t1;
|
||||
|
||||
return (
|
||||
<OverridesSelect
|
||||
onComplete={onComplete}
|
||||
currentMode={currentAllowUnsandboxed ? 'open' : 'closed'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// Split so useTabHeaderFocus() only runs when the Select renders. Calling it
|
||||
// above the early returns registers a down-arrow opt-in even when we return
|
||||
// static text — pressing ↓ then blurs the header with no way back.
|
||||
function OverridesSelect(t0) {
|
||||
const $ = _c(25);
|
||||
const {
|
||||
function OverridesSelect({
|
||||
onComplete,
|
||||
currentMode
|
||||
} = t0;
|
||||
const [theme] = useTheme();
|
||||
const {
|
||||
headerFocused,
|
||||
focusHeader
|
||||
} = useTabHeaderFocus();
|
||||
let t1;
|
||||
if ($[0] !== theme) {
|
||||
t1 = color("success", theme)("(current)");
|
||||
$[0] = theme;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const currentIndicator = t1;
|
||||
const t2 = currentMode === "open" ? `Allow unsandboxed fallback ${currentIndicator}` : "Allow unsandboxed fallback";
|
||||
let t3;
|
||||
if ($[2] !== t2) {
|
||||
t3 = {
|
||||
label: t2,
|
||||
value: "open"
|
||||
};
|
||||
$[2] = t2;
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t3 = $[3];
|
||||
}
|
||||
const t4 = currentMode === "closed" ? `Strict sandbox mode ${currentIndicator}` : "Strict sandbox mode";
|
||||
let t5;
|
||||
if ($[4] !== t4) {
|
||||
t5 = {
|
||||
label: t4,
|
||||
value: "closed"
|
||||
};
|
||||
$[4] = t4;
|
||||
$[5] = t5;
|
||||
} else {
|
||||
t5 = $[5];
|
||||
}
|
||||
let t6;
|
||||
if ($[6] !== t3 || $[7] !== t5) {
|
||||
t6 = [t3, t5];
|
||||
$[6] = t3;
|
||||
$[7] = t5;
|
||||
$[8] = t6;
|
||||
} else {
|
||||
t6 = $[8];
|
||||
}
|
||||
const options = t6;
|
||||
let t7;
|
||||
if ($[9] !== onComplete) {
|
||||
t7 = async function handleSelect(value) {
|
||||
const mode = value as OverrideMode;
|
||||
currentMode,
|
||||
}: Props & { currentMode: OverrideMode }): React.ReactNode {
|
||||
const [theme] = useTheme()
|
||||
const { headerFocused, focusHeader } = useTabHeaderFocus()
|
||||
const currentIndicator = color('success', theme)(`(current)`)
|
||||
|
||||
const options = [
|
||||
{
|
||||
label:
|
||||
currentMode === 'open'
|
||||
? `Allow unsandboxed fallback ${currentIndicator}`
|
||||
: 'Allow unsandboxed fallback',
|
||||
value: 'open',
|
||||
},
|
||||
{
|
||||
label:
|
||||
currentMode === 'closed'
|
||||
? `Strict sandbox mode ${currentIndicator}`
|
||||
: 'Strict sandbox mode',
|
||||
value: 'closed',
|
||||
},
|
||||
]
|
||||
|
||||
async function handleSelect(value: string) {
|
||||
const mode = value as OverrideMode
|
||||
|
||||
await SandboxManager.setSandboxSettings({
|
||||
allowUnsandboxedCommands: mode === "open"
|
||||
});
|
||||
const message = mode === "open" ? "\u2713 Unsandboxed fallback allowed - commands can run outside sandbox when necessary" : "\u2713 Strict sandbox mode - all commands must run in sandbox or be excluded via the `excludedCommands` option";
|
||||
onComplete(message);
|
||||
};
|
||||
$[9] = onComplete;
|
||||
$[10] = t7;
|
||||
} else {
|
||||
t7 = $[10];
|
||||
allowUnsandboxedCommands: mode === 'open',
|
||||
})
|
||||
|
||||
const message =
|
||||
mode === 'open'
|
||||
? '✓ Unsandboxed fallback allowed - commands can run outside sandbox when necessary'
|
||||
: '✓ Strict sandbox mode - all commands must run in sandbox or be excluded via the `excludedCommands` option'
|
||||
|
||||
onComplete(message)
|
||||
}
|
||||
const handleSelect = t7;
|
||||
let t8;
|
||||
if ($[11] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t8 = <Box marginBottom={1}><Text bold={true}>Configure Overrides:</Text></Box>;
|
||||
$[11] = t8;
|
||||
} else {
|
||||
t8 = $[11];
|
||||
}
|
||||
let t9;
|
||||
if ($[12] !== onComplete) {
|
||||
t9 = () => onComplete(undefined, {
|
||||
display: "skip"
|
||||
});
|
||||
$[12] = onComplete;
|
||||
$[13] = t9;
|
||||
} else {
|
||||
t9 = $[13];
|
||||
}
|
||||
let t10;
|
||||
if ($[14] !== focusHeader || $[15] !== handleSelect || $[16] !== headerFocused || $[17] !== options || $[18] !== t9) {
|
||||
t10 = <Select options={options} onChange={handleSelect} onCancel={t9} onUpFromFirstItem={focusHeader} isDisabled={headerFocused} />;
|
||||
$[14] = focusHeader;
|
||||
$[15] = handleSelect;
|
||||
$[16] = headerFocused;
|
||||
$[17] = options;
|
||||
$[18] = t9;
|
||||
$[19] = t10;
|
||||
} else {
|
||||
t10 = $[19];
|
||||
}
|
||||
let t11;
|
||||
if ($[20] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t11 = <Text dimColor={true}><Text bold={true} dimColor={true}>Allow unsandboxed fallback:</Text>{" "}When a command fails due to sandbox restrictions, Claude can retry with dangerouslyDisableSandbox to run outside the sandbox (falling back to default permissions).</Text>;
|
||||
$[20] = t11;
|
||||
} else {
|
||||
t11 = $[20];
|
||||
}
|
||||
let t12;
|
||||
if ($[21] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t12 = <Text dimColor={true}><Text bold={true} dimColor={true}>Strict sandbox mode:</Text>{" "}All bash commands invoked by the model must run in the sandbox unless they are explicitly listed in excludedCommands.</Text>;
|
||||
$[21] = t12;
|
||||
} else {
|
||||
t12 = $[21];
|
||||
}
|
||||
let t13;
|
||||
if ($[22] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t13 = <Box flexDirection="column" marginTop={1} gap={1}>{t11}{t12}<Text dimColor={true}>Learn more:{" "}<Link url="https://code.claude.com/docs/en/sandboxing#configure-sandboxing">code.claude.com/docs/en/sandboxing#configure-sandboxing</Link></Text></Box>;
|
||||
$[22] = t13;
|
||||
} else {
|
||||
t13 = $[22];
|
||||
}
|
||||
let t14;
|
||||
if ($[23] !== t10) {
|
||||
t14 = <Box flexDirection="column" paddingY={1}>{t8}{t10}{t13}</Box>;
|
||||
$[23] = t10;
|
||||
$[24] = t14;
|
||||
} else {
|
||||
t14 = $[24];
|
||||
}
|
||||
return t14;
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" paddingY={1}>
|
||||
<Box marginBottom={1}>
|
||||
<Text bold>Configure Overrides:</Text>
|
||||
</Box>
|
||||
<Select
|
||||
options={options}
|
||||
onChange={handleSelect}
|
||||
onCancel={() => onComplete(undefined, { display: 'skip' })}
|
||||
onUpFromFirstItem={focusHeader}
|
||||
isDisabled={headerFocused}
|
||||
/>
|
||||
<Box flexDirection="column" marginTop={1} gap={1}>
|
||||
<Text dimColor>
|
||||
<Text bold dimColor>
|
||||
Allow unsandboxed fallback:
|
||||
</Text>{' '}
|
||||
When a command fails due to sandbox restrictions, Claude can retry
|
||||
with dangerouslyDisableSandbox to run outside the sandbox (falling
|
||||
back to default permissions).
|
||||
</Text>
|
||||
<Text dimColor>
|
||||
<Text bold dimColor>
|
||||
Strict sandbox mode:
|
||||
</Text>{' '}
|
||||
All bash commands invoked by the model must run in the sandbox unless
|
||||
they are explicitly listed in excludedCommands.
|
||||
</Text>
|
||||
<Text dimColor>
|
||||
Learn more:{' '}
|
||||
<Link url="https://code.claude.com/docs/en/sandboxing#configure-sandboxing">
|
||||
code.claude.com/docs/en/sandboxing#configure-sandboxing
|
||||
</Link>
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,295 +1,211 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React from 'react';
|
||||
import { Box, color, Link, Text, useTheme } from '../../ink.js';
|
||||
import { useKeybindings } from '../../keybindings/useKeybinding.js';
|
||||
import type { CommandResultDisplay } from '../../types/command.js';
|
||||
import type { SandboxDependencyCheck } from '../../utils/sandbox/sandbox-adapter.js';
|
||||
import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js';
|
||||
import { getSettings_DEPRECATED } from '../../utils/settings/settings.js';
|
||||
import { Select } from '../CustomSelect/select.js';
|
||||
import { Pane } from '../design-system/Pane.js';
|
||||
import { Tab, Tabs, useTabHeaderFocus } from '../design-system/Tabs.js';
|
||||
import { SandboxConfigTab } from './SandboxConfigTab.js';
|
||||
import { SandboxDependenciesTab } from './SandboxDependenciesTab.js';
|
||||
import { SandboxOverridesTab } from './SandboxOverridesTab.js';
|
||||
import React from 'react'
|
||||
import { Box, color, Link, Text, useTheme } from '../../ink.js'
|
||||
import { useKeybindings } from '../../keybindings/useKeybinding.js'
|
||||
import type { CommandResultDisplay } from '../../types/command.js'
|
||||
import type { SandboxDependencyCheck } from '../../utils/sandbox/sandbox-adapter.js'
|
||||
import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js'
|
||||
import { getSettings_DEPRECATED } from '../../utils/settings/settings.js'
|
||||
import { Select } from '../CustomSelect/select.js'
|
||||
import { Pane } from '../design-system/Pane.js'
|
||||
import { Tab, Tabs, useTabHeaderFocus } from '../design-system/Tabs.js'
|
||||
import { SandboxConfigTab } from './SandboxConfigTab.js'
|
||||
import { SandboxDependenciesTab } from './SandboxDependenciesTab.js'
|
||||
import { SandboxOverridesTab } from './SandboxOverridesTab.js'
|
||||
|
||||
type Props = {
|
||||
onComplete: (result?: string, options?: {
|
||||
display?: CommandResultDisplay;
|
||||
}) => void;
|
||||
depCheck: SandboxDependencyCheck;
|
||||
};
|
||||
type SandboxMode = 'auto-allow' | 'regular' | 'disabled';
|
||||
export function SandboxSettings(t0) {
|
||||
const $ = _c(34);
|
||||
const {
|
||||
onComplete: (
|
||||
result?: string,
|
||||
options?: { display?: CommandResultDisplay },
|
||||
) => void
|
||||
depCheck: SandboxDependencyCheck
|
||||
}
|
||||
|
||||
type SandboxMode = 'auto-allow' | 'regular' | 'disabled'
|
||||
|
||||
export function SandboxSettings({
|
||||
onComplete,
|
||||
depCheck
|
||||
} = t0;
|
||||
const [theme] = useTheme();
|
||||
const currentEnabled = SandboxManager.isSandboxingEnabled();
|
||||
const currentAutoAllow = SandboxManager.isAutoAllowBashIfSandboxedEnabled();
|
||||
const hasWarnings = depCheck.warnings.length > 0;
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = getSettings_DEPRECATED();
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
depCheck,
|
||||
}: Props): React.ReactNode {
|
||||
const [theme] = useTheme()
|
||||
const currentEnabled = SandboxManager.isSandboxingEnabled()
|
||||
const currentAutoAllow = SandboxManager.isAutoAllowBashIfSandboxedEnabled()
|
||||
const hasWarnings = depCheck.warnings.length > 0
|
||||
const settings = getSettings_DEPRECATED()
|
||||
const allowAllUnixSockets = settings.sandbox?.network?.allowAllUnixSockets
|
||||
// Show warning if seccomp missing AND user hasn't allowed all unix sockets
|
||||
const showSocketWarning = hasWarnings && !allowAllUnixSockets
|
||||
|
||||
// Determine current mode
|
||||
const getCurrentMode = (): SandboxMode => {
|
||||
if (!currentEnabled) return 'disabled'
|
||||
if (currentAutoAllow) return 'auto-allow'
|
||||
return 'regular'
|
||||
}
|
||||
const settings = t1;
|
||||
const allowAllUnixSockets = settings.sandbox?.network?.allowAllUnixSockets;
|
||||
const showSocketWarning = hasWarnings && !allowAllUnixSockets;
|
||||
const getCurrentMode = () => {
|
||||
if (!currentEnabled) {
|
||||
return "disabled";
|
||||
}
|
||||
if (currentAutoAllow) {
|
||||
return "auto-allow";
|
||||
}
|
||||
return "regular";
|
||||
};
|
||||
const currentMode = getCurrentMode();
|
||||
let t2;
|
||||
if ($[1] !== theme) {
|
||||
t2 = color("success", theme)("(current)");
|
||||
$[1] = theme;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
const currentIndicator = t2;
|
||||
const t3 = currentMode === "auto-allow" ? `Sandbox BashTool, with auto-allow ${currentIndicator}` : "Sandbox BashTool, with auto-allow";
|
||||
let t4;
|
||||
if ($[3] !== t3) {
|
||||
t4 = {
|
||||
label: t3,
|
||||
value: "auto-allow"
|
||||
};
|
||||
$[3] = t3;
|
||||
$[4] = t4;
|
||||
} else {
|
||||
t4 = $[4];
|
||||
}
|
||||
const t5 = currentMode === "regular" ? `Sandbox BashTool, with regular permissions ${currentIndicator}` : "Sandbox BashTool, with regular permissions";
|
||||
let t6;
|
||||
if ($[5] !== t5) {
|
||||
t6 = {
|
||||
label: t5,
|
||||
value: "regular"
|
||||
};
|
||||
$[5] = t5;
|
||||
$[6] = t6;
|
||||
} else {
|
||||
t6 = $[6];
|
||||
}
|
||||
const t7 = currentMode === "disabled" ? `No Sandbox ${currentIndicator}` : "No Sandbox";
|
||||
let t8;
|
||||
if ($[7] !== t7) {
|
||||
t8 = {
|
||||
label: t7,
|
||||
value: "disabled"
|
||||
};
|
||||
$[7] = t7;
|
||||
$[8] = t8;
|
||||
} else {
|
||||
t8 = $[8];
|
||||
}
|
||||
let t9;
|
||||
if ($[9] !== t4 || $[10] !== t6 || $[11] !== t8) {
|
||||
t9 = [t4, t6, t8];
|
||||
$[9] = t4;
|
||||
$[10] = t6;
|
||||
$[11] = t8;
|
||||
$[12] = t9;
|
||||
} else {
|
||||
t9 = $[12];
|
||||
}
|
||||
const options = t9;
|
||||
let t10;
|
||||
if ($[13] !== onComplete) {
|
||||
t10 = async function handleSelect(value) {
|
||||
const mode = value as SandboxMode;
|
||||
bb33: switch (mode) {
|
||||
case "auto-allow":
|
||||
|
||||
const currentMode = getCurrentMode()
|
||||
const currentIndicator = color('success', theme)(`(current)`)
|
||||
|
||||
const options = [
|
||||
{
|
||||
label:
|
||||
currentMode === 'auto-allow'
|
||||
? `Sandbox BashTool, with auto-allow ${currentIndicator}`
|
||||
: 'Sandbox BashTool, with auto-allow',
|
||||
value: 'auto-allow',
|
||||
},
|
||||
{
|
||||
label:
|
||||
currentMode === 'regular'
|
||||
? `Sandbox BashTool, with regular permissions ${currentIndicator}`
|
||||
: 'Sandbox BashTool, with regular permissions',
|
||||
value: 'regular',
|
||||
},
|
||||
{
|
||||
label:
|
||||
currentMode === 'disabled'
|
||||
? `No Sandbox ${currentIndicator}`
|
||||
: 'No Sandbox',
|
||||
value: 'disabled',
|
||||
},
|
||||
]
|
||||
|
||||
async function handleSelect(value: string) {
|
||||
const mode = value as SandboxMode
|
||||
|
||||
switch (mode) {
|
||||
case 'auto-allow':
|
||||
await SandboxManager.setSandboxSettings({
|
||||
enabled: true,
|
||||
autoAllowBashIfSandboxed: true
|
||||
});
|
||||
onComplete("\u2713 Sandbox enabled with auto-allow for bash commands");
|
||||
break bb33;
|
||||
}
|
||||
case "regular":
|
||||
{
|
||||
autoAllowBashIfSandboxed: true,
|
||||
})
|
||||
onComplete('✓ Sandbox enabled with auto-allow for bash commands')
|
||||
break
|
||||
case 'regular':
|
||||
await SandboxManager.setSandboxSettings({
|
||||
enabled: true,
|
||||
autoAllowBashIfSandboxed: false
|
||||
});
|
||||
onComplete("\u2713 Sandbox enabled with regular bash permissions");
|
||||
break bb33;
|
||||
}
|
||||
case "disabled":
|
||||
{
|
||||
autoAllowBashIfSandboxed: false,
|
||||
})
|
||||
onComplete('✓ Sandbox enabled with regular bash permissions')
|
||||
break
|
||||
case 'disabled':
|
||||
await SandboxManager.setSandboxSettings({
|
||||
enabled: false,
|
||||
autoAllowBashIfSandboxed: false
|
||||
});
|
||||
onComplete("\u25CB Sandbox disabled");
|
||||
}
|
||||
}
|
||||
};
|
||||
$[13] = onComplete;
|
||||
$[14] = t10;
|
||||
} else {
|
||||
t10 = $[14];
|
||||
}
|
||||
const handleSelect = t10;
|
||||
let t11;
|
||||
if ($[15] !== onComplete) {
|
||||
t11 = {
|
||||
"confirm:no": () => onComplete(undefined, {
|
||||
display: "skip"
|
||||
autoAllowBashIfSandboxed: false,
|
||||
})
|
||||
};
|
||||
$[15] = onComplete;
|
||||
$[16] = t11;
|
||||
} else {
|
||||
t11 = $[16];
|
||||
onComplete('○ Sandbox disabled')
|
||||
break
|
||||
}
|
||||
let t12;
|
||||
if ($[17] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t12 = {
|
||||
context: "Settings"
|
||||
};
|
||||
$[17] = t12;
|
||||
} else {
|
||||
t12 = $[17];
|
||||
}
|
||||
useKeybindings(t11, t12);
|
||||
let t13;
|
||||
if ($[18] !== handleSelect || $[19] !== onComplete || $[20] !== options || $[21] !== showSocketWarning) {
|
||||
t13 = <Tab key="mode" title="Mode"><SandboxModeTab showSocketWarning={showSocketWarning} options={options} onSelect={handleSelect} onComplete={onComplete} /></Tab>;
|
||||
$[18] = handleSelect;
|
||||
$[19] = onComplete;
|
||||
$[20] = options;
|
||||
$[21] = showSocketWarning;
|
||||
$[22] = t13;
|
||||
} else {
|
||||
t13 = $[22];
|
||||
}
|
||||
const modeTab = t13;
|
||||
let t14;
|
||||
if ($[23] !== onComplete) {
|
||||
t14 = <Tab key="overrides" title="Overrides"><SandboxOverridesTab onComplete={onComplete} /></Tab>;
|
||||
$[23] = onComplete;
|
||||
$[24] = t14;
|
||||
} else {
|
||||
t14 = $[24];
|
||||
}
|
||||
const overridesTab = t14;
|
||||
let t15;
|
||||
if ($[25] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t15 = <Tab key="config" title="Config"><SandboxConfigTab /></Tab>;
|
||||
$[25] = t15;
|
||||
} else {
|
||||
t15 = $[25];
|
||||
}
|
||||
const configTab = t15;
|
||||
const hasErrors = depCheck.errors.length > 0;
|
||||
let t16;
|
||||
if ($[26] !== depCheck || $[27] !== hasErrors || $[28] !== hasWarnings || $[29] !== modeTab || $[30] !== overridesTab) {
|
||||
t16 = hasErrors ? [<Tab key="dependencies" title="Dependencies"><SandboxDependenciesTab depCheck={depCheck} /></Tab>] : [modeTab, ...(hasWarnings ? [<Tab key="dependencies" title="Dependencies"><SandboxDependenciesTab depCheck={depCheck} /></Tab>] : []), overridesTab, configTab];
|
||||
$[26] = depCheck;
|
||||
$[27] = hasErrors;
|
||||
$[28] = hasWarnings;
|
||||
$[29] = modeTab;
|
||||
$[30] = overridesTab;
|
||||
$[31] = t16;
|
||||
} else {
|
||||
t16 = $[31];
|
||||
}
|
||||
const tabs = t16;
|
||||
let t17;
|
||||
if ($[32] !== tabs) {
|
||||
t17 = <Pane color="permission"><Tabs title="Sandbox:" color="permission" defaultTab="Mode">{tabs}</Tabs></Pane>;
|
||||
$[32] = tabs;
|
||||
$[33] = t17;
|
||||
} else {
|
||||
t17 = $[33];
|
||||
}
|
||||
return t17;
|
||||
|
||||
useKeybindings(
|
||||
{
|
||||
'confirm:no': () => onComplete(undefined, { display: 'skip' }),
|
||||
},
|
||||
{ context: 'Settings' },
|
||||
)
|
||||
|
||||
const modeTab = (
|
||||
<Tab key="mode" title="Mode">
|
||||
<SandboxModeTab
|
||||
showSocketWarning={showSocketWarning}
|
||||
options={options}
|
||||
onSelect={handleSelect}
|
||||
onComplete={onComplete}
|
||||
/>
|
||||
</Tab>
|
||||
)
|
||||
|
||||
const overridesTab = (
|
||||
<Tab key="overrides" title="Overrides">
|
||||
<SandboxOverridesTab onComplete={onComplete} />
|
||||
</Tab>
|
||||
)
|
||||
|
||||
const configTab = (
|
||||
<Tab key="config" title="Config">
|
||||
<SandboxConfigTab />
|
||||
</Tab>
|
||||
)
|
||||
|
||||
const hasErrors = depCheck.errors.length > 0
|
||||
|
||||
// If required deps missing, only show Dependencies tab
|
||||
// If only optional deps missing, show all tabs
|
||||
const tabs = hasErrors
|
||||
? [
|
||||
<Tab key="dependencies" title="Dependencies">
|
||||
<SandboxDependenciesTab depCheck={depCheck} />
|
||||
</Tab>,
|
||||
]
|
||||
: [
|
||||
modeTab,
|
||||
...(hasWarnings
|
||||
? [
|
||||
<Tab key="dependencies" title="Dependencies">
|
||||
<SandboxDependenciesTab depCheck={depCheck} />
|
||||
</Tab>,
|
||||
]
|
||||
: []),
|
||||
overridesTab,
|
||||
configTab,
|
||||
]
|
||||
|
||||
return (
|
||||
<Pane color="permission">
|
||||
<Tabs title="Sandbox:" color="permission" defaultTab="Mode">
|
||||
{tabs}
|
||||
</Tabs>
|
||||
</Pane>
|
||||
)
|
||||
}
|
||||
function SandboxModeTab(t0) {
|
||||
const $ = _c(16);
|
||||
const {
|
||||
|
||||
function SandboxModeTab({
|
||||
showSocketWarning,
|
||||
options,
|
||||
onSelect,
|
||||
onComplete
|
||||
} = t0;
|
||||
const {
|
||||
headerFocused,
|
||||
focusHeader
|
||||
} = useTabHeaderFocus();
|
||||
let t1;
|
||||
if ($[0] !== showSocketWarning) {
|
||||
t1 = showSocketWarning && <Box marginBottom={1}><Text color="warning">Cannot block unix domain sockets (see Dependencies tab)</Text></Box>;
|
||||
$[0] = showSocketWarning;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
let t2;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <Box marginBottom={1}><Text bold={true}>Configure Mode:</Text></Box>;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
let t3;
|
||||
if ($[3] !== onComplete) {
|
||||
t3 = () => onComplete(undefined, {
|
||||
display: "skip"
|
||||
});
|
||||
$[3] = onComplete;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
let t4;
|
||||
if ($[5] !== focusHeader || $[6] !== headerFocused || $[7] !== onSelect || $[8] !== options || $[9] !== t3) {
|
||||
t4 = <Select options={options} onChange={onSelect} onCancel={t3} onUpFromFirstItem={focusHeader} isDisabled={headerFocused} />;
|
||||
$[5] = focusHeader;
|
||||
$[6] = headerFocused;
|
||||
$[7] = onSelect;
|
||||
$[8] = options;
|
||||
$[9] = t3;
|
||||
$[10] = t4;
|
||||
} else {
|
||||
t4 = $[10];
|
||||
}
|
||||
let t5;
|
||||
if ($[11] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t5 = <Text dimColor={true}><Text bold={true} dimColor={true}>Auto-allow mode:</Text>{" "}Commands will try to run in the sandbox automatically, and attempts to run outside of the sandbox fallback to regular permissions. Explicit ask/deny rules are always respected.</Text>;
|
||||
$[11] = t5;
|
||||
} else {
|
||||
t5 = $[11];
|
||||
}
|
||||
let t6;
|
||||
if ($[12] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t6 = <Box flexDirection="column" marginTop={1} gap={1}>{t5}<Text dimColor={true}>Learn more:{" "}<Link url="https://code.claude.com/docs/en/sandboxing">code.claude.com/docs/en/sandboxing</Link></Text></Box>;
|
||||
$[12] = t6;
|
||||
} else {
|
||||
t6 = $[12];
|
||||
}
|
||||
let t7;
|
||||
if ($[13] !== t1 || $[14] !== t4) {
|
||||
t7 = <Box flexDirection="column" paddingY={1}>{t1}{t2}{t4}{t6}</Box>;
|
||||
$[13] = t1;
|
||||
$[14] = t4;
|
||||
$[15] = t7;
|
||||
} else {
|
||||
t7 = $[15];
|
||||
}
|
||||
return t7;
|
||||
onComplete,
|
||||
}: {
|
||||
showSocketWarning: boolean
|
||||
options: Array<{ label: string; value: string }>
|
||||
onSelect: (value: string) => void
|
||||
onComplete: Props['onComplete']
|
||||
}): React.ReactNode {
|
||||
const { headerFocused, focusHeader } = useTabHeaderFocus()
|
||||
return (
|
||||
<Box flexDirection="column" paddingY={1}>
|
||||
{showSocketWarning && (
|
||||
<Box marginBottom={1}>
|
||||
<Text color="warning">
|
||||
Cannot block unix domain sockets (see Dependencies tab)
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
<Box marginBottom={1}>
|
||||
<Text bold>Configure Mode:</Text>
|
||||
</Box>
|
||||
<Select
|
||||
options={options}
|
||||
onChange={onSelect}
|
||||
onCancel={() => onComplete(undefined, { display: 'skip' })}
|
||||
onUpFromFirstItem={focusHeader}
|
||||
isDisabled={headerFocused}
|
||||
/>
|
||||
<Box flexDirection="column" marginTop={1} gap={1}>
|
||||
<Text dimColor>
|
||||
<Text bold dimColor>
|
||||
Auto-allow mode:
|
||||
</Text>{' '}
|
||||
Commands will try to run in the sandbox automatically, and attempts to
|
||||
run outside of the sandbox fallback to regular permissions. Explicit
|
||||
ask/deny rules are always respected.
|
||||
</Text>
|
||||
<Text dimColor>
|
||||
Learn more:{' '}
|
||||
<Link url="https://code.claude.com/docs/en/sandboxing">
|
||||
code.claude.com/docs/en/sandboxing
|
||||
</Link>
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user