mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-01 21:20:44 +00:00
feat(cli): add /btw slash command for ephemeral side questions
Allow users to ask quick "by the way" questions that use the current conversation context but don't pollute the main conversation history. Closes #2370 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1359563f45
commit
3818f8acd4
6 changed files with 201 additions and 1 deletions
|
|
@ -11,6 +11,7 @@ import { aboutCommand } from '../ui/commands/aboutCommand.js';
|
||||||
import { agentsCommand } from '../ui/commands/agentsCommand.js';
|
import { agentsCommand } from '../ui/commands/agentsCommand.js';
|
||||||
import { approvalModeCommand } from '../ui/commands/approvalModeCommand.js';
|
import { approvalModeCommand } from '../ui/commands/approvalModeCommand.js';
|
||||||
import { authCommand } from '../ui/commands/authCommand.js';
|
import { authCommand } from '../ui/commands/authCommand.js';
|
||||||
|
import { btwCommand } from '../ui/commands/btwCommand.js';
|
||||||
import { bugCommand } from '../ui/commands/bugCommand.js';
|
import { bugCommand } from '../ui/commands/bugCommand.js';
|
||||||
import { clearCommand } from '../ui/commands/clearCommand.js';
|
import { clearCommand } from '../ui/commands/clearCommand.js';
|
||||||
import { compressCommand } from '../ui/commands/compressCommand.js';
|
import { compressCommand } from '../ui/commands/compressCommand.js';
|
||||||
|
|
@ -63,6 +64,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
|
||||||
agentsCommand,
|
agentsCommand,
|
||||||
approvalModeCommand,
|
approvalModeCommand,
|
||||||
authCommand,
|
authCommand,
|
||||||
|
btwCommand,
|
||||||
bugCommand,
|
bugCommand,
|
||||||
clearCommand,
|
clearCommand,
|
||||||
compressCommand,
|
compressCommand,
|
||||||
|
|
|
||||||
138
packages/cli/src/ui/commands/btwCommand.ts
Normal file
138
packages/cli/src/ui/commands/btwCommand.ts
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Qwen Code
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type {
|
||||||
|
CommandContext,
|
||||||
|
SlashCommand,
|
||||||
|
SlashCommandActionReturn,
|
||||||
|
} from './types.js';
|
||||||
|
import { CommandKind } from './types.js';
|
||||||
|
import { MessageType } from '../types.js';
|
||||||
|
import type { HistoryItemBtw } from '../types.js';
|
||||||
|
import { t } from '../../i18n/index.js';
|
||||||
|
|
||||||
|
export const btwCommand: SlashCommand = {
|
||||||
|
name: 'btw',
|
||||||
|
get description() {
|
||||||
|
return t(
|
||||||
|
'Ask a quick side question without affecting the main conversation',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
kind: CommandKind.BUILT_IN,
|
||||||
|
action: async (
|
||||||
|
context: CommandContext,
|
||||||
|
args: string,
|
||||||
|
): Promise<void | SlashCommandActionReturn> => {
|
||||||
|
const question = args.trim();
|
||||||
|
|
||||||
|
if (!question) {
|
||||||
|
return {
|
||||||
|
type: 'message',
|
||||||
|
messageType: 'error',
|
||||||
|
content: t('Please provide a question. Usage: /btw <your question>'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { config } = context.services;
|
||||||
|
const { ui } = context;
|
||||||
|
const abortSignal = context.abortSignal;
|
||||||
|
|
||||||
|
if (!config) {
|
||||||
|
return {
|
||||||
|
type: 'message',
|
||||||
|
messageType: 'error',
|
||||||
|
content: t('Config not loaded.'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const geminiClient = config.getGeminiClient();
|
||||||
|
if (!geminiClient) {
|
||||||
|
return {
|
||||||
|
type: 'message',
|
||||||
|
messageType: 'error',
|
||||||
|
content: t('No chat client available.'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show pending state
|
||||||
|
const pendingItem: HistoryItemBtw = {
|
||||||
|
type: MessageType.BTW,
|
||||||
|
btw: {
|
||||||
|
question,
|
||||||
|
answer: '',
|
||||||
|
isPending: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
ui.setPendingItem(pendingItem);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get current conversation history
|
||||||
|
const history = geminiClient.getHistory();
|
||||||
|
|
||||||
|
// Make an ephemeral generateContent call with the conversation context
|
||||||
|
// but WITHOUT tools — the btw response is purely based on existing context
|
||||||
|
const response = await geminiClient.generateContent(
|
||||||
|
[
|
||||||
|
...history,
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
parts: [
|
||||||
|
{
|
||||||
|
text: `[Side question - answer briefly and concisely, this is a "by the way" question that doesn't need to be part of our main conversation]\n\n${question}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{},
|
||||||
|
abortSignal ?? new AbortController().signal,
|
||||||
|
config.getModel(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (abortSignal?.aborted) {
|
||||||
|
ui.setPendingItem(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the response text
|
||||||
|
const parts = response.candidates?.[0]?.content?.parts;
|
||||||
|
const answer =
|
||||||
|
parts
|
||||||
|
?.map((part) => part.text)
|
||||||
|
.filter((text): text is string => typeof text === 'string')
|
||||||
|
.join('') || t('No response received.');
|
||||||
|
|
||||||
|
// Clear pending and show the completed btw item
|
||||||
|
ui.setPendingItem(null);
|
||||||
|
ui.addItem(
|
||||||
|
{
|
||||||
|
type: MessageType.BTW,
|
||||||
|
btw: {
|
||||||
|
question,
|
||||||
|
answer,
|
||||||
|
isPending: false,
|
||||||
|
},
|
||||||
|
} as HistoryItemBtw,
|
||||||
|
Date.now(),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if (abortSignal?.aborted) {
|
||||||
|
ui.setPendingItem(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.setPendingItem(null);
|
||||||
|
ui.addItem(
|
||||||
|
{
|
||||||
|
type: MessageType.ERROR,
|
||||||
|
text: t('Failed to answer btw question: {{error}}', {
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
Date.now(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -39,6 +39,7 @@ import { SkillsList } from './views/SkillsList.js';
|
||||||
import { ToolsList } from './views/ToolsList.js';
|
import { ToolsList } from './views/ToolsList.js';
|
||||||
import { McpStatus } from './views/McpStatus.js';
|
import { McpStatus } from './views/McpStatus.js';
|
||||||
import { InsightProgressMessage } from './messages/InsightProgressMessage.js';
|
import { InsightProgressMessage } from './messages/InsightProgressMessage.js';
|
||||||
|
import { BtwMessage } from './messages/BtwMessage.js';
|
||||||
|
|
||||||
interface HistoryItemDisplayProps {
|
interface HistoryItemDisplayProps {
|
||||||
item: HistoryItem;
|
item: HistoryItem;
|
||||||
|
|
@ -194,6 +195,7 @@ const HistoryItemDisplayComponent: React.FC<HistoryItemDisplayProps> = ({
|
||||||
{itemForDisplay.type === 'insight_progress' && (
|
{itemForDisplay.type === 'insight_progress' && (
|
||||||
<InsightProgressMessage progress={itemForDisplay.progress} />
|
<InsightProgressMessage progress={itemForDisplay.progress} />
|
||||||
)}
|
)}
|
||||||
|
{itemForDisplay.type === 'btw' && <BtwMessage btw={itemForDisplay.btw} />}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
44
packages/cli/src/ui/components/messages/BtwMessage.tsx
Normal file
44
packages/cli/src/ui/components/messages/BtwMessage.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Qwen Code
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type React from 'react';
|
||||||
|
import { Box, Text } from 'ink';
|
||||||
|
import type { BtwProps } from '../../types.js';
|
||||||
|
import Spinner from 'ink-spinner';
|
||||||
|
import { Colors } from '../../colors.js';
|
||||||
|
|
||||||
|
export interface BtwDisplayProps {
|
||||||
|
btw: BtwProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BtwMessage renders the /btw (by the way) sidebar response.
|
||||||
|
* Shows an ephemeral question and answer that doesn't affect the main conversation.
|
||||||
|
*/
|
||||||
|
export const BtwMessage: React.FC<BtwDisplayProps> = ({ btw }) => (
|
||||||
|
<Box flexDirection="column">
|
||||||
|
<Box flexDirection="row">
|
||||||
|
<Text color={Colors.Gray} dimColor>
|
||||||
|
{'btw> '}
|
||||||
|
</Text>
|
||||||
|
<Text color={Colors.Gray}>{btw.question}</Text>
|
||||||
|
</Box>
|
||||||
|
<Box flexDirection="row" marginTop={0}>
|
||||||
|
{btw.isPending ? (
|
||||||
|
<Box>
|
||||||
|
<Box marginRight={1}>
|
||||||
|
<Spinner type="dots" />
|
||||||
|
</Box>
|
||||||
|
<Text color={Colors.AccentPurple}>Thinking...</Text>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Box flexDirection="column">
|
||||||
|
<Text color={Colors.AccentCyan}>{btw.answer}</Text>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
@ -62,6 +62,7 @@ const SLASH_COMMANDS_SKIP_RECORDING = new Set([
|
||||||
'reset',
|
'reset',
|
||||||
'new',
|
'new',
|
||||||
'resume',
|
'resume',
|
||||||
|
'btw',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
interface SlashCommandProcessorActions {
|
interface SlashCommandProcessorActions {
|
||||||
|
|
|
||||||
|
|
@ -262,6 +262,17 @@ export type HistoryItemInsightProgress = HistoryItemBase & {
|
||||||
progress: InsightProgressProps;
|
progress: InsightProgressProps;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface BtwProps {
|
||||||
|
question: string;
|
||||||
|
answer: string;
|
||||||
|
isPending: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HistoryItemBtw = HistoryItemBase & {
|
||||||
|
type: 'btw';
|
||||||
|
btw: BtwProps;
|
||||||
|
};
|
||||||
|
|
||||||
// Using Omit<HistoryItem, 'id'> seems to have some issues with typescript's
|
// Using Omit<HistoryItem, 'id'> seems to have some issues with typescript's
|
||||||
// type inference e.g. historyItem.type === 'tool_group' isn't auto-inferring that
|
// type inference e.g. historyItem.type === 'tool_group' isn't auto-inferring that
|
||||||
// 'tools' in historyItem.
|
// 'tools' in historyItem.
|
||||||
|
|
@ -291,7 +302,8 @@ export type HistoryItemWithoutId =
|
||||||
| HistoryItemToolsList
|
| HistoryItemToolsList
|
||||||
| HistoryItemSkillsList
|
| HistoryItemSkillsList
|
||||||
| HistoryItemMcpStatus
|
| HistoryItemMcpStatus
|
||||||
| HistoryItemInsightProgress;
|
| HistoryItemInsightProgress
|
||||||
|
| HistoryItemBtw;
|
||||||
|
|
||||||
export type HistoryItem = HistoryItemWithoutId & { id: number };
|
export type HistoryItem = HistoryItemWithoutId & { id: number };
|
||||||
|
|
||||||
|
|
@ -315,6 +327,7 @@ export enum MessageType {
|
||||||
SKILLS_LIST = 'skills_list',
|
SKILLS_LIST = 'skills_list',
|
||||||
MCP_STATUS = 'mcp_status',
|
MCP_STATUS = 'mcp_status',
|
||||||
INSIGHT_PROGRESS = 'insight_progress',
|
INSIGHT_PROGRESS = 'insight_progress',
|
||||||
|
BTW = 'btw',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InsightProgressProps {
|
export interface InsightProgressProps {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue