-
Notifications
You must be signed in to change notification settings - Fork 2
feat(ui): group tool calls into trow summaries #874
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 13 commits
Commits
Show all changes
28 commits
Select commit
Hold shift + click to select a range
acabc10
feat(ui): group tool calls into trow summaries
Astro-Han 2471b03
fix(ui): stabilize trow tool summaries
Astro-Han 381110a
test(ui): expand trow snap coverage
Astro-Han 39c23db
fix(ui): render single tools in trow style
Astro-Han 51b4752
fix(ui): tighten trow tool output spacing
Astro-Han d12fc2c
fix(ui): tighten collapsed trow text spacing
Astro-Han 7519b8d
fix(ui): align registered trow tools
Astro-Han f6721ec
fix(ui): make trow tool text selectable
Astro-Han 310fe38
fix(ui): label skill tool rows
Astro-Han 2cced2e
fix(ui): address trow review feedback
Astro-Han 7697582
fix(app): update trow perf expand probe
Astro-Han e7f2e6a
fix(app): make trow perf probe base compatible
Astro-Han 0a700f5
fix(app): preserve legacy perf trigger fallback
Astro-Han 66ba767
fix(app): make trow perf toggles idempotent
Astro-Han 04ad409
fix(ui): tighten trow visual rhythm
Astro-Han 76ca7b6
fix(ui): align nested trow expand affordance
Astro-Han e6c9f93
fix(ui): tighten expanded trow grouping
Astro-Han 3055b5c
fix(ui): scope active trow status
Astro-Han 317f88a
fix(ui): align question tool in trow groups
Astro-Han 518784b
fix(ui): respect tool default-open settings in trows
Astro-Han 6deef10
feat(ui): summarize trow tool activity
Astro-Han fed6d07
Merge remote-tracking branch 'origin/dev' into codex/session-trow-rev…
Astro-Han b56efcb
fix(ui): polish trow single-tool rows
Astro-Han 3710829
fix(ui): align single trow typography
Astro-Han 3f53427
fix(ui): align trow error rows
Astro-Han f9e6874
fix(ui): expose expandable trow details
Astro-Han 437f7a3
fix(ui): reveal dismissed question details
Astro-Han 27b112f
refactor(test): split trow snap fixture
Astro-Han File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,150 @@ | ||
| import { expect, type Locator } from "@playwright/test" | ||
| import { fileURLToPath } from "node:url" | ||
| import { test } from "../fixtures" | ||
| import { composeGrid, snapOutputPath, type Shot } from "./_compose" | ||
|
|
||
| test.use({ viewport: { width: 900, height: 560 }, deviceScaleFactor: 2 }) | ||
|
|
||
| const LANGUAGE_KEY = "pawwork.global.dat:language" | ||
| const fixturePath = fileURLToPath(new URL("../../src/testing/trow-snap-fixture.tsx", import.meta.url)) | ||
| async function captureBlock(name: string, block: Locator): Promise<Shot> { | ||
| await expect(block).toBeVisible({ timeout: 30_000 }) | ||
| return { name, buf: await block.screenshot() } | ||
| } | ||
|
|
||
| async function waitForThemeBoot(page: import("@playwright/test").Page): Promise<void> { | ||
| await page.waitForFunction( | ||
| () => getComputedStyle(document.documentElement).getPropertyValue("--bg-base").trim().length > 0, | ||
| null, | ||
| { timeout: 30_000 }, | ||
| ) | ||
| } | ||
|
|
||
| test("session-trow", async ({ page }) => { | ||
| test.setTimeout(180_000) | ||
|
|
||
| await page.addInitScript((key) => { | ||
| localStorage.setItem(key, JSON.stringify({ locale: "zh" })) | ||
| }, LANGUAGE_KEY) | ||
|
|
||
| await page.goto("/") | ||
| await waitForThemeBoot(page) | ||
| await page.evaluate(async (path) => { | ||
| const mod = await import(path) | ||
| mod.mountTrowSnapFixture(document.body) | ||
| }, `/@fs/${fixturePath}`) | ||
|
|
||
| const shots: Shot[] = [] | ||
| const running = page.locator('[data-snap="running-current"]') | ||
| await expect(running).toContainText("执行命令 third command", { timeout: 30_000 }) | ||
| shots.push(await captureBlock("running-current", running)) | ||
|
|
||
| shots.push(await captureBlock("mixed-collapsed", page.locator('[data-snap="mixed-collapsed"]'))) | ||
|
|
||
| const collapsedFollowedByText = page.locator('[data-snap="collapsed-followed-by-text"]') | ||
| const collapsedTextGap = await collapsedFollowedByText.evaluate((root) => { | ||
| const trow = root.querySelector<HTMLElement>('[data-component="session-turn-trow-block"]') | ||
| const text = root.querySelector<HTMLElement>('[data-component="text-part"]') | ||
| if (!trow || !text) return Number.NaN | ||
| return text.getBoundingClientRect().top - trow.getBoundingClientRect().bottom | ||
| }) | ||
| expect(collapsedTextGap).toBeGreaterThanOrEqual(0) | ||
| expect(collapsedTextGap).toBeLessThanOrEqual(12) | ||
| shots.push(await captureBlock("collapsed-followed-by-text", collapsedFollowedByText)) | ||
|
|
||
| shots.push(await captureBlock("mixed-expanded", page.locator('[data-snap="mixed-expanded"]'))) | ||
| await expect(page.locator('[data-snap="inner-bash-expanded"] [data-component="bash-output"]')).toBeVisible({ | ||
| timeout: 30_000, | ||
| }) | ||
| shots.push(await captureBlock("inner-bash-expanded", page.locator('[data-snap="inner-bash-expanded"]'))) | ||
|
|
||
| const toolOutputSpacing = page.locator('[data-snap="tool-output-spacing"]') | ||
| await expect(toolOutputSpacing.locator('[data-component="tool-output"]')).toBeVisible({ timeout: 30_000 }) | ||
| const toolOutputMetrics = await toolOutputSpacing.evaluate((root) => { | ||
| const output = root.querySelector<HTMLElement>('[data-component="tool-output"]') | ||
| const lastContent = | ||
| output?.querySelector<HTMLElement>('[data-component="markdown"] > :last-child') ?? | ||
| output?.querySelector<HTMLElement>("pre") ?? | ||
| output | ||
| const triggers = root.querySelectorAll<HTMLElement>('[data-slot="collapsible-trigger"]') | ||
| const nextTrigger = triggers[1] | ||
| if (!output || !lastContent || !nextTrigger) return { outputGap: Number.NaN, contentGap: Number.NaN } | ||
| return { | ||
| outputGap: nextTrigger.getBoundingClientRect().top - output.getBoundingClientRect().bottom, | ||
| contentGap: nextTrigger.getBoundingClientRect().top - lastContent.getBoundingClientRect().bottom, | ||
| } | ||
| }) | ||
| expect(toolOutputMetrics.outputGap).toBeGreaterThanOrEqual(0) | ||
| expect(toolOutputMetrics.outputGap).toBeLessThanOrEqual(8) | ||
| expect(toolOutputMetrics.contentGap).toBeGreaterThanOrEqual(0) | ||
| expect(toolOutputMetrics.contentGap).toBeLessThanOrEqual(8) | ||
| const toolOutputUserSelect = await toolOutputSpacing.evaluate((root) => { | ||
| const summary = root.querySelector<HTMLElement>('[data-slot="trow-summary"]') | ||
| const trigger = root.querySelector<HTMLElement>('[data-slot="collapsible-trigger"]') | ||
| const output = root.querySelector<HTMLElement>('[data-component="tool-output"]') | ||
| return { | ||
| summary: summary ? getComputedStyle(summary).userSelect : "", | ||
| trigger: trigger ? getComputedStyle(trigger).userSelect : "", | ||
| output: output ? getComputedStyle(output).userSelect : "", | ||
| } | ||
| }) | ||
| expect(toolOutputUserSelect.summary).toBe("text") | ||
| expect(toolOutputUserSelect.trigger).toBe("text") | ||
| expect(toolOutputUserSelect.output).toBe("text") | ||
| shots.push(await captureBlock("tool-output-spacing", toolOutputSpacing)) | ||
|
|
||
| const registeredToolRows = page.locator('[data-snap="registered-tool-rows"]') | ||
| await expect(registeredToolRows).toContainText("网络搜索", { timeout: 30_000 }) | ||
| await expect(registeredToolRows).toContainText("进入工作树", { timeout: 30_000 }) | ||
| await expect(registeredToolRows).toContainText("使用技能", { timeout: 30_000 }) | ||
| await expect(registeredToolRows).toContainText("learn-code", { timeout: 30_000 }) | ||
| const registeredMetrics = await registeredToolRows.evaluate((root) => { | ||
| const titleSelectors = ['[data-slot="basic-tool-tool-title"]', '[data-component="task-tool-title"]'] | ||
| const titles = titleSelectors.flatMap((selector) => | ||
| Array.from(root.querySelectorAll<HTMLElement>(selector), (el) => ({ | ||
| text: el.textContent?.trim() ?? "", | ||
| left: el.getBoundingClientRect().left, | ||
| })), | ||
| ) | ||
| const output = root.querySelector<HTMLElement>('[data-component="exa-tool-output"]') | ||
| const triggers = Array.from(root.querySelectorAll<HTMLElement>('[data-slot="collapsible-trigger"]')) | ||
| const nextTrigger = triggers[1] | ||
| return { | ||
| titleLefts: titles.filter((item) => item.text).map((item) => item.left), | ||
| webSearchOutputGap: | ||
| output && nextTrigger ? nextTrigger.getBoundingClientRect().top - output.getBoundingClientRect().bottom : Number.NaN, | ||
| } | ||
| }) | ||
| expect(registeredMetrics.titleLefts.length).toBeGreaterThanOrEqual(5) | ||
| expect(Math.max(...registeredMetrics.titleLefts) - Math.min(...registeredMetrics.titleLefts)).toBeLessThanOrEqual(1) | ||
| expect(registeredMetrics.webSearchOutputGap).toBeGreaterThanOrEqual(0) | ||
| expect(registeredMetrics.webSearchOutputGap).toBeLessThanOrEqual(8) | ||
| shots.push(await captureBlock("registered-tool-rows", registeredToolRows)) | ||
|
|
||
| const singleDirect = page.locator('[data-snap="single-command-direct"]') | ||
| await expect(singleDirect.locator('[data-component="session-turn-trow-block"][data-single]')).toBeVisible({ | ||
| timeout: 30_000, | ||
| }) | ||
| await expect(singleDirect.locator('[data-component="bash-output"]')).toBeVisible({ timeout: 30_000 }) | ||
| shots.push(await captureBlock("single-command-direct", singleDirect)) | ||
|
|
||
| const singleExpanded = page.locator('[data-snap="single-command-expanded"]') | ||
| await expect(singleExpanded.locator('[data-component="session-turn-trow-block"][data-single]')).toBeVisible({ | ||
| timeout: 30_000, | ||
| }) | ||
| await expect(singleExpanded.locator('[data-component="bash-output"]')).toBeVisible({ | ||
| timeout: 30_000, | ||
| }) | ||
| shots.push(await captureBlock("single-command-expanded", singleExpanded)) | ||
|
|
||
| const singleRunning = page.locator('[data-snap="single-command-running"]') | ||
| await expect(singleRunning.locator('[data-component="session-turn-trow-block"][data-single]')).toBeVisible({ | ||
| timeout: 30_000, | ||
| }) | ||
| await expect(singleRunning).toContainText("执行命令", { timeout: 30_000 }) | ||
| shots.push(await captureBlock("single-command-running", singleRunning)) | ||
|
|
||
| const out = snapOutputPath("session-trow") | ||
| await composeGrid(shots, out, { cols: 2 }) | ||
| process.stdout.write(`\n[snap] session-trow grid -> ${out}\n\n`) | ||
| }) |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.