- {!draftRevision && !text.trim() &&
}
+
+ {!draftRevision && !text.trim() && (definition?.shortcuts == null
+ ?
+ : definition.shortcuts.map((shortcut, index) => ))}
= {
comparison: { kind: 'comparison', title: '哪一种更像你想要的小狗?', columns: [{ id: 'click', label: '点了才回应' }, { id: 'active', label: '会主动找我' }], rows: [{ id: 'home', label: '刚回到家的时候', cells: [{ columnId: 'click', text: '等我点一下,才跳起来。' }, { columnId: 'active', text: '看见我就跑过来,还带着玩具。' }] }, { id: 'busy', label: '我忙着种东西的时候', cells: [{ columnId: 'click', text: '安静等着,不打扰我。' }, { columnId: 'active', text: '有时来蹭一蹭,可能也会打断我。' }] }] },
};
const scenario = new URLSearchParams(location.search).get('kind') ?? 'ideas';
+if (scenario === 'shortcuts') definition.shortcuts = [
+ { label: '看看进展', prompt: '请结合当前项目,分析我最近完成的工作。' },
+ { label: '解释代码', prompt: '请解释当前项目最近的代码改动,用一个具体例子帮助我理解。' },
+ ...Array.from({ length: 6 }, (_, i) => ({ label: `入口${i + 3}:看看我的项目进展,帮我理解当前代码并提出下一步的编程建议`, prompt: `请讨论第 ${i + 3} 个主题。` })),
+];
+if (scenario === 'shortcuts-empty') definition.shortcuts = [];
export const fixtureAgent = { teacher_id: definition.teacher_id, version: 1, is_default: true, definition };
const continuous = new URLSearchParams(location.search).get('chat') !== 'legacy';
let current: TeacherTopic = { schemaVersion: 1, revision: 1, id: 'discussion-fixture', accountId: 'fixture', projectId: 'fixture', sourceConversationId: 'project', definition, version: 1, createdAt: 'now', updatedAt: 'now', requests: Array.from({ length: 8 }, (_, i) => ({ id: `r${i}`, text: i === 7 ? '我还没想好,我们边聊边想。' : '我希望这个游戏更有意思。', references: [], projectId: 'fixture', sourceConversationId: 'source', teacherVersion: 1, createdAt: `2026-09-28T10:00:${String(i).padStart(2, '0')}Z`, sourceCursor: { workerGeneration: 0, seq: i }, sourceCapturedAt: 'now', includedSourceMessageIds: [], omittedMessages: 0, status: 'completed', response: i === 7 ? '可以,我们先把已经想到的放在一起。哪里还不确定,就留着继续聊。' : '先从你最在意的一个小地方开始想。', ...(i === 7 ? { suggestedQuestions: ['有什么好玩的想法?'] } : {}) })), ...(samples[scenario] ? { discussion: { id: 'tool', revision: 1, status: 'active', content: structuredClone(samples[scenario]) } } : {}) };
diff --git a/tests/e2e/teacher-reply-layout.spec.ts b/tests/e2e/teacher-reply-layout.spec.ts
index e6fbebe1..2668402d 100644
--- a/tests/e2e/teacher-reply-layout.spec.ts
+++ b/tests/e2e/teacher-reply-layout.spec.ts
@@ -47,6 +47,35 @@ test('retry stickers stay readable and preserve drafts in a 319px consultation',
});
for (const width of [319, 508]) {
+ test(`published shortcuts send their prompts and keep eight long labels scrollable at ${width}px`, async ({ page }) => {
+ await page.setViewportSize({ width: 1180, height: 800 });
+ await page.goto(`${url}?kind=shortcuts&width=${width}`);
+ const panel = page.getByTestId('teacher-chat-panel');
+ const shortcuts = page.locator('form .consultation-hint');
+ await expect(shortcuts).toHaveCount(8);
+ await expect(shortcuts.first()).toHaveText('看看进展');
+ await expect(shortcuts.nth(1)).toHaveText('解释代码');
+ await expect(shortcuts.nth(1)).toBeEnabled();
+ const form = (await panel.locator('form').boundingBox())!;
+ expect(form.y + form.height).toBeLessThanOrEqual(800);
+ expect(await panel.evaluate(el => el.scrollWidth <= el.clientWidth)).toBe(true);
+ await shortcuts.last().scrollIntoViewIfNeeded();
+ await panel.screenshot({ path: test.info().outputPath(`shortcuts-${width}.png`) });
+ await shortcuts.last().click();
+ await expect(page.getByText('请讨论第 8 个主题。', { exact: true })).toBeVisible();
+ await expect(page.getByText('好,我们就从这一个问题聊起。')).toBeVisible();
+ await shortcuts.nth(1).click();
+ await expect(page.getByText('请解释当前项目最近的代码改动,用一个具体例子帮助我理解。', { exact: true })).toBeVisible();
+ const inputs = await page.evaluate(() => (window as unknown as { __teacherReplyInputs: unknown[] }).__teacherReplyInputs);
+ expect(inputs).toEqual([
+ { requestId: expect.any(String), text: '请讨论第 8 个主题。', references: [], projectId: 'fixture', sourceConversationId: 'source' },
+ { requestId: expect.any(String), text: '请解释当前项目最近的代码改动,用一个具体例子帮助我理解。', references: [], projectId: 'fixture', sourceConversationId: 'source' },
+ ]);
+ await page.goto(`${url}?kind=shortcuts-empty&width=${width}`);
+ await expect(page.getByRole('textbox', { name: '向智能体提问' })).toBeEnabled();
+ await expect(page.locator('form .consultation-hint')).toHaveCount(0);
+ });
+
for (const chat of ['legacy', 'continuous']) test(`quoted ${chat} replies recover fully and incomplete replies retry without losing the draft at ${width}px`, async ({ page }) => {
await page.setViewportSize({ width: 1180, height: 800 });
const sent = () => page.evaluate(() => (window as unknown as { __teacherReplyInputs: unknown[] }).__teacherReplyInputs);
diff --git a/tests/unit/coding-teacher-ui.test.tsx b/tests/unit/coding-teacher-ui.test.tsx
index 86d6e345..1a233c5a 100644
--- a/tests/unit/coding-teacher-ui.test.tsx
+++ b/tests/unit/coding-teacher-ui.test.tsx
@@ -101,6 +101,56 @@ async function ready() {
await waitFor(() => expect(streams.has('first')).toBe(true));
}
describe('teacher side chat', () => {
+ it('renders published shortcut labels in order and sends each actual prompt with independent retry identity', async () => {
+ const shortcuts = [{ label: '看看进展', prompt: '请分析当前项目的进展。' }, { label: '解释代码', prompt: '请解释刚才的代码改动。' }];
+ const agent = { teacher_id: 'shortcuts', version: 1, is_default: true, definition: { ...definition, config_id: 'shortcuts', shortcuts } };
+ api.catalog.mockResolvedValue({ items: [agent] });
+ api.sendConversation.mockRejectedValue(new Error('结果暂未确认'));
+ render();
+ await waitFor(() => expect(screen.getByRole('button', { name: '看看进展' })).toBeEnabled());
+ expect([...screen.getByTestId('consultation-composer-actions').querySelectorAll('.consultation-hint')].map(el => el.textContent)).toEqual(shortcuts.map(item => item.label));
+ expect(screen.queryByRole('button', { name: '继续看看👀' })).not.toBeInTheDocument();
+ expect(screen.queryByText(shortcuts[0].prompt)).not.toBeInTheDocument();
+ fireEvent.click(screen.getByRole('button', { name: '看看进展' }));
+ await screen.findByText('结果暂未确认');
+ const original = api.sendConversation.mock.calls[0][2];
+ expect(original).toMatchObject({ text: shortcuts[0].prompt, projectId: 'p', sourceConversationId: 'c', references: [] });
+ expect(original.intent).toBeUndefined();
+ fireEvent.click(screen.getByRole('button', { name: '看看进展' }));
+ await waitFor(() => expect(api.sendConversation).toHaveBeenCalledTimes(2));
+ await waitFor(() => expect(screen.getByRole('button', { name: '解释代码' })).toBeEnabled());
+ expect(api.sendConversation.mock.calls[1][2]).toEqual(original);
+ fireEvent.click(screen.getByRole('button', { name: '解释代码' }));
+ await waitFor(() => expect(api.sendConversation).toHaveBeenCalledTimes(3));
+ expect(api.sendConversation.mock.calls[2][2].text).toBe(shortcuts[1].prompt);
+ expect(api.sendConversation.mock.calls[2][2].requestId).not.toBe(original.requestId);
+ });
+
+ it('updates published shortcuts without replacing chat history or a composed draft, and supports clearing the list', async () => {
+ const old = { teacher_id: 'shortcut-refresh', version: 1, is_default: true, definition: { ...definition, config_id: 'shortcut-refresh', shortcuts: [{ label: '旧入口', prompt: '旧提示词' }] } };
+ const conversation = { ...topic('shortcut-chat'), definition: old.definition,
+ conversation: { agentId: old.teacher_id, segmentTurns: 1, discussions: {}, seenThrough: 'past' },
+ requests: [request({ id: 'past', text: '之前的问题', response: '之前的回答' })] };
+ api.conversation.mockResolvedValue({ topic: conversation, before: null });
+ const state = (agent: typeof old) => ({ catalog: { revision: agent.version, items: [agent] }, config: { enabled: true, published_version: agent.version, revision: agent.version, definition: agent.definition } });
+ const view = render();
+ await screen.findByText('之前的回答');
+ await waitFor(() => expect(screen.getByRole('button', { name: '旧入口' })).toBeEnabled());
+ fireEvent.change(screen.getByLabelText('向智能体提问'), { target: { value: '尚未发出的草稿' } });
+ const updated = { ...old, version: 2, definition: { ...old.definition, shortcuts: [{ label: '新入口', prompt: '新的实际提示词' }] } };
+ view.rerender();
+ expect(screen.getByLabelText('向智能体提问')).toHaveValue('尚未发出的草稿');
+ expect(screen.queryByRole('button', { name: '新入口' })).not.toBeInTheDocument();
+ fireEvent.change(screen.getByLabelText('向智能体提问'), { target: { value: '' } });
+ expect(screen.getByRole('button', { name: '新入口' })).toBeVisible();
+ expect(screen.queryByRole('button', { name: '旧入口' })).not.toBeInTheDocument();
+ const cleared = { ...updated, version: 3, definition: { ...updated.definition, shortcuts: [] } };
+ view.rerender();
+ expect(screen.getByTestId('consultation-composer-actions').querySelectorAll('.consultation-hint')).toHaveLength(0);
+ expect(screen.getByText('之前的回答')).toBeVisible();
+ expect(api.sendConversation).not.toHaveBeenCalled();
+ });
+
it('opens directly into the consultation and puts new topics inside expanded history', async () => {
render();
const history = await screen.findByRole('button', { name: '以往讨论' });