mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
fix(vscode-ide-companion): improve message logging and permission handling
- Increase message logging truncation limit from 500 to 1500 characters - Fix permission option mapping logic for reject_once/cancel options - Add TODO comments for diff accept/cancel responses during permission requests Resolves issues with permission handling and improves debugging capabilities.
This commit is contained in:
@@ -50,11 +50,9 @@ export class AcpConnection {
|
|||||||
private nextRequestId = { value: 0 };
|
private nextRequestId = { value: 0 };
|
||||||
private backend: AcpBackend | null = null;
|
private backend: AcpBackend | null = null;
|
||||||
|
|
||||||
// Module instances
|
|
||||||
private messageHandler: AcpMessageHandler;
|
private messageHandler: AcpMessageHandler;
|
||||||
private sessionManager: AcpSessionManager;
|
private sessionManager: AcpSessionManager;
|
||||||
|
|
||||||
// Callback functions
|
|
||||||
onSessionUpdate: (data: AcpSessionUpdate) => void = () => {};
|
onSessionUpdate: (data: AcpSessionUpdate) => void = () => {};
|
||||||
onPermissionRequest: (data: AcpPermissionRequest) => Promise<{
|
onPermissionRequest: (data: AcpPermissionRequest) => Promise<{
|
||||||
optionId: string;
|
optionId: string;
|
||||||
@@ -206,7 +204,7 @@ export class AcpConnection {
|
|||||||
const message = JSON.parse(line) as AcpMessage;
|
const message = JSON.parse(line) as AcpMessage;
|
||||||
console.log(
|
console.log(
|
||||||
'[ACP] <<< Received message:',
|
'[ACP] <<< Received message:',
|
||||||
JSON.stringify(message).substring(0, 500),
|
JSON.stringify(message).substring(0, 500 * 3),
|
||||||
);
|
);
|
||||||
this.handleMessage(message);
|
this.handleMessage(message);
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
|
|||||||
@@ -217,7 +217,8 @@ export class AcpMessageHandler {
|
|||||||
return {
|
return {
|
||||||
outcome: {
|
outcome: {
|
||||||
outcome,
|
outcome,
|
||||||
optionId: optionId === 'cancel' ? 'reject_once' : optionId,
|
// optionId: optionId === 'cancel' ? 'reject_once' : optionId,
|
||||||
|
optionId: optionId === 'reject_once' ? 'cancel' : optionId,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
@@ -230,3 +231,21 @@ export class AcpMessageHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [
|
||||||
|
// {
|
||||||
|
// received: 'reject_once',
|
||||||
|
// code: 'invalid_enum_value',
|
||||||
|
// options: [
|
||||||
|
// 'proceed_once',
|
||||||
|
// 'proceed_always',
|
||||||
|
// 'proceed_always_server',
|
||||||
|
// 'proceed_always_tool',
|
||||||
|
// 'modify_with_editor',
|
||||||
|
// 'cancel',
|
||||||
|
// ],
|
||||||
|
// path: [],
|
||||||
|
// message:
|
||||||
|
// "Invalid enum value. Expected 'proceed_once' | 'proceed_always' | 'proceed_always_server' | 'proceed_always_tool' | 'modify_with_editor' | 'cancel', received 'reject_once'",
|
||||||
|
// },
|
||||||
|
// ];
|
||||||
|
|||||||
@@ -181,12 +181,16 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
if (docUri && docUri.scheme === DIFF_SCHEME) {
|
if (docUri && docUri.scheme === DIFF_SCHEME) {
|
||||||
diffManager.acceptDiff(docUri);
|
diffManager.acceptDiff(docUri);
|
||||||
}
|
}
|
||||||
|
// TODO: 如果 webview 在 request_permission 需要回复 cli
|
||||||
|
console.log('[Extension] Diff accepted');
|
||||||
}),
|
}),
|
||||||
vscode.commands.registerCommand('qwen.diff.cancel', (uri?: vscode.Uri) => {
|
vscode.commands.registerCommand('qwen.diff.cancel', (uri?: vscode.Uri) => {
|
||||||
const docUri = uri ?? vscode.window.activeTextEditor?.document.uri;
|
const docUri = uri ?? vscode.window.activeTextEditor?.document.uri;
|
||||||
if (docUri && docUri.scheme === DIFF_SCHEME) {
|
if (docUri && docUri.scheme === DIFF_SCHEME) {
|
||||||
diffManager.cancelDiff(docUri);
|
diffManager.cancelDiff(docUri);
|
||||||
}
|
}
|
||||||
|
// TODO: 如果 webview 在 request_permission 需要回复 cli
|
||||||
|
console.log('[Extension] Diff cancelled');
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import {
|
|||||||
AssistantMessage,
|
AssistantMessage,
|
||||||
ThinkingMessage,
|
ThinkingMessage,
|
||||||
WaitingMessage,
|
WaitingMessage,
|
||||||
|
InterruptedMessage,
|
||||||
} from './components/messages/index.js';
|
} from './components/messages/index.js';
|
||||||
import { InputForm } from './components/InputForm.js';
|
import { InputForm } from './components/InputForm.js';
|
||||||
import { SessionSelector } from './components/session/SessionSelector.js';
|
import { SessionSelector } from './components/session/SessionSelector.js';
|
||||||
@@ -172,15 +173,33 @@ export const App: React.FC = () => {
|
|||||||
isStreaming: messageHandling.isStreaming,
|
isStreaming: messageHandling.isStreaming,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle cancel streaming
|
// Handle cancel/stop from the input bar
|
||||||
|
// Emit a cancel to the extension and immediately reflect interruption locally.
|
||||||
const handleCancel = useCallback(() => {
|
const handleCancel = useCallback(() => {
|
||||||
if (messageHandling.isStreaming) {
|
if (messageHandling.isStreaming || messageHandling.isWaitingForResponse) {
|
||||||
vscode.postMessage({
|
// Proactively end local states and add an 'Interrupted' line
|
||||||
type: 'cancelStreaming',
|
try {
|
||||||
data: {},
|
messageHandling.endStreaming?.();
|
||||||
|
} catch {
|
||||||
|
/* no-op */
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
messageHandling.clearWaitingForResponse?.();
|
||||||
|
} catch {
|
||||||
|
/* no-op */
|
||||||
|
}
|
||||||
|
messageHandling.addMessage({
|
||||||
|
role: 'assistant',
|
||||||
|
content: 'Interrupted',
|
||||||
|
timestamp: Date.now(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [messageHandling.isStreaming, vscode]);
|
// Notify extension/agent to cancel server-side work
|
||||||
|
vscode.postMessage({
|
||||||
|
type: 'cancelStreaming',
|
||||||
|
data: {},
|
||||||
|
});
|
||||||
|
}, [messageHandling, vscode]);
|
||||||
|
|
||||||
// Message handling
|
// Message handling
|
||||||
useWebViewMessages({
|
useWebViewMessages({
|
||||||
@@ -562,14 +581,28 @@ export const App: React.FC = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
{
|
||||||
<AssistantMessage
|
const content = (msg.content || '').trim();
|
||||||
key={`message-${index}`}
|
if (
|
||||||
content={msg.content || ''}
|
content === 'Interrupted' ||
|
||||||
timestamp={msg.timestamp || 0}
|
content === 'Tool interrupted'
|
||||||
onFileClick={handleFileClick}
|
) {
|
||||||
/>
|
return (
|
||||||
);
|
<InterruptedMessage
|
||||||
|
key={`message-${index}`}
|
||||||
|
text={content}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<AssistantMessage
|
||||||
|
key={`message-${index}`}
|
||||||
|
content={content}
|
||||||
|
timestamp={msg.timestamp || 0}
|
||||||
|
onFileClick={handleFileClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// case 'in-progress-tool-call':
|
// case 'in-progress-tool-call':
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ export const InputForm: React.FC<InputFormProps> = ({
|
|||||||
inputText,
|
inputText,
|
||||||
inputFieldRef,
|
inputFieldRef,
|
||||||
isStreaming,
|
isStreaming,
|
||||||
|
isWaitingForResponse,
|
||||||
isComposing,
|
isComposing,
|
||||||
editMode,
|
editMode,
|
||||||
thinkingEnabled,
|
thinkingEnabled,
|
||||||
@@ -109,6 +110,12 @@ export const InputForm: React.FC<InputFormProps> = ({
|
|||||||
const editModeInfo = getEditModeInfo(editMode);
|
const editModeInfo = getEditModeInfo(editMode);
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
// ESC should cancel the current interaction (stop generation)
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
e.preventDefault();
|
||||||
|
onCancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
// If composing (Chinese IME input), don't process Enter key
|
// If composing (Chinese IME input), don't process Enter key
|
||||||
if (e.key === 'Enter' && !e.shiftKey && !isComposing) {
|
if (e.key === 'Enter' && !e.shiftKey && !isComposing) {
|
||||||
// If CompletionMenu is open, let it handle Enter key
|
// If CompletionMenu is open, let it handle Enter key
|
||||||
|
|||||||
@@ -1,158 +0,0 @@
|
|||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright 2025 Qwen Team
|
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
|
||||||
*
|
|
||||||
* MarkdownRenderer component - renders markdown content with syntax highlighting and clickable file paths
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type React from 'react';
|
|
||||||
import MarkdownIt from 'markdown-it';
|
|
||||||
import type { Options as MarkdownItOptions } from 'markdown-it';
|
|
||||||
import './MarkdownRenderer.css';
|
|
||||||
|
|
||||||
interface MarkdownRendererProps {
|
|
||||||
content: string;
|
|
||||||
onFileClick?: (filePath: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Regular expressions for parsing content
|
|
||||||
*/
|
|
||||||
const FILE_PATH_REGEX =
|
|
||||||
/([a-zA-Z]:)?([/\\][\w\-. ]+)+\.(tsx?|jsx?|css|scss|json|md|py|java|go|rs|c|cpp|h|hpp|sh|yaml|yml|toml|xml|html|vue|svelte)/gi;
|
|
||||||
// Match file paths with optional line numbers like: path/file.ts#7-14 or path/file.ts#7
|
|
||||||
const FILE_PATH_WITH_LINES_REGEX =
|
|
||||||
/([a-zA-Z]:)?([/\\][\w\-. ]+)+\.(tsx?|jsx?|css|scss|json|md|py|java|go|rs|c|cpp|h|hpp|sh|yaml|yml|toml|xml|html|vue|svelte)#(\d+)(?:-(\d+))?/gi;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* MarkdownRenderer component - renders markdown content with enhanced features
|
|
||||||
*/
|
|
||||||
export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
|
|
||||||
content,
|
|
||||||
onFileClick,
|
|
||||||
}) => {
|
|
||||||
/**
|
|
||||||
* Initialize markdown-it with plugins
|
|
||||||
*/
|
|
||||||
const getMarkdownInstance = (): MarkdownIt => {
|
|
||||||
// Create markdown-it instance with options
|
|
||||||
const md = new MarkdownIt({
|
|
||||||
html: false, // Disable HTML for security
|
|
||||||
xhtmlOut: false,
|
|
||||||
breaks: true,
|
|
||||||
linkify: true,
|
|
||||||
typographer: true,
|
|
||||||
} as MarkdownItOptions);
|
|
||||||
|
|
||||||
// Add syntax highlighting for code blocks
|
|
||||||
md.use((md) => {
|
|
||||||
md.renderer.rules.code_block = function (
|
|
||||||
tokens,
|
|
||||||
idx: number,
|
|
||||||
_options,
|
|
||||||
_env,
|
|
||||||
) {
|
|
||||||
const token = tokens[idx];
|
|
||||||
const lang = token.info || 'plaintext';
|
|
||||||
const content = token.content;
|
|
||||||
|
|
||||||
// Add syntax highlighting classes
|
|
||||||
return `<pre class="code-block language-${lang}"><code class="language-${lang}">${md.utils.escapeHtml(content)}</code></pre>`;
|
|
||||||
};
|
|
||||||
|
|
||||||
md.renderer.rules.fence = function (tokens, idx: number, _options, _env) {
|
|
||||||
const token = tokens[idx];
|
|
||||||
const lang = token.info || 'plaintext';
|
|
||||||
const content = token.content;
|
|
||||||
|
|
||||||
// Add syntax highlighting classes
|
|
||||||
return `<pre class="code-block language-${lang}"><code class="language-${lang}">${md.utils.escapeHtml(content)}</code></pre>`;
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return md;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render markdown content to HTML
|
|
||||||
*/
|
|
||||||
const renderMarkdown = (): string => {
|
|
||||||
try {
|
|
||||||
const md = getMarkdownInstance();
|
|
||||||
|
|
||||||
// Process the markdown content
|
|
||||||
let html = md.render(content);
|
|
||||||
|
|
||||||
// Post-process to add file path click handlers
|
|
||||||
html = processFilePaths(html);
|
|
||||||
|
|
||||||
return html;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error rendering markdown:', error);
|
|
||||||
// Fallback to plain text if markdown rendering fails
|
|
||||||
return escapeHtml(content);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Escape HTML characters for security
|
|
||||||
*/
|
|
||||||
const escapeHtml = (unsafe: string): string =>
|
|
||||||
unsafe
|
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/"/g, '"')
|
|
||||||
.replace(/'/g, ''');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Process file paths in HTML to make them clickable
|
|
||||||
*/
|
|
||||||
const processFilePaths = (html: string): string => {
|
|
||||||
// Process file paths with line numbers
|
|
||||||
html = html.replace(FILE_PATH_WITH_LINES_REGEX, (match) => {
|
|
||||||
const filePath = match.split('#')[0];
|
|
||||||
return `<button class="file-path-link" onclick="window.handleFileClick('${filePath}')" title="Open ${match}">${match}</button>`;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Process regular file paths
|
|
||||||
html = html.replace(FILE_PATH_REGEX, (match) => {
|
|
||||||
// Skip if this was already processed as a path with line numbers
|
|
||||||
if (FILE_PATH_WITH_LINES_REGEX.test(match)) {
|
|
||||||
return match;
|
|
||||||
}
|
|
||||||
return `<button class="file-path-link" onclick="window.handleFileClick('${match}')" title="Open ${match}">${match}</button>`;
|
|
||||||
});
|
|
||||||
|
|
||||||
return html;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle file click event
|
|
||||||
*/
|
|
||||||
const handleFileClick = (filePath: string) => {
|
|
||||||
if (onFileClick) {
|
|
||||||
onFileClick(filePath);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Attach the handler to window for use in HTML onclick attributes
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
(
|
|
||||||
window as { handleFileClick?: (filePath: string) => void }
|
|
||||||
).handleFileClick = handleFileClick;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="markdown-content"
|
|
||||||
dangerouslySetInnerHTML={{ __html: renderMarkdown() }}
|
|
||||||
style={{
|
|
||||||
wordWrap: 'break-word',
|
|
||||||
overflowWrap: 'break-word',
|
|
||||||
whiteSpace: 'normal',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -55,11 +55,44 @@
|
|||||||
|
|
||||||
.markdown-content ul,
|
.markdown-content ul,
|
||||||
.markdown-content ol {
|
.markdown-content ol {
|
||||||
margin-top: 0;
|
margin-top: 1em;
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
padding-left: 2em;
|
padding-left: 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Ensure list markers are visible even with global CSS resets */
|
||||||
|
.markdown-content ul {
|
||||||
|
list-style-type: disc;
|
||||||
|
list-style-position: outside;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content ol {
|
||||||
|
list-style-type: decimal;
|
||||||
|
list-style-position: outside;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Nested list styles */
|
||||||
|
.markdown-content ul ul {
|
||||||
|
list-style-type: circle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content ul ul ul {
|
||||||
|
list-style-type: square;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content ol ol {
|
||||||
|
list-style-type: lower-alpha;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content ol ol ol {
|
||||||
|
list-style-type: lower-roman;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style the marker explicitly so themes don't hide it */
|
||||||
|
.markdown-content li::marker {
|
||||||
|
color: var(--app-secondary-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
.markdown-content li {
|
.markdown-content li {
|
||||||
margin-bottom: 0.25em;
|
margin-bottom: 0.25em;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,263 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Qwen Team
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*
|
||||||
|
* MarkdownRenderer component - renders markdown content with syntax highlighting and clickable file paths
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type React from 'react';
|
||||||
|
import MarkdownIt from 'markdown-it';
|
||||||
|
import type { Options as MarkdownItOptions } from 'markdown-it';
|
||||||
|
import './MarkdownRenderer.css';
|
||||||
|
|
||||||
|
interface MarkdownRendererProps {
|
||||||
|
content: string;
|
||||||
|
onFileClick?: (filePath: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regular expressions for parsing content
|
||||||
|
*/
|
||||||
|
const FILE_PATH_REGEX =
|
||||||
|
/([a-zA-Z]:)?([/\\][\w\-. ]+)+\.(tsx?|jsx?|css|scss|json|md|py|java|go|rs|c|cpp|h|hpp|sh|yaml|yml|toml|xml|html|vue|svelte)/gi;
|
||||||
|
// Match file paths with optional line numbers like: path/file.ts#7-14 or path/file.ts#7
|
||||||
|
const FILE_PATH_WITH_LINES_REGEX =
|
||||||
|
/([a-zA-Z]:)?([/\\][\w\-. ]+)+\.(tsx?|jsx?|css|scss|json|md|py|java|go|rs|c|cpp|h|hpp|sh|yaml|yml|toml|xml|html|vue|svelte)#(\d+)(?:-(\d+))?/gi;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MarkdownRenderer component - renders markdown content with enhanced features
|
||||||
|
*/
|
||||||
|
export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
|
||||||
|
content,
|
||||||
|
onFileClick,
|
||||||
|
}) => {
|
||||||
|
/**
|
||||||
|
* Initialize markdown-it with plugins
|
||||||
|
*/
|
||||||
|
const getMarkdownInstance = (): MarkdownIt => {
|
||||||
|
// Create markdown-it instance with options
|
||||||
|
const md = new MarkdownIt({
|
||||||
|
html: false, // Disable HTML for security
|
||||||
|
xhtmlOut: false,
|
||||||
|
breaks: true,
|
||||||
|
linkify: true,
|
||||||
|
typographer: true,
|
||||||
|
} as MarkdownItOptions);
|
||||||
|
|
||||||
|
// Add syntax highlighting for code blocks
|
||||||
|
md.use((md) => {
|
||||||
|
md.renderer.rules.code_block = function (
|
||||||
|
tokens,
|
||||||
|
idx: number,
|
||||||
|
_options,
|
||||||
|
_env,
|
||||||
|
) {
|
||||||
|
const token = tokens[idx];
|
||||||
|
const lang = token.info || 'plaintext';
|
||||||
|
const content = token.content;
|
||||||
|
|
||||||
|
// Add syntax highlighting classes
|
||||||
|
return `<pre class="code-block language-${lang}"><code class="language-${lang}">${md.utils.escapeHtml(content)}</code></pre>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
md.renderer.rules.fence = function (tokens, idx: number, _options, _env) {
|
||||||
|
const token = tokens[idx];
|
||||||
|
const lang = token.info || 'plaintext';
|
||||||
|
const content = token.content;
|
||||||
|
|
||||||
|
// Add syntax highlighting classes
|
||||||
|
return `<pre class="code-block language-${lang}"><code class="language-${lang}">${md.utils.escapeHtml(content)}</code></pre>`;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return md;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render markdown content to HTML
|
||||||
|
*/
|
||||||
|
const renderMarkdown = (): string => {
|
||||||
|
try {
|
||||||
|
const md = getMarkdownInstance();
|
||||||
|
|
||||||
|
// Process the markdown content
|
||||||
|
let html = md.render(content);
|
||||||
|
|
||||||
|
// Post-process to add file path click handlers
|
||||||
|
html = processFilePaths(html);
|
||||||
|
|
||||||
|
return html;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error rendering markdown:', error);
|
||||||
|
// Fallback to plain text if markdown rendering fails
|
||||||
|
return escapeHtml(content);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escape HTML characters for security
|
||||||
|
*/
|
||||||
|
const escapeHtml = (unsafe: string): string =>
|
||||||
|
unsafe
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process file paths in HTML to make them clickable
|
||||||
|
*/
|
||||||
|
const processFilePaths = (html: string): string => {
|
||||||
|
// If DOM is not available, bail out to avoid breaking SSR
|
||||||
|
if (typeof document === 'undefined') return html;
|
||||||
|
|
||||||
|
// Build non-global variants to avoid .test() statefulness
|
||||||
|
const FILE_PATH_NO_G = new RegExp(
|
||||||
|
FILE_PATH_REGEX.source,
|
||||||
|
FILE_PATH_REGEX.flags.replace('g', ''),
|
||||||
|
);
|
||||||
|
const FILE_PATH_WITH_LINES_NO_G = new RegExp(
|
||||||
|
FILE_PATH_WITH_LINES_REGEX.source,
|
||||||
|
FILE_PATH_WITH_LINES_REGEX.flags.replace('g', ''),
|
||||||
|
);
|
||||||
|
// Match a bare file name like README.md (no leading slash)
|
||||||
|
const BARE_FILE_REGEX =
|
||||||
|
/[\w\-. ]+\.(tsx?|jsx?|css|scss|json|md|py|java|go|rs|c|cpp|h|hpp|sh|ya?ml|toml|xml|html|vue|svelte)/i;
|
||||||
|
|
||||||
|
// Parse HTML into a DOM tree so we don't replace inside attributes
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.innerHTML = html;
|
||||||
|
|
||||||
|
const union = new RegExp(
|
||||||
|
`${FILE_PATH_WITH_LINES_REGEX.source}|${FILE_PATH_REGEX.source}`,
|
||||||
|
'gi',
|
||||||
|
);
|
||||||
|
|
||||||
|
const makeLink = (text: string) => {
|
||||||
|
const link = document.createElement('a');
|
||||||
|
// Pass base path to the handler; keep the full text as label
|
||||||
|
const filePath = text.split('#')[0];
|
||||||
|
link.className = 'file-path-link';
|
||||||
|
link.textContent = text;
|
||||||
|
link.setAttribute('href', '#');
|
||||||
|
link.setAttribute('title', `Open ${text}`);
|
||||||
|
// Carry file path via data attribute; click handled by event delegation
|
||||||
|
link.setAttribute('data-file-path', filePath);
|
||||||
|
return link;
|
||||||
|
};
|
||||||
|
|
||||||
|
const upgradeAnchorIfFilePath = (a: HTMLAnchorElement) => {
|
||||||
|
const href = a.getAttribute('href') || '';
|
||||||
|
const text = (a.textContent || '').trim();
|
||||||
|
|
||||||
|
// If linkify turned a bare filename into http://<filename>, convert it back
|
||||||
|
const httpMatch = href.match(/^https?:\/\/(.+)$/i);
|
||||||
|
if (httpMatch && BARE_FILE_REGEX.test(text) && httpMatch[1] === text) {
|
||||||
|
// Treat as a file link instead of external URL
|
||||||
|
const filePath = text; // no leading slash
|
||||||
|
a.classList.add('file-path-link');
|
||||||
|
a.setAttribute('href', '#');
|
||||||
|
a.setAttribute('title', `Open ${text}`);
|
||||||
|
a.setAttribute('data-file-path', filePath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore other external protocols
|
||||||
|
if (/^(https?|mailto|ftp|data):/i.test(href)) return;
|
||||||
|
|
||||||
|
const candidate = href || text;
|
||||||
|
if (
|
||||||
|
FILE_PATH_WITH_LINES_NO_G.test(candidate) ||
|
||||||
|
FILE_PATH_NO_G.test(candidate)
|
||||||
|
) {
|
||||||
|
const filePath = candidate.split('#')[0];
|
||||||
|
a.classList.add('file-path-link');
|
||||||
|
a.setAttribute('href', '#');
|
||||||
|
a.setAttribute('title', `Open ${text || href}`);
|
||||||
|
a.setAttribute('data-file-path', filePath);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const walk = (node: Node) => {
|
||||||
|
// Do not transform inside existing anchors
|
||||||
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||||
|
const el = node as HTMLElement;
|
||||||
|
if (el.tagName.toLowerCase() === 'a') {
|
||||||
|
upgradeAnchorIfFilePath(el as HTMLAnchorElement);
|
||||||
|
return; // Don't descend into <a>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let child = node.firstChild; child; ) {
|
||||||
|
const next = child.nextSibling; // child may be replaced
|
||||||
|
if (child.nodeType === Node.TEXT_NODE) {
|
||||||
|
const text = child.nodeValue || '';
|
||||||
|
union.lastIndex = 0;
|
||||||
|
const hasMatch = union.test(text);
|
||||||
|
union.lastIndex = 0;
|
||||||
|
if (hasMatch) {
|
||||||
|
const frag = document.createDocumentFragment();
|
||||||
|
let lastIndex = 0;
|
||||||
|
let m: RegExpExecArray | null;
|
||||||
|
while ((m = union.exec(text))) {
|
||||||
|
const matchText = m[0];
|
||||||
|
const idx = m.index;
|
||||||
|
if (idx > lastIndex) {
|
||||||
|
frag.appendChild(
|
||||||
|
document.createTextNode(text.slice(lastIndex, idx)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
frag.appendChild(makeLink(matchText));
|
||||||
|
lastIndex = idx + matchText.length;
|
||||||
|
}
|
||||||
|
if (lastIndex < text.length) {
|
||||||
|
frag.appendChild(document.createTextNode(text.slice(lastIndex)));
|
||||||
|
}
|
||||||
|
node.replaceChild(frag, child);
|
||||||
|
}
|
||||||
|
} else if (child.nodeType === Node.ELEMENT_NODE) {
|
||||||
|
walk(child);
|
||||||
|
}
|
||||||
|
child = next;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
walk(container);
|
||||||
|
return container.innerHTML;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Event delegation: intercept clicks on generated file-path links
|
||||||
|
const handleContainerClick = (
|
||||||
|
e: React.MouseEvent<HTMLDivElement, MouseEvent>,
|
||||||
|
) => {
|
||||||
|
const target = e.target as HTMLElement | null;
|
||||||
|
if (!target) return;
|
||||||
|
|
||||||
|
// Find nearest anchor with our marker class
|
||||||
|
const anchor = (target.closest &&
|
||||||
|
target.closest('a.file-path-link')) as HTMLAnchorElement | null;
|
||||||
|
if (!anchor) return;
|
||||||
|
|
||||||
|
const filePath = anchor.getAttribute('data-file-path');
|
||||||
|
if (!filePath) return;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
onFileClick?.(filePath);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="markdown-content"
|
||||||
|
onClick={handleContainerClick}
|
||||||
|
dangerouslySetInnerHTML={{ __html: renderMarkdown() }}
|
||||||
|
style={{
|
||||||
|
wordWrap: 'break-word',
|
||||||
|
overflowWrap: 'break-word',
|
||||||
|
whiteSpace: 'normal',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import { MarkdownRenderer } from './MarkdownRenderer.tsx';
|
import { MarkdownRenderer } from './MarkdownRenderer/MarkdownRenderer.js';
|
||||||
|
|
||||||
interface MessageContentProps {
|
interface MessageContentProps {
|
||||||
content: string;
|
content: string;
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export const PermissionDrawer: React.FC<PermissionDrawerProps> = ({
|
|||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const customInputRef = useRef<HTMLDivElement>(null);
|
const customInputRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
console.log('PermissionDrawer rendered with isOpen:', isOpen, toolCall);
|
||||||
// Get the title for the permission request
|
// Get the title for the permission request
|
||||||
const getTitle = () => {
|
const getTitle = () => {
|
||||||
if (toolCall.kind === 'edit' || toolCall.kind === 'write') {
|
if (toolCall.kind === 'edit' || toolCall.kind === 'write') {
|
||||||
@@ -47,7 +48,7 @@ export const PermissionDrawer: React.FC<PermissionDrawerProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (toolCall.kind === 'execute' || toolCall.kind === 'bash') {
|
if (toolCall.kind === 'execute' || toolCall.kind === 'bash') {
|
||||||
return 'Allow command execution?';
|
return 'Allow this bash command?';
|
||||||
}
|
}
|
||||||
if (toolCall.kind === 'read') {
|
if (toolCall.kind === 'read') {
|
||||||
const fileName =
|
const fileName =
|
||||||
@@ -175,13 +176,8 @@ export const PermissionDrawer: React.FC<PermissionDrawerProps> = ({
|
|||||||
onMouseEnter={() => setFocusedIndex(index)}
|
onMouseEnter={() => setFocusedIndex(index)}
|
||||||
>
|
>
|
||||||
{/* Number badge */}
|
{/* Number badge */}
|
||||||
<span
|
{/* Plain number badge without hover background */}
|
||||||
className={`inline-flex items-center justify-center min-w-[20px] h-5 px-1.5 text-xs font-semibold rounded ${
|
<span className="inline-flex items-center justify-center min-w-[20px] h-5 px-1.5 text-xs font-semibold rounded">
|
||||||
isFocused
|
|
||||||
? 'text-inherit'
|
|
||||||
: 'bg-[var(--app-list-hover-background)]'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{index + 1}
|
{index + 1}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
@@ -208,13 +204,8 @@ export const PermissionDrawer: React.FC<PermissionDrawerProps> = ({
|
|||||||
onClick={() => customInputRef.current?.focus()}
|
onClick={() => customInputRef.current?.focus()}
|
||||||
>
|
>
|
||||||
{/* Number badge (N+1) */}
|
{/* Number badge (N+1) */}
|
||||||
<span
|
{/* Plain number badge without hover background */}
|
||||||
className={`inline-flex items-center justify-center min-w-[20px] h-5 px-1.5 text-xs font-semibold rounded ${
|
<span className="inline-flex items-center justify-center min-w-[20px] h-5 px-1.5 text-xs font-semibold rounded">
|
||||||
isFocused
|
|
||||||
? 'text-inherit'
|
|
||||||
: 'bg-[var(--app-list-hover-background)]'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{options.length + 1}
|
{options.length + 1}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,6 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type React from 'react';
|
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
export interface PermissionOption {
|
export interface PermissionOption {
|
||||||
name: string;
|
name: string;
|
||||||
kind: string;
|
kind: string;
|
||||||
@@ -39,192 +36,192 @@ export interface PermissionRequestProps {
|
|||||||
onResponse: (optionId: string) => void;
|
onResponse: (optionId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PermissionRequest: React.FC<PermissionRequestProps> = ({
|
// export const PermissionRequest: React.FC<PermissionRequestProps> = ({
|
||||||
options,
|
// options,
|
||||||
toolCall,
|
// toolCall,
|
||||||
onResponse,
|
// onResponse,
|
||||||
}) => {
|
// }) => {
|
||||||
const [selected, setSelected] = useState<string | null>(null);
|
// const [selected, setSelected] = useState<string | null>(null);
|
||||||
const [isResponding, setIsResponding] = useState(false);
|
// const [isResponding, setIsResponding] = useState(false);
|
||||||
const [hasResponded, setHasResponded] = useState(false);
|
// const [hasResponded, setHasResponded] = useState(false);
|
||||||
|
|
||||||
const getToolInfo = () => {
|
// const getToolInfo = () => {
|
||||||
if (!toolCall) {
|
// if (!toolCall) {
|
||||||
return {
|
// return {
|
||||||
title: 'Permission Request',
|
// title: 'Permission Request',
|
||||||
description: 'Agent is requesting permission',
|
// description: 'Agent is requesting permission',
|
||||||
icon: '🔐',
|
// icon: '🔐',
|
||||||
};
|
// };
|
||||||
}
|
// }
|
||||||
|
|
||||||
const displayTitle =
|
// const displayTitle =
|
||||||
toolCall.title || toolCall.rawInput?.description || 'Permission Request';
|
// toolCall.title || toolCall.rawInput?.description || 'Permission Request';
|
||||||
|
|
||||||
const kindIcons: Record<string, string> = {
|
// const kindIcons: Record<string, string> = {
|
||||||
edit: '✏️',
|
// edit: '✏️',
|
||||||
read: '📖',
|
// read: '📖',
|
||||||
fetch: '🌐',
|
// fetch: '🌐',
|
||||||
execute: '⚡',
|
// execute: '⚡',
|
||||||
delete: '🗑️',
|
// delete: '🗑️',
|
||||||
move: '📦',
|
// move: '📦',
|
||||||
search: '🔍',
|
// search: '🔍',
|
||||||
think: '💭',
|
// think: '💭',
|
||||||
other: '🔧',
|
// other: '🔧',
|
||||||
};
|
// };
|
||||||
|
|
||||||
return {
|
// return {
|
||||||
title: displayTitle,
|
// title: displayTitle,
|
||||||
icon: kindIcons[toolCall.kind || 'other'] || '🔧',
|
// icon: kindIcons[toolCall.kind || 'other'] || '🔧',
|
||||||
};
|
// };
|
||||||
};
|
// };
|
||||||
|
|
||||||
const { title, icon } = getToolInfo();
|
// const { title, icon } = getToolInfo();
|
||||||
|
|
||||||
const handleConfirm = async () => {
|
// const handleConfirm = async () => {
|
||||||
if (hasResponded || !selected) {
|
// if (hasResponded || !selected) {
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
setIsResponding(true);
|
// setIsResponding(true);
|
||||||
try {
|
// try {
|
||||||
await onResponse(selected);
|
// await onResponse(selected);
|
||||||
setHasResponded(true);
|
// setHasResponded(true);
|
||||||
} catch (error) {
|
// } catch (error) {
|
||||||
console.error('Error confirming permission:', error);
|
// console.error('Error confirming permission:', error);
|
||||||
} finally {
|
// } finally {
|
||||||
setIsResponding(false);
|
// setIsResponding(false);
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
|
|
||||||
if (!toolCall) {
|
// if (!toolCall) {
|
||||||
return null;
|
// return null;
|
||||||
}
|
// }
|
||||||
|
|
||||||
return (
|
// return (
|
||||||
<div className="permission-request-card">
|
// <div className="permission-request-card">
|
||||||
<div className="permission-card-body">
|
// <div className="permission-card-body">
|
||||||
{/* Header with icon and title */}
|
// {/* Header with icon and title */}
|
||||||
<div className="permission-header">
|
// <div className="permission-header">
|
||||||
<div className="permission-icon-wrapper">
|
// <div className="permission-icon-wrapper">
|
||||||
<span className="permission-icon">{icon}</span>
|
// <span className="permission-icon">{icon}</span>
|
||||||
</div>
|
// </div>
|
||||||
<div className="permission-info">
|
// <div className="permission-info">
|
||||||
<div className="permission-title">{title}</div>
|
// <div className="permission-title">{title}</div>
|
||||||
<div className="permission-subtitle">Waiting for your approval</div>
|
// <div className="permission-subtitle">Waiting for your approval</div>
|
||||||
</div>
|
// </div>
|
||||||
</div>
|
// </div>
|
||||||
|
|
||||||
{/* Show command if available */}
|
// {/* Show command if available */}
|
||||||
{(toolCall.rawInput?.command || toolCall.title) && (
|
// {(toolCall.rawInput?.command || toolCall.title) && (
|
||||||
<div className="permission-command-section">
|
// <div className="permission-command-section">
|
||||||
<div className="permission-command-header">
|
// <div className="permission-command-header">
|
||||||
<div className="permission-command-status">
|
// <div className="permission-command-status">
|
||||||
<span className="permission-command-dot">●</span>
|
// <span className="permission-command-dot">●</span>
|
||||||
<span className="permission-command-label">COMMAND</span>
|
// <span className="permission-command-label">COMMAND</span>
|
||||||
</div>
|
// </div>
|
||||||
</div>
|
// </div>
|
||||||
<div className="permission-command-content">
|
// <div className="permission-command-content">
|
||||||
<div className="permission-command-input-section">
|
// <div className="permission-command-input-section">
|
||||||
<span className="permission-command-io-label">IN</span>
|
// <span className="permission-command-io-label">IN</span>
|
||||||
<code className="permission-command-code">
|
// <code className="permission-command-code">
|
||||||
{toolCall.rawInput?.command || toolCall.title}
|
// {toolCall.rawInput?.command || toolCall.title}
|
||||||
</code>
|
// </code>
|
||||||
</div>
|
// </div>
|
||||||
{toolCall.rawInput?.description && (
|
// {toolCall.rawInput?.description && (
|
||||||
<div className="permission-command-description">
|
// <div className="permission-command-description">
|
||||||
{toolCall.rawInput.description}
|
// {toolCall.rawInput.description}
|
||||||
</div>
|
// </div>
|
||||||
)}
|
// )}
|
||||||
</div>
|
// </div>
|
||||||
</div>
|
// </div>
|
||||||
)}
|
// )}
|
||||||
|
|
||||||
{/* Show file locations if available */}
|
// {/* Show file locations if available */}
|
||||||
{toolCall.locations && toolCall.locations.length > 0 && (
|
// {toolCall.locations && toolCall.locations.length > 0 && (
|
||||||
<div className="permission-locations-section">
|
// <div className="permission-locations-section">
|
||||||
<div className="permission-locations-label">Affected Files</div>
|
// <div className="permission-locations-label">Affected Files</div>
|
||||||
{toolCall.locations.map((location, index) => (
|
// {toolCall.locations.map((location, index) => (
|
||||||
<div key={index} className="permission-location-item">
|
// <div key={index} className="permission-location-item">
|
||||||
<span className="permission-location-icon">📄</span>
|
// <span className="permission-location-icon">📄</span>
|
||||||
<span className="permission-location-path">
|
// <span className="permission-location-path">
|
||||||
{location.path}
|
// {location.path}
|
||||||
</span>
|
// </span>
|
||||||
{location.line !== null && location.line !== undefined && (
|
// {location.line !== null && location.line !== undefined && (
|
||||||
<span className="permission-location-line">
|
// <span className="permission-location-line">
|
||||||
::{location.line}
|
// ::{location.line}
|
||||||
</span>
|
// </span>
|
||||||
)}
|
// )}
|
||||||
</div>
|
// </div>
|
||||||
))}
|
// ))}
|
||||||
</div>
|
// </div>
|
||||||
)}
|
// )}
|
||||||
|
|
||||||
{/* Options */}
|
// {/* Options */}
|
||||||
{!hasResponded && (
|
// {!hasResponded && (
|
||||||
<div className="permission-options-section">
|
// <div className="permission-options-section">
|
||||||
<div className="permission-options-label">Choose an action:</div>
|
// <div className="permission-options-label">Choose an action:</div>
|
||||||
<div className="permission-options-list">
|
// <div className="permission-options-list">
|
||||||
{options && options.length > 0 ? (
|
// {options && options.length > 0 ? (
|
||||||
options.map((option, index) => {
|
// options.map((option, index) => {
|
||||||
const isSelected = selected === option.optionId;
|
// const isSelected = selected === option.optionId;
|
||||||
const isAllow = option.kind.includes('allow');
|
// const isAllow = option.kind.includes('allow');
|
||||||
const isAlways = option.kind.includes('always');
|
// const isAlways = option.kind.includes('always');
|
||||||
|
|
||||||
return (
|
// return (
|
||||||
<label
|
// <label
|
||||||
key={option.optionId}
|
// key={option.optionId}
|
||||||
className={`permission-option ${isSelected ? 'selected' : ''} ${
|
// className={`permission-option ${isSelected ? 'selected' : ''} ${
|
||||||
isAllow ? 'allow' : 'reject'
|
// isAllow ? 'allow' : 'reject'
|
||||||
} ${isAlways ? 'always' : ''}`}
|
// } ${isAlways ? 'always' : ''}`}
|
||||||
>
|
// >
|
||||||
<input
|
// <input
|
||||||
type="radio"
|
// type="radio"
|
||||||
name="permission"
|
// name="permission"
|
||||||
value={option.optionId}
|
// value={option.optionId}
|
||||||
checked={isSelected}
|
// checked={isSelected}
|
||||||
onChange={() => setSelected(option.optionId)}
|
// onChange={() => setSelected(option.optionId)}
|
||||||
className="permission-radio"
|
// className="permission-radio"
|
||||||
/>
|
// />
|
||||||
<span className="permission-option-content">
|
// <span className="permission-option-content">
|
||||||
<span className="permission-option-number">
|
// <span className="permission-option-number">
|
||||||
{index + 1}
|
// {index + 1}
|
||||||
</span>
|
// </span>
|
||||||
{isAlways && (
|
// {isAlways && (
|
||||||
<span className="permission-always-badge">⚡</span>
|
// <span className="permission-always-badge">⚡</span>
|
||||||
)}
|
// )}
|
||||||
{option.name}
|
// {option.name}
|
||||||
</span>
|
// </span>
|
||||||
</label>
|
// </label>
|
||||||
);
|
// );
|
||||||
})
|
// })
|
||||||
) : (
|
// ) : (
|
||||||
<div className="permission-no-options">
|
// <div className="permission-no-options">
|
||||||
No options available
|
// No options available
|
||||||
</div>
|
// </div>
|
||||||
)}
|
// )}
|
||||||
</div>
|
// </div>
|
||||||
<div className="permission-actions">
|
// <div className="permission-actions">
|
||||||
<button
|
// <button
|
||||||
className="permission-confirm-button"
|
// className="permission-confirm-button"
|
||||||
disabled={!selected || isResponding}
|
// disabled={!selected || isResponding}
|
||||||
onClick={handleConfirm}
|
// onClick={handleConfirm}
|
||||||
>
|
// >
|
||||||
{isResponding ? 'Processing...' : 'Confirm'}
|
// {isResponding ? 'Processing...' : 'Confirm'}
|
||||||
</button>
|
// </button>
|
||||||
</div>
|
// </div>
|
||||||
</div>
|
// </div>
|
||||||
)}
|
// )}
|
||||||
|
|
||||||
{/* Success message */}
|
// {/* Success message */}
|
||||||
{hasResponded && (
|
// {hasResponded && (
|
||||||
<div className="permission-success">
|
// <div className="permission-success">
|
||||||
<span className="permission-success-icon">✓</span>
|
// <span className="permission-success-icon">✓</span>
|
||||||
<span className="permission-success-text">
|
// <span className="permission-success-text">
|
||||||
Response sent successfully
|
// Response sent successfully
|
||||||
</span>
|
// </span>
|
||||||
</div>
|
// </div>
|
||||||
)}
|
// )}
|
||||||
</div>
|
// </div>
|
||||||
</div>
|
// </div>
|
||||||
);
|
// );
|
||||||
};
|
// };
|
||||||
|
|||||||
@@ -41,20 +41,12 @@
|
|||||||
color: #e1c08d;
|
color: #e1c08d;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Loading state - animated bullet (maps to .he) */
|
/* Loading state - static bullet (maps to .he) */
|
||||||
.assistant-message-container.assistant-message-loading::before {
|
.assistant-message-container.assistant-message-loading::before {
|
||||||
color: var(--app-secondary-foreground);
|
color: var(--app-secondary-foreground);
|
||||||
background-color: var(--app-secondary-background);
|
background-color: var(--app-secondary-background);
|
||||||
animation: assistantMessagePulse 1s linear infinite;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Pulse animation for loading state */
|
.assistant-message-container.assistant-message-loading::after {
|
||||||
@keyframes assistantMessagePulse {
|
display: none
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import { MessageContent } from '../MessageContent.js';
|
import { MessageContent } from '../../MessageContent.js';
|
||||||
import './AssistantMessage.css';
|
import './AssistantMessage.css';
|
||||||
|
|
||||||
interface AssistantMessageProps {
|
interface AssistantMessageProps {
|
||||||
@@ -13,6 +13,8 @@ interface AssistantMessageProps {
|
|||||||
timestamp: number;
|
timestamp: number;
|
||||||
onFileClick?: (path: string) => void;
|
onFileClick?: (path: string) => void;
|
||||||
status?: 'default' | 'success' | 'error' | 'warning' | 'loading';
|
status?: 'default' | 'success' | 'error' | 'warning' | 'loading';
|
||||||
|
// When true, render without the left status bullet (no ::before dot)
|
||||||
|
hideStatusIcon?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -31,6 +33,7 @@ export const AssistantMessage: React.FC<AssistantMessageProps> = ({
|
|||||||
timestamp: _timestamp,
|
timestamp: _timestamp,
|
||||||
onFileClick,
|
onFileClick,
|
||||||
status = 'default',
|
status = 'default',
|
||||||
|
hideStatusIcon = false,
|
||||||
}) => {
|
}) => {
|
||||||
// Empty content not rendered directly, avoid poor visual experience from only showing ::before dot
|
// Empty content not rendered directly, avoid poor visual experience from only showing ::before dot
|
||||||
if (!content || content.trim().length === 0) {
|
if (!content || content.trim().length === 0) {
|
||||||
@@ -39,6 +42,9 @@ export const AssistantMessage: React.FC<AssistantMessageProps> = ({
|
|||||||
|
|
||||||
// Map status to CSS class (only for ::before pseudo-element)
|
// Map status to CSS class (only for ::before pseudo-element)
|
||||||
const getStatusClass = () => {
|
const getStatusClass = () => {
|
||||||
|
if (hideStatusIcon) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'success':
|
case 'success':
|
||||||
return 'assistant-message-success';
|
return 'assistant-message-success';
|
||||||
@@ -16,7 +16,7 @@ export const StreamingMessage: React.FC<StreamingMessageProps> = ({
|
|||||||
content,
|
content,
|
||||||
onFileClick,
|
onFileClick,
|
||||||
}) => (
|
}) => (
|
||||||
<div className="flex gap-0 items-start text-left flex-col relative animate-[fadeIn_0.2s_ease-in]">
|
<div className="flex gap-0 items-start text-left flex-col relative">
|
||||||
<div
|
<div
|
||||||
className="inline-block my-1 relative whitespace-pre-wrap rounded-md max-w-full overflow-x-auto overflow-y-hidden select-text leading-[1.5]"
|
className="inline-block my-1 relative whitespace-pre-wrap rounded-md max-w-full overflow-x-auto overflow-y-hidden select-text leading-[1.5]"
|
||||||
style={{
|
style={{
|
||||||
@@ -30,7 +30,7 @@ export const StreamingMessage: React.FC<StreamingMessageProps> = ({
|
|||||||
<MessageContent content={content} onFileClick={onFileClick} />
|
<MessageContent content={content} onFileClick={onFileClick} />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="absolute right-3 bottom-3 animate-[pulse_1.5s_ease-in-out_infinite]"
|
className="absolute right-3 bottom-3"
|
||||||
style={{ color: 'var(--app-primary-foreground)' }}
|
style={{ color: 'var(--app-primary-foreground)' }}
|
||||||
>
|
>
|
||||||
●
|
●
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export const UserMessage: React.FC<UserMessageProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="qwen-message user-message-container flex gap-0 my-1 items-start text-left flex-col relative animate-[fadeIn_0.2s_ease-in]"
|
className="qwen-message user-message-container flex gap-0 my-1 items-start text-left flex-col relative"
|
||||||
style={{ position: 'relative' }}
|
style={{ position: 'relative' }}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -60,20 +60,20 @@ export const UserMessage: React.FC<UserMessageProps> = ({
|
|||||||
>
|
>
|
||||||
<MessageContent content={content} onFileClick={onFileClick} />
|
<MessageContent content={content} onFileClick={onFileClick} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* File context indicator */}
|
{/* File context indicator */}
|
||||||
{fileContextDisplay && (
|
{fileContextDisplay && (
|
||||||
<div>
|
<div className="mt-6">
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
className="mr"
|
className="mr inline-flex items-center py-0 pl-1 pr-2 ml-1 gap-1 rounded-sm cursor-pointer relative opacity-50 hover:opacity-100"
|
||||||
onClick={() => fileContext && onFileClick?.(fileContext.filePath)}
|
onClick={() => fileContext && onFileClick?.(fileContext.filePath)}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
fileContext && onFileClick?.(fileContext.filePath);
|
fileContext && onFileClick?.(fileContext.filePath);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
style={{ cursor: 'pointer' }}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="gr"
|
className="gr"
|
||||||
@@ -81,7 +81,6 @@ export const UserMessage: React.FC<UserMessageProps> = ({
|
|||||||
style={{
|
style={{
|
||||||
fontSize: '12px',
|
fontSize: '12px',
|
||||||
color: 'var(--app-secondary-foreground)',
|
color: 'var(--app-secondary-foreground)',
|
||||||
opacity: 0.8,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{fileContextDisplay}
|
{fileContextDisplay}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Qwen Team
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type React from 'react';
|
||||||
|
|
||||||
|
interface InterruptedMessageProps {
|
||||||
|
text?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A lightweight status line similar to WaitingMessage but without the left status icon.
|
||||||
|
export const InterruptedMessage: React.FC<InterruptedMessageProps> = ({
|
||||||
|
text = 'Interrupted',
|
||||||
|
}) => (
|
||||||
|
<div className="flex gap-0 items-start text-left py-2 flex-col opacity-85">
|
||||||
|
<div
|
||||||
|
className="qwen-message message-item interrupted-item"
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
paddingLeft: '30px', // keep alignment with other assistant messages, but no status icon
|
||||||
|
position: 'relative',
|
||||||
|
paddingTop: '8px',
|
||||||
|
paddingBottom: '8px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="opacity-70 italic">{text}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
@@ -4,6 +4,8 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@import url('../Assistant/AssistantMessage.css');
|
||||||
|
|
||||||
/* Subtle shimmering highlight across the loading text */
|
/* Subtle shimmering highlight across the loading text */
|
||||||
@keyframes waitingMessageShimmer {
|
@keyframes waitingMessageShimmer {
|
||||||
0% {
|
0% {
|
||||||
@@ -31,3 +33,6 @@
|
|||||||
animation: waitingMessageShimmer 1.6s linear infinite;
|
animation: waitingMessageShimmer 1.6s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.interrupted-item::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
@@ -6,9 +6,8 @@
|
|||||||
|
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import './AssistantMessage.css';
|
|
||||||
import './WaitingMessage.css';
|
import './WaitingMessage.css';
|
||||||
import { WITTY_LOADING_PHRASES } from '../../../constants/loadingMessages.js';
|
import { WITTY_LOADING_PHRASES } from '../../../../constants/loadingMessages.js';
|
||||||
|
|
||||||
interface WaitingMessageProps {
|
interface WaitingMessageProps {
|
||||||
loadingMessage: string;
|
loadingMessage: string;
|
||||||
@@ -66,7 +65,7 @@ export const WaitingMessage: React.FC<WaitingMessageProps> = ({
|
|||||||
}, [phrases]);
|
}, [phrases]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-0 items-start text-left py-2 flex-col opacity-85 animate-[fadeIn_0.2s_ease-in]">
|
<div className="flex gap-0 items-start text-left py-2 flex-col opacity-85">
|
||||||
{/* Use the same left status icon (pseudo-element) style as assistant-message-container */}
|
{/* Use the same left status icon (pseudo-element) style as assistant-message-container */}
|
||||||
<div
|
<div
|
||||||
className="assistant-message-container assistant-message-loading"
|
className="assistant-message-container assistant-message-loading"
|
||||||
@@ -5,8 +5,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export { UserMessage } from './UserMessage.js';
|
export { UserMessage } from './UserMessage.js';
|
||||||
export { AssistantMessage } from './AssistantMessage.js';
|
export { AssistantMessage } from './Assistant/AssistantMessage.js';
|
||||||
export { ThinkingMessage } from './ThinkingMessage.js';
|
export { ThinkingMessage } from './ThinkingMessage.js';
|
||||||
export { StreamingMessage } from './StreamingMessage.js';
|
export { StreamingMessage } from './StreamingMessage.js';
|
||||||
export { WaitingMessage } from './WaitingMessage.js';
|
export { WaitingMessage } from './Waiting/WaitingMessage.js';
|
||||||
|
export { InterruptedMessage } from './Waiting/InterruptedMessage.js';
|
||||||
export { PlanDisplay } from '../PlanDisplay.js';
|
export { PlanDisplay } from '../PlanDisplay.js';
|
||||||
|
|||||||
@@ -53,10 +53,27 @@ export const ExecuteToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Map tool status to container status for proper bullet coloring
|
||||||
|
const containerStatus:
|
||||||
|
| 'success'
|
||||||
|
| 'error'
|
||||||
|
| 'warning'
|
||||||
|
| 'loading'
|
||||||
|
| 'default' =
|
||||||
|
errors.length > 0
|
||||||
|
? 'error'
|
||||||
|
: toolCall.status === 'in_progress' || toolCall.status === 'pending'
|
||||||
|
? 'loading'
|
||||||
|
: 'success';
|
||||||
|
|
||||||
// Error case
|
// Error case
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
return (
|
return (
|
||||||
<ToolCallContainer label="Bash" status="error" toolCallId={toolCallId}>
|
<ToolCallContainer
|
||||||
|
label="Bash"
|
||||||
|
status={containerStatus}
|
||||||
|
toolCallId={toolCallId}
|
||||||
|
>
|
||||||
{/* Branch connector summary (Claude-like) */}
|
{/* Branch connector summary (Claude-like) */}
|
||||||
<div className="inline-flex text-[var(--app-secondary-foreground)] text-[0.85em] opacity-70 mt-[2px] mb-[2px] flex-row items-start w-full gap-1">
|
<div className="inline-flex text-[var(--app-secondary-foreground)] text-[0.85em] opacity-70 mt-[2px] mb-[2px] flex-row items-start w-full gap-1">
|
||||||
<span className="flex-shrink-0 relative top-[-0.1em]">⎿</span>
|
<span className="flex-shrink-0 relative top-[-0.1em]">⎿</span>
|
||||||
@@ -99,7 +116,11 @@ export const ExecuteToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
|||||||
output.length > 500 ? output.substring(0, 500) + '...' : output;
|
output.length > 500 ? output.substring(0, 500) + '...' : output;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToolCallContainer label="Bash" status="success" toolCallId={toolCallId}>
|
<ToolCallContainer
|
||||||
|
label="Bash"
|
||||||
|
status={containerStatus}
|
||||||
|
toolCallId={toolCallId}
|
||||||
|
>
|
||||||
{/* Branch connector summary (Claude-like) */}
|
{/* Branch connector summary (Claude-like) */}
|
||||||
<div className="inline-flex text-[var(--app-secondary-foreground)] text-[0.85em] opacity-70 mt-[2px] mb-[2px] flex-row items-start w-full gap-1">
|
<div className="inline-flex text-[var(--app-secondary-foreground)] text-[0.85em] opacity-70 mt-[2px] mb-[2px] flex-row items-start w-full gap-1">
|
||||||
<span className="flex-shrink-0 relative top-[-0.1em]">⎿</span>
|
<span className="flex-shrink-0 relative top-[-0.1em]">⎿</span>
|
||||||
@@ -141,7 +162,11 @@ export const ExecuteToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
|||||||
|
|
||||||
// Success without output: show command with branch connector
|
// Success without output: show command with branch connector
|
||||||
return (
|
return (
|
||||||
<ToolCallContainer label="Bash" status="success" toolCallId={toolCallId}>
|
<ToolCallContainer
|
||||||
|
label="Bash"
|
||||||
|
status={containerStatus}
|
||||||
|
toolCallId={toolCallId}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className="inline-flex text-[var(--app-secondary-foreground)] text-[0.85em] opacity-70 mt-[2px] mb-[2px] flex-row items-start w-full gap-1"
|
className="inline-flex text-[var(--app-secondary-foreground)] text-[0.85em] opacity-70 mt-[2px] mb-[2px] flex-row items-start w-full gap-1"
|
||||||
onClick={handleInClick}
|
onClick={handleInClick}
|
||||||
|
|||||||
@@ -9,10 +9,13 @@
|
|||||||
import { useEffect, useCallback } from 'react';
|
import { useEffect, useCallback } from 'react';
|
||||||
import type { BaseToolCallProps } from '../shared/types.js';
|
import type { BaseToolCallProps } from '../shared/types.js';
|
||||||
import { ToolCallContainer } from '../shared/LayoutComponents.js';
|
import { ToolCallContainer } from '../shared/LayoutComponents.js';
|
||||||
import { DiffDisplay } from '../shared/DiffDisplay.js';
|
import {
|
||||||
import { groupContent } from '../shared/utils.js';
|
groupContent,
|
||||||
|
mapToolStatusToContainerStatus,
|
||||||
|
} from '../shared/utils.js';
|
||||||
import { useVSCode } from '../../../hooks/useVSCode.js';
|
import { useVSCode } from '../../../hooks/useVSCode.js';
|
||||||
import { FileLink } from '../../ui/FileLink.js';
|
import { FileLink } from '../../ui/FileLink.js';
|
||||||
|
import { isDevelopmentMode } from '../../../utils/envUtils.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate diff summary (added/removed lines)
|
* Calculate diff summary (added/removed lines)
|
||||||
@@ -65,6 +68,7 @@ export const EditToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
|||||||
const getFileName = (path: string): string => path.split('/').pop() || path;
|
const getFileName = (path: string): string => path.split('/').pop() || path;
|
||||||
|
|
||||||
// Automatically trigger openDiff when diff content is detected (Claude Code style)
|
// Automatically trigger openDiff when diff content is detected (Claude Code style)
|
||||||
|
// Only trigger once per tool call by checking toolCallId
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Only auto-open if there are diffs and we have the required data
|
// Only auto-open if there are diffs and we have the required data
|
||||||
if (diffs.length > 0) {
|
if (diffs.length > 0) {
|
||||||
@@ -76,16 +80,16 @@ export const EditToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
|||||||
firstDiff.oldText !== undefined &&
|
firstDiff.oldText !== undefined &&
|
||||||
firstDiff.newText !== undefined
|
firstDiff.newText !== undefined
|
||||||
) {
|
) {
|
||||||
// TODO: 暂时注释
|
// TODO: 暂时注释自动打开功能,避免频繁触发
|
||||||
// Add a small delay to ensure the component is fully rendered
|
// Add a small delay to ensure the component is fully rendered
|
||||||
// const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
// handleOpenDiff(path, firstDiff.oldText, firstDiff.newText);
|
handleOpenDiff(path, firstDiff.oldText, firstDiff.newText);
|
||||||
// }, 100);
|
}, 100);
|
||||||
let timer;
|
// Proper cleanup function
|
||||||
return () => timer && clearTimeout(timer);
|
return () => timer && clearTimeout(timer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [diffs, locations, handleOpenDiff]);
|
}, [diffs, handleOpenDiff, locations]); // Add missing dependencies
|
||||||
|
|
||||||
// Error case: show error
|
// Error case: show error
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
@@ -121,15 +125,15 @@ export const EditToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
|||||||
const openFirstDiff = () =>
|
const openFirstDiff = () =>
|
||||||
handleOpenDiff(path, firstDiff.oldText, firstDiff.newText);
|
handleOpenDiff(path, firstDiff.oldText, firstDiff.newText);
|
||||||
|
|
||||||
|
const containerStatus = mapToolStatusToContainerStatus(toolCall.status);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="qwen-message message-item relative py-2 select-text cursor-pointer hover:bg-[var(--app-input-background)] toolcall-container toolcall-status-success"
|
className={`qwen-message message-item relative py-2 select-text cursor-pointer hover:bg-[var(--app-input-background)] toolcall-container toolcall-status-${containerStatus}`}
|
||||||
onClick={openFirstDiff}
|
onClick={openFirstDiff}
|
||||||
title="Open diff in VS Code"
|
title="Open diff in VS Code"
|
||||||
>
|
>
|
||||||
{/* Keep content within overall width: pl-[30px] provides the bullet indent; */}
|
|
||||||
{/* IMPORTANT: Always include min-w-0/max-w-full on inner wrappers to prevent overflow. */}
|
{/* IMPORTANT: Always include min-w-0/max-w-full on inner wrappers to prevent overflow. */}
|
||||||
<div className="toolcall-edit-content flex flex-col gap-1 min-w-0 max-w-full pl-[30px]">
|
<div className="toolcall-edit-content flex flex-col gap-1 min-w-0 max-w-full">
|
||||||
<div className="flex items-center justify-between min-w-0">
|
<div className="flex items-center justify-between min-w-0">
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
{/* Align the inline Edit label styling with shared toolcall label: larger + bold */}
|
{/* Align the inline Edit label styling with shared toolcall label: larger + bold */}
|
||||||
@@ -143,38 +147,18 @@ export const EditToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
|||||||
className="text-xs font-mono text-[var(--app-secondary-foreground)] hover:underline"
|
className="text-xs font-mono text-[var(--app-secondary-foreground)] hover:underline"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{/* {toolCallId && (
|
|
||||||
<span className="text-[10px] opacity-30">
|
|
||||||
[{toolCallId.slice(-8)}]
|
|
||||||
</span>
|
|
||||||
)} */}
|
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs opacity-60 ml-2">open</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="inline-flex text-[var(--app-secondary-foreground)] text-[0.85em] opacity-70 flex-row items-start w-full gap-1">
|
<div className="inline-flex text-[var(--app-secondary-foreground)] text-[0.85em] opacity-70 flex-row items-start w-full gap-1">
|
||||||
<span className="flex-shrink-0 relative top-[-0.1em]">⎿</span>
|
<span className="flex-shrink-0 relative top-[-0.1em]">⎿</span>
|
||||||
<span className="flex-shrink-0 w-full">{summary}</span>
|
<span className="flex-shrink-0 w-full">{summary}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content area aligned with bullet indent. Do NOT exceed container width. */}
|
{/* Show toolCallId only in development/debug mode */}
|
||||||
{/* For any custom blocks here, keep: min-w-0 max-w-full and avoid extra horizontal padding/margins. */}
|
{toolCallId && isDevelopmentMode() && (
|
||||||
<div className="pl-[30px] mt-1 min-w-0 max-w-full overflow-hidden">
|
<span className="text-[10px] opacity-30">
|
||||||
{diffs.map(
|
[{toolCallId.slice(-8)}]
|
||||||
(
|
</span>
|
||||||
item: import('../shared/types.js').ToolCallContent,
|
|
||||||
idx: number,
|
|
||||||
) => (
|
|
||||||
<DiffDisplay
|
|
||||||
key={`diff-${idx}`}
|
|
||||||
path={item.path}
|
|
||||||
oldText={item.oldText}
|
|
||||||
newText={item.newText}
|
|
||||||
onOpenDiff={() =>
|
|
||||||
handleOpenDiff(item.path, item.oldText, item.newText)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -184,10 +168,11 @@ export const EditToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
|||||||
// Success case without diff: show file in compact format
|
// Success case without diff: show file in compact format
|
||||||
if (locations && locations.length > 0) {
|
if (locations && locations.length > 0) {
|
||||||
const fileName = getFileName(locations[0].path);
|
const fileName = getFileName(locations[0].path);
|
||||||
|
const containerStatus = mapToolStatusToContainerStatus(toolCall.status);
|
||||||
return (
|
return (
|
||||||
<ToolCallContainer
|
<ToolCallContainer
|
||||||
label={`Edited ${fileName}`}
|
label={`Edited ${fileName}`}
|
||||||
status="success"
|
status={containerStatus}
|
||||||
toolCallId={toolCallId}
|
toolCallId={toolCallId}
|
||||||
>
|
>
|
||||||
<div className="inline-flex text-[var(--app-secondary-foreground)] text-[0.85em] opacity-70 flex-row items-start w-full gap-1">
|
<div className="inline-flex text-[var(--app-secondary-foreground)] text-[0.85em] opacity-70 flex-row items-start w-full gap-1">
|
||||||
|
|||||||
@@ -32,12 +32,25 @@ export const ExecuteToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
|||||||
inputCommand = rawInput;
|
inputCommand = rawInput;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Map tool status to container status for proper bullet coloring
|
||||||
|
const containerStatus:
|
||||||
|
| 'success'
|
||||||
|
| 'error'
|
||||||
|
| 'warning'
|
||||||
|
| 'loading'
|
||||||
|
| 'default' =
|
||||||
|
errors.length > 0 || toolCall.status === 'failed'
|
||||||
|
? 'error'
|
||||||
|
: toolCall.status === 'in_progress' || toolCall.status === 'pending'
|
||||||
|
? 'loading'
|
||||||
|
: 'success';
|
||||||
|
|
||||||
// Error case
|
// Error case
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
return (
|
return (
|
||||||
<ToolCallContainer
|
<ToolCallContainer
|
||||||
label="Execute"
|
label="Execute"
|
||||||
status="error"
|
status={containerStatus}
|
||||||
toolCallId={toolCallId}
|
toolCallId={toolCallId}
|
||||||
className="execute-default-toolcall"
|
className="execute-default-toolcall"
|
||||||
>
|
>
|
||||||
@@ -81,7 +94,7 @@ export const ExecuteToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
|||||||
return (
|
return (
|
||||||
<ToolCallContainer
|
<ToolCallContainer
|
||||||
label="Execute"
|
label="Execute"
|
||||||
status="success"
|
status={containerStatus}
|
||||||
toolCallId={toolCallId}
|
toolCallId={toolCallId}
|
||||||
>
|
>
|
||||||
{/* Branch connector summary (Claude-like) */}
|
{/* Branch connector summary (Claude-like) */}
|
||||||
@@ -117,7 +130,11 @@ export const ExecuteToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
|||||||
|
|
||||||
// Success without output: show command with branch connector
|
// Success without output: show command with branch connector
|
||||||
return (
|
return (
|
||||||
<ToolCallContainer label="Execute" status="success" toolCallId={toolCallId}>
|
<ToolCallContainer
|
||||||
|
label="Execute"
|
||||||
|
status={containerStatus}
|
||||||
|
toolCallId={toolCallId}
|
||||||
|
>
|
||||||
<div className="inline-flex text-[var(--app-secondary-foreground)] text-[0.85em] opacity-70 mt-[2px] mb-[2px] flex-row items-start w-full gap-1">
|
<div className="inline-flex text-[var(--app-secondary-foreground)] text-[0.85em] opacity-70 mt-[2px] mb-[2px] flex-row items-start w-full gap-1">
|
||||||
<span className="flex-shrink-0 relative top-[-0.1em]">⎿</span>
|
<span className="flex-shrink-0 relative top-[-0.1em]">⎿</span>
|
||||||
<span className="flex-shrink-0 w-full">{commandText}</span>
|
<span className="flex-shrink-0 w-full">{commandText}</span>
|
||||||
|
|||||||
@@ -9,7 +9,11 @@
|
|||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import type { BaseToolCallProps } from '../shared/types.js';
|
import type { BaseToolCallProps } from '../shared/types.js';
|
||||||
import { ToolCallContainer } from '../shared/LayoutComponents.js';
|
import { ToolCallContainer } from '../shared/LayoutComponents.js';
|
||||||
import { safeTitle, groupContent } from '../shared/utils.js';
|
import {
|
||||||
|
safeTitle,
|
||||||
|
groupContent,
|
||||||
|
mapToolStatusToContainerStatus,
|
||||||
|
} from '../shared/utils.js';
|
||||||
import './ExecuteNode.css';
|
import './ExecuteNode.css';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -60,7 +64,11 @@ export const ExecuteNodeToolCall: React.FC<BaseToolCallProps> = ({
|
|||||||
|
|
||||||
// Success case: show command with branch connector (similar to the example)
|
// Success case: show command with branch connector (similar to the example)
|
||||||
return (
|
return (
|
||||||
<ToolCallContainer label="Execute" status="success" toolCallId={toolCallId}>
|
<ToolCallContainer
|
||||||
|
label="Execute"
|
||||||
|
status={mapToolStatusToContainerStatus(toolCall.status)}
|
||||||
|
toolCallId={toolCallId}
|
||||||
|
>
|
||||||
<div className="inline-flex text-[var(--app-secondary-foreground)] text-[0.85em] opacity-70 mt-[2px] mb-[2px] flex-row items-start w-full gap-1">
|
<div className="inline-flex text-[var(--app-secondary-foreground)] text-[0.85em] opacity-70 mt-[2px] mb-[2px] flex-row items-start w-full gap-1">
|
||||||
<span className="flex-shrink-0 relative top-[-0.1em]">⎿</span>
|
<span className="flex-shrink-0 relative top-[-0.1em]">⎿</span>
|
||||||
<span className="flex-shrink-0 w-full">{commandText}</span>
|
<span className="flex-shrink-0 w-full">{commandText}</span>
|
||||||
|
|||||||
@@ -104,8 +104,16 @@ export const GenericToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Short output - compact format
|
// Short output - compact format
|
||||||
|
const statusFlag: 'success' | 'error' | 'warning' | 'loading' | 'default' =
|
||||||
|
toolCall.status === 'in_progress' || toolCall.status === 'pending'
|
||||||
|
? 'loading'
|
||||||
|
: 'success';
|
||||||
return (
|
return (
|
||||||
<ToolCallContainer label={kind} status="success" toolCallId={toolCallId}>
|
<ToolCallContainer
|
||||||
|
label={kind}
|
||||||
|
status={statusFlag}
|
||||||
|
toolCallId={toolCallId}
|
||||||
|
>
|
||||||
{operationText || output}
|
{operationText || output}
|
||||||
</ToolCallContainer>
|
</ToolCallContainer>
|
||||||
);
|
);
|
||||||
@@ -113,8 +121,16 @@ export const GenericToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
|||||||
|
|
||||||
// Success with files: show operation + file list in compact format
|
// Success with files: show operation + file list in compact format
|
||||||
if (locations && locations.length > 0) {
|
if (locations && locations.length > 0) {
|
||||||
|
const statusFlag: 'success' | 'error' | 'warning' | 'loading' | 'default' =
|
||||||
|
toolCall.status === 'in_progress' || toolCall.status === 'pending'
|
||||||
|
? 'loading'
|
||||||
|
: 'success';
|
||||||
return (
|
return (
|
||||||
<ToolCallContainer label={kind} status="success" toolCallId={toolCallId}>
|
<ToolCallContainer
|
||||||
|
label={kind}
|
||||||
|
status={statusFlag}
|
||||||
|
toolCallId={toolCallId}
|
||||||
|
>
|
||||||
<LocationsList locations={locations} />
|
<LocationsList locations={locations} />
|
||||||
</ToolCallContainer>
|
</ToolCallContainer>
|
||||||
);
|
);
|
||||||
@@ -122,8 +138,16 @@ export const GenericToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
|||||||
|
|
||||||
// No output - show just the operation
|
// No output - show just the operation
|
||||||
if (operationText) {
|
if (operationText) {
|
||||||
|
const statusFlag: 'success' | 'error' | 'warning' | 'loading' | 'default' =
|
||||||
|
toolCall.status === 'in_progress' || toolCall.status === 'pending'
|
||||||
|
? 'loading'
|
||||||
|
: 'success';
|
||||||
return (
|
return (
|
||||||
<ToolCallContainer label={kind} status="success" toolCallId={toolCallId}>
|
<ToolCallContainer
|
||||||
|
label={kind}
|
||||||
|
status={statusFlag}
|
||||||
|
toolCallId={toolCallId}
|
||||||
|
>
|
||||||
{operationText}
|
{operationText}
|
||||||
</ToolCallContainer>
|
</ToolCallContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,7 +9,10 @@
|
|||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import type { BaseToolCallProps } from '../shared/types.js';
|
import type { BaseToolCallProps } from '../shared/types.js';
|
||||||
import { ToolCallContainer } from '../shared/LayoutComponents.js';
|
import { ToolCallContainer } from '../shared/LayoutComponents.js';
|
||||||
import { groupContent } from '../shared/utils.js';
|
import {
|
||||||
|
groupContent,
|
||||||
|
mapToolStatusToContainerStatus,
|
||||||
|
} from '../shared/utils.js';
|
||||||
import { FileLink } from '../../ui/FileLink.js';
|
import { FileLink } from '../../ui/FileLink.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -23,6 +26,14 @@ export const ReadToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
|||||||
// Group content by type
|
// Group content by type
|
||||||
const { errors } = groupContent(content);
|
const { errors } = groupContent(content);
|
||||||
|
|
||||||
|
// Compute container status based on toolCall.status (pending/in_progress -> loading)
|
||||||
|
const containerStatus:
|
||||||
|
| 'success'
|
||||||
|
| 'error'
|
||||||
|
| 'warning'
|
||||||
|
| 'loading'
|
||||||
|
| 'default' = mapToolStatusToContainerStatus(toolCall.status);
|
||||||
|
|
||||||
// Error case: show error
|
// Error case: show error
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
const path = locations?.[0]?.path || '';
|
const path = locations?.[0]?.path || '';
|
||||||
@@ -54,7 +65,7 @@ export const ReadToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
|||||||
<ToolCallContainer
|
<ToolCallContainer
|
||||||
label={'Read'}
|
label={'Read'}
|
||||||
className="read-tool-call-success"
|
className="read-tool-call-success"
|
||||||
status="success"
|
status={containerStatus}
|
||||||
toolCallId={toolCallId}
|
toolCallId={toolCallId}
|
||||||
labelSuffix={
|
labelSuffix={
|
||||||
path ? (
|
path ? (
|
||||||
|
|||||||
@@ -7,14 +7,18 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import type { BaseToolCallProps } from './shared/types.js';
|
import type { BaseToolCallProps } from '../shared/types.js';
|
||||||
import {
|
import {
|
||||||
ToolCallContainer,
|
ToolCallContainer,
|
||||||
ToolCallCard,
|
ToolCallCard,
|
||||||
ToolCallRow,
|
ToolCallRow,
|
||||||
LocationsList,
|
LocationsList,
|
||||||
} from './shared/LayoutComponents.js';
|
} from '../shared/LayoutComponents.js';
|
||||||
import { safeTitle, groupContent } from './shared/utils.js';
|
import {
|
||||||
|
safeTitle,
|
||||||
|
groupContent,
|
||||||
|
mapToolStatusToContainerStatus,
|
||||||
|
} from '../shared/utils.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Specialized component for Search tool calls
|
* Specialized component for Search tool calls
|
||||||
@@ -44,6 +48,7 @@ export const SearchToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
|||||||
|
|
||||||
// Success case with results: show search query + file list
|
// Success case with results: show search query + file list
|
||||||
if (locations && locations.length > 0) {
|
if (locations && locations.length > 0) {
|
||||||
|
const containerStatus = mapToolStatusToContainerStatus(toolCall.status);
|
||||||
// If multiple results, use card layout; otherwise use compact format
|
// If multiple results, use card layout; otherwise use compact format
|
||||||
if (locations.length > 1) {
|
if (locations.length > 1) {
|
||||||
return (
|
return (
|
||||||
@@ -59,8 +64,13 @@ export const SearchToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
|||||||
}
|
}
|
||||||
// Single result - compact format
|
// Single result - compact format
|
||||||
return (
|
return (
|
||||||
<ToolCallContainer label="Search" status="success">
|
<ToolCallContainer
|
||||||
<span className="font-mono">{queryText}</span>
|
label="Search"
|
||||||
|
status={containerStatus}
|
||||||
|
className="search-toolcall"
|
||||||
|
labelSuffix={`(${queryText})`}
|
||||||
|
>
|
||||||
|
{/* <span className="font-mono">{queryText}</span> */}
|
||||||
<span className="mx-2 opacity-50">→</span>
|
<span className="mx-2 opacity-50">→</span>
|
||||||
<LocationsList locations={locations} />
|
<LocationsList locations={locations} />
|
||||||
</ToolCallContainer>
|
</ToolCallContainer>
|
||||||
@@ -69,8 +79,13 @@ export const SearchToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
|||||||
|
|
||||||
// No results - show query only
|
// No results - show query only
|
||||||
if (queryText) {
|
if (queryText) {
|
||||||
|
const containerStatus = mapToolStatusToContainerStatus(toolCall.status);
|
||||||
return (
|
return (
|
||||||
<ToolCallContainer label="Search" status="success">
|
<ToolCallContainer
|
||||||
|
label="Search"
|
||||||
|
status={containerStatus}
|
||||||
|
className="search-toolcall"
|
||||||
|
>
|
||||||
<span className="font-mono">{queryText}</span>
|
<span className="font-mono">{queryText}</span>
|
||||||
</ToolCallContainer>
|
</ToolCallContainer>
|
||||||
);
|
);
|
||||||
@@ -7,13 +7,13 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import type { BaseToolCallProps } from './shared/types.js';
|
import type { BaseToolCallProps } from '../shared/types.js';
|
||||||
import {
|
import {
|
||||||
ToolCallContainer,
|
ToolCallContainer,
|
||||||
ToolCallCard,
|
ToolCallCard,
|
||||||
ToolCallRow,
|
ToolCallRow,
|
||||||
} from './shared/LayoutComponents.js';
|
} from '../shared/LayoutComponents.js';
|
||||||
import { groupContent } from './shared/utils.js';
|
import { groupContent } from '../shared/utils.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Specialized component for Think tool calls
|
* Specialized component for Think tool calls
|
||||||
@@ -56,8 +56,12 @@ export const ThinkToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Short thoughts - compact format
|
// Short thoughts - compact format
|
||||||
|
const status =
|
||||||
|
toolCall.status === 'pending' || toolCall.status === 'in_progress'
|
||||||
|
? 'loading'
|
||||||
|
: 'default';
|
||||||
return (
|
return (
|
||||||
<ToolCallContainer label="Thinking" status="default">
|
<ToolCallContainer label="Thinking" status={status}>
|
||||||
<span className="italic opacity-90">{thoughts}</span>
|
<span className="italic opacity-90">{thoughts}</span>
|
||||||
</ToolCallContainer>
|
</ToolCallContainer>
|
||||||
);
|
);
|
||||||
@@ -7,10 +7,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import type { BaseToolCallProps } from './shared/types.js';
|
import type { BaseToolCallProps } from '../shared/types.js';
|
||||||
import { ToolCallContainer } from './shared/LayoutComponents.js';
|
import { ToolCallContainer } from '../shared/LayoutComponents.js';
|
||||||
import { groupContent, safeTitle } from './shared/utils.js';
|
import { groupContent, safeTitle } from '../shared/utils.js';
|
||||||
import { CheckboxDisplay } from '../ui/CheckboxDisplay.js';
|
import { CheckboxDisplay } from '../../ui/CheckboxDisplay.js';
|
||||||
|
|
||||||
type EntryStatus = 'pending' | 'in_progress' | 'completed';
|
type EntryStatus = 'pending' | 'in_progress' | 'completed';
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ interface TodoEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mapToolStatusToBullet = (
|
const mapToolStatusToBullet = (
|
||||||
status: import('./shared/types.js').ToolCallStatus,
|
status: import('../shared/types.js').ToolCallStatus,
|
||||||
): 'success' | 'error' | 'warning' | 'loading' | 'default' => {
|
): 'success' | 'error' | 'warning' | 'loading' | 'default' => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'completed':
|
case 'completed':
|
||||||
@@ -7,10 +7,13 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import type { BaseToolCallProps } from './shared/types.js';
|
import type { BaseToolCallProps } from '../shared/types.js';
|
||||||
import { ToolCallContainer } from './shared/LayoutComponents.js';
|
import { ToolCallContainer } from '../shared/LayoutComponents.js';
|
||||||
import { groupContent } from './shared/utils.js';
|
import {
|
||||||
import { FileLink } from '../ui/FileLink.js';
|
groupContent,
|
||||||
|
mapToolStatusToContainerStatus,
|
||||||
|
} from '../shared/utils.js';
|
||||||
|
import { FileLink } from '../../ui/FileLink.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Specialized component for Write tool calls
|
* Specialized component for Write tool calls
|
||||||
@@ -79,10 +82,11 @@ export const WriteToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
|||||||
if (locations && locations.length > 0) {
|
if (locations && locations.length > 0) {
|
||||||
const path = locations[0].path;
|
const path = locations[0].path;
|
||||||
const lineCount = writeContent.split('\n').length;
|
const lineCount = writeContent.split('\n').length;
|
||||||
|
const containerStatus = mapToolStatusToContainerStatus(toolCall.status);
|
||||||
return (
|
return (
|
||||||
<ToolCallContainer
|
<ToolCallContainer
|
||||||
label={'Created'}
|
label={'Created'}
|
||||||
status="success"
|
status={containerStatus}
|
||||||
toolCallId={toolCallId}
|
toolCallId={toolCallId}
|
||||||
labelSuffix={
|
labelSuffix={
|
||||||
path ? (
|
path ? (
|
||||||
@@ -104,8 +108,13 @@ export const WriteToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
|||||||
|
|
||||||
// Fallback: show generic success
|
// Fallback: show generic success
|
||||||
if (textOutputs.length > 0) {
|
if (textOutputs.length > 0) {
|
||||||
|
const containerStatus = mapToolStatusToContainerStatus(toolCall.status);
|
||||||
return (
|
return (
|
||||||
<ToolCallContainer label="Write" status="success" toolCallId={toolCallId}>
|
<ToolCallContainer
|
||||||
|
label="Write"
|
||||||
|
status={containerStatus}
|
||||||
|
toolCallId={toolCallId}
|
||||||
|
>
|
||||||
{textOutputs.join('\n')}
|
{textOutputs.join('\n')}
|
||||||
</ToolCallContainer>
|
</ToolCallContainer>
|
||||||
);
|
);
|
||||||
@@ -11,15 +11,14 @@ import type { BaseToolCallProps } from './shared/types.js';
|
|||||||
import { shouldShowToolCall } from './shared/utils.js';
|
import { shouldShowToolCall } from './shared/utils.js';
|
||||||
import { GenericToolCall } from './GenericToolCall.js';
|
import { GenericToolCall } from './GenericToolCall.js';
|
||||||
import { ReadToolCall } from './Read/ReadToolCall.js';
|
import { ReadToolCall } from './Read/ReadToolCall.js';
|
||||||
import { WriteToolCall } from './WriteToolCall.js';
|
import { WriteToolCall } from './Write/WriteToolCall.js';
|
||||||
import { EditToolCall } from './Edit/EditToolCall.js';
|
import { EditToolCall } from './Edit/EditToolCall.js';
|
||||||
import { ExecuteToolCall as BashExecuteToolCall } from './Bash/Bash.js';
|
import { ExecuteToolCall as BashExecuteToolCall } from './Bash/Bash.js';
|
||||||
import { ExecuteToolCall } from './Execute/Execute.js';
|
import { ExecuteToolCall } from './Execute/Execute.js';
|
||||||
import { UpdatedPlanToolCall } from './UpdatedPlan/UpdatedPlanToolCall.js';
|
import { UpdatedPlanToolCall } from './UpdatedPlan/UpdatedPlanToolCall.js';
|
||||||
import { ExecuteNodeToolCall } from './ExecuteNode/ExecuteNodeToolCall.js';
|
import { ExecuteNodeToolCall } from './ExecuteNode/ExecuteNodeToolCall.js';
|
||||||
import { SearchToolCall } from './SearchToolCall.js';
|
import { SearchToolCall } from './Search/SearchToolCall.js';
|
||||||
import { ThinkToolCall } from './ThinkToolCall.js';
|
import { ThinkToolCall } from './Think/ThinkToolCall.js';
|
||||||
import { TodoWriteToolCall } from './TodoWriteToolCall.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Factory function that returns the appropriate tool call component based on kind
|
* Factory function that returns the appropriate tool call component based on kind
|
||||||
@@ -69,7 +68,10 @@ export const getToolCallComponent = (
|
|||||||
case 'updated_plan':
|
case 'updated_plan':
|
||||||
case 'updatedplan':
|
case 'updatedplan':
|
||||||
case 'todo_write':
|
case 'todo_write':
|
||||||
|
case 'update_todos':
|
||||||
|
case 'todowrite':
|
||||||
return UpdatedPlanToolCall;
|
return UpdatedPlanToolCall;
|
||||||
|
// return TodoWriteToolCall;
|
||||||
|
|
||||||
case 'search':
|
case 'search':
|
||||||
case 'grep':
|
case 'grep':
|
||||||
@@ -81,18 +83,6 @@ export const getToolCallComponent = (
|
|||||||
case 'thinking':
|
case 'thinking':
|
||||||
return ThinkToolCall;
|
return ThinkToolCall;
|
||||||
|
|
||||||
case 'todowrite':
|
|
||||||
return TodoWriteToolCall;
|
|
||||||
// case 'todo_write':
|
|
||||||
case 'update_todos':
|
|
||||||
return TodoWriteToolCall;
|
|
||||||
|
|
||||||
// Add more specialized components as needed
|
|
||||||
// case 'fetch':
|
|
||||||
// return FetchToolCall;
|
|
||||||
// case 'delete':
|
|
||||||
// return DeleteToolCall;
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// Fallback to generic component
|
// Fallback to generic component
|
||||||
return GenericToolCall;
|
return GenericToolCall;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import { FileLink } from '../../ui/FileLink.js';
|
import { FileLink } from '../../ui/FileLink.js';
|
||||||
|
import { isDevelopmentMode } from '../../../utils/envUtils.js';
|
||||||
import './LayoutComponents.css';
|
import './LayoutComponents.css';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -53,12 +54,6 @@ export const ToolCallContainer: React.FC<ToolCallContainerProps> = ({
|
|||||||
<span className="text-[14px] leading-none font-bold text-[var(--app-primary-foreground)]">
|
<span className="text-[14px] leading-none font-bold text-[var(--app-primary-foreground)]">
|
||||||
{label}
|
{label}
|
||||||
</span>
|
</span>
|
||||||
{/* TODO: for 调试 */}
|
|
||||||
{_toolCallId && (
|
|
||||||
<span className="text-[10px] opacity-30">
|
|
||||||
[{_toolCallId.slice(-8)}]
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{labelSuffix}
|
{labelSuffix}
|
||||||
</div>
|
</div>
|
||||||
{children && (
|
{children && (
|
||||||
@@ -66,6 +61,13 @@ export const ToolCallContainer: React.FC<ToolCallContainerProps> = ({
|
|||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Show toolCallId only in development/debug mode */}
|
||||||
|
{_toolCallId && isDevelopmentMode() && (
|
||||||
|
<span className="text-[10px] opacity-30">
|
||||||
|
[{_toolCallId.slice(-8)}]
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,7 +6,11 @@
|
|||||||
* Shared utility functions for tool call components
|
* Shared utility functions for tool call components
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ToolCallContent, GroupedContent } from './types.js';
|
import type {
|
||||||
|
ToolCallContent,
|
||||||
|
GroupedContent,
|
||||||
|
ToolCallStatus,
|
||||||
|
} from './types.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format any value to a string for display
|
* Format any value to a string for display
|
||||||
@@ -194,3 +198,26 @@ export const groupContent = (content?: ToolCallContent[]): GroupedContent => {
|
|||||||
|
|
||||||
return { textOutputs, errors, diffs, otherData };
|
return { textOutputs, errors, diffs, otherData };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map a tool call status to a ToolCallContainer status (bullet color)
|
||||||
|
* - pending/in_progress -> loading
|
||||||
|
* - completed -> success
|
||||||
|
* - failed -> error
|
||||||
|
* - default fallback
|
||||||
|
*/
|
||||||
|
export const mapToolStatusToContainerStatus = (
|
||||||
|
status: ToolCallStatus,
|
||||||
|
): 'success' | 'error' | 'warning' | 'loading' | 'default' => {
|
||||||
|
switch (status) {
|
||||||
|
case 'pending':
|
||||||
|
case 'in_progress':
|
||||||
|
return 'loading';
|
||||||
|
case 'failed':
|
||||||
|
return 'error';
|
||||||
|
case 'completed':
|
||||||
|
return 'success';
|
||||||
|
default:
|
||||||
|
return 'default';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import type React from 'react';
|
||||||
|
|
||||||
export interface CheckboxDisplayProps {
|
export interface CheckboxDisplayProps {
|
||||||
checked?: boolean;
|
checked?: boolean;
|
||||||
@@ -29,44 +29,54 @@ export const CheckboxDisplay: React.FC<CheckboxDisplayProps> = ({
|
|||||||
style,
|
style,
|
||||||
title,
|
title,
|
||||||
}) => {
|
}) => {
|
||||||
const ref = React.useRef<HTMLInputElement | null>(null);
|
// Render as a span (not <input>) so we can draw a checkmark with CSS.
|
||||||
|
// Pseudo-elements do not reliably render on <input> in Chromium (VS Code webviews),
|
||||||
React.useEffect(() => {
|
// which caused the missing icon. This version is font-free and uses borders.
|
||||||
const el = ref.current;
|
const showCheck = !!checked && !indeterminate;
|
||||||
if (!el) {
|
const showDash = !!indeterminate;
|
||||||
return;
|
|
||||||
}
|
|
||||||
el.indeterminate = !!indeterminate;
|
|
||||||
if (indeterminate) {
|
|
||||||
el.setAttribute('data-indeterminate', 'true');
|
|
||||||
} else {
|
|
||||||
el.removeAttribute('data-indeterminate');
|
|
||||||
}
|
|
||||||
}, [indeterminate, checked]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<input
|
<span
|
||||||
ref={ref}
|
role="checkbox"
|
||||||
type="checkbox"
|
aria-checked={indeterminate ? 'mixed' : !!checked}
|
||||||
disabled={disabled}
|
aria-disabled={disabled || undefined}
|
||||||
checked={checked}
|
|
||||||
readOnly
|
|
||||||
aria-checked={indeterminate ? 'mixed' : checked}
|
|
||||||
title={title}
|
title={title}
|
||||||
style={style}
|
style={style}
|
||||||
className={[
|
className={[
|
||||||
// Base box style (equivalent to .q)
|
'q m-[2px] shrink-0 w-4 h-4 relative rounded-[2px] box-border',
|
||||||
'q appearance-none m-[2px] shrink-0 w-4 h-4 relative rounded-[2px] box-border',
|
'border border-[var(--app-input-border)] bg-[var(--app-input-background)]',
|
||||||
'border border-[var(--app-input-border)] bg-[var(--app-input-background)] text-[var(--app-primary-foreground)]',
|
|
||||||
'inline-flex items-center justify-center',
|
'inline-flex items-center justify-center',
|
||||||
// Checked visual state
|
showCheck ? 'opacity-70' : '',
|
||||||
'checked:opacity-70 checked:text-[#74c991]',
|
|
||||||
// Checkmark / indeterminate symbol via pseudo-element
|
|
||||||
'after:absolute after:left-1/2 after:top-1/2 after:-translate-x-1/2 after:-translate-y-1/2 after:opacity-0 after:pointer-events-none after:antialiased',
|
|
||||||
'checked:after:content-["\\2713"] checked:after:text-[0.9em] checked:after:opacity-100',
|
|
||||||
'data-[indeterminate=true]:text-[#e1c08d] data-[indeterminate=true]:after:content-["\\273d"] data-[indeterminate=true]:after:text-[0.8em] data-[indeterminate=true]:after:opacity-100',
|
|
||||||
className,
|
className,
|
||||||
].join(' ')}
|
].join(' ')}
|
||||||
/>
|
>
|
||||||
|
{showCheck ? (
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
className={[
|
||||||
|
'absolute block',
|
||||||
|
// Place the check slightly to the left/top so rotated arms stay inside the 16x16 box
|
||||||
|
'left-[3px] top-[3px]',
|
||||||
|
// 10x6 shape works well for a 16x16 checkbox
|
||||||
|
'w-2.5 h-1.5',
|
||||||
|
// Draw the L-corner and rotate to form a check
|
||||||
|
'border-l-2 border-b-2',
|
||||||
|
'border-[#74c991]',
|
||||||
|
'-rotate-45',
|
||||||
|
].join(' ')}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{showDash ? (
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
className={[
|
||||||
|
'absolute block',
|
||||||
|
'left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2',
|
||||||
|
'w-2 h-[2px] rounded-sm',
|
||||||
|
'bg-[#e1c08d]',
|
||||||
|
].join(' ')}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -112,7 +112,6 @@ export const FileLink: React.FC<FileLinkProps> = ({
|
|||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
className={[
|
className={[
|
||||||
// Keep a semantic handle for scoped overrides (e.g. DiffDisplay.css)
|
|
||||||
'file-link',
|
'file-link',
|
||||||
// Layout + interaction
|
// Layout + interaction
|
||||||
// Use items-center + leading-none to vertically center within surrounding rows
|
// Use items-center + leading-none to vertically center within surrounding rows
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import * as fs from 'fs';
|
|||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { BaseMessageHandler } from './BaseMessageHandler.js';
|
import { BaseMessageHandler } from './BaseMessageHandler.js';
|
||||||
|
import { FileOperations } from '../FileOperations.js';
|
||||||
import { getFileName } from '../utils/webviewUtils.js';
|
import { getFileName } from '../utils/webviewUtils.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -322,11 +323,7 @@ export class FileMessageHandler extends BaseMessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const uri = vscode.Uri.file(path);
|
await FileOperations.openFile(path);
|
||||||
await vscode.window.showTextDocument(uri, {
|
|
||||||
preview: false,
|
|
||||||
preserveFocus: false,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[FileMessageHandler] Failed to open file:', error);
|
console.error('[FileMessageHandler] Failed to open file:', error);
|
||||||
vscode.window.showErrorMessage(`Failed to open file: ${error}`);
|
vscode.window.showErrorMessage(`Failed to open file: ${error}`);
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ interface UseWebViewMessagesProps {
|
|||||||
breakAssistantSegment: () => void;
|
breakAssistantSegment: () => void;
|
||||||
appendThinkingChunk: (chunk: string) => void;
|
appendThinkingChunk: (chunk: string) => void;
|
||||||
clearThinking: () => void;
|
clearThinking: () => void;
|
||||||
|
setWaitingForResponse: (message: string) => void;
|
||||||
clearWaitingForResponse: () => void;
|
clearWaitingForResponse: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -421,6 +422,7 @@ export const useWebViewMessages = ({
|
|||||||
toolCallData.type = toolCallData.sessionUpdate;
|
toolCallData.type = toolCallData.sessionUpdate;
|
||||||
}
|
}
|
||||||
handlers.handleToolCallUpdate(toolCallData);
|
handlers.handleToolCallUpdate(toolCallData);
|
||||||
|
|
||||||
// Split assistant stream at tool boundaries similar to Claude/GPT rhythm
|
// Split assistant stream at tool boundaries similar to Claude/GPT rhythm
|
||||||
const status = (toolCallData.status || '').toString();
|
const status = (toolCallData.status || '').toString();
|
||||||
const isStart = toolCallData.type === 'tool_call';
|
const isStart = toolCallData.type === 'tool_call';
|
||||||
@@ -430,6 +432,31 @@ export const useWebViewMessages = ({
|
|||||||
if (isStart || isFinalUpdate) {
|
if (isStart || isFinalUpdate) {
|
||||||
handlers.messageHandling.breakAssistantSegment();
|
handlers.messageHandling.breakAssistantSegment();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// While long-running tools (e.g., execute/bash/command) are in progress,
|
||||||
|
// surface a lightweight loading indicator and expose the Stop button.
|
||||||
|
try {
|
||||||
|
const kind = (toolCallData.kind || '').toString().toLowerCase();
|
||||||
|
const isExec =
|
||||||
|
kind === 'execute' || kind === 'bash' || kind === 'command';
|
||||||
|
if (isExec && (status === 'pending' || status === 'in_progress')) {
|
||||||
|
const rawInput = toolCallData.rawInput;
|
||||||
|
let cmd = '';
|
||||||
|
if (typeof rawInput === 'string') {
|
||||||
|
cmd = rawInput;
|
||||||
|
} else if (rawInput && typeof rawInput === 'object') {
|
||||||
|
const maybe = rawInput as { command?: string };
|
||||||
|
cmd = maybe.command || '';
|
||||||
|
}
|
||||||
|
const hint = cmd ? `Running: ${cmd}` : 'Running command...';
|
||||||
|
handlers.messageHandling.setWaitingForResponse(hint);
|
||||||
|
}
|
||||||
|
if (status === 'completed' || status === 'failed') {
|
||||||
|
handlers.messageHandling.clearWaitingForResponse();
|
||||||
|
}
|
||||||
|
} catch (_err) {
|
||||||
|
// Best-effort UI hint; ignore errors
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,10 +9,10 @@
|
|||||||
|
|
||||||
/* Import component styles */
|
/* Import component styles */
|
||||||
@import '../components/toolcalls/shared/DiffDisplay.css';
|
@import '../components/toolcalls/shared/DiffDisplay.css';
|
||||||
@import '../components/messages/AssistantMessage.css';
|
@import '../components/messages/Assistant/AssistantMessage.css';
|
||||||
@import '../components/toolcalls/shared/SimpleTimeline.css';
|
@import '../components/toolcalls/shared/SimpleTimeline.css';
|
||||||
@import '../components/messages/QwenMessageTimeline.css';
|
@import '../components/messages/QwenMessageTimeline.css';
|
||||||
@import '../components/MarkdownRenderer.css';
|
@import '../components/MarkdownRenderer/MarkdownRenderer.css';
|
||||||
|
|
||||||
|
|
||||||
/* ===========================
|
/* ===========================
|
||||||
|
|||||||
@@ -155,4 +155,21 @@
|
|||||||
background-color: var(--app-qwen-clay-button-orange);
|
background-color: var(--app-qwen-clay-button-orange);
|
||||||
color: var(--app-qwen-ivory);
|
color: var(--app-qwen-ivory);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* File path styling inside tool call content
|
||||||
|
* Applies to: .toolcall-content-wrapper .file-link-path
|
||||||
|
* - Use monospace editor font
|
||||||
|
* - Slightly smaller size
|
||||||
|
* - Link color
|
||||||
|
* - Tighten top alignment and allow aggressive breaking for long paths
|
||||||
|
*/
|
||||||
|
.toolcall-content-wrapper .file-link-path {
|
||||||
|
/* Tailwind utilities where possible */
|
||||||
|
@apply text-[0.85em] pt-px break-all min-w-0;
|
||||||
|
/* Not covered by Tailwind defaults: use CSS vars / properties */
|
||||||
|
font-family: var(--app-monospace-font-family);
|
||||||
|
color: var(--app-link-color);
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
15
packages/vscode-ide-companion/src/webview/utils/envUtils.ts
Normal file
15
packages/vscode-ide-companion/src/webview/utils/envUtils.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Qwen Team
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function isDevelopmentMode(): boolean {
|
||||||
|
// TODO: 调试用
|
||||||
|
return false;
|
||||||
|
// return (
|
||||||
|
// process.env.NODE_ENV === 'development' ||
|
||||||
|
// process.env.DEBUG === 'true' ||
|
||||||
|
// process.env.NODE_ENV !== 'production'
|
||||||
|
// );
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ export default {
|
|||||||
'./src/webview/components/PermissionDrawer.tsx',
|
'./src/webview/components/PermissionDrawer.tsx',
|
||||||
'./src/webview/components/PlanDisplay.tsx',
|
'./src/webview/components/PlanDisplay.tsx',
|
||||||
'./src/webview/components/session/SessionSelector.tsx',
|
'./src/webview/components/session/SessionSelector.tsx',
|
||||||
|
'./src/webview/components/messages/UserMessage.tsx',
|
||||||
],
|
],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
|
|||||||
Reference in New Issue
Block a user