mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
feat(vscode-ide-companion): 增强工具调用与输入表单组件功能
- 新增 InProgressToolCall 组件用于展示进行中的工具调用状态 - 重构 InputForm 为独立组件,提升代码可维护性 - 改进 tool_call_update 处理逻辑,支持创建缺失的初始工具调用 - 添加思考块(thought chunk)日志以便调试 AI 思维过程 - 更新样式以支持新的进行中工具调用卡片显示 - 在权限请求时自动创建对应的工具调用记录 ```
This commit is contained in:
@@ -62,11 +62,21 @@ export class QwenSessionUpdateHandler {
|
||||
|
||||
case 'agent_thought_chunk':
|
||||
// 处理思考块 - 使用特殊回调
|
||||
console.log(
|
||||
'[SessionUpdateHandler] 🧠 THOUGHT CHUNK:',
|
||||
update.content?.text,
|
||||
);
|
||||
if (update.content?.text) {
|
||||
if (this.callbacks.onThoughtChunk) {
|
||||
console.log(
|
||||
'[SessionUpdateHandler] 🧠 Calling onThoughtChunk callback',
|
||||
);
|
||||
this.callbacks.onThoughtChunk(update.content.text);
|
||||
} else if (this.callbacks.onStreamChunk) {
|
||||
// 回退到常规流处理
|
||||
console.log(
|
||||
'[SessionUpdateHandler] 🧠 Falling back to onStreamChunk',
|
||||
);
|
||||
this.callbacks.onStreamChunk(update.content.text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,241 +233,6 @@ button {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ===========================
|
||||
Claude Code Style Input Form (.Me > .u)
|
||||
=========================== */
|
||||
/* Outer container (.Me) */
|
||||
.input-form-container {
|
||||
background-color: var(--app-primary-background);
|
||||
padding: 4px 16px 16px;
|
||||
}
|
||||
|
||||
/* Inner wrapper */
|
||||
.input-form-wrapper {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Input Form Container - matches Claude Code style */
|
||||
.input-form {
|
||||
background: var(--app-input-secondary-background, var(--app-input-background));
|
||||
border: 1px solid var(--app-input-border);
|
||||
border-radius: var(--corner-radius-large);
|
||||
color: var(--app-input-foreground);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
/* Inner background layer - creates depth effect */
|
||||
.input-form-background {
|
||||
background: var(--app-input-background);
|
||||
position: absolute;
|
||||
border-radius: var(--corner-radius-large);
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.input-form:focus-within {
|
||||
border-color: var(--app-qwen-orange);
|
||||
box-shadow: 0 1px 2px color-mix(in srgb,var(--app-qwen-orange),transparent 80%);
|
||||
}
|
||||
|
||||
/* Banner area - for warnings/messages */
|
||||
.input-banner {
|
||||
/* Empty for now, can be used for warnings/banners */
|
||||
}
|
||||
|
||||
/* Input wrapper - contains the contenteditable field */
|
||||
.input-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Contenteditable input field - matches Claude Code */
|
||||
.input-field-editable {
|
||||
padding: 10px 14px;
|
||||
outline: none;
|
||||
font-family: inherit;
|
||||
line-height: 1.5;
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
flex: 1;
|
||||
align-self: stretch;
|
||||
user-select: text;
|
||||
min-height: 1.5em;
|
||||
max-height: 200px;
|
||||
background-color: transparent;
|
||||
color: var(--app-input-foreground);
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
font-size: var(--vscode-chat-font-size, 13px);
|
||||
overflow-x: hidden;
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.input-field-editable:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.input-field-editable:empty:before {
|
||||
content: attr(data-placeholder);
|
||||
color: var(--app-input-placeholder-foreground);
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.input-field-editable:disabled,
|
||||
.input-field-editable[contenteditable='false'] {
|
||||
color: #999;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Actions row - matches Claude Code */
|
||||
.input-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 5px;
|
||||
color: var(--app-secondary-foreground);
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
border-top: 0.5px solid var(--app-input-border);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Edit mode button (.l) */
|
||||
.action-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
height: 32px;
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--corner-radius-small);
|
||||
color: var(--app-primary-foreground);
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
// font-weight: 500;
|
||||
transition: background-color 0.15s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.action-button:hover {
|
||||
background-color: var(--app-ghost-button-hover-background);
|
||||
}
|
||||
|
||||
.action-button svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Divider (.ii) */
|
||||
.action-divider {
|
||||
width: 1px;
|
||||
height: 24px;
|
||||
background-color: var(--app-transparent-inner-border);
|
||||
margin: 0 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Icon buttons (.H) */
|
||||
.action-icon-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--corner-radius-small);
|
||||
color: var(--app-secondary-foreground);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s, color 0.15s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.action-icon-button:hover {
|
||||
background-color: var(--app-ghost-button-hover-background);
|
||||
color: var(--app-primary-foreground);
|
||||
}
|
||||
|
||||
.action-icon-button.active {
|
||||
background-color: var(--app-qwen-clay-button-orange);
|
||||
color: var(--app-qwen-ivory);
|
||||
}
|
||||
|
||||
.action-icon-button.active svg {
|
||||
stroke: var(--app-qwen-ivory);
|
||||
fill: var(--app-qwen-ivory);
|
||||
}
|
||||
|
||||
.action-icon-button svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
/* Spacer to push file indicator to the right */
|
||||
.input-actions-spacer {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Active file indicator - shows current file selection */
|
||||
.active-file-indicator {
|
||||
// Inherits all styles from .action-button
|
||||
// Only add specific overrides here if needed
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Hide file indicator on very small screens */
|
||||
@media screen and (max-width: 330px) {
|
||||
.active-file-indicator {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Send button (.r) */
|
||||
.send-button-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
background: var(--app-qwen-clay-button-orange);
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--corner-radius-small);
|
||||
color: var(--app-qwen-ivory);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s, filter 0.15s;
|
||||
margin-left: auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.send-button-icon:hover:not(:disabled) {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
.send-button-icon:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.send-button-icon svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
/* ===========================
|
||||
Tool Call Card Styles (Grid Layout)
|
||||
=========================== */
|
||||
@@ -549,6 +314,92 @@ button {
|
||||
background: #f44336;
|
||||
}
|
||||
|
||||
/* ===========================
|
||||
In-Progress Tool Call Styles (Claude Code style)
|
||||
=========================== */
|
||||
.in-progress-tool-call {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--app-spacing-small);
|
||||
padding: var(--app-spacing-medium);
|
||||
margin: var(--app-spacing-small) 0;
|
||||
background: var(--app-input-background);
|
||||
border: 1px solid var(--app-input-border);
|
||||
border-radius: var(--corner-radius-small);
|
||||
animation: fadeIn 0.2s ease-in;
|
||||
}
|
||||
|
||||
.in-progress-tool-call-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--app-spacing-medium);
|
||||
}
|
||||
|
||||
.in-progress-tool-call-kind {
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
color: var(--app-primary-foreground);
|
||||
}
|
||||
|
||||
.in-progress-tool-call-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
color: var(--app-secondary-foreground);
|
||||
position: relative;
|
||||
padding-left: 14px;
|
||||
}
|
||||
|
||||
.in-progress-tool-call-status::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.in-progress-tool-call-status.pending::before {
|
||||
background: #ffc107;
|
||||
}
|
||||
|
||||
.in-progress-tool-call-status.in_progress::before {
|
||||
background: #2196f3;
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.in-progress-tool-call-status.completed::before {
|
||||
background: #4caf50;
|
||||
}
|
||||
|
||||
.in-progress-tool-call-status.failed::before {
|
||||
background: #f44336;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
.in-progress-tool-call-title {
|
||||
font-size: 12px;
|
||||
color: var(--app-secondary-foreground);
|
||||
font-family: var(--app-monospace-font-family);
|
||||
padding-left: 2px;
|
||||
}
|
||||
|
||||
.in-progress-tool-call-locations {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding-left: 2px;
|
||||
}
|
||||
|
||||
.code-block {
|
||||
font-family: var(--app-monospace-font-family);
|
||||
font-size: var(--app-monospace-font-size);
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
import { PermissionDrawer } from './components/PermissionDrawer.js';
|
||||
import { ToolCall, type ToolCallData } from './components/ToolCall.js';
|
||||
import { hasToolCallOutput } from './components/toolcalls/shared/utils.js';
|
||||
import { InProgressToolCall } from './components/InProgressToolCall.js';
|
||||
import { EmptyState } from './components/EmptyState.js';
|
||||
import { PlanDisplay, type PlanEntry } from './components/PlanDisplay.js';
|
||||
import {
|
||||
@@ -31,6 +32,7 @@ import {
|
||||
StreamingMessage,
|
||||
WaitingMessage,
|
||||
} from './components/messages/index.js';
|
||||
import { InputForm } from './components/InputForm.js';
|
||||
|
||||
interface ToolCallUpdate {
|
||||
type: 'tool_call' | 'tool_call_update';
|
||||
@@ -612,8 +614,8 @@ export const App: React.FC = () => {
|
||||
content,
|
||||
locations: update.locations,
|
||||
});
|
||||
} else if (update.type === 'tool_call_update' && existing) {
|
||||
// Update existing tool call
|
||||
} else if (update.type === 'tool_call_update') {
|
||||
// Update existing tool call, or create if it doesn't exist
|
||||
const updatedContent = update.content
|
||||
? update.content.map((item) => ({
|
||||
type: item.type as 'content' | 'diff',
|
||||
@@ -624,6 +626,8 @@ export const App: React.FC = () => {
|
||||
}))
|
||||
: undefined;
|
||||
|
||||
if (existing) {
|
||||
// Update existing tool call
|
||||
newMap.set(update.toolCallId, {
|
||||
...existing,
|
||||
...(update.kind && { kind: update.kind }),
|
||||
@@ -632,6 +636,18 @@ export const App: React.FC = () => {
|
||||
...(updatedContent && { content: updatedContent }),
|
||||
...(update.locations && { locations: update.locations }),
|
||||
});
|
||||
} else {
|
||||
// Create new tool call if it doesn't exist (missed the initial tool_call message)
|
||||
newMap.set(update.toolCallId, {
|
||||
toolCallId: update.toolCallId,
|
||||
kind: update.kind || 'other',
|
||||
title: safeTitle(update.title),
|
||||
status: update.status || 'pending',
|
||||
rawInput: update.rawInput as string | object | undefined,
|
||||
content: updatedContent,
|
||||
locations: update.locations,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return newMap;
|
||||
@@ -717,12 +733,14 @@ export const App: React.FC = () => {
|
||||
|
||||
case 'thoughtChunk': {
|
||||
const chunkData = message.data;
|
||||
console.log('[App] 🧠 THOUGHT CHUNK RECEIVED:', chunkData);
|
||||
// Handle thought chunks for AI thinking display
|
||||
const thinkingMessage: TextMessage = {
|
||||
role: 'thinking',
|
||||
content: chunkData.content || chunkData.chunk || '',
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
console.log('[App] 🧠 Adding thinking message:', thinkingMessage);
|
||||
setMessages((prev) => [...prev, thinkingMessage]);
|
||||
break;
|
||||
}
|
||||
@@ -760,10 +778,58 @@ export const App: React.FC = () => {
|
||||
// console.log('[App] Set notLoggedInMessage to:', (message.data as { message: string })?.message);
|
||||
// break;
|
||||
|
||||
case 'permissionRequest':
|
||||
case 'permissionRequest': {
|
||||
// Show permission dialog
|
||||
handlePermissionRequest(message.data);
|
||||
|
||||
// Also create a tool call entry for the permission request
|
||||
// This ensures that if it's rejected, we can show it properly
|
||||
const permToolCall = message.data?.toolCall as {
|
||||
toolCallId?: string;
|
||||
kind?: string;
|
||||
title?: string;
|
||||
status?: string;
|
||||
content?: unknown[];
|
||||
locations?: Array<{ path: string; line?: number | null }>;
|
||||
};
|
||||
|
||||
if (permToolCall?.toolCallId) {
|
||||
// Infer kind from title if not provided
|
||||
let kind = permToolCall.kind || 'execute';
|
||||
if (permToolCall.title) {
|
||||
const title = permToolCall.title.toLowerCase();
|
||||
if (title.includes('touch') || title.includes('echo')) {
|
||||
kind = 'execute';
|
||||
} else if (title.includes('read') || title.includes('cat')) {
|
||||
kind = 'read';
|
||||
} else if (title.includes('write') || title.includes('edit')) {
|
||||
kind = 'edit';
|
||||
}
|
||||
}
|
||||
|
||||
handleToolCallUpdate({
|
||||
type: 'tool_call',
|
||||
toolCallId: permToolCall.toolCallId,
|
||||
kind,
|
||||
title: permToolCall.title,
|
||||
status: permToolCall.status || 'pending',
|
||||
content: permToolCall.content as Array<{
|
||||
type: 'content' | 'diff';
|
||||
content?: {
|
||||
type: string;
|
||||
text?: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
path?: string;
|
||||
oldText?: string | null;
|
||||
newText?: string;
|
||||
[key: string]: unknown;
|
||||
}>,
|
||||
locations: permToolCall.locations,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'plan':
|
||||
// Update plan entries
|
||||
@@ -972,67 +1038,6 @@ export const App: React.FC = () => {
|
||||
setThinkingEnabled((prev) => !prev);
|
||||
};
|
||||
|
||||
// Get edit mode display info
|
||||
const getEditModeInfo = () => {
|
||||
switch (editMode) {
|
||||
case 'ask':
|
||||
return {
|
||||
text: 'Ask before edits',
|
||||
title: 'Qwen will ask before each edit. Click to switch modes.',
|
||||
icon: (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M11.013 2.513a1.75 1.75 0 0 1 2.475 2.474L6.226 12.25a2.751 2.751 0 0 1-.892.596l-2.047.848a.75.75 0 0 1-.98-.98l.848-2.047a2.75 2.75 0 0 1 .596-.892l7.262-7.261Z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
),
|
||||
};
|
||||
case 'auto':
|
||||
return {
|
||||
text: 'Edit automatically',
|
||||
title: 'Qwen will edit files automatically. Click to switch modes.',
|
||||
icon: (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M2.53 3.956A1 1 0 0 0 1 4.804v6.392a1 1 0 0 0 1.53.848l5.113-3.196c.16-.1.279-.233.357-.383v2.73a1 1 0 0 0 1.53.849l5.113-3.196a1 1 0 0 0 0-1.696L9.53 3.956A1 1 0 0 0 8 4.804v2.731a.992.992 0 0 0-.357-.383L2.53 3.956Z"></path>
|
||||
</svg>
|
||||
),
|
||||
};
|
||||
case 'plan':
|
||||
return {
|
||||
text: 'Plan mode',
|
||||
title: 'Qwen will plan before executing. Click to switch modes.',
|
||||
icon: (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M4.5 2a.5.5 0 0 0-.5.5v11a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-11a.5.5 0 0 0-.5-.5h-1ZM10.5 2a.5.5 0 0 0-.5.5v11a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-11a.5.5 0 0 0-.5-.5h-1Z"></path>
|
||||
</svg>
|
||||
),
|
||||
};
|
||||
default:
|
||||
return {
|
||||
text: 'Unknown mode',
|
||||
title: 'Unknown edit mode',
|
||||
icon: null,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -1417,9 +1422,28 @@ export const App: React.FC = () => {
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Tool Calls - only show those with actual output */}
|
||||
{/* In-Progress Tool Calls - show only pending/in_progress */}
|
||||
{Array.from(toolCalls.values())
|
||||
.filter((toolCall) => hasToolCallOutput(toolCall))
|
||||
.filter(
|
||||
(toolCall) =>
|
||||
toolCall.status === 'pending' ||
|
||||
toolCall.status === 'in_progress',
|
||||
)
|
||||
.map((toolCall) => (
|
||||
<InProgressToolCall
|
||||
key={toolCall.toolCallId}
|
||||
toolCall={toolCall}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Completed Tool Calls - only show those with actual output */}
|
||||
{Array.from(toolCalls.values())
|
||||
.filter(
|
||||
(toolCall) =>
|
||||
(toolCall.status === 'completed' ||
|
||||
toolCall.status === 'failed') &&
|
||||
hasToolCallOutput(toolCall),
|
||||
)
|
||||
.map((toolCall) => (
|
||||
<ToolCall key={toolCall.toolCallId} toolCall={toolCall} />
|
||||
))}
|
||||
@@ -1477,129 +1501,35 @@ export const App: React.FC = () => {
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="input-form-container">
|
||||
<div className="input-form-wrapper">
|
||||
{/* Context Pills - Removed: now using inline @mentions in input */}
|
||||
|
||||
<form className="input-form" onSubmit={handleSubmit}>
|
||||
<div className="input-form-background"></div>
|
||||
<div className="input-banner"></div>
|
||||
<div className="input-wrapper">
|
||||
<div
|
||||
ref={inputFieldRef}
|
||||
contentEditable="plaintext-only"
|
||||
className="input-field-editable"
|
||||
role="textbox"
|
||||
aria-label="Message input"
|
||||
aria-multiline="true"
|
||||
data-placeholder="Ask Qwen Code …"
|
||||
onInput={(e) => {
|
||||
const target = e.target as HTMLDivElement;
|
||||
setInputText(target.textContent || '');
|
||||
}}
|
||||
onCompositionStart={() => {
|
||||
setIsComposing(true);
|
||||
}}
|
||||
onCompositionEnd={() => {
|
||||
setIsComposing(false);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
// 如果正在进行中文输入法输入(拼音输入),不处理回车键
|
||||
if (e.key === 'Enter' && !e.shiftKey && !isComposing) {
|
||||
// 如果 CompletionMenu 打开,让它处理 Enter 键(选中文件)
|
||||
if (completion.isOpen) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
handleSubmit(e);
|
||||
}
|
||||
}}
|
||||
suppressContentEditableWarning
|
||||
/>
|
||||
</div>
|
||||
<div className="input-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="action-button edit-mode-button"
|
||||
title={getEditModeInfo().title}
|
||||
onClick={handleToggleEditMode}
|
||||
>
|
||||
{getEditModeInfo().icon}
|
||||
<span>{getEditModeInfo().text}</span>
|
||||
</button>
|
||||
{activeFileName && (
|
||||
<button
|
||||
type="button"
|
||||
className="action-button active-file-indicator"
|
||||
title={`Showing Qwen Code your current file selection: ${activeFileName}${activeSelection ? `#${activeSelection.startLine}-${activeSelection.endLine}` : ''}`}
|
||||
onClick={() => {
|
||||
// Request to focus/reveal the active file
|
||||
<InputForm
|
||||
inputText={inputText}
|
||||
inputFieldRef={inputFieldRef}
|
||||
isStreaming={isStreaming}
|
||||
isComposing={isComposing}
|
||||
editMode={editMode}
|
||||
thinkingEnabled={thinkingEnabled}
|
||||
activeFileName={activeFileName}
|
||||
activeSelection={activeSelection}
|
||||
onInputChange={setInputText}
|
||||
onCompositionStart={() => setIsComposing(true)}
|
||||
onCompositionEnd={() => setIsComposing(false)}
|
||||
onKeyDown={() => {}}
|
||||
onSubmit={handleSubmit}
|
||||
onToggleEditMode={handleToggleEditMode}
|
||||
onToggleThinking={handleToggleThinking}
|
||||
onFocusActiveEditor={() => {
|
||||
vscode.postMessage({
|
||||
type: 'focusActiveEditor',
|
||||
data: {},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
data-slot="icon"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M6.28 5.22a.75.75 0 0 1 0 1.06L2.56 10l3.72 3.72a.75.75 0 0 1-1.06 1.06L.97 10.53a.75.75 0 0 1 0-1.06l4.25-4.25a.75.75 0 0 1 1.06 0Zm7.44 0a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06-1.06L17.44 10l-3.72-3.72a.75.75 0 0 1 0-1.06ZM11.377 2.011a.75.75 0 0 1 .612.867l-2.5 14.5a.75.75 0 0 1-1.478-.255l2.5-14.5a.75.75 0 0 1 .866-.612Z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
<span>
|
||||
{activeFileName}
|
||||
{activeSelection &&
|
||||
` #${activeSelection.startLine}${activeSelection.startLine !== activeSelection.endLine ? `-${activeSelection.endLine}` : ''}`}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
<div className="action-divider"></div>
|
||||
{/* Spacer 将右侧按钮推到右边 */}
|
||||
<div className="input-actions-spacer"></div>
|
||||
<button
|
||||
type="button"
|
||||
className={`action-icon-button thinking-button ${thinkingEnabled ? 'active' : ''}`}
|
||||
title={thinkingEnabled ? 'Thinking on' : 'Thinking off'}
|
||||
onClick={handleToggleThinking}
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8.00293 1.11523L8.35059 1.12402H8.35352C11.9915 1.30834 14.8848 4.31624 14.8848 8C14.8848 11.8025 11.8025 14.8848 8 14.8848C4.19752 14.8848 1.11523 11.8025 1.11523 8C1.11523 7.67691 1.37711 7.41504 1.7002 7.41504C2.02319 7.41514 2.28516 7.67698 2.28516 8C2.28516 11.1563 4.84369 13.7148 8 13.7148C11.1563 13.7148 13.7148 11.1563 13.7148 8C13.7148 4.94263 11.3141 2.4464 8.29492 2.29297V2.29199L7.99609 2.28516H7.9873V2.28418L7.89648 2.27539L7.88281 2.27441V2.27344C7.61596 2.21897 7.41513 1.98293 7.41504 1.7002C7.41504 1.37711 7.67691 1.11523 8 1.11523H8.00293ZM8 3.81543C8.32309 3.81543 8.58496 4.0773 8.58496 4.40039V7.6377L10.9619 8.82715C11.2505 8.97169 11.3678 9.32256 11.2236 9.61133C11.0972 9.86425 10.8117 9.98544 10.5488 9.91504L10.5352 9.91211V9.91016L10.4502 9.87891L10.4385 9.87402V9.87305L7.73828 8.52344C7.54007 8.42433 7.41504 8.22155 7.41504 8V4.40039C7.41504 4.0773 7.67691 3.81543 8 3.81543ZM2.44336 5.12695C2.77573 5.19517 3.02597 5.48929 3.02637 5.8418C3.02637 6.19456 2.7761 6.49022 2.44336 6.55859L2.2959 6.57324C1.89241 6.57324 1.56543 6.24529 1.56543 5.8418C1.56588 5.43853 1.89284 5.1123 2.2959 5.1123L2.44336 5.12695ZM3.46094 2.72949C3.86418 2.72984 4.19017 3.05712 4.19043 3.45996V3.46094C4.19009 3.86393 3.86392 4.19008 3.46094 4.19043H3.45996C3.05712 4.19017 2.72983 3.86419 2.72949 3.46094V3.45996C2.72976 3.05686 3.05686 2.72976 3.45996 2.72949H3.46094ZM5.98926 1.58008C6.32235 1.64818 6.57324 1.94276 6.57324 2.2959L6.55859 2.44336C6.49022 2.7761 6.19456 3.02637 5.8418 3.02637C5.43884 3.02591 5.11251 2.69895 5.1123 2.2959L5.12695 2.14844C5.19504 1.81591 5.48906 1.56583 5.8418 1.56543L5.98926 1.58008Z"
|
||||
strokeWidth="0.27"
|
||||
style={{
|
||||
stroke: 'var(--app-secondary-foreground)',
|
||||
fill: 'var(--app-secondary-foreground)',
|
||||
}}
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="action-icon-button command-button"
|
||||
title="Show command menu (/)"
|
||||
onClick={async () => {
|
||||
onShowCommandMenu={async () => {
|
||||
if (inputFieldRef.current) {
|
||||
// Focus the input first to ensure cursor is in the right place
|
||||
inputFieldRef.current.focus();
|
||||
|
||||
// Get cursor position for menu placement
|
||||
const selection = window.getSelection();
|
||||
let position = { top: 0, left: 0 };
|
||||
|
||||
// Try to get precise cursor position
|
||||
if (selection && selection.rangeCount > 0) {
|
||||
try {
|
||||
const range = selection.getRangeAt(0);
|
||||
@@ -1610,90 +1540,26 @@ export const App: React.FC = () => {
|
||||
left: rangeRect.left,
|
||||
};
|
||||
} else {
|
||||
// Fallback to input element position
|
||||
const inputRect =
|
||||
inputFieldRef.current.getBoundingClientRect();
|
||||
position = {
|
||||
top: inputRect.top,
|
||||
left: inputRect.left,
|
||||
};
|
||||
position = { top: inputRect.top, left: inputRect.left };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'[App] Error getting cursor position:',
|
||||
error,
|
||||
);
|
||||
const inputRect =
|
||||
inputFieldRef.current.getBoundingClientRect();
|
||||
console.error('[App] Error getting cursor position:', error);
|
||||
const inputRect = inputFieldRef.current.getBoundingClientRect();
|
||||
position = { top: inputRect.top, left: inputRect.left };
|
||||
}
|
||||
} else {
|
||||
// No selection, use input element position
|
||||
const inputRect =
|
||||
inputFieldRef.current.getBoundingClientRect();
|
||||
const inputRect = inputFieldRef.current.getBoundingClientRect();
|
||||
position = { top: inputRect.top, left: inputRect.left };
|
||||
}
|
||||
|
||||
// Open completion menu with / commands
|
||||
await completion.openCompletion('/', '', position);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M12.528 3.047a.75.75 0 0 1 .449.961L8.433 16.504a.75.75 0 1 1-1.41-.512l4.544-12.496a.75.75 0 0 1 .961-.449Z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="action-icon-button attach-button"
|
||||
title="Attach context (Cmd/Ctrl + /)"
|
||||
onClick={handleAttachContextClick}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M15.621 4.379a3 3 0 0 0-4.242 0l-7 7a3 3 0 0 0 4.241 4.243h.001l.497-.5a.75.75 0 0 1 1.064 1.057l-.498.501-.002.002a4.5 4.5 0 0 1-6.364-6.364l7-7a4.5 4.5 0 0 1 6.368 6.36l-3.455 3.553A2.625 2.625 0 1 1 9.52 9.52l3.45-3.451a.75.75 0 1 1 1.061 1.06l-3.45 3.451a1.125 1.125 0 0 0 1.587 1.595l3.454-3.553a3 3 0 0 0 0-4.242Z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="send-button-icon"
|
||||
disabled={isStreaming || !inputText.trim()}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10 17a.75.75 0 0 1-.75-.75V5.612L5.29 9.77a.75.75 0 0 1-1.08-1.04l5.25-5.5a.75.75 0 0 1 1.08 0l5.25 5.5a.75.75 0 1 1-1.08 1.04l-3.96-4.158V16.25A.75.75 0 0 1 10 17Z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
onAttachContext={handleAttachContextClick}
|
||||
completionIsOpen={completion.isOpen}
|
||||
/>
|
||||
|
||||
{/* Save Session Dialog */}
|
||||
<SaveSessionDialog
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* In-progress tool call component - displays active tool calls with Claude Code style
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import type { ToolCallData } from './toolcalls/shared/types.js';
|
||||
import { FileLink } from './shared/FileLink.js';
|
||||
|
||||
interface InProgressToolCallProps {
|
||||
toolCall: ToolCallData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the kind name to a readable label
|
||||
*/
|
||||
const formatKind = (kind: string): string => {
|
||||
const kindMap: Record<string, string> = {
|
||||
read: 'Read',
|
||||
write: 'Write',
|
||||
edit: 'Edit',
|
||||
execute: 'Execute',
|
||||
bash: 'Execute',
|
||||
command: 'Execute',
|
||||
search: 'Search',
|
||||
grep: 'Search',
|
||||
glob: 'Search',
|
||||
find: 'Search',
|
||||
think: 'Think',
|
||||
thinking: 'Think',
|
||||
fetch: 'Fetch',
|
||||
delete: 'Delete',
|
||||
move: 'Move',
|
||||
};
|
||||
|
||||
return kindMap[kind.toLowerCase()] || 'Tool Call';
|
||||
};
|
||||
|
||||
/**
|
||||
* Get status display text
|
||||
*/
|
||||
const getStatusText = (status: string): string => {
|
||||
const statusMap: Record<string, string> = {
|
||||
pending: 'Pending',
|
||||
in_progress: 'In Progress',
|
||||
completed: 'Completed',
|
||||
failed: 'Failed',
|
||||
};
|
||||
|
||||
return statusMap[status] || status;
|
||||
};
|
||||
|
||||
/**
|
||||
* Component to display in-progress tool calls with Claude Code styling
|
||||
* Shows kind, status, and file locations
|
||||
*/
|
||||
export const InProgressToolCall: React.FC<InProgressToolCallProps> = ({
|
||||
toolCall,
|
||||
}) => {
|
||||
const { kind, status, title, locations } = toolCall;
|
||||
|
||||
// Format the kind label
|
||||
const kindLabel = formatKind(kind);
|
||||
|
||||
// Get status text
|
||||
const statusText = getStatusText(status || 'in_progress');
|
||||
|
||||
return (
|
||||
<div className="in-progress-tool-call">
|
||||
<div className="in-progress-tool-call-header">
|
||||
<span className="in-progress-tool-call-kind">{kindLabel}</span>
|
||||
<span
|
||||
className={`in-progress-tool-call-status ${status || 'in_progress'}`}
|
||||
>
|
||||
{statusText}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{title && title !== kindLabel && (
|
||||
<div className="in-progress-tool-call-title">{title}</div>
|
||||
)}
|
||||
|
||||
{locations && locations.length > 0 && (
|
||||
<div className="in-progress-tool-call-locations">
|
||||
{locations.map((loc, idx) => (
|
||||
<FileLink
|
||||
key={idx}
|
||||
path={loc.path}
|
||||
line={loc.line}
|
||||
showFullPath={false}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,354 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
|
||||
type EditMode = 'ask' | 'auto' | 'plan';
|
||||
|
||||
interface InputFormProps {
|
||||
inputText: string;
|
||||
inputFieldRef: React.RefObject<HTMLDivElement | null>;
|
||||
isStreaming: boolean;
|
||||
isComposing: boolean;
|
||||
editMode: EditMode;
|
||||
thinkingEnabled: boolean;
|
||||
activeFileName: string | null;
|
||||
activeSelection: { startLine: number; endLine: number } | null;
|
||||
onInputChange: (text: string) => void;
|
||||
onCompositionStart: () => void;
|
||||
onCompositionEnd: () => void;
|
||||
onKeyDown: (e: React.KeyboardEvent) => void;
|
||||
onSubmit: (e: React.FormEvent) => void;
|
||||
onToggleEditMode: () => void;
|
||||
onToggleThinking: () => void;
|
||||
onFocusActiveEditor: () => void;
|
||||
onShowCommandMenu: () => void;
|
||||
onAttachContext: () => void;
|
||||
completionIsOpen: boolean;
|
||||
}
|
||||
|
||||
// Get edit mode display info
|
||||
const getEditModeInfo = (editMode: EditMode) => {
|
||||
switch (editMode) {
|
||||
case 'ask':
|
||||
return {
|
||||
text: 'Ask before edits',
|
||||
title: 'Qwen will ask before each edit. Click to switch modes.',
|
||||
icon: (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M11.013 2.513a1.75 1.75 0 0 1 2.475 2.474L6.226 12.25a2.751 2.751 0 0 1-.892.596l-2.047.848a.75.75 0 0 1-.98-.98l.848-2.047a2.75 2.75 0 0 1 .596-.892l7.262-7.261Z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
),
|
||||
};
|
||||
case 'auto':
|
||||
return {
|
||||
text: 'Edit automatically',
|
||||
title: 'Qwen will edit files automatically. Click to switch modes.',
|
||||
icon: (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M2.53 3.956A1 1 0 0 0 1 4.804v6.392a1 1 0 0 0 1.53.848l5.113-3.196c.16-.1.279-.233.357-.383v2.73a1 1 0 0 0 1.53.849l5.113-3.196a1 1 0 0 0 0-1.696L9.53 3.956A1 1 0 0 0 8 4.804v2.731a.992.992 0 0 0-.357-.383L2.53 3.956Z"></path>
|
||||
</svg>
|
||||
),
|
||||
};
|
||||
case 'plan':
|
||||
return {
|
||||
text: 'Plan mode',
|
||||
title: 'Qwen will plan before executing. Click to switch modes.',
|
||||
icon: (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M4.5 2a.5.5 0 0 0-.5.5v11a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-11a.5.5 0 0 0-.5-.5h-1ZM10.5 2a.5.5 0 0 0-.5.5v11a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-11a.5.5 0 0 0-.5-.5h-1Z"></path>
|
||||
</svg>
|
||||
),
|
||||
};
|
||||
default:
|
||||
return {
|
||||
text: 'Unknown mode',
|
||||
title: 'Unknown edit mode',
|
||||
icon: null,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const InputForm: React.FC<InputFormProps> = ({
|
||||
inputText,
|
||||
inputFieldRef,
|
||||
isStreaming,
|
||||
isComposing,
|
||||
editMode,
|
||||
thinkingEnabled,
|
||||
activeFileName,
|
||||
activeSelection,
|
||||
onInputChange,
|
||||
onCompositionStart,
|
||||
onCompositionEnd,
|
||||
onKeyDown,
|
||||
onSubmit,
|
||||
onToggleEditMode,
|
||||
onToggleThinking,
|
||||
onFocusActiveEditor,
|
||||
onShowCommandMenu,
|
||||
onAttachContext,
|
||||
completionIsOpen,
|
||||
}) => {
|
||||
const editModeInfo = getEditModeInfo(editMode);
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
// If composing (Chinese IME input), don't process Enter key
|
||||
if (e.key === 'Enter' && !e.shiftKey && !isComposing) {
|
||||
// If CompletionMenu is open, let it handle Enter key
|
||||
if (completionIsOpen) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
onSubmit(e);
|
||||
}
|
||||
onKeyDown(e);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="p-1 px-4 pb-4"
|
||||
style={{ backgroundColor: 'var(--app-primary-background)' }}
|
||||
>
|
||||
<div className="block">
|
||||
<form
|
||||
className="relative flex flex-col rounded-large border shadow-sm transition-all duration-200 focus-within:shadow-md"
|
||||
style={{
|
||||
backgroundColor:
|
||||
'var(--app-input-secondary-background, var(--app-input-background))',
|
||||
borderColor: 'var(--app-input-border)',
|
||||
color: 'var(--app-input-foreground)',
|
||||
}}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
{/* Inner background layer */}
|
||||
<div
|
||||
className="absolute inset-0 rounded-large z-0"
|
||||
style={{ backgroundColor: 'var(--app-input-background)' }}
|
||||
/>
|
||||
|
||||
{/* Banner area */}
|
||||
<div className="input-banner" />
|
||||
|
||||
{/* Input wrapper */}
|
||||
<div className="relative flex z-[1]">
|
||||
<div
|
||||
ref={inputFieldRef}
|
||||
contentEditable="plaintext-only"
|
||||
className="flex-1 self-stretch p-2.5 px-3.5 outline-none font-inherit leading-relaxed overflow-y-auto relative select-text min-h-[1.5em] max-h-[200px] bg-transparent border-none rounded-none overflow-x-hidden break-words whitespace-pre-wrap empty:before:content-[attr(data-placeholder)] empty:before:absolute empty:before:pointer-events-none disabled:text-gray-400 disabled:cursor-not-allowed"
|
||||
style={{
|
||||
color: 'var(--app-input-foreground)',
|
||||
fontSize: 'var(--vscode-chat-font-size, 13px)',
|
||||
}}
|
||||
role="textbox"
|
||||
aria-label="Message input"
|
||||
aria-multiline="true"
|
||||
data-placeholder="Ask Qwen Code …"
|
||||
onInput={(e) => {
|
||||
const target = e.target as HTMLDivElement;
|
||||
onInputChange(target.textContent || '');
|
||||
}}
|
||||
onCompositionStart={onCompositionStart}
|
||||
onCompositionEnd={onCompositionEnd}
|
||||
onKeyDown={handleKeyDown}
|
||||
suppressContentEditableWarning
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Actions row */}
|
||||
<div
|
||||
className="flex items-center p-1.5 gap-1.5 min-w-0 z-[1]"
|
||||
style={{
|
||||
color: 'var(--app-secondary-foreground)',
|
||||
borderTop: '0.5px solid var(--app-input-border)',
|
||||
}}
|
||||
>
|
||||
{/* Edit mode button */}
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-1.5 px-2.5 py-1.5 h-8 bg-transparent border border-transparent rounded-small cursor-pointer text-xs whitespace-nowrap transition-colors duration-150 hover:bg-[var(--app-ghost-button-hover-background)] [&>svg]:w-4 [&>svg]:h-4 [&>svg]:flex-shrink-0"
|
||||
style={{ color: 'var(--app-primary-foreground)' }}
|
||||
title={editModeInfo.title}
|
||||
onClick={onToggleEditMode}
|
||||
>
|
||||
{editModeInfo.icon}
|
||||
<span>{editModeInfo.text}</span>
|
||||
</button>
|
||||
|
||||
{/* Active file indicator */}
|
||||
{activeFileName && (
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-1.5 px-2.5 py-1.5 h-8 bg-transparent border border-transparent rounded-small cursor-pointer text-xs whitespace-nowrap transition-colors duration-150 hover:bg-[var(--app-ghost-button-hover-background)] [&>svg]:w-4 [&>svg]:h-4 [&>svg]:flex-shrink-0 max-w-[200px] overflow-hidden text-ellipsis flex-shrink min-w-0"
|
||||
style={{ color: 'var(--app-primary-foreground)' }}
|
||||
title={`Showing Qwen Code your current file selection: ${activeFileName}${activeSelection ? `#${activeSelection.startLine}-${activeSelection.endLine}` : ''}`}
|
||||
onClick={onFocusActiveEditor}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
data-slot="icon"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M6.28 5.22a.75.75 0 0 1 0 1.06L2.56 10l3.72 3.72a.75.75 0 0 1-1.06 1.06L.97 10.53a.75.75 0 0 1 0-1.06l4.25-4.25a.75.75 0 0 1 1.06 0Zm7.44 0a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06-1.06L17.44 10l-3.72-3.72a.75.75 0 0 1 0-1.06ZM11.377 2.011a.75.75 0 0 1 .612.867l-2.5 14.5a.75.75 0 0 1-1.478-.255l2.5-14.5a.75.75 0 0 1 .866-.612Z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
<span>
|
||||
{activeFileName}
|
||||
{activeSelection &&
|
||||
` #${activeSelection.startLine}${activeSelection.startLine !== activeSelection.endLine ? `-${activeSelection.endLine}` : ''}`}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Divider */}
|
||||
<div
|
||||
className="w-px h-6 mx-0.5 flex-shrink-0"
|
||||
style={{
|
||||
backgroundColor: 'var(--app-transparent-inner-border)',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Spacer */}
|
||||
<div className="flex-1 min-w-0" />
|
||||
|
||||
{/* Thinking button */}
|
||||
<button
|
||||
type="button"
|
||||
className={`flex items-center justify-center w-8 h-8 p-0 bg-transparent border border-transparent rounded-small cursor-pointer transition-all duration-150 flex-shrink-0 hover:bg-[var(--app-ghost-button-hover-background)] [&>svg]:w-4 [&>svg]:h-4 ${
|
||||
thinkingEnabled
|
||||
? 'bg-qwen-clay-orange text-qwen-ivory [&>svg]:stroke-qwen-ivory [&>svg]:fill-qwen-ivory'
|
||||
: ''
|
||||
}`}
|
||||
style={{
|
||||
color: thinkingEnabled
|
||||
? 'var(--app-qwen-ivory)'
|
||||
: 'var(--app-secondary-foreground)',
|
||||
backgroundColor: thinkingEnabled
|
||||
? 'var(--app-qwen-clay-button-orange)'
|
||||
: undefined,
|
||||
}}
|
||||
title={thinkingEnabled ? 'Thinking on' : 'Thinking off'}
|
||||
onClick={onToggleThinking}
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8.00293 1.11523L8.35059 1.12402H8.35352C11.9915 1.30834 14.8848 4.31624 14.8848 8C14.8848 11.8025 11.8025 14.8848 8 14.8848C4.19752 14.8848 1.11523 11.8025 1.11523 8C1.11523 7.67691 1.37711 7.41504 1.7002 7.41504C2.02319 7.41514 2.28516 7.67698 2.28516 8C2.28516 11.1563 4.84369 13.7148 8 13.7148C11.1563 13.7148 13.7148 11.1563 13.7148 8C13.7148 4.94263 11.3141 2.4464 8.29492 2.29297V2.29199L7.99609 2.28516H7.9873V2.28418L7.89648 2.27539L7.88281 2.27441V2.27344C7.61596 2.21897 7.41513 1.98293 7.41504 1.7002C7.41504 1.37711 7.67691 1.11523 8 1.11523H8.00293ZM8 3.81543C8.32309 3.81543 8.58496 4.0773 8.58496 4.40039V7.6377L10.9619 8.82715C11.2505 8.97169 11.3678 9.32256 11.2236 9.61133C11.0972 9.86425 10.8117 9.98544 10.5488 9.91504L10.5352 9.91211V9.91016L10.4502 9.87891L10.4385 9.87402V9.87305L7.73828 8.52344C7.54007 8.42433 7.41504 8.22155 7.41504 8V4.40039C7.41504 4.0773 7.67691 3.81543 8 3.81543ZM2.44336 5.12695C2.77573 5.19517 3.02597 5.48929 3.02637 5.8418C3.02637 6.19456 2.7761 6.49022 2.44336 6.55859L2.2959 6.57324C1.89241 6.57324 1.56543 6.24529 1.56543 5.8418C1.56588 5.43853 1.89284 5.1123 2.2959 5.1123L2.44336 5.12695ZM3.46094 2.72949C3.86418 2.72984 4.19017 3.05712 4.19043 3.45996V3.46094C4.19009 3.86393 3.86392 4.19008 3.46094 4.19043H3.45996C3.05712 4.19017 2.72983 3.86419 2.72949 3.46094V3.45996C2.72976 3.05686 3.05686 2.72976 3.45996 2.72949H3.46094ZM5.98926 1.58008C6.32235 1.64818 6.57324 1.94276 6.57324 2.2959L6.55859 2.44336C6.49022 2.7761 6.19456 3.02637 5.8418 3.02637C5.43884 3.02591 5.11251 2.69895 5.1123 2.2959L5.12695 2.14844C5.19504 1.81591 5.48906 1.56583 5.8418 1.56543L5.98926 1.58008Z"
|
||||
strokeWidth="0.27"
|
||||
style={{
|
||||
stroke: thinkingEnabled
|
||||
? 'var(--app-qwen-ivory)'
|
||||
: 'var(--app-secondary-foreground)',
|
||||
fill: thinkingEnabled
|
||||
? 'var(--app-qwen-ivory)'
|
||||
: 'var(--app-secondary-foreground)',
|
||||
}}
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Command button */}
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center justify-center w-8 h-8 p-0 bg-transparent border border-transparent rounded-small cursor-pointer transition-all duration-150 flex-shrink-0 hover:bg-[var(--app-ghost-button-hover-background)] hover:text-[var(--app-primary-foreground)] [&>svg]:w-4 [&>svg]:h-4"
|
||||
style={{ color: 'var(--app-secondary-foreground)' }}
|
||||
title="Show command menu (/)"
|
||||
onClick={onShowCommandMenu}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M12.528 3.047a.75.75 0 0 1 .449.961L8.433 16.504a.75.75 0 1 1-1.41-.512l4.544-12.496a.75.75 0 0 1 .961-.449Z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Attach button */}
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center justify-center w-8 h-8 p-0 bg-transparent border border-transparent rounded-small cursor-pointer transition-all duration-150 flex-shrink-0 hover:bg-[var(--app-ghost-button-hover-background)] hover:text-[var(--app-primary-foreground)] [&>svg]:w-4 [&>svg]:h-4"
|
||||
style={{ color: 'var(--app-secondary-foreground)' }}
|
||||
title="Attach context (Cmd/Ctrl + /)"
|
||||
onClick={onAttachContext}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M15.621 4.379a3 3 0 0 0-4.242 0l-7 7a3 3 0 0 0 4.241 4.243h.001l.497-.5a.75.75 0 0 1 1.064 1.057l-.498.501-.002.002a4.5 4.5 0 0 1-6.364-6.364l7-7a4.5 4.5 0 0 1 6.368 6.36l-3.455 3.553A2.625 2.625 0 1 1 9.52 9.52l3.45-3.451a.75.75 0 1 1 1.061 1.06l-3.45 3.451a1.125 1.125 0 0 0 1.587 1.595l3.454-3.553a3 3 0 0 0 0-4.242Z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Send button */}
|
||||
<button
|
||||
type="submit"
|
||||
className="flex items-center justify-center w-8 h-8 p-0 border border-transparent rounded-small cursor-pointer transition-all duration-150 ml-auto flex-shrink-0 hover:brightness-110 disabled:opacity-40 disabled:cursor-not-allowed [&>svg]:w-5 [&>svg]:h-5"
|
||||
style={{
|
||||
backgroundColor: 'var(--app-qwen-clay-button-orange)',
|
||||
color: 'var(--app-qwen-ivory)',
|
||||
}}
|
||||
disabled={isStreaming || !inputText.trim()}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10 17a.75.75 0 0 1-.75-.75V5.612L5.29 9.77a.75.75 0 0 1-1.08-1.04l5.25-5.5a.75.75 0 0 1 1.08 0l5.25 5.5a.75.75 0 1 1-1.08 1.04l-3.96-4.158V16.25A.75.75 0 0 1 10 17Z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,560 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Permission Drawer - Bottom sheet style for permission requests
|
||||
*/
|
||||
|
||||
/* Backdrop overlay */
|
||||
.permission-drawer-backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: var(--app-modal-background);
|
||||
z-index: 998;
|
||||
animation: fadeIn 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
/* Drawer container - bottom sheet style */
|
||||
.permission-drawer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 8px;
|
||||
background-color: var(--app-input-secondary-background);
|
||||
border: 1px solid var(--app-input-border);
|
||||
border-radius: var(--corner-radius-large);
|
||||
max-height: 70vh;
|
||||
outline: 0;
|
||||
position: relative;
|
||||
margin-bottom: 6px;
|
||||
z-index: 999;
|
||||
animation: slideUpFromBottom 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* Background layer */
|
||||
.permission-drawer-background {
|
||||
background-color: var(--app-input-background);
|
||||
border-radius: var(--corner-radius-large);
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUpFromBottom {
|
||||
from {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Drawer header */
|
||||
.permission-drawer-header {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 28px 28px 24px;
|
||||
border-bottom: 1px solid var(--app-primary-border-color);
|
||||
background-color: var(--app-header-background);
|
||||
border-top-left-radius: 20px;
|
||||
border-top-right-radius: 20px;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.permission-drawer-title {
|
||||
font-weight: 700;
|
||||
color: var(--app-primary-foreground);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.permission-drawer-close {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
padding: 0;
|
||||
background: var(--app-secondary-background);
|
||||
border: 1px solid var(--app-transparent-inner-border);
|
||||
border-radius: 8px;
|
||||
color: var(--app-secondary-foreground);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.permission-drawer-close:hover {
|
||||
background-color: var(--app-ghost-button-hover-background);
|
||||
color: var(--app-primary-foreground);
|
||||
transform: scale(1.05);
|
||||
border-color: var(--app-primary-border-color);
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.permission-drawer-close:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
/* Drawer content */
|
||||
.permission-drawer-content {
|
||||
font-size: 1.1em;
|
||||
color: var(--app-primary-foreground);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
z-index: 1;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Override permission card styles when in drawer */
|
||||
.permission-drawer-content .permission-request-card {
|
||||
border: none;
|
||||
margin: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.permission-drawer-content .permission-card-body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Add a subtle border at the top of the card when in drawer */
|
||||
.permission-drawer-content .permission-request-card::before {
|
||||
content: '';
|
||||
display: block;
|
||||
height: 1px;
|
||||
background: var(--app-primary-border-color);
|
||||
margin-bottom: 24px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Scrollbar for drawer content */
|
||||
.permission-drawer-content::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.permission-drawer-content::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.permission-drawer-content::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.permission-drawer-content::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
/* Add a drag handle indicator at the top */
|
||||
.permission-drawer-header::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 48px;
|
||||
height: 5px;
|
||||
background-color: var(--app-secondary-foreground);
|
||||
opacity: 0.2;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.permission-drawer {
|
||||
max-height: 90vh;
|
||||
border-top-left-radius: 16px;
|
||||
border-top-right-radius: 16px;
|
||||
}
|
||||
|
||||
.permission-drawer-header {
|
||||
padding: 24px 24px 20px;
|
||||
border-top-left-radius: 16px;
|
||||
border-top-right-radius: 16px;
|
||||
}
|
||||
|
||||
.permission-drawer-header::before {
|
||||
top: 10px;
|
||||
width: 40px;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
.permission-drawer-content {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.permission-drawer-content::after {
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Permission Request Card Styles
|
||||
=========================================== */
|
||||
|
||||
.permission-request-card {
|
||||
background: var(--app-primary-background);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--app-primary-border-color);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.permission-card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
/* Permission Header */
|
||||
.permission-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.permission-icon-wrapper {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: var(--app-secondary-background);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--app-transparent-inner-border);
|
||||
}
|
||||
|
||||
.permission-icon {
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.permission-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.permission-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--app-primary-foreground);
|
||||
line-height: 1.4;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.permission-subtitle {
|
||||
font-size: 13px;
|
||||
color: var(--app-secondary-foreground);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Command Section - Bash style */
|
||||
.permission-command-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
background: var(--app-secondary-background);
|
||||
border: 1px solid var(--app-transparent-inner-border);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.permission-command-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
background: var(--app-secondary-background);
|
||||
border-bottom: 1px solid var(--app-transparent-inner-border);
|
||||
}
|
||||
|
||||
.permission-command-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.permission-command-dot {
|
||||
color: var(--app-qwen-orange, #ff8c00);
|
||||
font-size: 10px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.permission-command-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--app-secondary-foreground);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.permission-command-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.permission-command-input-section {
|
||||
display: grid;
|
||||
grid-template-columns: 40px 1fr;
|
||||
align-items: flex-start;
|
||||
padding: 12px 0;
|
||||
background: var(--app-primary-background);
|
||||
}
|
||||
|
||||
.permission-command-io-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--app-secondary-foreground);
|
||||
opacity: 0.7;
|
||||
text-align: right;
|
||||
padding-right: 16px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.permission-command-code {
|
||||
font-family: var(--vscode-editor-font-family, 'Monaco', 'Courier New', monospace);
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: var(--app-primary-foreground);
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0 16px 0 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.permission-command-description {
|
||||
font-size: 13px;
|
||||
color: var(--app-secondary-foreground);
|
||||
opacity: 0.8;
|
||||
padding: 12px 16px;
|
||||
border-top: 1px solid var(--app-transparent-inner-border);
|
||||
background: var(--app-secondary-background);
|
||||
}
|
||||
|
||||
/* Locations Section */
|
||||
.permission-locations-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.permission-locations-label {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--app-secondary-foreground);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.permission-location-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
background: var(--app-secondary-background);
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
border: 1px solid var(--app-transparent-inner-border);
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.permission-location-item:hover {
|
||||
background: var(--app-ghost-button-hover-background);
|
||||
border-color: var(--app-primary-border-color);
|
||||
}
|
||||
|
||||
.permission-location-icon {
|
||||
font-size: 16px;
|
||||
flex-shrink: 0;
|
||||
color: var(--app-qwen-orange, #ff8c00);
|
||||
}
|
||||
|
||||
.permission-location-path {
|
||||
flex: 1;
|
||||
color: var(--app-primary-foreground);
|
||||
font-family: var(--vscode-editor-font-family, 'Monaco', 'Courier New', monospace);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.permission-location-line {
|
||||
color: var(--app-secondary-foreground);
|
||||
opacity: 0.7;
|
||||
font-family: var(--vscode-editor-font-family, 'Monaco', 'Courier New', monospace);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Options Section */
|
||||
.permission-options-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.permission-options-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--app-secondary-foreground);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.permission-options-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.permission-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
background: var(--app-secondary-background);
|
||||
border: 1px solid var(--app-transparent-inner-border);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.permission-option:hover {
|
||||
background: var(--app-ghost-button-hover-background);
|
||||
border-color: var(--app-primary-border-color);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.permission-option.selected {
|
||||
background: var(--app-qwen-clay-button-orange);
|
||||
border-color: var(--app-qwen-orange, #ff8c00);
|
||||
box-shadow: 0 2px 6px rgba(255, 140, 0, 0.2);
|
||||
}
|
||||
|
||||
.permission-radio {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
accent-color: var(--app-qwen-orange, #ff8c00);
|
||||
}
|
||||
|
||||
.permission-option-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
color: var(--app-primary-foreground);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.permission-option-number {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: var(--app-qwen-orange, #ff8c00);
|
||||
color: var(--app-qwen-ivory, #fff);
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.permission-always-badge {
|
||||
font-size: 16px;
|
||||
flex-shrink: 0;
|
||||
color: #ffd700;
|
||||
}
|
||||
|
||||
.permission-no-options {
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
color: var(--app-secondary-foreground);
|
||||
opacity: 0.6;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* Actions */
|
||||
.permission-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
.permission-confirm-button {
|
||||
padding: 10px 20px;
|
||||
background: var(--app-qwen-orange, #ff8c00);
|
||||
color: var(--app-qwen-ivory, #fff);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 4px rgba(255, 140, 0, 0.3);
|
||||
}
|
||||
|
||||
.permission-confirm-button:hover:not(:disabled) {
|
||||
background: #e67e00;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(255, 140, 0, 0.4);
|
||||
}
|
||||
|
||||
.permission-confirm-button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* Success Message */
|
||||
.permission-success {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
background: var(--app-qwen-green, #6BCF7F);
|
||||
color: var(--app-qwen-ivory, #fff);
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
box-shadow: 0 2px 8px rgba(107, 207, 127, 0.3);
|
||||
animation: fadeIn 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.permission-success-icon {
|
||||
font-size: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.permission-success-text {
|
||||
flex: 1;
|
||||
font-weight: 500;
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* PermissionDrawer component using Tailwind CSS
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import {
|
||||
PermissionRequest,
|
||||
type PermissionOption,
|
||||
type ToolCall,
|
||||
} from './PermissionRequest.js';
|
||||
import { buttonClasses, commonClasses } from '../../lib/tailwindUtils.js';
|
||||
|
||||
interface PermissionDrawerProps {
|
||||
isOpen: boolean;
|
||||
options: PermissionOption[];
|
||||
toolCall: ToolCall;
|
||||
onResponse: (optionId: string) => void;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Permission drawer component - displays permission requests in a bottom sheet
|
||||
* Uses Tailwind CSS for styling
|
||||
* @param isOpen - Whether the drawer is open
|
||||
* @param options - Permission options to display
|
||||
* @param toolCall - Tool call information
|
||||
* @param onResponse - Callback when user responds
|
||||
* @param onClose - Optional callback when drawer closes
|
||||
*/
|
||||
export const PermissionDrawerTailwind: React.FC<PermissionDrawerProps> = ({
|
||||
isOpen,
|
||||
options,
|
||||
toolCall,
|
||||
onResponse,
|
||||
onClose,
|
||||
}) => {
|
||||
// Close drawer on Escape key
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (!isOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Close on Escape
|
||||
if (e.key === 'Escape' && onClose) {
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
|
||||
// Quick select with number keys (1-9)
|
||||
const numMatch = e.key.match(/^[1-9]$/);
|
||||
if (numMatch) {
|
||||
const index = parseInt(e.key, 10) - 1;
|
||||
if (index < options.length) {
|
||||
e.preventDefault();
|
||||
onResponse(options[index].optionId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [isOpen, onClose, options, onResponse]);
|
||||
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="fixed inset-0 bg-black bg-opacity-75 z-[998] animate-fadeIn"
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
{/* Drawer */}
|
||||
<div className="flex flex-col p-2 bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-large max-h-[70vh] outline-0 relative mb-1.5 z-[999] animate-slideUpFromBottom">
|
||||
<div className="bg-white dark:bg-gray-900 rounded-large absolute inset-0"></div>
|
||||
<div className="relative flex items-center justify-between p-7 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 rounded-t-2xl shadow-sm flex-shrink-0">
|
||||
<h3 className="font-bold text-gray-900 dark:text-white mb-1">Permission Required</h3>
|
||||
{onClose && (
|
||||
<button
|
||||
className={buttonClasses('icon')}
|
||||
onClick={onClose}
|
||||
aria-label="Close drawer"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M2 2L14 14M2 14L14 2"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="text-lg text-gray-900 dark:text-white flex flex-col min-h-0 z-10 flex-1 overflow-y-auto p-0 min-h-0">
|
||||
<PermissionRequest
|
||||
options={options}
|
||||
toolCall={toolCall}
|
||||
onResponse={onResponse}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -5,13 +5,8 @@
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import {
|
||||
PermissionRequest,
|
||||
type PermissionOption,
|
||||
type ToolCall,
|
||||
} from './PermissionRequest.js';
|
||||
import './PermissionDrawer.css';
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import type { PermissionOption, ToolCall } from './PermissionRequest.js';
|
||||
|
||||
interface PermissionDrawerProps {
|
||||
isOpen: boolean;
|
||||
@@ -22,12 +17,7 @@ interface PermissionDrawerProps {
|
||||
}
|
||||
|
||||
/**
|
||||
* Permission drawer component - displays permission requests in a bottom sheet
|
||||
* @param isOpen - Whether the drawer is open
|
||||
* @param options - Permission options to display
|
||||
* @param toolCall - Tool call information
|
||||
* @param onResponse - Callback when user responds
|
||||
* @param onClose - Optional callback when drawer closes
|
||||
* Permission drawer component - Claude Code style bottom sheet
|
||||
*/
|
||||
export const PermissionDrawer: React.FC<PermissionDrawerProps> = ({
|
||||
isOpen,
|
||||
@@ -36,80 +26,229 @@ export const PermissionDrawer: React.FC<PermissionDrawerProps> = ({
|
||||
onResponse,
|
||||
onClose,
|
||||
}) => {
|
||||
// Close drawer on Escape key
|
||||
const [focusedIndex, setFocusedIndex] = useState(0);
|
||||
const [customMessage, setCustomMessage] = useState('');
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const customInputRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Get the title for the permission request
|
||||
const getTitle = () => {
|
||||
if (toolCall.kind === 'edit' || toolCall.kind === 'write') {
|
||||
const fileName =
|
||||
toolCall.locations?.[0]?.path?.split('/').pop() || 'file';
|
||||
return (
|
||||
<>
|
||||
Allow write to{' '}
|
||||
<span className="font-mono text-[var(--app-primary-foreground)]">
|
||||
{fileName}
|
||||
</span>
|
||||
?
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (toolCall.kind === 'execute' || toolCall.kind === 'bash') {
|
||||
return 'Allow command execution?';
|
||||
}
|
||||
if (toolCall.kind === 'read') {
|
||||
const fileName =
|
||||
toolCall.locations?.[0]?.path?.split('/').pop() || 'file';
|
||||
return (
|
||||
<>
|
||||
Allow read from{' '}
|
||||
<span className="font-mono text-[var(--app-primary-foreground)]">
|
||||
{fileName}
|
||||
</span>
|
||||
?
|
||||
</>
|
||||
);
|
||||
}
|
||||
return toolCall.title || 'Permission Required';
|
||||
};
|
||||
|
||||
// Handle keyboard navigation
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (!isOpen) {
|
||||
return;
|
||||
}
|
||||
if (!isOpen) return;
|
||||
|
||||
// Close on Escape
|
||||
if (e.key === 'Escape' && onClose) {
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
|
||||
// Quick select with number keys (1-9)
|
||||
// Number keys 1-9 for quick select
|
||||
const numMatch = e.key.match(/^[1-9]$/);
|
||||
if (numMatch) {
|
||||
if (
|
||||
numMatch &&
|
||||
!customInputRef.current?.contains(document.activeElement)
|
||||
) {
|
||||
const index = parseInt(e.key, 10) - 1;
|
||||
if (index < options.length) {
|
||||
e.preventDefault();
|
||||
onResponse(options[index].optionId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Arrow keys for navigation
|
||||
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
const totalItems = options.length + 1; // +1 for custom input
|
||||
if (e.key === 'ArrowDown') {
|
||||
setFocusedIndex((prev) => (prev + 1) % totalItems);
|
||||
} else {
|
||||
setFocusedIndex((prev) => (prev - 1 + totalItems) % totalItems);
|
||||
}
|
||||
}
|
||||
|
||||
// Enter to select
|
||||
if (
|
||||
e.key === 'Enter' &&
|
||||
!customInputRef.current?.contains(document.activeElement)
|
||||
) {
|
||||
e.preventDefault();
|
||||
if (focusedIndex < options.length) {
|
||||
onResponse(options[focusedIndex].optionId);
|
||||
}
|
||||
}
|
||||
|
||||
// Escape to close
|
||||
if (e.key === 'Escape' && onClose) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [isOpen, onClose, options, onResponse]);
|
||||
}, [isOpen, options, onResponse, onClose, focusedIndex]);
|
||||
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
// Focus container when opened
|
||||
useEffect(() => {
|
||||
if (isOpen && containerRef.current) {
|
||||
containerRef.current.focus();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
<div className="permission-drawer-backdrop" onClick={onClose} />
|
||||
<div className="fixed inset-x-0 bottom-0 z-[1000] p-2">
|
||||
{/* Main container */}
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="relative flex flex-col rounded-large border p-2 outline-none animate-[slideUp_0.2s_ease-out]"
|
||||
style={{
|
||||
backgroundColor: 'var(--app-input-secondary-background)',
|
||||
borderColor: 'var(--app-input-border)',
|
||||
}}
|
||||
tabIndex={0}
|
||||
data-focused-index={focusedIndex}
|
||||
>
|
||||
{/* Background layer */}
|
||||
<div
|
||||
className="absolute inset-0 rounded-large"
|
||||
style={{ backgroundColor: 'var(--app-input-background)' }}
|
||||
/>
|
||||
|
||||
{/* Drawer */}
|
||||
<div className="permission-drawer">
|
||||
<div className="permission-drawer-background"></div>
|
||||
<div className="permission-drawer-header">
|
||||
<h3 className="permission-drawer-title">Permission Required</h3>
|
||||
{onClose && (
|
||||
{/* Title */}
|
||||
<div className="relative z-[1] px-3 py-3">
|
||||
<div
|
||||
className="text-sm font-medium"
|
||||
style={{ color: 'var(--app-secondary-foreground)' }}
|
||||
>
|
||||
{getTitle()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Options */}
|
||||
<div className="relative z-[1] flex flex-col gap-1 px-1 pb-1">
|
||||
{options.map((option, index) => {
|
||||
const isAlways = option.kind.includes('always');
|
||||
const isFocused = focusedIndex === index;
|
||||
|
||||
return (
|
||||
<button
|
||||
className="permission-drawer-close"
|
||||
onClick={onClose}
|
||||
aria-label="Close drawer"
|
||||
key={option.optionId}
|
||||
className={`flex items-center gap-2 px-3 py-2 text-left rounded-small border transition-colors duration-150 ${
|
||||
isFocused
|
||||
? 'bg-[var(--app-list-active-background)] text-[var(--app-list-active-foreground)] border-transparent'
|
||||
: 'hover:bg-[var(--app-list-hover-background)] border-transparent'
|
||||
}`}
|
||||
style={{
|
||||
color: isFocused
|
||||
? 'var(--app-list-active-foreground)'
|
||||
: 'var(--app-primary-foreground)',
|
||||
}}
|
||||
onClick={() => onResponse(option.optionId)}
|
||||
onMouseEnter={() => setFocusedIndex(index)}
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{/* Number badge */}
|
||||
<span
|
||||
className={`inline-flex items-center justify-center min-w-[20px] h-5 px-1.5 text-xs font-semibold rounded ${
|
||||
isFocused
|
||||
? 'bg-white/20 text-inherit'
|
||||
: 'bg-[var(--app-list-hover-background)] text-[var(--app-secondary-foreground)]'
|
||||
}`}
|
||||
>
|
||||
<path
|
||||
d="M2 2L14 14M2 14L14 2"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
{index + 1}
|
||||
</span>
|
||||
|
||||
{/* Option text */}
|
||||
<span className="text-sm">{option.name}</span>
|
||||
|
||||
{/* Always badge */}
|
||||
{isAlways && <span className="text-sm">⚡</span>}
|
||||
</button>
|
||||
)}
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Custom message input */}
|
||||
<div
|
||||
className={`rounded-small border transition-colors duration-150 ${
|
||||
focusedIndex === options.length
|
||||
? 'bg-[var(--app-list-hover-background)] border-transparent'
|
||||
: 'border-transparent'
|
||||
}`}
|
||||
onMouseEnter={() => setFocusedIndex(options.length)}
|
||||
>
|
||||
<div
|
||||
className="px-3 py-2 text-sm"
|
||||
style={{ color: 'var(--app-secondary-foreground)' }}
|
||||
>
|
||||
Tell Qwen what to do instead
|
||||
</div>
|
||||
<div
|
||||
ref={customInputRef}
|
||||
contentEditable="plaintext-only"
|
||||
spellCheck={false}
|
||||
className="px-3 pb-2 text-sm outline-none bg-transparent min-h-[1.5em]"
|
||||
style={{ color: 'var(--app-input-foreground)' }}
|
||||
onInput={(e) => {
|
||||
const target = e.target as HTMLDivElement;
|
||||
setCustomMessage(target.textContent || '');
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey && customMessage.trim()) {
|
||||
e.preventDefault();
|
||||
const rejectOption = options.find((o) =>
|
||||
o.kind.includes('reject'),
|
||||
);
|
||||
if (rejectOption) {
|
||||
onResponse(rejectOption.optionId);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="permission-drawer-content">
|
||||
<PermissionRequest
|
||||
options={options}
|
||||
toolCall={toolCall}
|
||||
onResponse={onResponse}
|
||||
/>
|
||||
<style>{`
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
transform: translateY(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -109,18 +109,118 @@ export function useCompletionTrigger(
|
||||
const handleInput = async () => {
|
||||
const text = inputElement.textContent || '';
|
||||
const selection = window.getSelection();
|
||||
|
||||
console.log(
|
||||
'[useCompletionTrigger] handleInput - text:',
|
||||
JSON.stringify(text),
|
||||
'length:',
|
||||
text.length,
|
||||
);
|
||||
|
||||
if (!selection || selection.rangeCount === 0) {
|
||||
console.log('[useCompletionTrigger] No selection or rangeCount === 0');
|
||||
return;
|
||||
}
|
||||
|
||||
const range = selection.getRangeAt(0);
|
||||
const cursorPosition = range.startOffset;
|
||||
console.log(
|
||||
'[useCompletionTrigger] range.startContainer:',
|
||||
range.startContainer,
|
||||
'startOffset:',
|
||||
range.startOffset,
|
||||
);
|
||||
console.log(
|
||||
'[useCompletionTrigger] startContainer === inputElement:',
|
||||
range.startContainer === inputElement,
|
||||
);
|
||||
console.log(
|
||||
'[useCompletionTrigger] startContainer.nodeType:',
|
||||
range.startContainer.nodeType,
|
||||
'TEXT_NODE:',
|
||||
Node.TEXT_NODE,
|
||||
);
|
||||
|
||||
// Get cursor position more reliably
|
||||
// For contentEditable, we need to calculate the actual text offset
|
||||
let cursorPosition = text.length; // Default to end of text
|
||||
|
||||
if (range.startContainer === inputElement) {
|
||||
// Cursor is directly in the container (e.g., empty or at boundary)
|
||||
// Use childNodes to determine position
|
||||
const childIndex = range.startOffset;
|
||||
let offset = 0;
|
||||
for (
|
||||
let i = 0;
|
||||
i < childIndex && i < inputElement.childNodes.length;
|
||||
i++
|
||||
) {
|
||||
offset += inputElement.childNodes[i].textContent?.length || 0;
|
||||
}
|
||||
cursorPosition = offset || text.length;
|
||||
console.log(
|
||||
'[useCompletionTrigger] Container mode - childIndex:',
|
||||
childIndex,
|
||||
'offset:',
|
||||
offset,
|
||||
'cursorPosition:',
|
||||
cursorPosition,
|
||||
);
|
||||
} else if (range.startContainer.nodeType === Node.TEXT_NODE) {
|
||||
// Cursor is in a text node - calculate offset from start of input
|
||||
const walker = document.createTreeWalker(
|
||||
inputElement,
|
||||
NodeFilter.SHOW_TEXT,
|
||||
null,
|
||||
);
|
||||
|
||||
let offset = 0;
|
||||
let found = false;
|
||||
let node: Node | null = walker.nextNode();
|
||||
while (node) {
|
||||
if (node === range.startContainer) {
|
||||
offset += range.startOffset;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
offset += node.textContent?.length || 0;
|
||||
node = walker.nextNode();
|
||||
}
|
||||
// If we found the node, use the calculated offset; otherwise use text length
|
||||
cursorPosition = found ? offset : text.length;
|
||||
console.log(
|
||||
'[useCompletionTrigger] Text node mode - found:',
|
||||
found,
|
||||
'offset:',
|
||||
offset,
|
||||
'cursorPosition:',
|
||||
cursorPosition,
|
||||
);
|
||||
}
|
||||
|
||||
// Find trigger character before cursor
|
||||
const textBeforeCursor = text.substring(0, cursorPosition);
|
||||
// Use text length if cursorPosition is 0 but we have text (edge case for first character)
|
||||
const effectiveCursorPosition =
|
||||
cursorPosition === 0 && text.length > 0 ? text.length : cursorPosition;
|
||||
console.log(
|
||||
'[useCompletionTrigger] cursorPosition:',
|
||||
cursorPosition,
|
||||
'effectiveCursorPosition:',
|
||||
effectiveCursorPosition,
|
||||
);
|
||||
|
||||
const textBeforeCursor = text.substring(0, effectiveCursorPosition);
|
||||
const lastAtMatch = textBeforeCursor.lastIndexOf('@');
|
||||
const lastSlashMatch = textBeforeCursor.lastIndexOf('/');
|
||||
|
||||
console.log(
|
||||
'[useCompletionTrigger] textBeforeCursor:',
|
||||
JSON.stringify(textBeforeCursor),
|
||||
'lastAtMatch:',
|
||||
lastAtMatch,
|
||||
'lastSlashMatch:',
|
||||
lastSlashMatch,
|
||||
);
|
||||
|
||||
// Check if we're in a trigger context
|
||||
let triggerPos = -1;
|
||||
let triggerChar: '@' | '/' | null = null;
|
||||
@@ -133,19 +233,46 @@ export function useCompletionTrigger(
|
||||
triggerChar = '/';
|
||||
}
|
||||
|
||||
console.log(
|
||||
'[useCompletionTrigger] triggerPos:',
|
||||
triggerPos,
|
||||
'triggerChar:',
|
||||
triggerChar,
|
||||
);
|
||||
|
||||
// Check if trigger is at word boundary (start of line or after space)
|
||||
if (triggerPos >= 0 && triggerChar) {
|
||||
const charBefore = triggerPos > 0 ? text[triggerPos - 1] : ' ';
|
||||
const isValidTrigger =
|
||||
charBefore === ' ' || charBefore === '\n' || triggerPos === 0;
|
||||
|
||||
console.log(
|
||||
'[useCompletionTrigger] charBefore:',
|
||||
JSON.stringify(charBefore),
|
||||
'isValidTrigger:',
|
||||
isValidTrigger,
|
||||
);
|
||||
|
||||
if (isValidTrigger) {
|
||||
const query = text.substring(triggerPos + 1, cursorPosition);
|
||||
const query = text.substring(triggerPos + 1, effectiveCursorPosition);
|
||||
|
||||
console.log(
|
||||
'[useCompletionTrigger] query:',
|
||||
JSON.stringify(query),
|
||||
'hasSpace:',
|
||||
query.includes(' '),
|
||||
'hasNewline:',
|
||||
query.includes('\n'),
|
||||
);
|
||||
|
||||
// Only show if query doesn't contain spaces (still typing the reference)
|
||||
if (!query.includes(' ') && !query.includes('\n')) {
|
||||
// Get precise cursor position for menu
|
||||
const cursorPos = getCursorPosition();
|
||||
console.log(
|
||||
'[useCompletionTrigger] Opening completion - cursorPos:',
|
||||
cursorPos,
|
||||
);
|
||||
if (cursorPos) {
|
||||
await openCompletion(triggerChar, query, cursorPos);
|
||||
return;
|
||||
@@ -155,6 +282,10 @@ export function useCompletionTrigger(
|
||||
}
|
||||
|
||||
// Close if no valid trigger
|
||||
console.log(
|
||||
'[useCompletionTrigger] No valid trigger, state.isOpen:',
|
||||
state.isOpen,
|
||||
);
|
||||
if (state.isOpen) {
|
||||
closeCompletion();
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ module.exports = {
|
||||
'./src/webview/components/messages/**/*.{js,jsx,ts,tsx}',
|
||||
'./src/webview/components/MessageContent.tsx',
|
||||
'./src/webview/components/InfoBanner.tsx',
|
||||
'./src/webview/components/InputForm.tsx',
|
||||
'./src/webview/components/PermissionDrawer.tsx',
|
||||
// 当需要在更多组件中使用Tailwind时,可以逐步添加路径
|
||||
// "./src/webview/components/NewComponent/**/*.{js,jsx,ts,tsx}",
|
||||
// "./src/webview/pages/**/*.{js,jsx,ts,tsx}",
|
||||
|
||||
Reference in New Issue
Block a user