From 4dfbdcddca0fa1cfa3c712d394143462ca0265c2 Mon Sep 17 00:00:00 2001
From: yiliang114 <1204183885@qq.com>
Date: Sun, 23 Nov 2025 22:28:11 +0800
Subject: [PATCH] =?UTF-8?q?feat(vscode-ide-companion):=20=E5=A2=9E?=
=?UTF-8?q?=E5=BC=BA=E5=B7=A5=E5=85=B7=E8=B0=83=E7=94=A8=E4=B8=8E=E8=BE=93?=
=?UTF-8?q?=E5=85=A5=E8=A1=A8=E5=8D=95=E7=BB=84=E4=BB=B6=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 新增 InProgressToolCall 组件用于展示进行中的工具调用状态
- 重构 InputForm 为独立组件,提升代码可维护性
- 改进 tool_call_update 处理逻辑,支持创建缺失的初始工具调用
- 添加思考块(thought chunk)日志以便调试 AI 思维过程
- 更新样式以支持新的进行中工具调用卡片显示
- 在权限请求时自动创建对应的工具调用记录
```
---
.../src/agents/qwenSessionUpdateHandler.ts | 10 +
.../vscode-ide-companion/src/webview/App.scss | 321 +++-------
.../vscode-ide-companion/src/webview/App.tsx | 442 +++++---------
.../webview/components/InProgressToolCall.tsx | 100 ++++
.../src/webview/components/InputForm.tsx | 354 +++++++++++
.../webview/components/PermissionDrawer.css | 560 ------------------
.../components/PermissionDrawer.tailwind.tsx | 121 ----
.../webview/components/PermissionDrawer.tsx | 271 ++++++---
.../src/webview/hooks/useCompletionTrigger.ts | 137 ++++-
.../vscode-ide-companion/tailwind.config.js | 2 +
10 files changed, 1045 insertions(+), 1273 deletions(-)
create mode 100644 packages/vscode-ide-companion/src/webview/components/InProgressToolCall.tsx
create mode 100644 packages/vscode-ide-companion/src/webview/components/InputForm.tsx
delete mode 100644 packages/vscode-ide-companion/src/webview/components/PermissionDrawer.css
delete mode 100644 packages/vscode-ide-companion/src/webview/components/PermissionDrawer.tailwind.tsx
diff --git a/packages/vscode-ide-companion/src/agents/qwenSessionUpdateHandler.ts b/packages/vscode-ide-companion/src/agents/qwenSessionUpdateHandler.ts
index 61fde052..a138326a 100644
--- a/packages/vscode-ide-companion/src/agents/qwenSessionUpdateHandler.ts
+++ b/packages/vscode-ide-companion/src/agents/qwenSessionUpdateHandler.ts
@@ -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);
}
}
diff --git a/packages/vscode-ide-companion/src/webview/App.scss b/packages/vscode-ide-companion/src/webview/App.scss
index 298f8009..f4b550a4 100644
--- a/packages/vscode-ide-companion/src/webview/App.scss
+++ b/packages/vscode-ide-companion/src/webview/App.scss
@@ -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);
diff --git a/packages/vscode-ide-companion/src/webview/App.tsx b/packages/vscode-ide-companion/src/webview/App.tsx
index 2790019d..e4bdff9d 100644
--- a/packages/vscode-ide-companion/src/webview/App.tsx
+++ b/packages/vscode-ide-companion/src/webview/App.tsx
@@ -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,14 +626,28 @@ export const App: React.FC = () => {
}))
: undefined;
- newMap.set(update.toolCallId, {
- ...existing,
- ...(update.kind && { kind: update.kind }),
- ...(update.title && { title: safeTitle(update.title) }),
- ...(update.status && { status: update.status }),
- ...(updatedContent && { content: updatedContent }),
- ...(update.locations && { locations: update.locations }),
- });
+ if (existing) {
+ // Update existing tool call
+ newMap.set(update.toolCallId, {
+ ...existing,
+ ...(update.kind && { kind: update.kind }),
+ ...(update.title && { title: safeTitle(update.title) }),
+ ...(update.status && { status: update.status }),
+ ...(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: (
-
- ),
- };
- case 'auto':
- return {
- text: 'Edit automatically',
- title: 'Qwen will edit files automatically. Click to switch modes.',
- icon: (
-
- ),
- };
- case 'plan':
- return {
- text: 'Plan mode',
- title: 'Qwen will plan before executing. Click to switch modes.',
- icon: (
-
- ),
- };
- 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) => (
+
+ ))}
+
+ {/* 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) => (
))}
@@ -1477,223 +1501,65 @@ export const App: React.FC = () => {
}}
/>
-
-
- {/* Context Pills - Removed: now using inline @mentions in input */}
+
setIsComposing(true)}
+ onCompositionEnd={() => setIsComposing(false)}
+ onKeyDown={() => {}}
+ onSubmit={handleSubmit}
+ onToggleEditMode={handleToggleEditMode}
+ onToggleThinking={handleToggleThinking}
+ onFocusActiveEditor={() => {
+ vscode.postMessage({
+ type: 'focusActiveEditor',
+ data: {},
+ });
+ }}
+ onShowCommandMenu={async () => {
+ if (inputFieldRef.current) {
+ inputFieldRef.current.focus();
-
+ await completion.openCompletion('/', '', position);
+ }
+ }}
+ onAttachContext={handleAttachContextClick}
+ completionIsOpen={completion.isOpen}
+ />
{/* Save Session Dialog */}
{
+ const kindMap: Record = {
+ 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 = {
+ 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 = ({
+ 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 (
+
+
+ {kindLabel}
+
+ {statusText}
+
+
+
+ {title && title !== kindLabel && (
+
{title}
+ )}
+
+ {locations && locations.length > 0 && (
+
+ {locations.map((loc, idx) => (
+
+ ))}
+
+ )}
+
+ );
+};
diff --git a/packages/vscode-ide-companion/src/webview/components/InputForm.tsx b/packages/vscode-ide-companion/src/webview/components/InputForm.tsx
new file mode 100644
index 00000000..13ec8d85
--- /dev/null
+++ b/packages/vscode-ide-companion/src/webview/components/InputForm.tsx
@@ -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;
+ 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: (
+
+ ),
+ };
+ case 'auto':
+ return {
+ text: 'Edit automatically',
+ title: 'Qwen will edit files automatically. Click to switch modes.',
+ icon: (
+
+ ),
+ };
+ case 'plan':
+ return {
+ text: 'Plan mode',
+ title: 'Qwen will plan before executing. Click to switch modes.',
+ icon: (
+
+ ),
+ };
+ default:
+ return {
+ text: 'Unknown mode',
+ title: 'Unknown edit mode',
+ icon: null,
+ };
+ }
+};
+
+export const InputForm: React.FC = ({
+ 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 (
+
+
+ );
+};
diff --git a/packages/vscode-ide-companion/src/webview/components/PermissionDrawer.css b/packages/vscode-ide-companion/src/webview/components/PermissionDrawer.css
deleted file mode 100644
index e91684eb..00000000
--- a/packages/vscode-ide-companion/src/webview/components/PermissionDrawer.css
+++ /dev/null
@@ -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;
-}
diff --git a/packages/vscode-ide-companion/src/webview/components/PermissionDrawer.tailwind.tsx b/packages/vscode-ide-companion/src/webview/components/PermissionDrawer.tailwind.tsx
deleted file mode 100644
index 621cf843..00000000
--- a/packages/vscode-ide-companion/src/webview/components/PermissionDrawer.tailwind.tsx
+++ /dev/null
@@ -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
= ({
- 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 */}
-
-
- {/* Drawer */}
-
-
-
-
Permission Required
- {onClose && (
-
- )}
-
-
-
-
- >
- );
-};
\ No newline at end of file
diff --git a/packages/vscode-ide-companion/src/webview/components/PermissionDrawer.tsx b/packages/vscode-ide-companion/src/webview/components/PermissionDrawer.tsx
index e8227915..0777ec9e 100644
--- a/packages/vscode-ide-companion/src/webview/components/PermissionDrawer.tsx
+++ b/packages/vscode-ide-companion/src/webview/components/PermissionDrawer.tsx
@@ -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 = ({
isOpen,
@@ -36,80 +26,229 @@ export const PermissionDrawer: React.FC = ({
onResponse,
onClose,
}) => {
- // Close drawer on Escape key
+ const [focusedIndex, setFocusedIndex] = useState(0);
+ const [customMessage, setCustomMessage] = useState('');
+ const containerRef = useRef(null);
+ const customInputRef = useRef(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{' '}
+
+ {fileName}
+
+ ?
+ >
+ );
+ }
+ 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{' '}
+
+ {fileName}
+
+ ?
+ >
+ );
+ }
+ 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 */}
-
+
+ {/* Main container */}
+
+ {/* Background layer */}
+
- {/* Drawer */}
-
-
-
-
Permission Required
- {onClose && (
-
- )}
+ {/* Title */}
+
-
-
+ {/* Options */}
+
+ {options.map((option, index) => {
+ const isAlways = option.kind.includes('always');
+ const isFocused = focusedIndex === index;
+
+ return (
+
+ );
+ })}
+
+ {/* Custom message input */}
+
setFocusedIndex(options.length)}
+ >
+
+ Tell Qwen what to do instead
+
+
{
+ 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);
+ }
+ }
+ }}
+ />
+
- >
+
+
+
);
};
diff --git a/packages/vscode-ide-companion/src/webview/hooks/useCompletionTrigger.ts b/packages/vscode-ide-companion/src/webview/hooks/useCompletionTrigger.ts
index 0b9cabb0..e4f285d7 100644
--- a/packages/vscode-ide-companion/src/webview/hooks/useCompletionTrigger.ts
+++ b/packages/vscode-ide-companion/src/webview/hooks/useCompletionTrigger.ts
@@ -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();
}
diff --git a/packages/vscode-ide-companion/tailwind.config.js b/packages/vscode-ide-companion/tailwind.config.js
index b973f78b..d0cec1f1 100644
--- a/packages/vscode-ide-companion/tailwind.config.js
+++ b/packages/vscode-ide-companion/tailwind.config.js
@@ -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}",