mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-22 09:47:47 +00:00
Fix/vscode ide companion completion menu content (#1243)
* refactor(vscode-ide-companion/types): move ApprovalModeValue type to dedicated file feat(vscode-ide-companion/file-context): improve file context handling and search Enhance file context hook to better handle search queries and reduce redundant requests. Track last query to optimize when to refetch full file list. Improve logging for debugging purposes. * feat(vscode-ide-companion/completion): enhance completion menu performance and refresh logic Implement item comparison to prevent unnecessary re-renders when completion items haven't actually changed. Optimize refresh logic to only trigger when workspace files content changes. Improve completion menu stability and responsiveness. refactor(vscode-ide-companion/handlers): remove SettingsMessageHandler and consolidate functionality Move setApprovalMode functionality from SettingsMessageHandler to SessionMessageHandler to reduce code duplication and simplify message handling architecture. Remove unused settings-related imports and clean up message router configuration. chore(vscode-ide-companion/ui): minor UI improvements and code cleanup Consolidate imports in SessionSelector component. Remove debug console log statement from FileMessageHandler. Move getTimeAgo utility function to sessionGrouping file and remove obsolete timeUtils file. Clean up completion menu CSS classes. * fix(vscode-ide-companion): resolve all ESLint errors Fixed unused variable errors in SessionMessageHandler.ts: - Commented out unused conversation and messages variables Also includes previous commits: 1. feat(vscode-ide-companion): add upgrade button to CLI version warning 2. fix(vscode-ide-companion): resolve ESLint errors in InputForm component When the Qwen Code CLI version is below the minimum required version, the warning message now includes an "Upgrade Now" button that opens a terminal and runs the npm install command to upgrade the CLI. Added tests to verify the functionality works correctly.
This commit is contained in:
@@ -43,7 +43,7 @@ import { InputForm } from './components/layout/InputForm.js';
|
||||
import { SessionSelector } from './components/layout/SessionSelector.js';
|
||||
import { FileIcon, UserIcon } from './components/icons/index.js';
|
||||
import { ApprovalMode, NEXT_APPROVAL_MODE } from '../types/acpTypes.js';
|
||||
import type { ApprovalModeValue } from '../types/acpTypes.js';
|
||||
import type { ApprovalModeValue } from '../types/approvalModeValueTypes.js';
|
||||
import type { PlanEntry } from '../types/chatTypes.js';
|
||||
|
||||
export const App: React.FC = () => {
|
||||
@@ -90,9 +90,13 @@ export const App: React.FC = () => {
|
||||
const getCompletionItems = React.useCallback(
|
||||
async (trigger: '@' | '/', query: string): Promise<CompletionItem[]> => {
|
||||
if (trigger === '@') {
|
||||
if (!fileContext.hasRequestedFiles) {
|
||||
fileContext.requestWorkspaceFiles();
|
||||
}
|
||||
console.log('[App] getCompletionItems @ called', {
|
||||
query,
|
||||
requested: fileContext.hasRequestedFiles,
|
||||
workspaceFiles: fileContext.workspaceFiles.length,
|
||||
});
|
||||
// 始终根据当前 query 触发请求,让 hook 判断是否需要真正请求
|
||||
fileContext.requestWorkspaceFiles(query);
|
||||
|
||||
const fileIcon = <FileIcon />;
|
||||
const allItems: CompletionItem[] = fileContext.workspaceFiles.map(
|
||||
@@ -109,7 +113,6 @@ export const App: React.FC = () => {
|
||||
);
|
||||
|
||||
if (query && query.length >= 1) {
|
||||
fileContext.requestWorkspaceFiles(query);
|
||||
const lowerQuery = query.toLowerCase();
|
||||
return allItems.filter(
|
||||
(item) =>
|
||||
@@ -154,17 +157,39 @@ export const App: React.FC = () => {
|
||||
|
||||
const completion = useCompletionTrigger(inputFieldRef, getCompletionItems);
|
||||
|
||||
// Track a lightweight signature of workspace files to detect content changes even when length is unchanged
|
||||
const workspaceFilesSignature = useMemo(
|
||||
() =>
|
||||
fileContext.workspaceFiles
|
||||
.map(
|
||||
(file) =>
|
||||
`${file.id}|${file.label}|${file.description ?? ''}|${file.path}`,
|
||||
)
|
||||
.join('||'),
|
||||
[fileContext.workspaceFiles],
|
||||
);
|
||||
|
||||
// When workspace files update while menu open for @, refresh items so the first @ shows the list
|
||||
// Note: Avoid depending on the entire `completion` object here, since its identity
|
||||
// changes on every render which would retrigger this effect and can cause a refresh loop.
|
||||
useEffect(() => {
|
||||
if (completion.isOpen && completion.triggerChar === '@') {
|
||||
// Only auto-refresh when there's no query (first @ popup) to avoid repeated refreshes during search
|
||||
if (
|
||||
completion.isOpen &&
|
||||
completion.triggerChar === '@' &&
|
||||
!completion.query
|
||||
) {
|
||||
// Only refresh items; do not change other completion state to avoid re-renders loops
|
||||
completion.refreshCompletion();
|
||||
}
|
||||
// Only re-run when the actual data source changes, not on every render
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [fileContext.workspaceFiles, completion.isOpen, completion.triggerChar]);
|
||||
}, [
|
||||
workspaceFilesSignature,
|
||||
completion.isOpen,
|
||||
completion.triggerChar,
|
||||
completion.query,
|
||||
]);
|
||||
|
||||
// Message submission
|
||||
const { handleSubmit: submitMessage } = useMessageSubmit({
|
||||
|
||||
@@ -73,11 +73,4 @@ export class MessageHandler {
|
||||
appendStreamContent(chunk: string): void {
|
||||
this.router.appendStreamContent(chunk);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if saving checkpoint
|
||||
*/
|
||||
getIsSavingCheckpoint(): boolean {
|
||||
return this.router.getIsSavingCheckpoint();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import { MessageHandler } from '../webview/MessageHandler.js';
|
||||
import { WebViewContent } from '../webview/WebViewContent.js';
|
||||
import { CliInstaller } from '../cli/cliInstaller.js';
|
||||
import { getFileName } from './utils/webviewUtils.js';
|
||||
import { type ApprovalModeValue } from '../types/acpTypes.js';
|
||||
import { type ApprovalModeValue } from '../types/approvalModeValueTypes.js';
|
||||
|
||||
export class WebViewProvider {
|
||||
private panelManager: PanelManager;
|
||||
|
||||
@@ -92,9 +92,8 @@ export const CompletionMenu: React.FC<CompletionMenuProps> = ({
|
||||
ref={containerRef}
|
||||
role="menu"
|
||||
className={[
|
||||
// Semantic class name for readability (no CSS attached)
|
||||
'completion-menu',
|
||||
// Positioning and container styling (Tailwind)
|
||||
// Positioning and container styling
|
||||
'absolute bottom-full left-0 right-0 mb-2 flex flex-col overflow-hidden',
|
||||
'rounded-large border bg-[var(--app-menu-background)]',
|
||||
'border-[var(--app-input-border)] max-h-[50vh] z-[1000]',
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
PlanModeIcon,
|
||||
CodeBracketsIcon,
|
||||
HideContextIcon,
|
||||
ThinkingIcon,
|
||||
// ThinkingIcon, // Temporarily disabled
|
||||
SlashCommandIcon,
|
||||
LinkIcon,
|
||||
ArrowUpIcon,
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
import { CompletionMenu } from '../layout/CompletionMenu.js';
|
||||
import type { CompletionItem } from '../../../types/completionItemTypes.js';
|
||||
import { getApprovalModeInfoFromString } from '../../../types/acpTypes.js';
|
||||
import type { ApprovalModeValue } from '../../../types/acpTypes.js';
|
||||
import type { ApprovalModeValue } from '../../../types/approvalModeValueTypes.js';
|
||||
|
||||
interface InputFormProps {
|
||||
inputText: string;
|
||||
@@ -92,7 +92,7 @@ export const InputForm: React.FC<InputFormProps> = ({
|
||||
isWaitingForResponse,
|
||||
isComposing,
|
||||
editMode,
|
||||
thinkingEnabled,
|
||||
// thinkingEnabled, // Temporarily disabled
|
||||
activeFileName,
|
||||
activeSelection,
|
||||
skipAutoActiveContext,
|
||||
@@ -103,7 +103,7 @@ export const InputForm: React.FC<InputFormProps> = ({
|
||||
onSubmit,
|
||||
onCancel,
|
||||
onToggleEditMode,
|
||||
onToggleThinking,
|
||||
// onToggleThinking, // Temporarily disabled
|
||||
onToggleSkipAutoActiveContext,
|
||||
onShowCommandMenu,
|
||||
onAttachContext,
|
||||
@@ -236,15 +236,16 @@ export const InputForm: React.FC<InputFormProps> = ({
|
||||
{/* Spacer */}
|
||||
<div className="flex-1 min-w-0" />
|
||||
|
||||
{/* @yiliang114. closed temporarily */}
|
||||
{/* Thinking button */}
|
||||
<button
|
||||
{/* <button
|
||||
type="button"
|
||||
className={`btn-icon-compact ${thinkingEnabled ? 'btn-icon-compact--active' : ''}`}
|
||||
title={thinkingEnabled ? 'Thinking on' : 'Thinking off'}
|
||||
onClick={onToggleThinking}
|
||||
>
|
||||
<ThinkingIcon enabled={thinkingEnabled} />
|
||||
</button>
|
||||
</button> */}
|
||||
|
||||
{/* Command button */}
|
||||
<button
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { groupSessionsByDate } from '../../utils/sessionGrouping.js';
|
||||
import { getTimeAgo } from '../../utils/timeUtils.js';
|
||||
import {
|
||||
getTimeAgo,
|
||||
groupSessionsByDate,
|
||||
} from '../../utils/sessionGrouping.js';
|
||||
import { SearchIcon } from '../icons/index.js';
|
||||
|
||||
interface SessionSelectorProps {
|
||||
|
||||
@@ -49,7 +49,6 @@ export class FileMessageHandler extends BaseMessageHandler {
|
||||
break;
|
||||
|
||||
case 'openDiff':
|
||||
console.log('[FileMessageHandler ===== ] openDiff called with:', data);
|
||||
await this.handleOpenDiff(data);
|
||||
break;
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import { SessionMessageHandler } from './SessionMessageHandler.js';
|
||||
import { FileMessageHandler } from './FileMessageHandler.js';
|
||||
import { EditorMessageHandler } from './EditorMessageHandler.js';
|
||||
import { AuthMessageHandler } from './AuthMessageHandler.js';
|
||||
import { SettingsMessageHandler } from './SettingsMessageHandler.js';
|
||||
|
||||
/**
|
||||
* Message Router
|
||||
@@ -63,20 +62,12 @@ export class MessageRouter {
|
||||
sendToWebView,
|
||||
);
|
||||
|
||||
const settingsHandler = new SettingsMessageHandler(
|
||||
agentManager,
|
||||
conversationStore,
|
||||
currentConversationId,
|
||||
sendToWebView,
|
||||
);
|
||||
|
||||
// Register handlers in order of priority
|
||||
this.handlers = [
|
||||
this.sessionHandler,
|
||||
fileHandler,
|
||||
editorHandler,
|
||||
this.authHandler,
|
||||
settingsHandler,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -159,11 +150,4 @@ export class MessageRouter {
|
||||
appendStreamContent(chunk: string): void {
|
||||
this.sessionHandler.appendStreamContent(chunk);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if saving checkpoint
|
||||
*/
|
||||
getIsSavingCheckpoint(): boolean {
|
||||
return this.sessionHandler.getIsSavingCheckpoint();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { BaseMessageHandler } from './BaseMessageHandler.js';
|
||||
import type { ChatMessage } from '../../services/qwenAgentManager.js';
|
||||
import type { ApprovalModeValue } from '../../types/approvalModeValueTypes.js';
|
||||
|
||||
/**
|
||||
* Session message handler
|
||||
@@ -14,7 +15,6 @@ import type { ChatMessage } from '../../services/qwenAgentManager.js';
|
||||
*/
|
||||
export class SessionMessageHandler extends BaseMessageHandler {
|
||||
private currentStreamContent = '';
|
||||
private isSavingCheckpoint = false;
|
||||
private loginHandler: (() => Promise<void>) | null = null;
|
||||
private isTitleSet = false; // Flag to track if title has been set
|
||||
|
||||
@@ -29,6 +29,8 @@ export class SessionMessageHandler extends BaseMessageHandler {
|
||||
'cancelStreaming',
|
||||
// UI action: open a new chat tab (new WebviewPanel)
|
||||
'openNewChatTab',
|
||||
// Settings-related messages
|
||||
'setApprovalMode',
|
||||
].includes(messageType);
|
||||
}
|
||||
|
||||
@@ -112,6 +114,14 @@ export class SessionMessageHandler extends BaseMessageHandler {
|
||||
await this.handleCancelStreaming();
|
||||
break;
|
||||
|
||||
case 'setApprovalMode':
|
||||
await this.handleSetApprovalMode(
|
||||
message.data as {
|
||||
modeId?: ApprovalModeValue;
|
||||
},
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn(
|
||||
'[SessionMessageHandler] Unknown message type:',
|
||||
@@ -142,13 +152,6 @@ export class SessionMessageHandler extends BaseMessageHandler {
|
||||
this.currentStreamContent = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if saving checkpoint
|
||||
*/
|
||||
getIsSavingCheckpoint(): boolean {
|
||||
return this.isSavingCheckpoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt user to login and invoke the registered login handler/command.
|
||||
* Returns true if a login was initiated.
|
||||
@@ -374,41 +377,6 @@ export class SessionMessageHandler extends BaseMessageHandler {
|
||||
type: 'streamEnd',
|
||||
data: { timestamp: Date.now() },
|
||||
});
|
||||
|
||||
// Auto-save checkpoint
|
||||
if (this.currentConversationId) {
|
||||
try {
|
||||
const conversation = await this.conversationStore.getConversation(
|
||||
this.currentConversationId,
|
||||
);
|
||||
|
||||
const messages = conversation?.messages || [];
|
||||
|
||||
this.isSavingCheckpoint = true;
|
||||
|
||||
const result = await this.agentManager.saveCheckpoint(
|
||||
messages,
|
||||
this.currentConversationId,
|
||||
);
|
||||
|
||||
setTimeout(() => {
|
||||
this.isSavingCheckpoint = false;
|
||||
}, 2000);
|
||||
|
||||
if (result.success) {
|
||||
console.log(
|
||||
'[SessionMessageHandler] Checkpoint saved:',
|
||||
result.tag,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'[SessionMessageHandler] Checkpoint save failed:',
|
||||
error,
|
||||
);
|
||||
this.isSavingCheckpoint = false;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[SessionMessageHandler] Error sending message:', error);
|
||||
|
||||
@@ -482,23 +450,6 @@ export class SessionMessageHandler extends BaseMessageHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// Save current session before creating new one
|
||||
if (this.currentConversationId && this.agentManager.isConnected) {
|
||||
try {
|
||||
const conversation = await this.conversationStore.getConversation(
|
||||
this.currentConversationId,
|
||||
);
|
||||
const messages = conversation?.messages || [];
|
||||
|
||||
await this.agentManager.saveCheckpoint(
|
||||
messages,
|
||||
this.currentConversationId,
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn('[SessionMessageHandler] Failed to auto-save:', error);
|
||||
}
|
||||
}
|
||||
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||
const workingDir = workspaceFolder?.uri.fsPath || process.cwd();
|
||||
|
||||
@@ -578,27 +529,6 @@ export class SessionMessageHandler extends BaseMessageHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// Save current session before switching
|
||||
if (
|
||||
this.currentConversationId &&
|
||||
this.currentConversationId !== sessionId &&
|
||||
this.agentManager.isConnected
|
||||
) {
|
||||
try {
|
||||
const conversation = await this.conversationStore.getConversation(
|
||||
this.currentConversationId,
|
||||
);
|
||||
const messages = conversation?.messages || [];
|
||||
|
||||
await this.agentManager.saveCheckpoint(
|
||||
messages,
|
||||
this.currentConversationId,
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn('[SessionMessageHandler] Failed to auto-save:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Get session details (includes cwd and filePath when using ACP)
|
||||
let sessionDetails: Record<string, unknown> | null = null;
|
||||
try {
|
||||
@@ -841,11 +771,6 @@ export class SessionMessageHandler extends BaseMessageHandler {
|
||||
throw new Error('No active conversation to save');
|
||||
}
|
||||
|
||||
const conversation = await this.conversationStore.getConversation(
|
||||
this.currentConversationId,
|
||||
);
|
||||
const messages = conversation?.messages || [];
|
||||
|
||||
// Try ACP save first
|
||||
try {
|
||||
const response = await this.agentManager.saveSessionViaAcp(
|
||||
@@ -880,17 +805,6 @@ export class SessionMessageHandler extends BaseMessageHandler {
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback to direct save
|
||||
const response = await this.agentManager.saveSessionDirect(
|
||||
messages,
|
||||
tag,
|
||||
);
|
||||
|
||||
this.sendToWebView({
|
||||
type: 'saveSessionResponse',
|
||||
data: response,
|
||||
});
|
||||
}
|
||||
|
||||
await this.handleGetQwenSessions();
|
||||
@@ -1025,20 +939,6 @@ export class SessionMessageHandler extends BaseMessageHandler {
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback to direct load
|
||||
const messages = await this.agentManager.loadSessionDirect(sessionId);
|
||||
|
||||
if (messages) {
|
||||
this.currentConversationId = sessionId;
|
||||
|
||||
this.sendToWebView({
|
||||
type: 'qwenSessionSwitched',
|
||||
data: { sessionId, messages },
|
||||
});
|
||||
} else {
|
||||
throw new Error('Failed to load session');
|
||||
}
|
||||
}
|
||||
|
||||
await this.handleGetQwenSessions();
|
||||
@@ -1073,4 +973,23 @@ export class SessionMessageHandler extends BaseMessageHandler {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set approval mode via agent (ACP session/set_mode)
|
||||
*/
|
||||
private async handleSetApprovalMode(data?: {
|
||||
modeId?: ApprovalModeValue;
|
||||
}): Promise<void> {
|
||||
try {
|
||||
const modeId = data?.modeId || 'default';
|
||||
await this.agentManager.setApprovalModeFromUi(modeId);
|
||||
// No explicit response needed; WebView listens for modeChanged
|
||||
} catch (error) {
|
||||
console.error('[SessionMessageHandler] Failed to set mode:', error);
|
||||
this.sendToWebView({
|
||||
type: 'error',
|
||||
data: { message: `Failed to set mode: ${error}` },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { BaseMessageHandler } from './BaseMessageHandler.js';
|
||||
import type { ApprovalModeValue } from '../../types/acpTypes.js';
|
||||
|
||||
/**
|
||||
* Settings message handler
|
||||
* Handles all settings-related messages
|
||||
*/
|
||||
export class SettingsMessageHandler extends BaseMessageHandler {
|
||||
canHandle(messageType: string): boolean {
|
||||
return ['openSettings', 'recheckCli', 'setApprovalMode'].includes(
|
||||
messageType,
|
||||
);
|
||||
}
|
||||
|
||||
async handle(message: { type: string; data?: unknown }): Promise<void> {
|
||||
switch (message.type) {
|
||||
case 'openSettings':
|
||||
await this.handleOpenSettings();
|
||||
break;
|
||||
|
||||
case 'recheckCli':
|
||||
await this.handleRecheckCli();
|
||||
break;
|
||||
|
||||
case 'setApprovalMode':
|
||||
await this.handleSetApprovalMode(
|
||||
message.data as {
|
||||
modeId?: ApprovalModeValue;
|
||||
},
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn(
|
||||
'[SettingsMessageHandler] Unknown message type:',
|
||||
message.type,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open settings page
|
||||
*/
|
||||
private async handleOpenSettings(): Promise<void> {
|
||||
try {
|
||||
// Open settings in a side panel
|
||||
await vscode.commands.executeCommand('workbench.action.openSettings', {
|
||||
query: 'qwenCode',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[SettingsMessageHandler] Failed to open settings:', error);
|
||||
vscode.window.showErrorMessage(`Failed to open settings: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recheck CLI
|
||||
*/
|
||||
private async handleRecheckCli(): Promise<void> {
|
||||
try {
|
||||
await vscode.commands.executeCommand('qwenCode.recheckCli');
|
||||
this.sendToWebView({
|
||||
type: 'cliRechecked',
|
||||
data: { success: true },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[SettingsMessageHandler] Failed to recheck CLI:', error);
|
||||
this.sendToWebView({
|
||||
type: 'error',
|
||||
data: { message: `Failed to recheck CLI: ${error}` },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set approval mode via agent (ACP session/set_mode)
|
||||
*/
|
||||
private async handleSetApprovalMode(data?: {
|
||||
modeId?: ApprovalModeValue;
|
||||
}): Promise<void> {
|
||||
try {
|
||||
const modeId = data?.modeId || 'default';
|
||||
await this.agentManager.setApprovalModeFromUi(modeId);
|
||||
// No explicit response needed; WebView listens for modeChanged
|
||||
} catch (error) {
|
||||
console.error('[SettingsMessageHandler] Failed to set mode:', error);
|
||||
this.sendToWebView({
|
||||
type: 'error',
|
||||
data: { message: `Failed to set mode: ${error}` },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,9 @@ export const useFileContext = (vscode: VSCodeAPI) => {
|
||||
// Whether workspace files have been requested
|
||||
const hasRequestedFilesRef = useRef(false);
|
||||
|
||||
// Last non-empty query to decide when to refetch full list
|
||||
const lastQueryRef = useRef<string | undefined>(undefined);
|
||||
|
||||
// Search debounce timer
|
||||
const searchTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
@@ -42,12 +45,10 @@ export const useFileContext = (vscode: VSCodeAPI) => {
|
||||
*/
|
||||
const requestWorkspaceFiles = useCallback(
|
||||
(query?: string) => {
|
||||
if (!hasRequestedFilesRef.current && !query) {
|
||||
hasRequestedFilesRef.current = true;
|
||||
}
|
||||
const normalizedQuery = query?.trim();
|
||||
|
||||
// If there's a query, clear previous timer and set up debounce
|
||||
if (query && query.length >= 1) {
|
||||
if (normalizedQuery && normalizedQuery.length >= 1) {
|
||||
if (searchTimerRef.current) {
|
||||
clearTimeout(searchTimerRef.current);
|
||||
}
|
||||
@@ -55,14 +56,23 @@ export const useFileContext = (vscode: VSCodeAPI) => {
|
||||
searchTimerRef.current = setTimeout(() => {
|
||||
vscode.postMessage({
|
||||
type: 'getWorkspaceFiles',
|
||||
data: { query },
|
||||
data: { query: normalizedQuery },
|
||||
});
|
||||
}, 300);
|
||||
lastQueryRef.current = normalizedQuery;
|
||||
} else {
|
||||
vscode.postMessage({
|
||||
type: 'getWorkspaceFiles',
|
||||
data: query ? { query } : {},
|
||||
});
|
||||
// For empty query, request once initially and whenever we are returning from a search
|
||||
const shouldRequestFullList =
|
||||
!hasRequestedFilesRef.current || lastQueryRef.current !== undefined;
|
||||
|
||||
if (shouldRequestFullList) {
|
||||
lastQueryRef.current = undefined;
|
||||
hasRequestedFilesRef.current = true;
|
||||
vscode.postMessage({
|
||||
type: 'getWorkspaceFiles',
|
||||
data: {},
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
[vscode],
|
||||
|
||||
@@ -131,12 +131,55 @@ export function useCompletionTrigger(
|
||||
[getCompletionItems, LOADING_ITEM, TIMEOUT_ITEM],
|
||||
);
|
||||
|
||||
// Helper function to compare completion items arrays
|
||||
const areItemsEqual = (
|
||||
items1: CompletionItem[],
|
||||
items2: CompletionItem[],
|
||||
): boolean => {
|
||||
if (items1.length !== items2.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Compare each item by stable fields (ignore non-deterministic props like icons)
|
||||
for (let i = 0; i < items1.length; i++) {
|
||||
const a = items1[i];
|
||||
const b = items2[i];
|
||||
if (a.id !== b.id) {
|
||||
return false;
|
||||
}
|
||||
if (a.label !== b.label) {
|
||||
return false;
|
||||
}
|
||||
if ((a.description ?? '') !== (b.description ?? '')) {
|
||||
return false;
|
||||
}
|
||||
if (a.type !== b.type) {
|
||||
return false;
|
||||
}
|
||||
if ((a.value ?? '') !== (b.value ?? '')) {
|
||||
return false;
|
||||
}
|
||||
if ((a.path ?? '') !== (b.path ?? '')) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const refreshCompletion = useCallback(async () => {
|
||||
if (!state.isOpen || !state.triggerChar) {
|
||||
return;
|
||||
}
|
||||
const items = await getCompletionItems(state.triggerChar, state.query);
|
||||
setState((prev) => ({ ...prev, items }));
|
||||
|
||||
// Only update state if items have actually changed
|
||||
setState((prev) => {
|
||||
if (areItemsEqual(prev.items, items)) {
|
||||
return prev;
|
||||
}
|
||||
return { ...prev, items };
|
||||
});
|
||||
}, [state.isOpen, state.triggerChar, state.query, getCompletionItems]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -12,7 +12,7 @@ import type {
|
||||
ToolCall as PermissionToolCall,
|
||||
} from '../components/PermissionDrawer/PermissionRequest.js';
|
||||
import type { ToolCallUpdate } from '../../types/chatTypes.js';
|
||||
import type { ApprovalModeValue } from '../../types/acpTypes.js';
|
||||
import type { ApprovalModeValue } from '../../types/approvalModeValueTypes.js';
|
||||
import type { PlanEntry } from '../../types/chatTypes.js';
|
||||
|
||||
interface UseWebViewMessagesProps {
|
||||
|
||||
@@ -62,3 +62,38 @@ export const groupSessionsByDate = (
|
||||
.filter(([, sessions]) => sessions.length > 0)
|
||||
.map(([label, sessions]) => ({ label, sessions }));
|
||||
};
|
||||
|
||||
/**
|
||||
* Time ago formatter
|
||||
*
|
||||
* @param timestamp - ISO timestamp string
|
||||
* @returns Formatted time string
|
||||
*/
|
||||
export const getTimeAgo = (timestamp: string): string => {
|
||||
if (!timestamp) {
|
||||
return '';
|
||||
}
|
||||
const now = new Date().getTime();
|
||||
const then = new Date(timestamp).getTime();
|
||||
const diffMs = now - then;
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
const diffHours = Math.floor(diffMs / 3600000);
|
||||
const diffDays = Math.floor(diffMs / 86400000);
|
||||
|
||||
if (diffMins < 1) {
|
||||
return 'now';
|
||||
}
|
||||
if (diffMins < 60) {
|
||||
return `${diffMins}m`;
|
||||
}
|
||||
if (diffHours < 24) {
|
||||
return `${diffHours}h`;
|
||||
}
|
||||
if (diffDays === 1) {
|
||||
return 'Yesterday';
|
||||
}
|
||||
if (diffDays < 7) {
|
||||
return `${diffDays}d`;
|
||||
}
|
||||
return new Date(timestamp).toLocaleDateString();
|
||||
};
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Time ago formatter
|
||||
*
|
||||
* @param timestamp - ISO timestamp string
|
||||
* @returns Formatted time string
|
||||
*/
|
||||
export const getTimeAgo = (timestamp: string): string => {
|
||||
if (!timestamp) {
|
||||
return '';
|
||||
}
|
||||
const now = new Date().getTime();
|
||||
const then = new Date(timestamp).getTime();
|
||||
const diffMs = now - then;
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
const diffHours = Math.floor(diffMs / 3600000);
|
||||
const diffDays = Math.floor(diffMs / 86400000);
|
||||
|
||||
if (diffMins < 1) {
|
||||
return 'now';
|
||||
}
|
||||
if (diffMins < 60) {
|
||||
return `${diffMins}m`;
|
||||
}
|
||||
if (diffHours < 24) {
|
||||
return `${diffHours}h`;
|
||||
}
|
||||
if (diffDays === 1) {
|
||||
return 'Yesterday';
|
||||
}
|
||||
if (diffDays < 7) {
|
||||
return `${diffDays}d`;
|
||||
}
|
||||
return new Date(timestamp).toLocaleDateString();
|
||||
};
|
||||
Reference in New Issue
Block a user