Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 51 additions & 4 deletions packages/web-shell/client/components/messages/ToolGroup.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,11 @@ const t = (key: string, values?: Record<string, string | number>): string => {
if (key === 'toolGroup.summary.updatedTodos') {
return `Updated todos ${values?.count ?? 0} times`;
}
if (key === 'toolGroup.summary.askedUser') {
return 'Asked user';
if (key === 'toolGroup.summary.provideInformation') {
return 'Provide information';
}
if (key === 'toolGroup.summary.askedQuestions') {
return `Asked ${values?.count ?? 0} question${values?.count === 1 ? '' : 's'}`;
}
if (key === 'toolGroup.summary.otherTools') {
return `Called ${values?.count ?? 0} other tools`;
Expand Down Expand Up @@ -168,6 +171,18 @@ describe('tool group summary logic', () => {
expect(formatToolGroupSummary(tools, zhT)).toBe('Running 读取文件');
});

it('asks for information while AskUserQuestion is running', () => {
const tools = [
makeTool({
toolName: 'ask_user_question',
status: 'in_progress',
args: { questions: [{}, {}] },
}),
];

expect(formatToolGroupSummary(tools, t)).toBe('Provide information');
});

it('summarizes completed tool groups by common action type', () => {
const tools = [
makeTool({ callId: 'shell', status: 'completed' }),
Expand All @@ -183,13 +198,14 @@ describe('tool group summary logic', () => {
callId: 'ask',
toolName: 'ask_user_question',
status: 'completed',
args: { questions: [{}, {}] },
}),
];

expect(hasActiveTool(tools)).toBe(false);
expect(getActiveTool(tools).callId).toBe('ask');
expect(formatToolGroupSummary(tools, t)).toBe(
'Edited 1 files Ran 1 commands Read 1 files Searched 1 times Updated todos 1 times Asked user',
'Edited 1 files Ran 1 commands Read 1 files Searched 1 times Updated todos 1 times Asked 2 questions',
);
});

Expand Down Expand Up @@ -242,9 +258,40 @@ describe('tool group summary logic', () => {
expect(
formatSingleToolSummary(makeTool({ toolName: 'todo_write' }), t),
).toBe('Updated todos 1 times');
expect(
formatSingleToolSummary(
makeTool({
toolName: 'ask_user_question',
args: { questions: [{}, {}, {}] },
}),
t,
),
).toBe('Asked 3 questions');
expect(
formatSingleToolSummary(
makeTool({
toolName: 'ask_user_question',
status: 'in_progress',
args: { questions: [{}, {}, {}] },
}),
t,
),
).toBe('Provide information');
});

it('counts legacy or empty AskUserQuestion inputs as one question', () => {
expect(
formatSingleToolSummary(makeTool({ toolName: 'ask_user_question' }), t),
).toBe('Asked user');
).toBe('Asked 1 question');
expect(
formatSingleToolSummary(
makeTool({
toolName: 'ask_user_question',
args: { questions: [] },
}),
t,
),
).toBe('Asked 1 question');
});

it('truncates long single tool descriptions in the chat summary', () => {
Expand Down
34 changes: 25 additions & 9 deletions packages/web-shell/client/components/messages/ToolGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,9 @@ export function formatToolGroupSummary(
): string {
if (hasActiveTool(tools)) {
const activeTool = getActiveTool(tools);
if (isAskUserQuestionToolName(activeTool.toolName)) {
return t('toolGroup.summary.provideInformation');
}
return t('toolGroup.running', {
name: localizeToolDisplayName(activeTool.toolName, t),
count: tools.length,
Expand All @@ -595,7 +598,11 @@ export function formatSingleToolSummary(
return t('toolGroup.summary.updatedTodos', { count: 1 });
}
if (isAskUserQuestionToolName(tool.toolName)) {
return t('toolGroup.summary.askedUser', { count: 1 });
return isActiveToolStatus(tool.status)
? t('toolGroup.summary.provideInformation')
: t('toolGroup.summary.askedQuestions', {
count: getAskUserQuestionCount(tool),
});
}

const { displayName, description, hideDisplayName } =
Expand Down Expand Up @@ -636,13 +643,13 @@ function SingleToolSummary({
workspaceCwd?: string;
}) {
const { t } = useI18n();
const isAskUserQuestion = isAskUserQuestionToolName(tool.toolName);
const runningPrefix =
isActiveToolStatus(tool.status) && t('toolGroup.runningPrefix').trim();
!isAskUserQuestion &&
isActiveToolStatus(tool.status) &&
t('toolGroup.runningPrefix').trim();

if (
isTodoWriteToolName(tool.toolName) ||
isAskUserQuestionToolName(tool.toolName)
) {
if (isTodoWriteToolName(tool.toolName) || isAskUserQuestion) {
return (
<>
{runningPrefix && <span>{runningPrefix} </span>}
Expand Down Expand Up @@ -677,7 +684,7 @@ function formatCompletedToolSummary(
let read = 0;
let searched = 0;
let todos = 0;
let asked = 0;
let askedQuestions = 0;
let other = 0;

for (const tool of tools) {
Expand Down Expand Up @@ -706,7 +713,7 @@ function formatCompletedToolSummary(
} else if (isTodoWriteToolName(name)) {
todos++;
} else if (isAskUserQuestionToolName(name)) {
asked++;
askedQuestions += getAskUserQuestionCount(tool);
} else {
other++;
}
Expand All @@ -718,13 +725,22 @@ function formatCompletedToolSummary(
read ? t('toolGroup.summary.readFiles', { count: read }) : '',
searched ? t('toolGroup.summary.searched', { count: searched }) : '',
todos ? t('toolGroup.summary.updatedTodos', { count: todos }) : '',
asked ? t('toolGroup.summary.askedUser') : '',
askedQuestions
? t('toolGroup.summary.askedQuestions', { count: askedQuestions })
: '',
other ? t('toolGroup.summary.otherTools', { count: other }) : '',
].filter(Boolean);

return parts.join(' ');
}

function getAskUserQuestionCount(tool: ACPToolCall): number {
const questions = tool.args?.questions;
return Array.isArray(questions) && questions.length > 0
? questions.length
: 1;
}
Comment thread
ytahdn marked this conversation as resolved.

export function hasActiveTool(tools: ACPToolCall[]): boolean {
return tools.some((tool) => isActiveToolStatus(tool.status));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -796,13 +796,37 @@

.sessionMetaSlot {
position: relative;
flex: 0 0 auto;
height: 30px;
margin-left: auto;
display: flex;
align-items: center;
justify-content: flex-end;
}

.sessionAttention {
height: 22px;
display: inline-flex;
align-items: center;
padding: 0 9px;
border-radius: 999px;
background: color-mix(in srgb, var(--success-color) 10%, transparent);
color: color-mix(in srgb, var(--success-color) 95%, var(--foreground));
font-size: 12px;
font-weight: 500;
line-height: 20px;
white-space: nowrap;
}

.sessionAttentionUserInput {
background: color-mix(in srgb, var(--agent-blue-500) 10%, transparent);
color: color-mix(in srgb, var(--agent-blue-500) 95%, var(--foreground));
}

.sessionAttention + .sessionLoading {
margin-left: 10px;
}

.sessionRow:hover .sessionMetaSlot,
.sessionMetaSlot:has(.sessionActionButton:focus-visible),
.sessionMetaSlot:has([data-state='open']) {
Expand Down Expand Up @@ -830,12 +854,17 @@

.sessionRow:hover:not(.runningSession) .sessionTime,
.sessionRow:focus-within:not(.runningSession) .sessionTime,
.sessionRow:hover:not(.runningSession) .sessionAttention,
.sessionRow:focus-within:not(.runningSession) .sessionAttention,
.sessionMetaSlot:hover .sessionTime,
.sessionMetaSlot:hover .sessionLoading,
.sessionMetaSlot:hover .sessionAttention,
.sessionMetaSlot:has(.sessionActionButton:focus-visible) .sessionTime,
.sessionMetaSlot:has([data-state='open']) .sessionTime,
.sessionMetaSlot:has(.sessionActionButton:focus-visible) .sessionLoading,
.sessionMetaSlot:has([data-state='open']) .sessionLoading {
.sessionMetaSlot:has([data-state='open']) .sessionLoading,
.sessionMetaSlot:has(.sessionActionButton:focus-visible) .sessionAttention,
.sessionMetaSlot:has([data-state='open']) .sessionAttention {
opacity: 0;
}

Expand Down
22 changes: 20 additions & 2 deletions packages/web-shell/client/components/sidebar/WebShellSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2425,6 +2425,13 @@ export function WebShellSidebar({
const isCurrent = isCurrentSession(session);
const isEditing = isCurrent && editingSessionId === session.sessionId;
const exporting = exportingSessionIds.has(sessionIdentity);
const needsUserInput =
!session.isWaitingForPermission && session.isWaitingForUserQuestion;
const attentionLabel = session.isWaitingForPermission
? t('sidebar.waitingForApproval')
: needsUserInput
? t('sidebar.userInputNeeded')
: null;
Comment thread
ytahdn marked this conversation as resolved.
return (
<div
key={sessionIdentity}
Expand Down Expand Up @@ -2487,14 +2494,25 @@ export function WebShellSidebar({
<>
<span className={styles.sessionText}>{label}</span>
<div className={styles.sessionMetaSlot}>
{attentionLabel && (
<span
className={cx(
styles.sessionAttention,
needsUserInput && styles.sessionAttentionUserInput,
)}
aria-label={attentionLabel}
>
{attentionLabel}
</span>
)}
{session.hasActivePrompt ? (
<span
className={styles.sessionLoading}
aria-label={t('sidebar.running')}
/>
) : (
) : !attentionLabel ? (
<span className={styles.sessionTime}>{time}</span>
)}
) : null}
{readOnly && canMutateSessionArchive(session) && (
<div
className={styles.sessionActions}
Expand Down
11 changes: 9 additions & 2 deletions packages/web-shell/client/i18n.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -923,6 +923,8 @@ const EN: Messages = {
`Delete "${v?.name ?? ''}"? This cannot be undone.`,
'sidebar.clients': (v) => `${v?.count ?? 0} client(s)`,
'sidebar.running': 'Running',
'sidebar.waitingForApproval': 'Waiting for approval',
'sidebar.userInputNeeded': 'User input needed',
'sidebar.completedUnread': 'Finished',
'sidebar.pin': 'Pin',
'sidebar.unpin': 'Unpin',
Expand Down Expand Up @@ -1835,7 +1837,9 @@ const EN: Messages = {
`Searched ${v?.count ?? 0} time${v?.count === 1 ? '' : 's'}`,
'toolGroup.summary.updatedTodos': (v) =>
`Updated task list${v?.count === 1 ? '' : ` ${v?.count ?? 0} times`}`,
'toolGroup.summary.askedUser': 'Asked user',
'toolGroup.summary.provideInformation': 'Provide information',
'toolGroup.summary.askedQuestions': (v) =>
`Asked ${v?.count ?? 0} question${v?.count === 1 ? '' : 's'}`,
'toolGroup.summary.otherTools': (v) =>
`Called ${v?.count ?? 0} other tool${v?.count === 1 ? '' : 's'}`,
'toolGroup.running': (v) =>
Expand Down Expand Up @@ -2854,6 +2858,8 @@ const ZH: Messages = {
`确定删除“${v?.name ?? ''}”吗?删除后不可恢复。`,
'sidebar.clients': (v) => `${v?.count ?? 0} 个客户端`,
'sidebar.running': '运行中',
'sidebar.waitingForApproval': '等待批准',
'sidebar.userInputNeeded': '需要用户输入',
'sidebar.completedUnread': '刚完成',
'sidebar.pin': '置顶',
'sidebar.unpin': '取消置顶',
Expand Down Expand Up @@ -3707,7 +3713,8 @@ const ZH: Messages = {
Number(v?.count ?? 0) > 1
? `已更新任务清单 ${v?.count ?? 0} 次`
: '已更新任务清单',
'toolGroup.summary.askedUser': '已询问用户',
'toolGroup.summary.provideInformation': '补充信息',
'toolGroup.summary.askedQuestions': (v) => `已询问 ${v?.count ?? 0} 个问题`,
'toolGroup.summary.otherTools': (v) => `调用了 ${v?.count ?? 0} 个工具`,
'toolGroup.running': (v) =>
`正在执行 ${v?.name ?? '工具'}${v?.duration ? ` ${v.duration}` : ''}${
Expand Down
Loading