mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-01 21:20:44 +00:00
Merge remote-tracking branch 'origin/main' into feat/plan-mode
# Conflicts: # packages/cli/src/i18n/locales/de.js # packages/cli/src/i18n/locales/en.js # packages/cli/src/i18n/locales/ja.js # packages/cli/src/i18n/locales/pt.js # packages/cli/src/i18n/locales/ru.js # packages/cli/src/i18n/locales/zh.js
This commit is contained in:
commit
6bb5e0a276
45 changed files with 1185 additions and 2362 deletions
|
|
@ -22,18 +22,7 @@ import { CompletionMenu } from './CompletionMenu.js';
|
|||
import { ContextIndicator } from './ContextIndicator.js';
|
||||
import type { CompletionItem } from '../../types/completion.js';
|
||||
import type { ContextUsage } from './ContextIndicator.js';
|
||||
/**
|
||||
* Minimal follow-up state shape used by InputForm.
|
||||
* Defined locally to avoid pulling @qwen-code/qwen-code-core into the
|
||||
* root entry's type declarations. The full FollowupState lives in
|
||||
* '@qwen-code/webui/followup'.
|
||||
*/
|
||||
interface InputFormFollowupState {
|
||||
/** Current suggestion text */
|
||||
suggestion: string | null;
|
||||
/** Whether to show suggestion */
|
||||
isVisible: boolean;
|
||||
}
|
||||
import type { FollowupState } from '../../types/followup.js';
|
||||
|
||||
/**
|
||||
* Edit mode display information
|
||||
|
|
@ -141,7 +130,7 @@ export interface InputFormProps {
|
|||
/** Whether the current draft is eligible to submit */
|
||||
canSubmit?: boolean;
|
||||
/** Prompt suggestion state */
|
||||
followupState?: InputFormFollowupState;
|
||||
followupState?: FollowupState;
|
||||
/** Callback to accept prompt suggestion */
|
||||
onAcceptFollowup?: (method?: 'tab' | 'enter' | 'right') => void;
|
||||
/** Callback to dismiss prompt suggestion */
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Prompt Suggestion Subpath Entry
|
||||
*
|
||||
* Separated from the root entry to avoid forcing all @qwen-code/webui
|
||||
* consumers to install @qwen-code/qwen-code-core as a dependency.
|
||||
*
|
||||
* Usage: import { useFollowupSuggestions } from '@qwen-code/webui/followup';
|
||||
*/
|
||||
|
||||
export { useFollowupSuggestions } from './hooks/useFollowupSuggestions';
|
||||
export type {
|
||||
FollowupState,
|
||||
UseFollowupSuggestionsOptions,
|
||||
UseFollowupSuggestionsReturn,
|
||||
} from './hooks/useFollowupSuggestions';
|
||||
|
|
@ -2,34 +2,27 @@
|
|||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Prompt Suggestion Hook
|
||||
*
|
||||
* Thin React wrapper around the framework-agnostic controller from core.
|
||||
*
|
||||
* Note: For browser environments, the parent component should handle
|
||||
* suggestion generation and pass the results to this hook.
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useMemo, useRef, useEffect } from 'react';
|
||||
import {
|
||||
INITIAL_FOLLOWUP_STATE,
|
||||
createFollowupController,
|
||||
} from '@qwen-code/qwen-code-core';
|
||||
import type { FollowupState } from '@qwen-code/qwen-code-core';
|
||||
import type { FollowupState } from '../types/followup.js';
|
||||
import { INITIAL_FOLLOWUP_STATE } from '../types/followup.js';
|
||||
|
||||
// Re-export types from core for convenience
|
||||
export type { FollowupState } from '@qwen-code/qwen-code-core';
|
||||
export type { FollowupState } from '../types/followup.js';
|
||||
|
||||
/**
|
||||
* Options for the hook
|
||||
*/
|
||||
export interface UseFollowupSuggestionsOptions {
|
||||
/** Whether the feature is enabled */
|
||||
// ---------------------------------------------------------------------------
|
||||
// Controller (framework-agnostic)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Delay before showing suggestion after response completes */
|
||||
const SUGGESTION_DELAY_MS = 300;
|
||||
/** Debounce lock duration to prevent rapid-fire accepts */
|
||||
const ACCEPT_DEBOUNCE_MS = 100;
|
||||
|
||||
interface FollowupControllerOptions {
|
||||
enabled?: boolean;
|
||||
/** Callback when suggestion is accepted */
|
||||
onAccept?: (suggestion: string) => void;
|
||||
/** Callback when a suggestion outcome is determined */
|
||||
onStateChange: (state: FollowupState) => void;
|
||||
getOnAccept?: () => ((text: string) => void) | undefined;
|
||||
onOutcome?: (params: {
|
||||
outcome: 'accepted' | 'ignored';
|
||||
accept_method?: 'tab' | 'enter' | 'right';
|
||||
|
|
@ -38,31 +31,172 @@ export interface UseFollowupSuggestionsOptions {
|
|||
}) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result returned by the hook
|
||||
*/
|
||||
export interface UseFollowupSuggestionsReturn {
|
||||
/** Current state */
|
||||
state: FollowupState;
|
||||
/** Get current placeholder text */
|
||||
getPlaceholder: (defaultPlaceholder: string) => string;
|
||||
/** Set suggestion text (called by parent component) */
|
||||
interface FollowupControllerActions {
|
||||
setSuggestion: (text: string | null) => void;
|
||||
accept: (method?: 'tab' | 'enter' | 'right') => void;
|
||||
dismiss: () => void;
|
||||
clear: () => void;
|
||||
cleanup: () => void;
|
||||
}
|
||||
|
||||
function createFollowupController(
|
||||
options: FollowupControllerOptions,
|
||||
): FollowupControllerActions {
|
||||
const { enabled = true, onStateChange, getOnAccept, onOutcome } = options;
|
||||
|
||||
let currentState: FollowupState = INITIAL_FOLLOWUP_STATE;
|
||||
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||
let accepting = false;
|
||||
let acceptTimeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
function applyState(next: FollowupState): void {
|
||||
currentState = next;
|
||||
onStateChange(next);
|
||||
}
|
||||
|
||||
function clearTimers(): void {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = null;
|
||||
}
|
||||
if (acceptTimeoutId) {
|
||||
clearTimeout(acceptTimeoutId);
|
||||
acceptTimeoutId = null;
|
||||
}
|
||||
}
|
||||
|
||||
const setSuggestion = (text: string | null): void => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = null;
|
||||
}
|
||||
|
||||
if (!text) {
|
||||
applyState(INITIAL_FOLLOWUP_STATE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
applyState({ suggestion: text, isVisible: true, shownAt: Date.now() });
|
||||
}, SUGGESTION_DELAY_MS);
|
||||
};
|
||||
|
||||
const accept = (method?: 'tab' | 'enter' | 'right'): void => {
|
||||
if (accepting) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = null;
|
||||
}
|
||||
|
||||
accepting = true;
|
||||
|
||||
const text = currentState.suggestion;
|
||||
const { shownAt } = currentState;
|
||||
if (!text) {
|
||||
accepting = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
onOutcome?.({
|
||||
outcome: 'accepted',
|
||||
accept_method: method,
|
||||
time_ms: shownAt > 0 ? Date.now() - shownAt : 0,
|
||||
suggestion_length: text.length,
|
||||
});
|
||||
} catch (e: unknown) {
|
||||
console.error('[followup] onOutcome callback threw:', e);
|
||||
}
|
||||
|
||||
applyState(INITIAL_FOLLOWUP_STATE);
|
||||
|
||||
queueMicrotask(() => {
|
||||
try {
|
||||
getOnAccept?.()?.(text);
|
||||
} catch (error: unknown) {
|
||||
console.error('[followup] onAccept callback threw:', error);
|
||||
} finally {
|
||||
if (acceptTimeoutId) {
|
||||
clearTimeout(acceptTimeoutId);
|
||||
}
|
||||
acceptTimeoutId = setTimeout(() => {
|
||||
accepting = false;
|
||||
}, ACCEPT_DEBOUNCE_MS);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const dismiss = (): void => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = null;
|
||||
}
|
||||
|
||||
if (!currentState.isVisible && !currentState.suggestion) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentState.isVisible && currentState.suggestion) {
|
||||
try {
|
||||
onOutcome?.({
|
||||
outcome: 'ignored',
|
||||
time_ms:
|
||||
currentState.shownAt > 0 ? Date.now() - currentState.shownAt : 0,
|
||||
suggestion_length: currentState.suggestion.length,
|
||||
});
|
||||
} catch (e: unknown) {
|
||||
console.error('[followup] onOutcome callback threw:', e);
|
||||
}
|
||||
}
|
||||
|
||||
applyState(INITIAL_FOLLOWUP_STATE);
|
||||
};
|
||||
|
||||
const clear = (): void => {
|
||||
clearTimers();
|
||||
accepting = false;
|
||||
applyState(INITIAL_FOLLOWUP_STATE);
|
||||
};
|
||||
|
||||
const cleanup = (): void => {
|
||||
clearTimers();
|
||||
accepting = false;
|
||||
};
|
||||
|
||||
return { setSuggestion, accept, dismiss, clear, cleanup };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// React hook
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface UseFollowupSuggestionsOptions {
|
||||
enabled?: boolean;
|
||||
onAccept?: (suggestion: string) => void;
|
||||
onOutcome?: (params: {
|
||||
outcome: 'accepted' | 'ignored';
|
||||
accept_method?: 'tab' | 'enter' | 'right';
|
||||
time_ms: number;
|
||||
suggestion_length: number;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
export interface UseFollowupSuggestionsReturn {
|
||||
state: FollowupState;
|
||||
getPlaceholder: (defaultPlaceholder: string) => string;
|
||||
setSuggestion: (text: string | null) => void;
|
||||
/** Accept the current suggestion */
|
||||
accept: (method?: 'tab' | 'enter' | 'right') => void;
|
||||
/** Dismiss the current suggestion */
|
||||
dismiss: () => void;
|
||||
/** Clear all state */
|
||||
clear: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for managing prompt suggestions in the Web UI.
|
||||
*
|
||||
* Delegates all timer/debounce/state logic to the shared
|
||||
* `createFollowupController` from core. Adds a `getPlaceholder`
|
||||
* helper specific to the WebUI input form.
|
||||
*/
|
||||
export function useFollowupSuggestions(
|
||||
options: UseFollowupSuggestionsOptions = {},
|
||||
): UseFollowupSuggestionsReturn {
|
||||
|
|
@ -70,13 +204,11 @@ export function useFollowupSuggestions(
|
|||
|
||||
const [state, setState] = useState<FollowupState>(INITIAL_FOLLOWUP_STATE);
|
||||
|
||||
// Keep mutable refs so the controller always sees the latest callbacks
|
||||
const onAcceptRef = useRef(onAccept);
|
||||
onAcceptRef.current = onAccept;
|
||||
const onOutcomeRef = useRef(onOutcome);
|
||||
onOutcomeRef.current = onOutcome;
|
||||
|
||||
// Create the controller once — it is stable across renders
|
||||
const controller = useMemo(
|
||||
() =>
|
||||
createFollowupController({
|
||||
|
|
@ -88,7 +220,6 @@ export function useFollowupSuggestions(
|
|||
[enabled],
|
||||
);
|
||||
|
||||
// Clear state when disabled; clean up timers on unmount
|
||||
useEffect(() => {
|
||||
if (!enabled) {
|
||||
controller.clear();
|
||||
|
|
@ -96,7 +227,6 @@ export function useFollowupSuggestions(
|
|||
return () => controller.cleanup();
|
||||
}, [controller, enabled]);
|
||||
|
||||
// WebUI-specific helper: resolves placeholder text
|
||||
const getPlaceholder = useCallback(
|
||||
(defaultPlaceholder: string) => {
|
||||
if (state.isVisible && state.suggestion) {
|
||||
|
|
|
|||
|
|
@ -231,8 +231,12 @@ export { StopIcon } from './components/icons/StopIcon';
|
|||
// Hooks
|
||||
export { useTheme } from './hooks/useTheme';
|
||||
export { useLocalStorage } from './hooks/useLocalStorage';
|
||||
// NOTE: useFollowupSuggestions is exported from '@qwen-code/webui/followup'
|
||||
// subpath to avoid forcing all consumers to install @qwen-code/qwen-code-core.
|
||||
export { useFollowupSuggestions } from './hooks/useFollowupSuggestions';
|
||||
export type {
|
||||
FollowupState,
|
||||
UseFollowupSuggestionsOptions,
|
||||
UseFollowupSuggestionsReturn,
|
||||
} from './hooks/useFollowupSuggestions';
|
||||
|
||||
// Types
|
||||
export type { Theme } from './types/theme';
|
||||
|
|
|
|||
20
packages/webui/src/types/followup.ts
Normal file
20
packages/webui/src/types/followup.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
export interface FollowupState {
|
||||
/** Current suggestion text */
|
||||
suggestion: string | null;
|
||||
/** Whether to show suggestion */
|
||||
isVisible: boolean;
|
||||
/** Timestamp when suggestion was shown (for telemetry) */
|
||||
shownAt: number;
|
||||
}
|
||||
|
||||
export const INITIAL_FOLLOWUP_STATE: Readonly<FollowupState> = Object.freeze({
|
||||
suggestion: null,
|
||||
isVisible: false,
|
||||
shownAt: 0,
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue