mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-01-24 09:46:20 +00:00
Compare commits
16 Commits
feat/add-u
...
fix/acp-se
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ced1b1db80 | ||
|
|
cf140b1b9d | ||
|
|
1f1e78aa3b | ||
|
|
511269446f | ||
|
|
0681c71894 | ||
|
|
155c4b9728 | ||
|
|
57ca2823b3 | ||
|
|
620341eeae | ||
|
|
c6c33233c5 | ||
|
|
106b69e5c0 | ||
|
|
6afe0f8c29 | ||
|
|
0b3be1a82c | ||
|
|
8af43e3ac3 | ||
|
|
63406b4ba4 | ||
|
|
52db3a766d | ||
|
|
6f33d92b2c |
@@ -275,6 +275,7 @@ If you are experiencing performance issues with file searching (e.g., with `@` c
|
||||
| `tools.truncateToolOutputThreshold` | number | Truncate tool output if it is larger than this many characters. Applies to Shell, Grep, Glob, ReadFile and ReadManyFiles tools. | `25000` | Requires restart: Yes |
|
||||
| `tools.truncateToolOutputLines` | number | Maximum lines or entries kept when truncating tool output. Applies to Shell, Grep, Glob, ReadFile and ReadManyFiles tools. | `1000` | Requires restart: Yes |
|
||||
| `tools.autoAccept` | boolean | Controls whether the CLI automatically accepts and executes tool calls that are considered safe (e.g., read-only operations) without explicit user confirmation. If set to `true`, the CLI will bypass the confirmation prompt for tools deemed safe. | `false` | |
|
||||
| `tools.experimental.skills` | boolean | Enable experimental Agent Skills feature | `false` | |
|
||||
|
||||
#### mcp
|
||||
|
||||
|
||||
@@ -11,12 +11,29 @@ This guide shows you how to create, use, and manage Agent Skills in **Qwen Code*
|
||||
## Prerequisites
|
||||
|
||||
- Qwen Code (recent version)
|
||||
- Run with the experimental flag enabled:
|
||||
|
||||
## How to enable
|
||||
|
||||
### Via CLI flag
|
||||
|
||||
```bash
|
||||
qwen --experimental-skills
|
||||
```
|
||||
|
||||
### Via settings.json
|
||||
|
||||
Add to your `~/.qwen/settings.json` or project's `.qwen/settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"tools": {
|
||||
"experimental": {
|
||||
"skills": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- Basic familiarity with Qwen Code ([Quickstart](../quickstart.md))
|
||||
|
||||
## What are Agent Skills?
|
||||
|
||||
@@ -311,9 +311,9 @@ function setupAcpTest(
|
||||
}
|
||||
});
|
||||
|
||||
it('returns modes on initialize and allows setting approval mode', async () => {
|
||||
it('returns modes on initialize and allows setting mode and model', async () => {
|
||||
const rig = new TestRig();
|
||||
rig.setup('acp approval mode');
|
||||
rig.setup('acp mode and model');
|
||||
|
||||
const { sendRequest, cleanup, stderr } = setupAcpTest(rig);
|
||||
|
||||
@@ -366,8 +366,14 @@ function setupAcpTest(
|
||||
const newSession = (await sendRequest('session/new', {
|
||||
cwd: rig.testDir!,
|
||||
mcpServers: [],
|
||||
})) as { sessionId: string };
|
||||
})) as {
|
||||
sessionId: string;
|
||||
models: {
|
||||
availableModels: Array<{ modelId: string }>;
|
||||
};
|
||||
};
|
||||
expect(newSession.sessionId).toBeTruthy();
|
||||
expect(newSession.models.availableModels.length).toBeGreaterThan(0);
|
||||
|
||||
// Test 4: Set approval mode to 'yolo'
|
||||
const setModeResult = (await sendRequest('session/set_mode', {
|
||||
@@ -392,6 +398,15 @@ function setupAcpTest(
|
||||
})) as { modeId: string };
|
||||
expect(setModeResult3).toBeDefined();
|
||||
expect(setModeResult3.modeId).toBe('default');
|
||||
|
||||
// Test 7: Set model using first available model
|
||||
const firstModel = newSession.models.availableModels[0];
|
||||
const setModelResult = (await sendRequest('session/set_model', {
|
||||
sessionId: newSession.sessionId,
|
||||
modelId: firstModel.modelId,
|
||||
})) as { modelId: string };
|
||||
expect(setModelResult).toBeDefined();
|
||||
expect(setModelResult.modelId).toBeTruthy();
|
||||
} catch (e) {
|
||||
if (stderr.length) {
|
||||
console.error('Agent stderr:', stderr.join(''));
|
||||
|
||||
@@ -70,6 +70,13 @@ export class AgentSideConnection implements Client {
|
||||
const validatedParams = schema.setModeRequestSchema.parse(params);
|
||||
return agent.setMode(validatedParams);
|
||||
}
|
||||
case schema.AGENT_METHODS.session_set_model: {
|
||||
if (!agent.setModel) {
|
||||
throw RequestError.methodNotFound();
|
||||
}
|
||||
const validatedParams = schema.setModelRequestSchema.parse(params);
|
||||
return agent.setModel(validatedParams);
|
||||
}
|
||||
default:
|
||||
throw RequestError.methodNotFound(method);
|
||||
}
|
||||
@@ -408,4 +415,5 @@ export interface Agent {
|
||||
prompt(params: schema.PromptRequest): Promise<schema.PromptResponse>;
|
||||
cancel(params: schema.CancelNotification): Promise<void>;
|
||||
setMode?(params: schema.SetModeRequest): Promise<schema.SetModeResponse>;
|
||||
setModel?(params: schema.SetModelRequest): Promise<schema.SetModelResponse>;
|
||||
}
|
||||
|
||||
@@ -165,30 +165,11 @@ class GeminiAgent {
|
||||
this.setupFileSystem(config);
|
||||
|
||||
const session = await this.createAndStoreSession(config);
|
||||
const configuredModel = (
|
||||
config.getModel() ||
|
||||
this.config.getModel() ||
|
||||
''
|
||||
).trim();
|
||||
const modelId = configuredModel || 'default';
|
||||
const modelName = configuredModel || modelId;
|
||||
const availableModels = this.buildAvailableModels(config);
|
||||
|
||||
return {
|
||||
sessionId: session.getId(),
|
||||
models: {
|
||||
currentModelId: modelId,
|
||||
availableModels: [
|
||||
{
|
||||
modelId,
|
||||
name: modelName,
|
||||
description: null,
|
||||
_meta: {
|
||||
contextLimit: tokenLimit(modelId),
|
||||
},
|
||||
},
|
||||
],
|
||||
_meta: null,
|
||||
},
|
||||
models: availableModels,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -305,15 +286,29 @@ class GeminiAgent {
|
||||
async setMode(params: acp.SetModeRequest): Promise<acp.SetModeResponse> {
|
||||
const session = this.sessions.get(params.sessionId);
|
||||
if (!session) {
|
||||
throw new Error(`Session not found: ${params.sessionId}`);
|
||||
throw acp.RequestError.invalidParams(
|
||||
`Session not found for id: ${params.sessionId}`,
|
||||
);
|
||||
}
|
||||
return session.setMode(params);
|
||||
}
|
||||
|
||||
async setModel(params: acp.SetModelRequest): Promise<acp.SetModelResponse> {
|
||||
const session = this.sessions.get(params.sessionId);
|
||||
if (!session) {
|
||||
throw acp.RequestError.invalidParams(
|
||||
`Session not found for id: ${params.sessionId}`,
|
||||
);
|
||||
}
|
||||
return session.setModel(params);
|
||||
}
|
||||
|
||||
private async ensureAuthenticated(config: Config): Promise<void> {
|
||||
const selectedType = this.settings.merged.security?.auth?.selectedType;
|
||||
if (!selectedType) {
|
||||
throw acp.RequestError.authRequired('No Selected Type');
|
||||
throw acp.RequestError.authRequired(
|
||||
'Use Qwen Code CLI to authenticate first.',
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -382,4 +377,43 @@ class GeminiAgent {
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
private buildAvailableModels(
|
||||
config: Config,
|
||||
): acp.NewSessionResponse['models'] {
|
||||
const currentModelId = (
|
||||
config.getModel() ||
|
||||
this.config.getModel() ||
|
||||
''
|
||||
).trim();
|
||||
const availableModels = config.getAvailableModels();
|
||||
|
||||
const mappedAvailableModels = availableModels.map((model) => ({
|
||||
modelId: model.id,
|
||||
name: model.label,
|
||||
description: model.description ?? null,
|
||||
_meta: {
|
||||
contextLimit: tokenLimit(model.id),
|
||||
},
|
||||
}));
|
||||
|
||||
if (
|
||||
currentModelId &&
|
||||
!mappedAvailableModels.some((model) => model.modelId === currentModelId)
|
||||
) {
|
||||
mappedAvailableModels.unshift({
|
||||
modelId: currentModelId,
|
||||
name: currentModelId,
|
||||
description: null,
|
||||
_meta: {
|
||||
contextLimit: tokenLimit(currentModelId),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
currentModelId,
|
||||
availableModels: mappedAvailableModels,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ export const AGENT_METHODS = {
|
||||
session_prompt: 'session/prompt',
|
||||
session_list: 'session/list',
|
||||
session_set_mode: 'session/set_mode',
|
||||
session_set_model: 'session/set_model',
|
||||
};
|
||||
|
||||
export const CLIENT_METHODS = {
|
||||
@@ -266,6 +267,18 @@ export const modelInfoSchema = z.object({
|
||||
name: z.string(),
|
||||
});
|
||||
|
||||
export const setModelRequestSchema = z.object({
|
||||
sessionId: z.string(),
|
||||
modelId: z.string(),
|
||||
});
|
||||
|
||||
export const setModelResponseSchema = z.object({
|
||||
modelId: z.string(),
|
||||
});
|
||||
|
||||
export type SetModelRequest = z.infer<typeof setModelRequestSchema>;
|
||||
export type SetModelResponse = z.infer<typeof setModelResponseSchema>;
|
||||
|
||||
export const sessionModelStateSchema = z.object({
|
||||
_meta: acpMetaSchema,
|
||||
availableModels: z.array(modelInfoSchema),
|
||||
@@ -592,6 +605,7 @@ export const agentResponseSchema = z.union([
|
||||
promptResponseSchema,
|
||||
listSessionsResponseSchema,
|
||||
setModeResponseSchema,
|
||||
setModelResponseSchema,
|
||||
]);
|
||||
|
||||
export const requestPermissionRequestSchema = z.object({
|
||||
@@ -624,6 +638,7 @@ export const agentRequestSchema = z.union([
|
||||
promptRequestSchema,
|
||||
listSessionsRequestSchema,
|
||||
setModeRequestSchema,
|
||||
setModelRequestSchema,
|
||||
]);
|
||||
|
||||
export const agentNotificationSchema = sessionNotificationSchema;
|
||||
|
||||
174
packages/cli/src/acp-integration/session/Session.test.ts
Normal file
174
packages/cli/src/acp-integration/session/Session.test.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { Session } from './Session.js';
|
||||
import type { Config, GeminiChat } from '@qwen-code/qwen-code-core';
|
||||
import { ApprovalMode } from '@qwen-code/qwen-code-core';
|
||||
import type * as acp from '../acp.js';
|
||||
import type { LoadedSettings } from '../../config/settings.js';
|
||||
import * as nonInteractiveCliCommands from '../../nonInteractiveCliCommands.js';
|
||||
|
||||
vi.mock('../../nonInteractiveCliCommands.js', () => ({
|
||||
getAvailableCommands: vi.fn(),
|
||||
handleSlashCommand: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('Session', () => {
|
||||
let mockChat: GeminiChat;
|
||||
let mockConfig: Config;
|
||||
let mockClient: acp.Client;
|
||||
let mockSettings: LoadedSettings;
|
||||
let session: Session;
|
||||
let currentModel: string;
|
||||
let setModelSpy: ReturnType<typeof vi.fn>;
|
||||
let getAvailableCommandsSpy: ReturnType<typeof vi.fn>;
|
||||
|
||||
beforeEach(() => {
|
||||
currentModel = 'qwen3-code-plus';
|
||||
setModelSpy = vi.fn().mockImplementation(async (modelId: string) => {
|
||||
currentModel = modelId;
|
||||
});
|
||||
|
||||
mockChat = {
|
||||
sendMessageStream: vi.fn(),
|
||||
addHistory: vi.fn(),
|
||||
} as unknown as GeminiChat;
|
||||
|
||||
mockConfig = {
|
||||
setApprovalMode: vi.fn(),
|
||||
setModel: setModelSpy,
|
||||
getModel: vi.fn().mockImplementation(() => currentModel),
|
||||
} as unknown as Config;
|
||||
|
||||
mockClient = {
|
||||
sessionUpdate: vi.fn().mockResolvedValue(undefined),
|
||||
requestPermission: vi.fn().mockResolvedValue({
|
||||
outcome: { outcome: 'selected', optionId: 'proceed_once' },
|
||||
}),
|
||||
sendCustomNotification: vi.fn().mockResolvedValue(undefined),
|
||||
} as unknown as acp.Client;
|
||||
|
||||
mockSettings = {
|
||||
merged: {},
|
||||
} as LoadedSettings;
|
||||
|
||||
getAvailableCommandsSpy = vi.mocked(nonInteractiveCliCommands)
|
||||
.getAvailableCommands as unknown as ReturnType<typeof vi.fn>;
|
||||
getAvailableCommandsSpy.mockResolvedValue([]);
|
||||
|
||||
session = new Session(
|
||||
'test-session-id',
|
||||
mockChat,
|
||||
mockConfig,
|
||||
mockClient,
|
||||
mockSettings,
|
||||
);
|
||||
});
|
||||
|
||||
describe('setMode', () => {
|
||||
it.each([
|
||||
['plan', ApprovalMode.PLAN],
|
||||
['default', ApprovalMode.DEFAULT],
|
||||
['auto-edit', ApprovalMode.AUTO_EDIT],
|
||||
['yolo', ApprovalMode.YOLO],
|
||||
] as const)('maps %s mode', async (modeId, expected) => {
|
||||
const result = await session.setMode({
|
||||
sessionId: 'test-session-id',
|
||||
modeId,
|
||||
});
|
||||
|
||||
expect(mockConfig.setApprovalMode).toHaveBeenCalledWith(expected);
|
||||
expect(result).toEqual({ modeId });
|
||||
});
|
||||
});
|
||||
|
||||
describe('setModel', () => {
|
||||
it('sets model via config and returns current model', async () => {
|
||||
const result = await session.setModel({
|
||||
sessionId: 'test-session-id',
|
||||
modelId: ' qwen3-coder-plus ',
|
||||
});
|
||||
|
||||
expect(mockConfig.setModel).toHaveBeenCalledWith('qwen3-coder-plus', {
|
||||
reason: 'user_request_acp',
|
||||
context: 'session/set_model',
|
||||
});
|
||||
expect(mockConfig.getModel).toHaveBeenCalled();
|
||||
expect(result).toEqual({ modelId: 'qwen3-coder-plus' });
|
||||
});
|
||||
|
||||
it('rejects empty/whitespace model IDs', async () => {
|
||||
await expect(
|
||||
session.setModel({
|
||||
sessionId: 'test-session-id',
|
||||
modelId: ' ',
|
||||
}),
|
||||
).rejects.toThrow('Invalid params');
|
||||
|
||||
expect(mockConfig.setModel).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('propagates errors from config.setModel', async () => {
|
||||
const configError = new Error('Invalid model');
|
||||
setModelSpy.mockRejectedValueOnce(configError);
|
||||
|
||||
await expect(
|
||||
session.setModel({
|
||||
sessionId: 'test-session-id',
|
||||
modelId: 'invalid-model',
|
||||
}),
|
||||
).rejects.toThrow('Invalid model');
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendAvailableCommandsUpdate', () => {
|
||||
it('sends available_commands_update from getAvailableCommands()', async () => {
|
||||
getAvailableCommandsSpy.mockResolvedValueOnce([
|
||||
{
|
||||
name: 'init',
|
||||
description: 'Initialize project context',
|
||||
},
|
||||
]);
|
||||
|
||||
await session.sendAvailableCommandsUpdate();
|
||||
|
||||
expect(getAvailableCommandsSpy).toHaveBeenCalledWith(
|
||||
mockConfig,
|
||||
expect.any(AbortSignal),
|
||||
);
|
||||
expect(mockClient.sessionUpdate).toHaveBeenCalledWith({
|
||||
sessionId: 'test-session-id',
|
||||
update: {
|
||||
sessionUpdate: 'available_commands_update',
|
||||
availableCommands: [
|
||||
{
|
||||
name: 'init',
|
||||
description: 'Initialize project context',
|
||||
input: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('swallows errors and does not throw', async () => {
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => undefined);
|
||||
getAvailableCommandsSpy.mockRejectedValueOnce(
|
||||
new Error('Command discovery failed'),
|
||||
);
|
||||
|
||||
await expect(
|
||||
session.sendAvailableCommandsUpdate(),
|
||||
).resolves.toBeUndefined();
|
||||
expect(mockClient.sessionUpdate).not.toHaveBeenCalled();
|
||||
expect(consoleErrorSpy).toHaveBeenCalled();
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -52,6 +52,8 @@ import type {
|
||||
AvailableCommandsUpdate,
|
||||
SetModeRequest,
|
||||
SetModeResponse,
|
||||
SetModelRequest,
|
||||
SetModelResponse,
|
||||
ApprovalModeValue,
|
||||
CurrentModeUpdate,
|
||||
} from '../schema.js';
|
||||
@@ -348,6 +350,31 @@ export class Session implements SessionContext {
|
||||
return { modeId: params.modeId };
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the model for the current session.
|
||||
* Validates the model ID and switches the model via Config.
|
||||
*/
|
||||
async setModel(params: SetModelRequest): Promise<SetModelResponse> {
|
||||
const modelId = params.modelId.trim();
|
||||
|
||||
if (!modelId) {
|
||||
throw acp.RequestError.invalidParams('modelId cannot be empty');
|
||||
}
|
||||
|
||||
// Attempt to set the model using config
|
||||
await this.config.setModel(modelId, {
|
||||
reason: 'user_request_acp',
|
||||
context: 'session/set_model',
|
||||
});
|
||||
|
||||
// Get updated model info
|
||||
const currentModel = this.config.getModel();
|
||||
|
||||
return {
|
||||
modelId: currentModel,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a current_mode_update notification to the client.
|
||||
* Called after the agent switches modes (e.g., from exit_plan_mode tool).
|
||||
|
||||
@@ -334,7 +334,7 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
|
||||
.option('experimental-skills', {
|
||||
type: 'boolean',
|
||||
description: 'Enable experimental Skills feature',
|
||||
default: false,
|
||||
default: settings.tools?.experimental?.skills ?? false,
|
||||
})
|
||||
.option('channel', {
|
||||
type: 'string',
|
||||
|
||||
@@ -434,16 +434,6 @@ const SETTINGS_SCHEMA = {
|
||||
'Show welcome back dialog when returning to a project with conversation history.',
|
||||
showInDialog: true,
|
||||
},
|
||||
enableUserFeedback: {
|
||||
type: 'boolean',
|
||||
label: 'Enable User Feedback',
|
||||
category: 'UI',
|
||||
requiresRestart: false,
|
||||
default: true,
|
||||
description:
|
||||
'Show optional feedback dialog after conversations to help improve Qwen performance.',
|
||||
showInDialog: true,
|
||||
},
|
||||
accessibility: {
|
||||
type: 'object',
|
||||
label: 'Accessibility',
|
||||
@@ -474,15 +464,6 @@ const SETTINGS_SCHEMA = {
|
||||
},
|
||||
},
|
||||
},
|
||||
feedbackLastShownTimestamp: {
|
||||
type: 'number',
|
||||
label: 'Feedback Last Shown Timestamp',
|
||||
category: 'UI',
|
||||
requiresRestart: false,
|
||||
default: 0,
|
||||
description: 'The last time the feedback dialog was shown.',
|
||||
showInDialog: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1000,6 +981,27 @@ const SETTINGS_SCHEMA = {
|
||||
description: 'The number of lines to keep when truncating tool output.',
|
||||
showInDialog: true,
|
||||
},
|
||||
experimental: {
|
||||
type: 'object',
|
||||
label: 'Experimental',
|
||||
category: 'Tools',
|
||||
requiresRestart: true,
|
||||
default: {},
|
||||
description: 'Experimental tool features.',
|
||||
showInDialog: false,
|
||||
properties: {
|
||||
skills: {
|
||||
type: 'boolean',
|
||||
label: 'Skills',
|
||||
category: 'Tools',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description:
|
||||
'Enable experimental Agent Skills feature. When enabled, Qwen Code can use Skills from .qwen/skills/ and ~/.qwen/skills/.',
|
||||
showInDialog: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -289,13 +289,6 @@ export default {
|
||||
'Show Citations': 'Quellenangaben anzeigen',
|
||||
'Custom Witty Phrases': 'Benutzerdefinierte Witzige Sprüche',
|
||||
'Enable Welcome Back': 'Willkommen-zurück aktivieren',
|
||||
'Enable User Feedback': 'Benutzerfeedback aktivieren',
|
||||
'How is Qwen doing this session? (optional)':
|
||||
'Wie macht sich Qwen in dieser Sitzung? (optional)',
|
||||
Bad: 'Schlecht',
|
||||
Good: 'Gut',
|
||||
'Not Sure Yet': 'Noch nicht sicher',
|
||||
'Any other key': 'Beliebige andere Taste',
|
||||
'Disable Loading Phrases': 'Ladesprüche deaktivieren',
|
||||
'Screen Reader Mode': 'Bildschirmleser-Modus',
|
||||
'IDE Mode': 'IDE-Modus',
|
||||
|
||||
@@ -286,13 +286,6 @@ export default {
|
||||
'Show Citations': 'Show Citations',
|
||||
'Custom Witty Phrases': 'Custom Witty Phrases',
|
||||
'Enable Welcome Back': 'Enable Welcome Back',
|
||||
'Enable User Feedback': 'Enable User Feedback',
|
||||
'How is Qwen doing this session? (optional)':
|
||||
'How is Qwen doing this session? (optional)',
|
||||
Bad: 'Bad',
|
||||
Good: 'Good',
|
||||
'Not Sure Yet': 'Not Sure Yet',
|
||||
'Any other key': 'Any other key',
|
||||
'Disable Loading Phrases': 'Disable Loading Phrases',
|
||||
'Screen Reader Mode': 'Screen Reader Mode',
|
||||
'IDE Mode': 'IDE Mode',
|
||||
|
||||
@@ -289,13 +289,6 @@ export default {
|
||||
'Show Citations': 'Показывать цитаты',
|
||||
'Custom Witty Phrases': 'Пользовательские остроумные фразы',
|
||||
'Enable Welcome Back': 'Включить приветствие при возврате',
|
||||
'Enable User Feedback': 'Включить отзывы пользователей',
|
||||
'How is Qwen doing this session? (optional)':
|
||||
'Как дела у Qwen в этой сессии? (необязательно)',
|
||||
Bad: 'Плохо',
|
||||
Good: 'Хорошо',
|
||||
'Not Sure Yet': 'Пока не уверен',
|
||||
'Any other key': 'Любая другая клавиша',
|
||||
'Disable Loading Phrases': 'Отключить фразы при загрузке',
|
||||
'Screen Reader Mode': 'Режим программы чтения с экрана',
|
||||
'IDE Mode': 'Режим IDE',
|
||||
|
||||
@@ -277,12 +277,6 @@ export default {
|
||||
'Show Citations': '显示引用',
|
||||
'Custom Witty Phrases': '自定义诙谐短语',
|
||||
'Enable Welcome Back': '启用欢迎回来',
|
||||
'Enable User Feedback': '启用用户反馈',
|
||||
'How is Qwen doing this session? (optional)': 'Qwen 这次表现如何?(可选)',
|
||||
Bad: '不满意',
|
||||
Good: '满意',
|
||||
'Not Sure Yet': '暂不评价',
|
||||
'Any other key': '任意其他键',
|
||||
'Disable Loading Phrases': '禁用加载短语',
|
||||
'Screen Reader Mode': '屏幕阅读器模式',
|
||||
'IDE Mode': 'IDE 模式',
|
||||
@@ -879,11 +873,11 @@ export default {
|
||||
'Session Stats': '会话统计',
|
||||
'Model Usage': '模型使用情况',
|
||||
Reqs: '请求数',
|
||||
'Input Tokens': '输入令牌',
|
||||
'Output Tokens': '输出令牌',
|
||||
'Input Tokens': '输入 token 数',
|
||||
'Output Tokens': '输出 token 数',
|
||||
'Savings Highlight:': '节省亮点:',
|
||||
'of input tokens were served from the cache, reducing costs.':
|
||||
'的输入令牌来自缓存,降低了成本',
|
||||
'从缓存载入 token ,降低了成本',
|
||||
'Tip: For a full token breakdown, run `/stats model`.':
|
||||
'提示:要查看完整的令牌明细,请运行 `/stats model`',
|
||||
'Model Stats For Nerds': '模型统计(技术细节)',
|
||||
|
||||
@@ -45,7 +45,6 @@ import process from 'node:process';
|
||||
import { useHistory } from './hooks/useHistoryManager.js';
|
||||
import { useMemoryMonitor } from './hooks/useMemoryMonitor.js';
|
||||
import { useThemeCommand } from './hooks/useThemeCommand.js';
|
||||
import { useFeedbackDialog } from './hooks/useFeedbackDialog.js';
|
||||
import { useAuthCommand } from './auth/useAuth.js';
|
||||
import { useEditorSettings } from './hooks/useEditorSettings.js';
|
||||
import { useSettingsCommand } from './hooks/useSettingsCommand.js';
|
||||
@@ -1197,19 +1196,6 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
isApprovalModeDialogOpen ||
|
||||
isResumeDialogOpen;
|
||||
|
||||
const {
|
||||
isFeedbackDialogOpen,
|
||||
openFeedbackDialog,
|
||||
closeFeedbackDialog,
|
||||
submitFeedback,
|
||||
} = useFeedbackDialog({
|
||||
config,
|
||||
settings,
|
||||
streamingState,
|
||||
history: historyManager.history,
|
||||
sessionStats,
|
||||
});
|
||||
|
||||
const pendingHistoryItems = useMemo(
|
||||
() => [...pendingSlashCommandHistoryItems, ...pendingGeminiHistoryItems],
|
||||
[pendingSlashCommandHistoryItems, pendingGeminiHistoryItems],
|
||||
@@ -1306,8 +1292,6 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
// Subagent dialogs
|
||||
isSubagentCreateDialogOpen,
|
||||
isAgentsManagerDialogOpen,
|
||||
// Feedback dialog
|
||||
isFeedbackDialogOpen,
|
||||
}),
|
||||
[
|
||||
isThemeDialogOpen,
|
||||
@@ -1398,8 +1382,6 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
// Subagent dialogs
|
||||
isSubagentCreateDialogOpen,
|
||||
isAgentsManagerDialogOpen,
|
||||
// Feedback dialog
|
||||
isFeedbackDialogOpen,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -1440,10 +1422,6 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
openResumeDialog,
|
||||
closeResumeDialog,
|
||||
handleResume,
|
||||
// Feedback dialog
|
||||
openFeedbackDialog,
|
||||
closeFeedbackDialog,
|
||||
submitFeedback,
|
||||
}),
|
||||
[
|
||||
handleThemeSelect,
|
||||
@@ -1479,10 +1457,6 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
openResumeDialog,
|
||||
closeResumeDialog,
|
||||
handleResume,
|
||||
// Feedback dialog
|
||||
openFeedbackDialog,
|
||||
closeFeedbackDialog,
|
||||
submitFeedback,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import { Box, Text } from 'ink';
|
||||
import type React from 'react';
|
||||
import { t } from '../i18n/index.js';
|
||||
import { useUIActions } from './contexts/UIActionsContext.js';
|
||||
import { useUIState } from './contexts/UIStateContext.js';
|
||||
import { useKeypress } from './hooks/useKeypress.js';
|
||||
|
||||
const FEEDBACK_OPTIONS = {
|
||||
GOOD: 1,
|
||||
BAD: 2,
|
||||
NOT_SURE: 3,
|
||||
} as const;
|
||||
|
||||
const FEEDBACK_OPTION_KEYS = {
|
||||
[FEEDBACK_OPTIONS.GOOD]: '1',
|
||||
[FEEDBACK_OPTIONS.BAD]: '2',
|
||||
[FEEDBACK_OPTIONS.NOT_SURE]: 'any',
|
||||
} as const;
|
||||
|
||||
export const FEEDBACK_DIALOG_KEYS = ['1', '2'] as const;
|
||||
|
||||
export const FeedbackDialog: React.FC = () => {
|
||||
const uiState = useUIState();
|
||||
const uiActions = useUIActions();
|
||||
|
||||
useKeypress(
|
||||
(key) => {
|
||||
if (key.name === FEEDBACK_OPTION_KEYS[FEEDBACK_OPTIONS.GOOD]) {
|
||||
uiActions.submitFeedback(FEEDBACK_OPTIONS.GOOD);
|
||||
} else if (key.name === FEEDBACK_OPTION_KEYS[FEEDBACK_OPTIONS.BAD]) {
|
||||
uiActions.submitFeedback(FEEDBACK_OPTIONS.BAD);
|
||||
} else {
|
||||
uiActions.submitFeedback(FEEDBACK_OPTIONS.NOT_SURE);
|
||||
}
|
||||
|
||||
uiActions.closeFeedbackDialog();
|
||||
},
|
||||
{ isActive: uiState.isFeedbackDialogOpen },
|
||||
);
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" marginY={1}>
|
||||
<Box>
|
||||
<Text color="cyan">● </Text>
|
||||
<Text bold>{t('How is Qwen doing this session? (optional)')}</Text>
|
||||
</Box>
|
||||
<Box marginTop={1}>
|
||||
<Text color="cyan">
|
||||
{FEEDBACK_OPTION_KEYS[FEEDBACK_OPTIONS.GOOD]}:{' '}
|
||||
</Text>
|
||||
<Text>{t('Good')}</Text>
|
||||
<Text> </Text>
|
||||
<Text color="cyan">{FEEDBACK_OPTION_KEYS[FEEDBACK_OPTIONS.BAD]}: </Text>
|
||||
<Text>{t('Bad')}</Text>
|
||||
<Text> </Text>
|
||||
<Text color="cyan">{t('Any other key')}: </Text>
|
||||
<Text>{t('Not Sure Yet')}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -26,7 +26,6 @@ import { useSettings } from '../contexts/SettingsContext.js';
|
||||
import { ApprovalMode } from '@qwen-code/qwen-code-core';
|
||||
import { StreamingState } from '../types.js';
|
||||
import { ConfigInitDisplay } from '../components/ConfigInitDisplay.js';
|
||||
import { FeedbackDialog } from '../FeedbackDialog.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
|
||||
export const Composer = () => {
|
||||
@@ -135,8 +134,6 @@ export const Composer = () => {
|
||||
</OverflowProvider>
|
||||
)}
|
||||
|
||||
{uiState.isFeedbackDialogOpen && <FeedbackDialog />}
|
||||
|
||||
{uiState.isInputActive && (
|
||||
<InputPrompt
|
||||
buffer={uiState.buffer}
|
||||
|
||||
@@ -33,9 +33,6 @@ vi.mock('../hooks/useCommandCompletion.js');
|
||||
vi.mock('../hooks/useInputHistory.js');
|
||||
vi.mock('../hooks/useReverseSearchCompletion.js');
|
||||
vi.mock('../utils/clipboardUtils.js');
|
||||
vi.mock('../contexts/UIStateContext.js', () => ({
|
||||
useUIState: vi.fn(() => ({ isFeedbackDialogOpen: false })),
|
||||
}));
|
||||
|
||||
const mockSlashCommands: SlashCommand[] = [
|
||||
{
|
||||
|
||||
@@ -36,8 +36,6 @@ import {
|
||||
import * as path from 'node:path';
|
||||
import { SCREEN_READER_USER_PREFIX } from '../textConstants.js';
|
||||
import { useShellFocusState } from '../contexts/ShellFocusContext.js';
|
||||
import { useUIState } from '../contexts/UIStateContext.js';
|
||||
import { FEEDBACK_DIALOG_KEYS } from '../FeedbackDialog.js';
|
||||
export interface InputPromptProps {
|
||||
buffer: TextBuffer;
|
||||
onSubmit: (value: string) => void;
|
||||
@@ -102,7 +100,6 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
isEmbeddedShellFocused,
|
||||
}) => {
|
||||
const isShellFocused = useShellFocusState();
|
||||
const uiState = useUIState();
|
||||
const [justNavigatedHistory, setJustNavigatedHistory] = useState(false);
|
||||
const [escPressCount, setEscPressCount] = useState(0);
|
||||
const [showEscapePrompt, setShowEscapePrompt] = useState(false);
|
||||
@@ -329,14 +326,6 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
// Intercept feedback dialog option keys (1, 2) when dialog is open
|
||||
if (
|
||||
uiState.isFeedbackDialogOpen &&
|
||||
(FEEDBACK_DIALOG_KEYS as readonly string[]).includes(key.name)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset ESC count and hide prompt on any non-ESC key
|
||||
if (key.name !== 'escape') {
|
||||
if (escPressCount > 0 || showEscapePrompt) {
|
||||
@@ -681,7 +670,6 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
recentPasteTime,
|
||||
commandSearchActive,
|
||||
commandSearchCompletion,
|
||||
uiState,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -66,10 +66,6 @@ export interface UIActions {
|
||||
openResumeDialog: () => void;
|
||||
closeResumeDialog: () => void;
|
||||
handleResume: (sessionId: string) => void;
|
||||
// Feedback dialog
|
||||
openFeedbackDialog: () => void;
|
||||
closeFeedbackDialog: () => void;
|
||||
submitFeedback: (rating: number) => void;
|
||||
}
|
||||
|
||||
export const UIActionsContext = createContext<UIActions | null>(null);
|
||||
|
||||
@@ -126,8 +126,6 @@ export interface UIState {
|
||||
// Subagent dialogs
|
||||
isSubagentCreateDialogOpen: boolean;
|
||||
isAgentsManagerDialogOpen: boolean;
|
||||
// Feedback dialog
|
||||
isFeedbackDialogOpen: boolean;
|
||||
}
|
||||
|
||||
export const UIStateContext = createContext<UIState | null>(null);
|
||||
|
||||
@@ -1,178 +0,0 @@
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import * as fs from 'node:fs';
|
||||
import {
|
||||
type Config,
|
||||
logUserFeedback,
|
||||
UserFeedbackEvent,
|
||||
type UserFeedbackRating,
|
||||
isNodeError,
|
||||
AuthType,
|
||||
} from '@qwen-code/qwen-code-core';
|
||||
import { StreamingState, MessageType, type HistoryItem } from '../types.js';
|
||||
import {
|
||||
SettingScope,
|
||||
type LoadedSettings,
|
||||
USER_SETTINGS_PATH,
|
||||
} from '../../config/settings.js';
|
||||
import type { SessionStatsState } from '../contexts/SessionContext.js';
|
||||
import stripJsonComments from 'strip-json-comments';
|
||||
|
||||
const FEEDBACK_SHOW_PROBABILITY = 0.25; // 25% probability of showing feedback dialog
|
||||
const MIN_TOOL_CALLS = 10; // Minimum tool calls to show feedback dialog
|
||||
const MIN_USER_MESSAGES = 5; // Minimum user messages to show feedback dialog
|
||||
|
||||
// Fatigue mechanism constants
|
||||
const FEEDBACK_COOLDOWN_HOURS = 24; // Hours to wait before showing feedback dialog again
|
||||
|
||||
/**
|
||||
* Check if the last message in the conversation history is an AI response
|
||||
*/
|
||||
const lastMessageIsAIResponse = (history: HistoryItem[]): boolean =>
|
||||
history.length > 0 && history[history.length - 1].type === MessageType.GEMINI;
|
||||
|
||||
/**
|
||||
* Read feedbackLastShownTimestamp directly from the user settings file
|
||||
*/
|
||||
const getFeedbackLastShownTimestampFromFile = (): number => {
|
||||
try {
|
||||
if (fs.existsSync(USER_SETTINGS_PATH)) {
|
||||
const content = fs.readFileSync(USER_SETTINGS_PATH, 'utf-8');
|
||||
const settings = JSON.parse(stripJsonComments(content));
|
||||
return settings?.ui?.feedbackLastShownTimestamp ?? 0;
|
||||
}
|
||||
} catch (error) {
|
||||
if (isNodeError(error) && error.code !== 'ENOENT') {
|
||||
console.warn(
|
||||
'Failed to read feedbackLastShownTimestamp from settings file:',
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if we should show the feedback dialog based on fatigue mechanism
|
||||
*/
|
||||
const shouldShowFeedbackBasedOnFatigue = (): boolean => {
|
||||
const feedbackLastShownTimestamp = getFeedbackLastShownTimestampFromFile();
|
||||
|
||||
const now = Date.now();
|
||||
const timeSinceLastShown = now - feedbackLastShownTimestamp;
|
||||
const cooldownMs = FEEDBACK_COOLDOWN_HOURS * 60 * 60 * 1000;
|
||||
|
||||
return timeSinceLastShown >= cooldownMs;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the session meets the minimum requirements for showing feedback
|
||||
* Either tool calls > 10 OR user messages > 5
|
||||
*/
|
||||
const meetsMinimumSessionRequirements = (
|
||||
sessionStats: SessionStatsState,
|
||||
): boolean => {
|
||||
const toolCallsCount = sessionStats.metrics.tools.totalCalls;
|
||||
const userMessagesCount = sessionStats.promptCount;
|
||||
|
||||
return (
|
||||
toolCallsCount > MIN_TOOL_CALLS || userMessagesCount > MIN_USER_MESSAGES
|
||||
);
|
||||
};
|
||||
|
||||
export interface UseFeedbackDialogProps {
|
||||
config: Config;
|
||||
settings: LoadedSettings;
|
||||
streamingState: StreamingState;
|
||||
history: HistoryItem[];
|
||||
sessionStats: SessionStatsState;
|
||||
}
|
||||
|
||||
export const useFeedbackDialog = ({
|
||||
config,
|
||||
settings,
|
||||
streamingState,
|
||||
history,
|
||||
sessionStats,
|
||||
}: UseFeedbackDialogProps) => {
|
||||
// Feedback dialog state
|
||||
const [isFeedbackDialogOpen, setIsFeedbackDialogOpen] = useState(false);
|
||||
|
||||
const openFeedbackDialog = useCallback(() => {
|
||||
setIsFeedbackDialogOpen(true);
|
||||
|
||||
// Record the timestamp when feedback dialog is shown (fire and forget)
|
||||
settings.setValue(
|
||||
SettingScope.User,
|
||||
'ui.feedbackLastShownTimestamp',
|
||||
Date.now(),
|
||||
);
|
||||
}, [settings]);
|
||||
|
||||
const closeFeedbackDialog = useCallback(
|
||||
() => setIsFeedbackDialogOpen(false),
|
||||
[],
|
||||
);
|
||||
|
||||
const submitFeedback = useCallback(
|
||||
(rating: number) => {
|
||||
// Create and log the feedback event
|
||||
const feedbackEvent = new UserFeedbackEvent(
|
||||
sessionStats.sessionId,
|
||||
rating as UserFeedbackRating,
|
||||
config.getModel(),
|
||||
config.getApprovalMode(),
|
||||
);
|
||||
|
||||
logUserFeedback(config, feedbackEvent);
|
||||
closeFeedbackDialog();
|
||||
},
|
||||
[config, sessionStats, closeFeedbackDialog],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const checkAndShowFeedback = () => {
|
||||
if (streamingState === StreamingState.Idle && history.length > 0) {
|
||||
// Show feedback dialog if:
|
||||
// 1. User is authenticated via QWEN_OAUTH
|
||||
// 2. Qwen logger is enabled (required for feedback submission)
|
||||
// 3. User feedback is enabled in settings
|
||||
// 4. The last message is an AI response
|
||||
// 5. Random chance (25% probability)
|
||||
// 6. Meets minimum requirements (tool calls > 10 OR user messages > 5)
|
||||
// 7. Fatigue mechanism allows showing (not shown recently across sessions)
|
||||
if (
|
||||
config.getAuthType() !== AuthType.QWEN_OAUTH ||
|
||||
!config.getUsageStatisticsEnabled() ||
|
||||
settings.merged.ui?.enableUserFeedback === false ||
|
||||
!lastMessageIsAIResponse(history) ||
|
||||
Math.random() > FEEDBACK_SHOW_PROBABILITY ||
|
||||
!meetsMinimumSessionRequirements(sessionStats)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check fatigue mechanism (synchronous)
|
||||
if (shouldShowFeedbackBasedOnFatigue()) {
|
||||
openFeedbackDialog();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
checkAndShowFeedback();
|
||||
}, [
|
||||
streamingState,
|
||||
history,
|
||||
sessionStats,
|
||||
isFeedbackDialogOpen,
|
||||
openFeedbackDialog,
|
||||
settings.merged.ui?.enableUserFeedback,
|
||||
config,
|
||||
]);
|
||||
|
||||
return {
|
||||
isFeedbackDialogOpen,
|
||||
openFeedbackDialog,
|
||||
closeFeedbackDialog,
|
||||
submitFeedback,
|
||||
};
|
||||
};
|
||||
@@ -8,7 +8,10 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import * as os from 'node:os';
|
||||
import { updateSettingsFilePreservingFormat } from './commentJson.js';
|
||||
import {
|
||||
updateSettingsFilePreservingFormat,
|
||||
applyUpdates,
|
||||
} from './commentJson.js';
|
||||
|
||||
describe('commentJson', () => {
|
||||
let tempDir: string;
|
||||
@@ -180,3 +183,18 @@ describe('commentJson', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyUpdates', () => {
|
||||
it('should apply updates correctly', () => {
|
||||
const original = { a: 1, b: { c: 2 } };
|
||||
const updates = { b: { c: 3 } };
|
||||
const result = applyUpdates(original, updates);
|
||||
expect(result).toEqual({ a: 1, b: { c: 3 } });
|
||||
});
|
||||
it('should apply updates correctly when empty', () => {
|
||||
const original = { a: 1, b: { c: 2 } };
|
||||
const updates = { b: {} };
|
||||
const result = applyUpdates(original, updates);
|
||||
expect(result).toEqual({ a: 1, b: {} });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -38,7 +38,7 @@ export function updateSettingsFilePreservingFormat(
|
||||
fs.writeFileSync(filePath, updatedContent, 'utf-8');
|
||||
}
|
||||
|
||||
function applyUpdates(
|
||||
export function applyUpdates(
|
||||
current: Record<string, unknown>,
|
||||
updates: Record<string, unknown>,
|
||||
): Record<string, unknown> {
|
||||
@@ -50,6 +50,7 @@ function applyUpdates(
|
||||
typeof value === 'object' &&
|
||||
value !== null &&
|
||||
!Array.isArray(value) &&
|
||||
Object.keys(value).length > 0 &&
|
||||
typeof result[key] === 'object' &&
|
||||
result[key] !== null &&
|
||||
!Array.isArray(result[key])
|
||||
|
||||
@@ -35,7 +35,6 @@ export const EVENT_MODEL_SLASH_COMMAND = 'qwen-code.slash_command.model';
|
||||
export const EVENT_SUBAGENT_EXECUTION = 'qwen-code.subagent_execution';
|
||||
export const EVENT_SKILL_LAUNCH = 'qwen-code.skill_launch';
|
||||
export const EVENT_AUTH = 'qwen-code.auth';
|
||||
export const EVENT_USER_FEEDBACK = 'qwen-code.user_feedback';
|
||||
|
||||
// Performance Events
|
||||
export const EVENT_STARTUP_PERFORMANCE = 'qwen-code.startup.performance';
|
||||
|
||||
@@ -45,7 +45,6 @@ export {
|
||||
logNextSpeakerCheck,
|
||||
logAuth,
|
||||
logSkillLaunch,
|
||||
logUserFeedback,
|
||||
} from './loggers.js';
|
||||
export type { SlashCommandEvent, ChatCompressionEvent } from './types.js';
|
||||
export {
|
||||
@@ -66,8 +65,6 @@ export {
|
||||
NextSpeakerCheckEvent,
|
||||
AuthEvent,
|
||||
SkillLaunchEvent,
|
||||
UserFeedbackEvent,
|
||||
UserFeedbackRating,
|
||||
} from './types.js';
|
||||
export { makeSlashCommandEvent, makeChatCompressionEvent } from './types.js';
|
||||
export type { TelemetryEvent } from './types.js';
|
||||
|
||||
@@ -38,7 +38,6 @@ import {
|
||||
EVENT_INVALID_CHUNK,
|
||||
EVENT_AUTH,
|
||||
EVENT_SKILL_LAUNCH,
|
||||
EVENT_USER_FEEDBACK,
|
||||
} from './constants.js';
|
||||
import {
|
||||
recordApiErrorMetrics,
|
||||
@@ -87,7 +86,6 @@ import type {
|
||||
InvalidChunkEvent,
|
||||
AuthEvent,
|
||||
SkillLaunchEvent,
|
||||
UserFeedbackEvent,
|
||||
} from './types.js';
|
||||
import type { UiEvent } from './uiTelemetry.js';
|
||||
import { uiTelemetryService } from './uiTelemetry.js';
|
||||
@@ -889,32 +887,3 @@ export function logSkillLaunch(config: Config, event: SkillLaunchEvent): void {
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
}
|
||||
|
||||
export function logUserFeedback(
|
||||
config: Config,
|
||||
event: UserFeedbackEvent,
|
||||
): void {
|
||||
const uiEvent = {
|
||||
...event,
|
||||
'event.name': EVENT_USER_FEEDBACK,
|
||||
'event.timestamp': new Date().toISOString(),
|
||||
} as UiEvent;
|
||||
uiTelemetryService.addEvent(uiEvent);
|
||||
config.getChatRecordingService()?.recordUiTelemetryEvent(uiEvent);
|
||||
QwenLogger.getInstance(config)?.logUserFeedbackEvent(event);
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
|
||||
const attributes: LogAttributes = {
|
||||
...getCommonAttributes(config),
|
||||
...event,
|
||||
'event.name': EVENT_USER_FEEDBACK,
|
||||
'event.timestamp': new Date().toISOString(),
|
||||
};
|
||||
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: `User feedback: Rating ${event.rating} for session ${event.session_id}.`,
|
||||
attributes,
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
}
|
||||
|
||||
@@ -39,7 +39,6 @@ import type {
|
||||
ExtensionDisableEvent,
|
||||
AuthEvent,
|
||||
SkillLaunchEvent,
|
||||
UserFeedbackEvent,
|
||||
RipgrepFallbackEvent,
|
||||
EndSessionEvent,
|
||||
} from '../types.js';
|
||||
@@ -843,21 +842,6 @@ export class QwenLogger {
|
||||
this.flushIfNeeded();
|
||||
}
|
||||
|
||||
logUserFeedbackEvent(event: UserFeedbackEvent): void {
|
||||
const rumEvent = this.createActionEvent('user', 'user_feedback', {
|
||||
properties: {
|
||||
session_id: event.session_id,
|
||||
rating: event.rating,
|
||||
model: event.model,
|
||||
approval_mode: event.approval_mode,
|
||||
prompt_id: event.prompt_id || '',
|
||||
},
|
||||
});
|
||||
|
||||
this.enqueueLogEvent(rumEvent);
|
||||
this.flushIfNeeded();
|
||||
}
|
||||
|
||||
logChatCompressionEvent(event: ChatCompressionEvent): void {
|
||||
const rumEvent = this.createActionEvent('misc', 'chat_compression', {
|
||||
properties: {
|
||||
|
||||
@@ -757,38 +757,6 @@ export class SkillLaunchEvent implements BaseTelemetryEvent {
|
||||
}
|
||||
}
|
||||
|
||||
export enum UserFeedbackRating {
|
||||
BAD = 1,
|
||||
FINE = 2,
|
||||
GOOD = 3,
|
||||
}
|
||||
|
||||
export class UserFeedbackEvent implements BaseTelemetryEvent {
|
||||
'event.name': 'user_feedback';
|
||||
'event.timestamp': string;
|
||||
session_id: string;
|
||||
rating: UserFeedbackRating;
|
||||
model: string;
|
||||
approval_mode: string;
|
||||
prompt_id?: string;
|
||||
|
||||
constructor(
|
||||
session_id: string,
|
||||
rating: UserFeedbackRating,
|
||||
model: string,
|
||||
approval_mode: string,
|
||||
prompt_id?: string,
|
||||
) {
|
||||
this['event.name'] = 'user_feedback';
|
||||
this['event.timestamp'] = new Date().toISOString();
|
||||
this.session_id = session_id;
|
||||
this.rating = rating;
|
||||
this.model = model;
|
||||
this.approval_mode = approval_mode;
|
||||
this.prompt_id = prompt_id;
|
||||
}
|
||||
}
|
||||
|
||||
export type TelemetryEvent =
|
||||
| StartSessionEvent
|
||||
| EndSessionEvent
|
||||
@@ -818,8 +786,7 @@ export type TelemetryEvent =
|
||||
| ToolOutputTruncatedEvent
|
||||
| ModelSlashCommandEvent
|
||||
| AuthEvent
|
||||
| SkillLaunchEvent
|
||||
| UserFeedbackEvent;
|
||||
| SkillLaunchEvent;
|
||||
|
||||
export class ExtensionDisableEvent implements BaseTelemetryEvent {
|
||||
'event.name': 'extension_disable';
|
||||
|
||||
Reference in New Issue
Block a user