mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-07 06:19:43 -10:00
feat(web-shell): show a dismissible Workflow chip and menu summary (#16107)
* feat(web-shell): show a dismissible Workflow chip and menu summary Workflow moved into the composer + menu, so once it was on the toolbar no longer showed it. Show a Workflow chip in the toolbar while the saved preference is on, the same way Plan does: hovering reveals a close mark, clicking turns the mode off (also during a running response), and an on-but-unavailable preference is tinted with the warning color. The menu entry gains a one-line summary of what the mode does, or of its state when it is not simply available. * fix(web-shell): keep the Workflow chip warning off the Plan chip selectors
This commit is contained in:
1 parent
73a7268447
commit
6b6df2a8b4
11 files changed
+372
-34
No files matched your search
+15
-11
@@ -42,23 +42,27 @@ capability or option. A failed write leaves the published mode unchanged.
|
||||
|
||||
## Web Shell and VS Code
|
||||
|
||||
The composer in Web Shell, split panes, and the modern VS Code chat includes
|
||||
an independent **Workflow** control for supported sessions. The switch shows
|
||||
the saved preference, not whether the mode can currently take effect. If the
|
||||
preference is on but the tools are unavailable, it stays checked and can still
|
||||
be turned off. Click the Workflow label or focus and activate it to open its status details;
|
||||
the same details are available by tapping on touch devices.
|
||||
The composer in Web Shell, split panes, and the modern VS Code chat lists
|
||||
**Workflow** in its **+** menu for supported sessions. The check mark shows the
|
||||
saved preference, not whether the mode can currently take effect; on wider
|
||||
screens a second line summarizes what the mode does or its current state. While
|
||||
the preference is on, the toolbar shows a **Workflow** chip; its tooltip carries
|
||||
the status details, and clicking it turns the mode off. If the preference is on
|
||||
but the tools are unavailable, the entry stays checked, the chip is shown in the
|
||||
warning color, and both can still turn the mode off.
|
||||
|
||||
Before a session is created or restored, the control shows an availability
|
||||
placeholder. It does not create a session or save a future preference. Hosts
|
||||
that do not advertise support, including older servers, hide the mode switch.
|
||||
that do not advertise support, including older servers, hide the entry. Hosts
|
||||
that hide the **+** menu show neither the entry nor the chip.
|
||||
|
||||
Changes from another client update the same session's control automatically.
|
||||
During a write or reconnection, the last confirmed value remains visible and
|
||||
the switch is disabled until state is confirmed. A timeout is an unconfirmed
|
||||
outcome, not a successful change; the client reads the current state instead
|
||||
of automatically repeating the write. An active response does not disable the
|
||||
switch: the change applies to the next fresh turn and leaves effort unchanged.
|
||||
the entry and chip are disabled until state is confirmed. A timeout is an
|
||||
unconfirmed outcome, not a successful change; the client reads the current
|
||||
state instead of automatically repeating the write. An active response does
|
||||
not disable the entry or chip: the change applies to the next fresh turn and
|
||||
leaves effort unchanged.
|
||||
|
||||
## Reminder and input behavior
|
||||
|
||||
|
||||
@@ -33,18 +33,20 @@ Managed、SSH、派生/子代理、session-agent 及关闭聊天录制的会话
|
||||
|
||||
## Web Shell 与 VS Code
|
||||
|
||||
Web Shell、分屏和现代 VS Code 聊天的输入栏为受支持会话提供独立的 **Workflow**
|
||||
控件。开关表示已保存的偏好,而不是当前是否实际生效。偏好已开启但工具不可用时,
|
||||
开关仍保持选中,并允许关闭。点击 Workflow 标签可打开状态详情;键盘聚焦后激活标签
|
||||
或在触屏设备上轻点,也能查看相同详情。
|
||||
Web Shell、分屏和现代 VS Code 聊天的输入栏为受支持会话在 **+** 菜单中提供
|
||||
**Workflow** 条目。勾选表示已保存的偏好,而不是当前是否实际生效;较宽的屏幕上,
|
||||
条目第二行说明该模式的作用或当前状态。偏好开启期间,工具栏显示 **Workflow**
|
||||
标签,悬停提示给出状态详情,点击即可关闭。偏好已开启但工具不可用时,条目仍保持
|
||||
勾选,标签以警告色显示,两者都能关闭该模式。
|
||||
|
||||
尚未创建或恢复会话时,控件只显示可用性占位提示,不会创建会话或保存将来的偏好。
|
||||
未广告支持能力的宿主(包括旧版服务端)不显示模式开关。
|
||||
未广告支持能力的宿主(包括旧版服务端)不显示该条目;隐藏 **+** 菜单的宿主
|
||||
既不显示条目,也不显示标签。
|
||||
|
||||
其他客户端的更改会自动同步到同一会话的控件。写入或重连期间保留最后确认值,
|
||||
直到状态重新确认前禁用开关。超时表示结果尚未确认,不是修改成功;客户端读取当前
|
||||
状态,而不会自动重复写入。正在输出回复不会禁用开关:更改对下一轮新输入生效,
|
||||
并保持思考强度不变。
|
||||
直到状态重新确认前禁用条目和标签。超时表示结果尚未确认,不是修改成功;客户端
|
||||
读取当前状态,而不会自动重复写入。正在输出回复不会禁用条目或标签:更改对下一轮
|
||||
新输入生效,并保持思考强度不变。
|
||||
|
||||
## 提醒与输入行为
|
||||
|
||||
|
||||
@@ -1905,6 +1905,11 @@
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* Workflow mode saved on but not currently able to take effect. */
|
||||
.workflowChip[data-unavailable] {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
/* No dimming: like every other .toolBtn, including the permission button
|
||||
beside it, the chip keeps its appearance while mode controls are busy. That
|
||||
state is brief, and dimming one control of the pair would flicker. */
|
||||
|
||||
@@ -6909,6 +6909,90 @@ describe('ChatEditor Workflow mode', () => {
|
||||
expect(composerCoreState.submitText).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows a dismissible chip while Workflow is requested, even mid-response', () => {
|
||||
const onWorkflowModeChange = vi.fn();
|
||||
const props = {
|
||||
...ready,
|
||||
isRunning: true,
|
||||
disabled: true,
|
||||
visibleToolbarActions: ['addMenu', 'workflowMode', 'model'] as const,
|
||||
onWorkflowModeChange,
|
||||
};
|
||||
const container = renderChatEditor(props);
|
||||
const chip = container.querySelector<HTMLButtonElement>(
|
||||
'[data-web-shell-workflow-chip]',
|
||||
)!;
|
||||
expect(chip.getAttribute('aria-pressed')).toBe('true');
|
||||
expect(chip.getAttribute('aria-disabled')).toBeNull();
|
||||
expect(chip.hasAttribute('data-unavailable')).toBe(false);
|
||||
act(() => chip.click());
|
||||
expect(onWorkflowModeChange).toHaveBeenCalledExactlyOnceWith(false);
|
||||
|
||||
rerenderChatEditor(container, {
|
||||
...props,
|
||||
workflowMode: { requested: false, available: true, effective: false },
|
||||
});
|
||||
expect(
|
||||
container.querySelector('[data-web-shell-workflow-chip]'),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('keeps the chip dismissible but marked while the requested mode is unavailable', () => {
|
||||
const onWorkflowModeChange = vi.fn();
|
||||
const container = renderChatEditor({
|
||||
workflowModeStatus: 'ready',
|
||||
workflowMode: {
|
||||
requested: true,
|
||||
available: false,
|
||||
effective: false,
|
||||
reason: 'workflow_unavailable',
|
||||
},
|
||||
visibleToolbarActions: ['addMenu', 'workflowMode'],
|
||||
onWorkflowModeChange,
|
||||
});
|
||||
const chip = container.querySelector<HTMLButtonElement>(
|
||||
'[data-web-shell-workflow-chip]',
|
||||
)!;
|
||||
expect(chip.hasAttribute('data-unavailable')).toBe(true);
|
||||
act(() => chip.click());
|
||||
expect(onWorkflowModeChange).toHaveBeenCalledExactlyOnceWith(false);
|
||||
});
|
||||
|
||||
it('makes the chip inert while a Workflow write is pending', () => {
|
||||
const onWorkflowModeChange = vi.fn();
|
||||
const container = renderChatEditor({
|
||||
...ready,
|
||||
workflowModePending: true,
|
||||
visibleToolbarActions: ['addMenu', 'workflowMode'],
|
||||
onWorkflowModeChange,
|
||||
});
|
||||
const chip = container.querySelector<HTMLButtonElement>(
|
||||
'[data-web-shell-workflow-chip]',
|
||||
)!;
|
||||
expect(chip.getAttribute('aria-disabled')).toBe('true');
|
||||
act(() => chip.click());
|
||||
expect(onWorkflowModeChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('hands focus to the composer when the focused chip goes away', () => {
|
||||
const props = {
|
||||
...ready,
|
||||
visibleToolbarActions: ['addMenu', 'workflowMode'] as const,
|
||||
onWorkflowModeChange: vi.fn(),
|
||||
};
|
||||
const container = renderChatEditor(props);
|
||||
const chip = container.querySelector<HTMLButtonElement>(
|
||||
'[data-web-shell-workflow-chip]',
|
||||
)!;
|
||||
act(() => chip.focus());
|
||||
composerCoreState.focus.mockClear();
|
||||
rerenderChatEditor(container, {
|
||||
...props,
|
||||
workflowMode: { requested: false, available: true, effective: false },
|
||||
});
|
||||
expect(composerCoreState.focus).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('hides Workflow when the host excludes the add menu', () => {
|
||||
const container = renderChatEditor({
|
||||
...ready,
|
||||
@@ -6917,6 +7001,9 @@ describe('ChatEditor Workflow mode', () => {
|
||||
expect(
|
||||
container.querySelector('[data-testid="composer-add-menu-trigger"]'),
|
||||
).toBeNull();
|
||||
expect(
|
||||
container.querySelector('[data-web-shell-workflow-chip]'),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it.each([
|
||||
|
||||
@@ -80,6 +80,7 @@ import {
|
||||
} from '../utils/composerTag';
|
||||
import { isSafeImageSrc } from './messages/Markdown';
|
||||
import { ModeIcon } from './ModeIcon';
|
||||
import { getWorkflowModeControlState } from './workflow-mode-control-state';
|
||||
import { planSlashSectionRows } from '../utils/slashSectionPlan';
|
||||
import { getModelDisplayName } from '../utils/modelDisplay';
|
||||
import { getContextUsageLevel } from '../utils/contextUsage';
|
||||
@@ -109,6 +110,7 @@ import { WorkspaceIndicator } from './WorkspaceIndicator';
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
ChevronRightIcon,
|
||||
GitBranchIcon,
|
||||
PanelBottomCloseIcon,
|
||||
Settings2Icon,
|
||||
FolderClosedIcon,
|
||||
@@ -1849,7 +1851,7 @@ export const ChatEditor = memo(
|
||||
editorTheme: CHAT_EDITOR_THEME,
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
const { t, language } = useI18n();
|
||||
|
||||
useImperativeHandle(ref, () => core.handle, [core.handle]);
|
||||
|
||||
@@ -2183,6 +2185,7 @@ export const ChatEditor = memo(
|
||||
}, [core.hasAttachments, onAttachmentsChange]);
|
||||
|
||||
const planDescriptionId = useId();
|
||||
const workflowChipDescriptionId = useId();
|
||||
const modeItems = useMemo<DropdownItem[]>(
|
||||
() =>
|
||||
EXECUTION_APPROVAL_MODES.filter((id) =>
|
||||
@@ -2349,6 +2352,7 @@ export const ChatEditor = memo(
|
||||
gitBranch: false,
|
||||
mode: false,
|
||||
plan: false,
|
||||
workflow: false,
|
||||
model: false,
|
||||
});
|
||||
const toolbarLabelVisibilityRef = useRef(toolbarLabelVisibility);
|
||||
@@ -2462,9 +2466,7 @@ export const ChatEditor = memo(
|
||||
// so it is handed on: to the input, or past a disabled one to a toolbar
|
||||
// control beside where the chip was.
|
||||
const planChipHadFocusRef = useRef(false);
|
||||
useLayoutEffect(() => {
|
||||
if (showPlanChip || !planChipHadFocusRef.current) return;
|
||||
planChipHadFocusRef.current = false;
|
||||
const handOffChipFocus = useCallback(() => {
|
||||
const toolbar = toolbarLeadingRef.current;
|
||||
// Focus that is anywhere but lost is somewhere the user put it.
|
||||
const doc = toolbar?.ownerDocument;
|
||||
@@ -2485,7 +2487,41 @@ export const ChatEditor = memo(
|
||||
'[data-web-shell-mode-button]:not(:disabled), [data-web-shell-model-button]',
|
||||
)
|
||||
?.focus();
|
||||
}, [showPlanChip, disabled, focusComposer]);
|
||||
}, [disabled, focusComposer]);
|
||||
useLayoutEffect(() => {
|
||||
if (showPlanChip || !planChipHadFocusRef.current) return;
|
||||
planChipHadFocusRef.current = false;
|
||||
handOffChipFocus();
|
||||
}, [showPlanChip, handOffChipFocus]);
|
||||
// Workflow, like Plan, is toggled from the add menu, so the toolbar shows
|
||||
// a dismissible chip while the saved preference is on — including while
|
||||
// it cannot take effect, since that is when turning it off matters most.
|
||||
const workflowControl =
|
||||
showAddMenuAction && showToolbarAction('workflowMode')
|
||||
? getWorkflowModeControlState(
|
||||
{
|
||||
workflowMode,
|
||||
workflowModeStatus,
|
||||
workflowModePending,
|
||||
onWorkflowModeChange,
|
||||
},
|
||||
language,
|
||||
)
|
||||
: null;
|
||||
const showWorkflowChip = Boolean(workflowControl?.mode?.requested);
|
||||
const workflowChipUnavailable =
|
||||
showWorkflowChip && workflowControl?.mode?.effective === false;
|
||||
const workflowChipTooltip = workflowControl
|
||||
? workflowControl.disabled
|
||||
? workflowControl.description
|
||||
: `${workflowControl.description} ${workflowControl.messages.clickToTurnOff}`
|
||||
: '';
|
||||
const workflowChipHadFocusRef = useRef(false);
|
||||
useLayoutEffect(() => {
|
||||
if (showWorkflowChip || !workflowChipHadFocusRef.current) return;
|
||||
workflowChipHadFocusRef.current = false;
|
||||
handOffChipFocus();
|
||||
}, [showWorkflowChip, handOffChipFocus]);
|
||||
const showModelAction = showToolbarAction('model');
|
||||
const showCommandAction = showToolbarAction('commands');
|
||||
const modelItems = useMemo<DropdownItem[]>(
|
||||
@@ -2729,6 +2765,7 @@ export const ChatEditor = memo(
|
||||
const showGitBranchLabel = isMobile || toolbarLabelVisibility.gitBranch;
|
||||
const showModeLabel = toolbarLabelVisibility.mode;
|
||||
const showPlanLabel = toolbarLabelVisibility.plan;
|
||||
const showWorkflowLabel = toolbarLabelVisibility.workflow;
|
||||
const showModelLabel = toolbarLabelVisibility.model;
|
||||
// One renderer per chip so the toolbar, the row under the composer, and the
|
||||
// mobile row share them: the toolbar drops a label when its width budget is
|
||||
@@ -2878,6 +2915,9 @@ export const ChatEditor = memo(
|
||||
...(showPlanToolbarControl
|
||||
? [{ id: 'plan', expansionWidth: expansionWidth('plan') }]
|
||||
: []),
|
||||
...(showWorkflowChip
|
||||
? [{ id: 'workflow', expansionWidth: expansionWidth('workflow') }]
|
||||
: []),
|
||||
...(showModelAction
|
||||
? [
|
||||
{
|
||||
@@ -2922,6 +2962,7 @@ export const ChatEditor = memo(
|
||||
gitBranch: itemVisibility.gitBranch ?? false,
|
||||
mode: itemVisibility.mode ?? false,
|
||||
plan: itemVisibility.plan ?? false,
|
||||
workflow: itemVisibility.workflow ?? false,
|
||||
model: itemVisibility.model ?? false,
|
||||
};
|
||||
const unchanged = Object.keys(next).every(
|
||||
@@ -2990,6 +3031,7 @@ export const ChatEditor = memo(
|
||||
showModelAction,
|
||||
showModeAction,
|
||||
showPlanToolbarControl,
|
||||
showWorkflowChip,
|
||||
workspaceChipInToolbar,
|
||||
workspaceIndicatorVisible,
|
||||
workspaceName,
|
||||
@@ -3263,6 +3305,57 @@ export const ChatEditor = memo(
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
{showWorkflowChip && workflowControl && (
|
||||
<TooltipProvider delayDuration={300}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className={`${styles.toolBtn} ${styles.planChip} ${styles.workflowChip}`}
|
||||
data-web-shell-workflow-chip
|
||||
data-workflow-state={workflowControl.status}
|
||||
data-unavailable={workflowChipUnavailable ? '' : undefined}
|
||||
data-labelled={showWorkflowLabel ? '' : undefined}
|
||||
aria-pressed
|
||||
aria-label={workflowControl.messages.label}
|
||||
aria-describedby={workflowChipDescriptionId}
|
||||
aria-busy={workflowControl.pending || undefined}
|
||||
// Not the native attribute, so focus survives the
|
||||
// pending write the chip's own click starts.
|
||||
aria-disabled={workflowControl.disabled || undefined}
|
||||
onFocus={() => {
|
||||
workflowChipHadFocusRef.current = true;
|
||||
}}
|
||||
onBlur={() => {
|
||||
workflowChipHadFocusRef.current = false;
|
||||
}}
|
||||
onClick={() => {
|
||||
if (workflowControl.disabled) return;
|
||||
onWorkflowModeChange?.(false);
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className={`${styles.toolBtnModeIcon} ${styles.planChipIcon}`}
|
||||
>
|
||||
<GitBranchIcon />
|
||||
<span className={styles.planChipClose} aria-hidden="true">
|
||||
<XIcon size={12} strokeWidth={2} />
|
||||
</span>
|
||||
</span>
|
||||
{showWorkflowLabel && (
|
||||
<span className={styles.toolBtnText}>Workflow</span>
|
||||
)}
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<span id={workflowChipDescriptionId} className="sr-only">
|
||||
{workflowChipTooltip}
|
||||
</span>
|
||||
<TooltipContent side="top">
|
||||
{workflowChipTooltip}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
{showPlanSwitch && (
|
||||
<TooltipProvider delayDuration={300}>
|
||||
<Tooltip>
|
||||
@@ -4451,6 +4544,27 @@ export const ChatEditor = memo(
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{showWorkflowChip && (
|
||||
<>
|
||||
<span
|
||||
data-toolbar-measure="workflow:collapsed"
|
||||
className={`${styles.toolBtn} ${styles.planChip}`}
|
||||
>
|
||||
<span className={styles.toolBtnModeIcon}>
|
||||
<GitBranchIcon />
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
data-toolbar-measure="workflow:expanded"
|
||||
className={`${styles.toolBtn} ${styles.planChip}`}
|
||||
>
|
||||
<span className={styles.toolBtnModeIcon}>
|
||||
<GitBranchIcon />
|
||||
</span>
|
||||
<span className={styles.toolBtnText}>Workflow</span>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
<span
|
||||
data-toolbar-measure="model:collapsed"
|
||||
className={`${styles.toolBtn} ${styles.modelToolBtn} ${styles.toolBtnCompact}`}
|
||||
|
||||
@@ -236,6 +236,11 @@ describe('AddMenu', () => {
|
||||
}),
|
||||
);
|
||||
await openMenu();
|
||||
expect(
|
||||
menuItem('composer-add-menu-workflow')!.querySelector(
|
||||
'[data-workflow-summary]',
|
||||
)?.textContent,
|
||||
).toBe('Let new tasks use workflows');
|
||||
await act(async () => menuItem('composer-add-menu-workflow')!.click());
|
||||
await settle();
|
||||
expect(onWorkflowModeChange).toHaveBeenCalledExactlyOnceWith(true);
|
||||
|
||||
@@ -1193,8 +1193,15 @@ export function AddMenu({
|
||||
}
|
||||
>
|
||||
<GitBranchIcon />
|
||||
<span className="min-w-0 flex-1 truncate">
|
||||
<span>Workflow</span>
|
||||
<span className="flex min-w-0 flex-1 flex-col">
|
||||
<span className="truncate">Workflow</span>
|
||||
<span
|
||||
className="hidden truncate text-xs text-muted-foreground sm:block"
|
||||
aria-hidden="true"
|
||||
data-workflow-summary
|
||||
>
|
||||
{workflowControl.summary}
|
||||
</span>
|
||||
<span id={workflowDescriptionId} className="sr-only">
|
||||
{workflowControl.description}
|
||||
</span>
|
||||
|
||||
@@ -74,6 +74,34 @@ describe('getWorkflowModeControlState', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[{ workflowModeStatus: 'ready' as const }, 'Let new tasks use workflows'],
|
||||
[
|
||||
{ workflowModeStatus: 'ready' as const, workflowModePending: true },
|
||||
'Updating…',
|
||||
],
|
||||
[{ workflowModeStatus: 'syncing' as const }, 'Syncing…'],
|
||||
])('summarizes an available mode in one line (%o)', (props, summary) => {
|
||||
expect(
|
||||
getWorkflowModeControlState(
|
||||
{ workflowMode: availableOff, ...props },
|
||||
'en',
|
||||
)?.summary,
|
||||
).toBe(summary);
|
||||
});
|
||||
|
||||
it('summarizes an unavailable saved preference by its state', () => {
|
||||
expect(
|
||||
getWorkflowModeControlState(
|
||||
{
|
||||
workflowModeStatus: 'ready',
|
||||
workflowMode: { requested: true, available: false, effective: false },
|
||||
},
|
||||
'zh-CN',
|
||||
)?.summary,
|
||||
).toBe('已开启,当前不可用');
|
||||
});
|
||||
|
||||
it('disables enabling an unavailable mode', () => {
|
||||
const control = getWorkflowModeControlState(
|
||||
{
|
||||
|
||||
@@ -61,5 +61,21 @@ export function getWorkflowModeControlState(
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
|
||||
return { mode, disabled, description, messages, status, pending };
|
||||
const short = messages.short;
|
||||
const summary =
|
||||
status === 'unbound'
|
||||
? short.unbound
|
||||
: pending
|
||||
? short.updating
|
||||
: status === 'syncing'
|
||||
? short.syncing
|
||||
: status === 'unknown' || !mode
|
||||
? short.unknown
|
||||
: mode.available
|
||||
? messages.menuDescription
|
||||
: mode.requested
|
||||
? short.onUnavailable
|
||||
: short.unavailable;
|
||||
|
||||
return { mode, disabled, description, summary, messages, status, pending };
|
||||
}
|
||||
@@ -3,6 +3,15 @@ import type { WebShellLanguage } from '../i18n';
|
||||
// Composer-only messages must not grow the read-only transcript renderer.
|
||||
const EN = {
|
||||
label: 'Workflow mode',
|
||||
menuDescription: 'Let new tasks use workflows',
|
||||
short: {
|
||||
unbound: 'Needs an active session',
|
||||
updating: 'Updating…',
|
||||
syncing: 'Syncing…',
|
||||
unknown: 'State not confirmed',
|
||||
onUnavailable: 'On, but unavailable now',
|
||||
unavailable: 'Unavailable now',
|
||||
},
|
||||
details: 'Workflow mode details',
|
||||
unbound: 'Create or resume a session to check availability.',
|
||||
updating: 'Updating Workflow mode…',
|
||||
@@ -18,10 +27,20 @@ const EN = {
|
||||
nextTurn:
|
||||
'Changes apply to the next fresh turn without interrupting the current turn or changing effort.',
|
||||
updateFailed: 'Failed to update Workflow mode',
|
||||
clickToTurnOff: 'Click to turn off.',
|
||||
};
|
||||
|
||||
const ZH: typeof EN = {
|
||||
label: 'Workflow 模式',
|
||||
menuDescription: '新任务可交给 workflow 执行',
|
||||
short: {
|
||||
unbound: '需要先有会话',
|
||||
updating: '正在更新…',
|
||||
syncing: '正在同步…',
|
||||
unknown: '状态未确认',
|
||||
onUnavailable: '已开启,当前不可用',
|
||||
unavailable: '当前不可用',
|
||||
},
|
||||
details: 'Workflow 模式详情',
|
||||
unbound: '创建或恢复会话后查看可用性。',
|
||||
updating: '正在更新 Workflow 模式…',
|
||||
@@ -36,6 +55,7 @@ const ZH: typeof EN = {
|
||||
unsupported: '此会话不支持 Workflow 模式。',
|
||||
nextTurn: '更改从下一轮新输入开始生效,不会中断当前轮次或修改思考强度。',
|
||||
updateFailed: '更新 Workflow 模式失败',
|
||||
clickToTurnOff: '点击关闭。',
|
||||
};
|
||||
|
||||
export function getWorkflowModeMessages(language: WebShellLanguage) {
|
||||
|
||||
@@ -195,13 +195,11 @@ test('preserves requested on when unavailable, permits off, and clears omitted s
|
||||
await expect(toggle).toHaveAccessibleDescription(
|
||||
/Workflow mode is on, but currently unavailable\./,
|
||||
);
|
||||
const rowBounds = await toggle.boundingBox();
|
||||
const referenceBounds = await page
|
||||
.getByTestId('composer-add-menu-reference-file')
|
||||
.boundingBox();
|
||||
expect(rowBounds).not.toBeNull();
|
||||
expect(referenceBounds).not.toBeNull();
|
||||
expect(rowBounds!.height).toBeLessThanOrEqual(referenceBounds!.height + 1);
|
||||
const summary = toggle.locator('[data-workflow-summary]');
|
||||
await expect(summary).toHaveText('On, but unavailable now');
|
||||
const summaryBounds = await summary.boundingBox();
|
||||
expect(summaryBounds).not.toBeNull();
|
||||
expect(summaryBounds!.height).toBeLessThanOrEqual(17);
|
||||
await page.screenshot({
|
||||
path: testInfo.outputPath('workflow-unavailable-on.png'),
|
||||
});
|
||||
@@ -218,6 +216,58 @@ test('preserves requested on when unavailable, permits off, and clears omitted s
|
||||
]);
|
||||
});
|
||||
|
||||
test('shows a dismissible toolbar chip while Workflow is requested @smoke', async ({
|
||||
page,
|
||||
baseURL,
|
||||
}, testInfo) => {
|
||||
const scenario = createWebShellDaemonScenario({
|
||||
state: { configOptions: [workflowOption()] },
|
||||
});
|
||||
const daemon = await installMockDaemon(page, scenario, { baseURL });
|
||||
await openSession(page, daemon);
|
||||
const chip = page.locator('[data-web-shell-workflow-chip]');
|
||||
await expect(chip).toHaveCount(0);
|
||||
await (await modeSwitch(page)).click();
|
||||
await expect(chip).toBeVisible();
|
||||
await expect(chip).toHaveAttribute('aria-pressed', 'true');
|
||||
await expect(chip).not.toHaveAttribute('data-unavailable', /.*/);
|
||||
await chip.hover();
|
||||
await expect(page.getByRole('tooltip')).toContainText('Click to turn off.');
|
||||
await page.screenshot({ path: testInfo.outputPath('workflow-chip-on.png') });
|
||||
|
||||
await chip.click();
|
||||
await expect(chip).toHaveCount(0);
|
||||
expect(daemon.configOptionRequests().map((request) => request.body)).toEqual([
|
||||
{ configId: 'workflow_mode', value: 'on' },
|
||||
{ configId: 'workflow_mode', value: 'off' },
|
||||
]);
|
||||
expect(daemon.promptRequests()).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('marks the chip while the requested mode is unavailable', async ({
|
||||
page,
|
||||
baseURL,
|
||||
}, testInfo) => {
|
||||
const scenario = createWebShellDaemonScenario({
|
||||
state: { configOptions: [workflowOption(true, false)] },
|
||||
});
|
||||
const daemon = await installMockDaemon(page, scenario, { baseURL });
|
||||
await openSession(page, daemon);
|
||||
const chip = page.locator('[data-web-shell-workflow-chip]');
|
||||
await expect(chip).toHaveAttribute('data-unavailable', '');
|
||||
await expect(chip).toHaveAccessibleDescription(
|
||||
/Workflow mode is on, but currently unavailable\./,
|
||||
);
|
||||
await page.screenshot({
|
||||
path: testInfo.outputPath('workflow-chip-unavailable.png'),
|
||||
});
|
||||
await chip.click();
|
||||
await expect(chip).toHaveCount(0);
|
||||
expect(daemon.configOptionRequests().map((request) => request.body)).toEqual([
|
||||
{ configId: 'workflow_mode', value: 'off' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('a live config push beats an already pending context response', async ({
|
||||
page,
|
||||
baseURL,
|
||||
|
||||
Reference in new issue
Block a user