mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-22 09:47:47 +00:00
Implement authenticate/update message handling for Qwen OAuth authentication
- Added authenticate_update method to ACP schema constants - Added AuthenticateUpdateNotification type definitions - Updated ACP message handler to process authenticate/update notifications - Added VS Code notification handler with 'Open in Browser' and 'Copy Link' options - Integrated authenticate update handling in QwenAgentManager This implementation allows users to easily authenticate with Qwen OAuth when automatic browser launch fails by providing a notification with direct link and copy functionality. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -44,7 +44,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 = () => {
|
||||
@@ -92,9 +92,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(
|
||||
@@ -111,7 +115,6 @@ export const App: React.FC = () => {
|
||||
);
|
||||
|
||||
if (query && query.length >= 1) {
|
||||
fileContext.requestWorkspaceFiles(query);
|
||||
const lowerQuery = query.toLowerCase();
|
||||
return allItems.filter(
|
||||
(item) =>
|
||||
@@ -156,17 +159,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';
|
||||
import { isAuthenticationRequiredError } from '../utils/authErrors.js';
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
@@ -243,15 +243,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
|
||||
|
||||
@@ -17,42 +17,30 @@ export const Onboarding: React.FC<OnboardingPageProps> = ({ onLogin }) => {
|
||||
<div className="flex flex-col items-center justify-center h-full p-5 md:p-10">
|
||||
<div className="flex flex-col items-center gap-8 w-full max-w-md">
|
||||
<div className="flex flex-col items-center gap-6">
|
||||
{/* Application icon container with brand logo and decorative close icon */}
|
||||
{/* Application icon container */}
|
||||
<div className="relative">
|
||||
<img
|
||||
src={iconUri}
|
||||
alt="Qwen Code Logo"
|
||||
className="w-[80px] h-[80px] object-contain"
|
||||
/>
|
||||
{/* Decorative close icon for enhanced visual effect */}
|
||||
<div className="absolute -top-2 -right-2 w-6 h-6 bg-[#4f46e5] rounded-full flex items-center justify-center">
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="white">
|
||||
<path
|
||||
d="M2.5 1.5L9.5 8.5M9.5 1.5L2.5 8.5"
|
||||
stroke="white"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Text content area */}
|
||||
<div className="text-center">
|
||||
<h1 className="text-2xl font-bold text-app-primary-foreground mb-2">
|
||||
Welcome to Qwen Code
|
||||
</h1>
|
||||
<p className="text-app-secondary-foreground max-w-sm">
|
||||
Qwen Code helps you understand, navigate, and transform your
|
||||
codebase with AI assistance.
|
||||
Unlock the power of AI to understand, navigate, and transform your
|
||||
codebase faster than ever before.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={onLogin}
|
||||
className="w-full px-4 py-3 bg-[#4f46e5] text-white font-medium rounded-lg shadow-sm"
|
||||
className="w-full px-4 py-3 bg-[#4f46e5] text-white font-medium rounded-lg shadow-sm hover:bg-[#4338ca] transition-colors duration-200"
|
||||
>
|
||||
Log in to Qwen Code
|
||||
Get Started with Qwen Code
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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