The Gitea repo was initialised from an earlier snapshot, so main and the
machine this runs on had drifted apart in 153 files before any UI work
started. This commit brings the branch up to the local tree as it stood
on 2026-08-15 21:31 (from cowork_local.7z), so the redesign that follows
shows up as its own reviewable diff instead of being mixed in with the
pre-existing divergence.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Implements the redesign from docs/ui-audit.html. Rearrangement only — no
feature was removed; every control that moved kept its handler, and the
gates that used to hide things now grey them out instead.
Navigation
* The rail is one flat list: the five Workspace sub-views sit at the top
level instead of behind an accordion, with Dashboard/Monitoring pinned
at the foot and Settings below them.
* Cowork and GraphRAG stay listed and greyed while no project is
selected, rather than vanishing and resizing the menu under the user.
* Monitoring keeps its eight sub-views in its own tab strip (unhidden)
instead of doubling the rail's length.
* _goto now moves the highlight itself, fixing a long-standing bug where
programmatic navigation left the rail pointing at the previous screen.
* Rail header gained the project picker and "New chat"; RECENTS lists the
active project's threads. Both are second views of existing state — the
Cowork toolbar button and the full History panel are untouched.
* Provider / language / theme moved from the top bar to an account row at
the foot of the rail (same widgets, same signals).
Screens
* Co4E: the flow tab strip is gone (per the design); Flow Status became a
toolbar toggle with its own way back, and the three icon-only tabs became
four labelled, foldable sections in one column. One flow open at a time
is the one capability this costs; background runs are unaffected.
* Dashboard: header split into two rows; cost promoted to a hero card.
* Monitoring Overview: one scrolling column of titled sections; the model
price table got its own full-width section instead of sharing a row with
the CPU meters.
* Settings and Task editor gained a section index down the left.
* Help dock: 84x64 launcher + chevron became one 26px dot that expands to
a labelled pill on hover; "hide to the edge" moved into the panel's menu.
Layout
* The window's minimum width dropped from 1453px to 768px. The main cause
was a QTabWidget taking its minimum from the widest page even when that
page is hidden, so Co4E was forcing Project and Cowork wide.
* Secondary panes fold themselves on a narrow window and restore when it
grows, never overriding a fold the user made.
* The long dialogs no longer scroll sideways at any font size.
Verification: tools/check_*.py build a real MainWindow offscreen against a
copy of ~/.cowork_local with the schedulers no-oped. check_design_parity.py
reads its checklist straight from the audit page's own proposals.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
check_design_parity.py reads its checklist from the audit page's own
proposals; it now reports every one of the 31 as implemented, with no
deliberate divergences left.
* Schedule: the Kanban/Calendar drop-list became a pair of tabs, and the
Running lane is outlined while it holds anything — dropping a card there
starts the task for real, so it should not look like the other six.
* Cowork: agent / routing / usage / folder moved out of the typing box into
their own status strip beneath it, styled as status rather than a second
toolbar. All of them stay interactive; the design's read-only strip would
have cost features.
* Folder: the path is written as the screen's title instead of sitting in a
read-only text box that looked editable and cost a row.
* GraphRAG: the second toolbar row is gone (Export joined the first), and
the one button that relabelled itself became Đồ thị | Tin nhắn tabs, so
the view you are NOT in is named too.
* Settings gained the theme picker, so language / provider / theme are all
reachable there as well as on the rail's account row.
* Task editor: the five group boxes are grouped into three step tabs
(Nội dung → Lịch chạy → Liên kết). All 22 fields verified present after
the move; only the old section index is gone, replaced by the tabs.
* The assistant dot now clears a screen's own bottom bar (Cowork's
composer), measured from the composer's top edge in window coordinates.
Also adds .gitattributes: without it a Windows checkout records CRLF and
every file reads as fully rewritten to a Linux CI runner.
Verification: 7 check_*.py suites green, no screen clipped at 1920/1366/1280,
and no dialog scrolls sideways at 9/11/14pt.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
check_design_parity read its checklist from build_audit_page.ANALYSIS. Eight
sections of the audit page are hand-written, and for those the generator's
text is NOT what the page says — so Settings and the Task editor were reported
as matching a design they had never been compared against. The Task editor was
in fact built backwards.
* The checker now reads the "Thay đổi" bullets straight out of
docs/ui-audit.html, and reports how many bullets on the page still have no
probe (57 on the page, 32 probed) instead of implying full coverage.
* Task editor: reverted from three step tabs to a left list + right panel,
five rows matching the five real group boxes — which is what the page asks
for, in as many words ("thay vì chia tab"), for consistency with Settings.
* Settings now switches panels rather than scrolling, so both dialogs are
navigated identically and neither is a long scroll any more.
* Settings field presentation, as the page's second bullet asks: the six
checkboxes became switches, and Language/Theme became segmented controls.
ToggleSwitch subclasses QCheckBox and SegmentedControl exposes the slice
of the QComboBox API this dialog uses, so no save/load path changed —
verified by round-tripping language/theme/tray through _save().
All 22 task-editor fields and 14 settings fields verified present after the
move; 7 suites green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Five rounds, each looking from an angle the previous one cannot:
1. text — every "Thay đổi" bullet on the page vs a probe (existing)
2. geometry — check_layout_geometry.py: reading order of the rail, section
order down Monitoring, which side each pane is on, and the size
relationships the design names (hero card, 26px dot)
3. inventory— check_controls_alive.py: every control in controls.json still
present in the built app, attributed to its owning CLASS via the
baseline commit (a file holds several classes, so a per-file
check reported eight dialog controls as missing)
4. eyes — rendered screens read against the wireframes
5. adversarial — check_probes_bite.py: break one feature at a time and fail if
the matching check still passes
What rounds 4 and 5 caught, which 1-3 could not:
* Schedule showed six of seven lanes; the seventh needed a horizontal
scroll. The design says "giữ đủ 7 lane, thu hẹp cho vừa một màn". Lane
minimum width 190 → 150, so 7 × 150 + gaps fits a 1280 window. Round 2 now
measures this instead of relying on someone noticing.
* check_design_parity ALWAYS returned 0. It was a report, not a check: every
probe in it was incapable of failing, so a regression would print on screen
and still exit green. It now exits non-zero when anything is CHUA — which
is what let round 5 detect the two mutations it had been sleeping through.
Also fixed in the harness itself: it rewrote line endings while restoring
mutated files (read_text/write_text translate both ways), and it compared the
tree against "clean" rather than against its own starting state.
All ten checkers green by exit code.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two things differ between machines and only one of them is width: a 4K panel
has more pixels, while a 125%/150% display has the same logical pixels holding
LESS, because every label and margin is taller. Breakpoints written as raw
pixels only hold on the machine they were tuned on.
* ui_scale() derives a factor from font height (1.0 at the 15px line the
layouts were measured against) and every narrow-guard threshold is
multiplied by it, so panes fold when the content is cramped rather than
when a number is crossed.
* The window takes a share of the available screen (80% × 85%) with the old
1180×760 as the floor, instead of opening at that size on any monitor.
* Moving the window to another screen re-pins the assistant and re-decides
the fold, since the new screen's work area and scaling may differ.
Found by tools/check_multi_screen.py, which walks 5 window sizes × 3 font
scales:
* At 150%, Schedule was clipped on 1280 and 1366 screens and the window's
own minimum grew to 1459px — wider than a 1280 laptop, so the app could
not fit at all. The cause was not the lanes: the one-line lane-count
summary in the header reported a sizeHint wide enough to set the minimum
width of the entire window. It now yields first (its text stays in the
tooltip); the window minimum drops 1459 → 752 and holds there at every
scale.
Also: these checkers exited 0xC0000409 from a Qt teardown crash AFTER printing
their verdict. check_probes_bite decides whether a probe caught its mutation by
reading exit codes, so a crash would have counted as "caught" — the round could
have passed while proving nothing. They now flush and os._exit with the real
verdict, and round 5 still catches all six mutations.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two problems, both only visible at the window size this machine actually opens
(1024×571 on a 1280×672 work area):
* The rail drew the "GẦN ĐÂY" heading on top of the last nav row. nav and the
bottom group have fixed heights, so a short window put the entire squeeze on
RECENTS; once that hit zero the layout had nowhere left to give. The
destinations and RECENTS now scroll together inside their own area, while
the bottom group, Settings and the account row stay pinned.
* Overview did not look like the drawing. Fixed, in the order the page draws
them: sections are quiet caps headings with flat content instead of six
bordered boxes; the four resource metrics sit ACROSS one row rather than
stacked four deep; and "Hoạt động gần đây" moved from second place to the
bottom beside the audit log, so the reading order is cost → machine →
permissions → prices → what happened. At 1280×760 the page now reaches the
price table on first screen; before it showed one and a half sections.
Two deliberate differences from the drawing, both because matching it would
remove information: the wireframe shows four KPI tiles where the app has five
real metrics plus the budget entry card, and it writes resources as one text
line where the app keeps the CPU/RAM gauges.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* "+ Mới" / "Quản lý skill…" were full buttons, ~40px tall each — the
loudest thing in a 230px sidebar, and three rows of height spent on
them. The wireframe writes these as small accent text beside the
heading; they are now 17px.
* "+ Mới" looked broken. It was not: it replaces the canvas with a fresh
empty flow, verified by loading a saved 3-node flow and pressing it
(3 nodes → 0). But pressing it while the canvas ALREADY held an empty
untitled flow produced an identical empty untitled flow — right, and
invisible. It now focuses and selects the name field, and says
"Flow mới — đặt tên rồi kéo agent vào canvas" in the status bar, so the
press always has a visible result.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Section 10 (Sự kiện bảo mật) — the one item on that page that was in scope:
the "Kết quả" column always showed ✕, because every security_block is recorded
with ok=False, so the column distinguished nothing. It is replaced by an
"Hành động" column tinted by which rule actually fired (dangerous_command red,
path_outside_sandbox green, network_blocked blue, secret_in_output amber). The
other three items on that page are the ones marked "vượt phạm vi" — KPI tiles,
three filter drop-downs, pagination and the sliding detail panel — and two of
their four action types have no real log source yet.
Section 9 (Tổng quan) — the KPI row is four tiles as drawn: Tổng token · Tổng
chi phí · Lượt gọi · Ngân sách. Input/Output/Cache are NOT dropped: their
figures moved onto the token tile's sub-line ("Input 1.52M · Output 513.2K ·
Cache 641.9K"), so the screen still carries every number it did before while
the row reads as the drawing does.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The KPI row already matched the wireframe, but the two sections under it did
not, which is what still read as "unchanged":
* TÀI NGUYÊN was four stretched columns — on a 1900px screen each value sat
at the far right of its quarter, with a 700px progress bar drawn as a rule
between. It is now one line: CPU 0% · Bộ nhớ 141 MB · Disk I/O 1 MB/s ·
Mạng 1 KB/s, packed left. 130px → 57px. The bars are kept and still
updated, just not drawn.
* SANDBOX and QUYỀN were two four-row columns side by side. The wireframe
draws ONE section with one line, so they are merged: the summary line
carries file system · network · process · resource limits, and both the
sandbox rows (ID, created, uptime) and the permission rows sit behind a
"Chi tiết" fold. Nothing is dropped — 168px → 80px closed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reading the page's Overview line by line against the app turned up more than
layout:
* Tab strip: "Sự kiện bảo mật / Lịch sử gọi MCP / Nhật ký hành động /
Trạng thái Agent" → "Bảo mật / MCP / Hành động / Agent", as drawn. The long
names are also what pushed "Icon" off a 1024px window behind scroll arrows;
all eight now fit.
* Headings: "SỬ DỤNG TOKEN & CHI PHÍ" → "TOKEN & CHI PHÍ", "SỬ DỤNG TÀI
NGUYÊN" → "TÀI NGUYÊN", "CHI TIẾT SANDBOX" → "SANDBOX & QUYỀN",
"HOẠT ĐỘNG GẦN ĐÂY" → "NHẬT KÝ GẦN ĐÂY".
* Tiles put the number FIRST and its name under it, which is how the page
draws them; "Tổng chi phí" → "Chi phí" and Budget → "Ngân sách" in
Vietnamese. Applies to the Dashboard's cards too, which share the widget.
* Resources showed I/O and network RATES; the drawing shows capacity —
memory as used/total (138 MB/15 GB) and the workspace drive's free space
(15 GB trống). The rates are still measured and kept.
* "SANDBOX & QUYỀN" rendered as "SANDBOX _QUYỀN": a group-box title treats
"&" as a mnemonic. Same bug as the usage title, now fixed in both.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The panel still said "Trợ lý App". I reported that name as changed several
turns ago — I had added help_agent.badge for the launcher pill and never
touched help_agent.title, so the panel header kept the old name in all three
languages. It is "AI Assistant" now, as the audit page names it.
Colours were invented rather than read. docs/ui-audit.html draws the assistant
in a fixed teal — .fab background #E6F6F4, border #7FD0C4, .spark #0F9B8A,
pill text #0F6E62 — and the app was using the theme accent instead. The dot,
the hover pill, the edge tab and the sparkle now use those values, so the
floating assistant is one recognisable object rather than something that
changes colour with the theme. The panel header also shows the sparkle instead
of the app's own icon.png, which is what made the header look unrelated to the
dot that opens it.
Collapsed rail: the nav tree kept sizing its column to the widest label, so at
54px it was ~100px wide inside a 66px viewport and grew a horizontal
scrollbar — which slid the icons off the x they hold while the rail is open.
The column now stretches to the viewport and horizontal scrolling is off; the
first item's x is 0 in both states, measured.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Settings sat 7px further in than the Dashboard/Giám sát rows above it — its
QSS gave it a 6px side margin where those rows start at the rail edge. At the
collapsed 54px width that put its icon near the middle of the rail, which is
what "thu gọn menu lại ra giữa" was describing. Icon and label now start on
the same x as the rows, open and collapsed, in both themes.
The help panel's transcript is rendered HTML, so switching language re-labelled
the chrome but left the greeting — and the "AI Assistant" speaker label — in
whatever language the panel was built in. retranslate() now rewrites the
greeting (matched by identity, so a real reply is never touched) and re-renders.
The sparkle is #FDBE59, sampled from the audit page's own render. Its CSS says
.spark{color:#0F9B8A}, but the glyph is the ✨ emoji and a colour emoji ignores
CSS colour, so the page has always drawn a gold star.
Behind all three: MainWindow does not style itself — run() calls
app.setStyleSheet — so 12 of 13 checkers were measuring a window with no
padding, margins or borders. Every QSS-driven layout bug was invisible to them,
and an unstyled window reported an icon drift that does not exist. Added
_apply_theme() and wired it through.
Two checker repairs that followed:
· check_no_hscroll flagged the 9pt dialogs on sizeHintForColumn(0), which
returns 182px at 9pt, 11pt and 14pt alike. Nothing was clipped. It now
compares the painted text against the width actually on screen, and fails
on a squeezed list (24 combos) where the old test passed.
· the checkers print Vietnamese and died mid-report on a cp932 console.
New: check_rail_align (icons hold one line, both themes, both states) and
check_help_i18n (transcript follows the language). Both verified to fail
without their fix.
15/15 checkers pass. check_nav and check_design_parity segfault in Qt teardown
roughly one run in three — pre-existing, after the verdict prints, and it
happens with or without the theme change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
QStyledItemDelegate hands the style decorationAlignment = AlignHCenter |
AlignVCenter. For a row with a label that is invisible — the icon sits left of
the text either way — but every rail row loses its label when the rail
collapses to 54px, and a centred decoration then lands wherever the column
happens to leave room. _NavItemDelegate overrides it to AlignLeft, so the icon
is on the row's left padding regardless of column width, style or platform.
Honest limit: this was reported from the running app and I could not reproduce
it offscreen — sweeping the column width leaves the icon at a fixed x with or
without the delegate. What is verifiable is the instruction, and centring is
the only mechanism by which Qt can put a label-less row's icon anywhere but
the left. check_rail_align now asserts the flag itself, which fails (132) the
moment the delegate is removed, alongside the column sweep.
Settings follows the rows to their new inset (padding-left 7px → 4px). Its
label still sat 3px left of Dashboard's because a button leaves less air
between icon and label than a tree row does; a 19px icon box closes that to
1px, with the 16px glyph left-aligned inside it.
15/15 checkers pass, no flakes this run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The scroll body holds [nav, RECENTS header, RECENTS], and RECENTS carries the
only stretch factor. Collapsing hides it, which leaves a fixed-height nav tree
(app.py sets one per row count) and nothing that can expand — so the box layout
centred what was left and the destinations dropped 297px down a 1048px window,
with the same gap opening below them.
A stretch spacer at the foot of the body takes the slack instead. It takes none
of it while RECENTS is visible, because RECENTS carries stretch 1 against its 0,
so the open rail is byte-for-byte what it was.
This is what "thu gọn menu lại ra giữa" meant, reported three times. I read it
as horizontal each time and measured x — which was 4px and correct throughout.
The report was about the vertical axis and I never checked it.
check_rail_align now measures the gap between the + button and the first
destination in both states: 6px/6px now, 6px/223px with the spacer removed.
15/15 checkers pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The picker was hidden outright at 54px, on the grounds that a combo box there
could only show a stub of a name. True, but it left the collapsed rail with no
way to change project at all — and the audit page's own inventory of the 11
collapsible controls says the rail's fold "giữ nguyên toàn bộ".
A folder QToolButton stands in for it: same items, and choosing one moves the
combo, so _on_rail_project_pick stays the single code path. Its tooltip carries
the current project name, which the combo could not have shown anyway. It
matches the + button's width and drops its menu arrow — at 42px the arrow
would take a third of the row to say nothing.
check_nav now collapses the rail and requires a reachable picker whose menu
matches the combo item for item, then picks a row and asserts the project
actually changed. Hiding the button fails it.
15/15 checkers pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The rail was setFixedWidth, so the handle QSplitter draws between it and the
content was inert — it looked draggable and did nothing when dragged.
The expanded rail is now a range (132–360px) instead of one number, so the
handle has something to give. Collapsing still pins it at 54px exactly, and a
width dragged to is remembered across the fold rather than snapping back to the
150px default. Long project names and RECENTS threads are what a wider rail
buys.
check_rail_resize drives the splitter the way the handle does and asserts the
rail follows, clamps at both ends, folds to 54px and comes back to the chosen
width. Restoring setFixedWidth fails it — the first attempt at that mutation
passed, because setMaximumWidth on the next line quietly undid it.
16/16 checkers pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Showing Cowork and GraphRAG always, greyed, instead of letting them appear and
vanish, moved the gate from "the row is absent" to "the row is disabled". That
guards the rail — Qt will not select a disabled row — but it guards only the
rail, and _goto is also reached from a RECENTS click and from startup restore.
Called with a gated sub-tab it opened the page while the workspace's own tab
strip still had it hidden.
_goto now asks WorkspaceTab.subtab_available() — the same isTabVisible state
that greys the row — and shows "Chọn project trước" in the status bar instead
of navigating. One gate, one place, whatever triggered the jump.
check_project_gate runs against an EMPTY home, because with any project on disk
the gate is open and the test proves nothing. It asserts the gated rows are
disabled and say why, that clicking them does nothing, that _goto refuses them,
and that + new chat and the project picker are off. Dropping the _goto guard
fails it with "_goto mo duoc 'Cowork' trong khi cong dang dong".
17/17 checkers pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A1 — hiding the assistant from the dot. The old build kept a chevron button
beside the dot wired to _hide_to_edge; the redesign moved that action into the
panel's ⋯ menu, so hiding went from one click to open-panel → ⋯ → Ẩn. The
drawing has no button there (26px, no text, no chevron), so the reach comes
back as a right-click on the dot, with the tooltip saying so.
A2 — per-part cost. Folding Input/Output/Cache into the total tile's sub-line
carried their token counts but not their prices, which live only on those three
cards, so the cost breakdown left the screen entirely. The wireframe asks for
the tiles anyway — "$0.31 Tổng chi phí · 57 lượt / 395.4K Tổng token / 292.8K
Input / 102.7K Output / 108.9K Cache" — so they are back as tiles, each with its
price, and the turn count rides on the cost tile's label as drawn.
The dot on Cowork. _update_dock_guard lifted it whenever a composer existed,
testing only the vertical axis. On a wide window the composer ends at the chat
column's right edge — x=1688 against a dot at x=1892 — so the dot rose 156px to
clear something that was never under it, and Cowork became the one screen where
it was not in the corner. It now lifts only when the two actually overlap:
18px from the bottom at 1936px wide, still lifted at 1200 and 900 where the
composer does reach under it.
Also: open_tooltip and show_tooltip still said "App Assistant" after the rename.
check_dock_corner covers the corner rule at three widths and fails if the lift
comes back unconditionally.
18/18 checkers pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"Some screens scroll sideways, some don't" had two causes, and both were a
number that was right on one monitor.
The lanes. _KanbanColumn set a 150px floor with a comment claiming seven of
them fit a 1280 window; measured, the row wanted 1242px where 1280 leaves 1091,
so a 1280 screen scrolled and a 1920 one did not — this machine has one of each.
The floor is gone. The seven lanes share the board through the layout's
stretch, which is a proportion of whatever width there is, and the only pixel
question left — how narrow before scrolling beats squeezing — is answered from
the font: eight characters of a title plus padding, so it holds at 125%/150%
scaling too. No horizontal scroll now at 1280×720 through 1936×1048, at three
rail widths each, including the rail dragged to its maximum.
Each lane also grew its own scrollbar: QListWidget's column hint runs 1-6px past
its viewport, and which lanes overflowed changed with the window width — 36 of
38 widths had at least one. Cards word-wrap, so there was never anything to
reach sideways; the bar is off.
The rail. Its 360px ceiling was a quarter of a 1440 screen and more than that of
a 1280 one. It is now 22% of the window, capped at 360 — and recomputed on
resize, which is its own bug fixed: read once at construction off a not-yet-sized
window, it sat at 162px on every monitor, so the handle barely moved.
check_kanban_scroll covers five screen sizes × three rail widths. A rail dragged
to maximum on a 1280 screen still scrolls the board — that is the user's own
trade, so it is reported rather than failed.
19/19 checkers pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Section 16 of the audit page draws the three actions on the title row, a
magnifier inside the search box, caps section headings, and names its complaint
outright: "Ô icon trong lưới hiện tại không có viền/hover rõ khi rê chuột hay
khi đang chọn" — you could not tell which cell you were about to pick.
So: Thêm / Dán / Xoá move from the strip under both grids (where they read as
belonging to the custom grid alone) up beside the title; the search box gets its
magnifier; the two headings become caps through the same #navSectionHdr style
the rail uses; and #iconGrid cells take an accent border on hover and on
selection, the card language Agent and Công cụ already use.
Wording follows the drawing too: "ICON TÍCH HỢP" rather than "Icon có sẵn", and
"Tìm icon theo tên…" rather than "Tìm icon có sẵn…".
Left alone, and worth a decision: the drawing labels the buttons "+ Thêm icon"
and "Xoá", where the app says "Thêm tệp SVG" and "Xóa tùy chỉnh". The longer
labels say which file type is wanted and that only custom icons can be deleted,
so shortening them to match the drawing would cost more than it gains.
check_icons_screen covers the four points. Its first version passed a mutation
that dropped the buttons from the header layout — unparented they sit at (0,0),
which read as "the same row" — so it now requires the vertical centres to line
up AND each button to start right of the title.
20/20 checkers pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Launching the app from the working copy left .cowork_history/ next to the
source, and one transcript rode along into the previous commit and up to the
remote. It is the user's data, not code. Untracked and ignored, along with
.cowork_local/ for the same reason.
The file stays in the history of e5bd679 — removing it there rewrites the
branch, which is the user's call.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Settings sat closer to its icon than Dashboard and Giám sát do. Those are tree
rows, laid out by the style; Settings was a QPushButton, whose icon-to-label gap
is also the style's — and the two do not agree. The Windows fix for this was a
19px icon box that nudged the label 1px, which is exactly the kind of tuning
that only holds on the machine it was measured on: on macOS the gap is tighter
again. The row now lays itself out, 4px in and 6px between, the same two numbers
the tree uses. Icon x=4 and label x=26 against the rows' 4 and 26 — equal, not
close.
The language drop-list showed a tick over its own text. macOS marks the current
row with a checkmark and Windows does not, and the popup inherits the combo's
width — which for "EN/JP/VN" is about 50px, all of it needed by the letters.
widen_popup() measures the longest item plus the platform's indicator and sets
the view's minimum, so the tick has its own room wherever it is drawn.
That check found two more: the project picker (191px of text in a 138px popup)
and the provider list (221px in 138px) were both truncating names here, tick or
no tick.
Neither could be reproduced on this machine, so the checks assert the property
that made the bug possible — spacing we control rather than the style's, and a
popup measured against text + indicator — not the platform.
21/21 checkers pass. check_nav segfaults in Qt teardown after printing its
verdict, twice in one suite run and 0 times in 8 standalone runs; pre-existing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The selected row carries selection-background-color, so a checkmark repeats
what the colour says — and in a combo as wide as "VN" it repeated it on top of
the letters. A combo's default delegate paints menu-style, which is where the
glyph comes from; a plain QStyledItemDelegate paints item-view style, which has
none. widen_popup does both jobs now, so it is tidy_popup.
check_combo_popup additionally requires the delegate to be the plain one and the
stylesheet to still tint the current row — removing the delegate line fails it.
21/21 checkers pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Section 4 draws a "Quản lý project" title with + Project mới on its right, a
caps PROJECT heading over the list, every row carrying "N đoạn chat · M task",
and the form reading Tên / Mô tả / Instructions / Thư mục làm việc. The screen
had none of that: the header still said "Workspace — Projects", the create
button sat at the foot of the list among the list's own controls, rows were
bare names, the folder field had no label, and Instructions carried a
parenthetical the drawing does not.
The row counts are the substantive part — they are the only thing on the screen
that says a project contains anything. Read once per refresh from
list_conversations() and list_tasks() grouped by project_id, not per row.
Delete stays under the list it acts on. The drawing does not show it, but it
does not show it moved either, and dropping a control is not something a layout
pass gets to do.
Also shortened by the drawing: "Đổi thư mục…"/"Mở thư mục" to "Đổi"/"Mở", which
the new "Thư mục làm việc" label above them now disambiguates.
check_project_screen covers the four points and fails when the count line is
removed.
22/22 checkers pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Giving each project row a widget left the QListWidgetItem with no text, and
project_choices() read exactly that. So the rail's picker listed every project
as a bare "📁 " with nothing after it, and the more projects there were the more
identical blanks you got. Reported after creating a second one.
Setting the text back on the item is not the fix: with a transparent row widget
on top, the name paints twice, one string ghosting the other — visible in a
render. project_choices() now reads list_projects(), the same source the list
is built from, so the picker no longer depends on how a row happens to be drawn.
check_project_screen creates a project and requires project_choices() to return
non-empty names and the picker to show each of them. Pointing it back at
item.text() fails it.
22/22 checkers pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Re-read both drawings element by element rather than by eye. Five things were
still wrong.
Cowork headed itself "Cowork" — the screen's own name, which the rail already
shows — where the drawing puts the THREAD's title. The title now follows the
conversation, falling back to the screen name for a chat that has none yet;
ChatPanel notifies on load, on reset and when a title is first derived from the
opening turn.
Its files panel read "Tệp đầu ra"; every section heading in the drawings is
caps, so it matches the rail and Monitoring now.
Project kept its three-line explanation above the panes on every visit. The
drawing heads a populated screen with the title alone and gives the text to the
EMPTY state instead, which is where it is actually needed — so it shows only
when there is no project yet.
The workspace path sat as grey caption text where the drawing shows a field. A
read-only line edit looks like one and lets the path be selected and copied.
And Lưu project sat directly under the folder row; the drawing floats it at the
foot of the panel, so a stretch went in above it.
Checked against the control inventory, not just the picture: "Nén" and "Thư mục
Local…" are marked giữ nguyên tại chỗ, so they stay in the status strip even
though the drawing's status line is text only.
New check_cowork_screen; check_project_screen gains the three points. Both
mutation-tested — pinning the title back to tr("cowork.title") and making the
hint unconditional each fail.
23/23 checkers pass. The Qt teardown segfault is still around: check_no_hscroll
took it 1 run in 3, after printing its verdict.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Read the wireframe as a tree this time instead of a flat list of its text, and
the difference was structural rather than cosmetic.
The drawing nests it: a toolbar row, then a body row holding the transcript and
the files column side by side, then the composer as its OWN full-width row under
both, then the status line. The app had the composer inside the chat column, so
it stopped at the files panel's edge — the input narrowed by ~220px the moment a
deliverable appeared. It is now a sibling of the splitter, spanning 1474 of 1490
px with the panel open.
The panel also carried two headings: a "Files" label, and under it the section's
own "TỆP ĐẦU RA (n)". The drawing has one — "TỆP ĐẦU RA (3) ›" — so the section
header moves onto that row with the collapse chevron at its right, and the label
steps out of the layout.
The check for that needed two attempts. Asking whether the "Files" label is
visible passes either way: unparented, it reports invisible whether or not the
code hides it. It now counts the headings the panel actually shows.
23/23 checkers pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
You are right that the new Cowork should not carry a History pane. The page
lists it under "Thành phần bị dời chỗ": History, pane giữa, chỉ ở tab Cowork →
Sidebar ▸ RECENTS. Its own complaint about this screen is that three panes
squeezed the conversation under 60% of the width, and the wireframe draws two
columns — transcript and files.
But the same page also keeps the pane's chevron ("Pane Lịch sử: chevron trên
đầu History, gập thành dải mỏng — giữ"), and RECENTS ends in "Tất cả project…",
which opens it. So it is folded, not gone: search, filters, pin, rename and
multi-select delete only exist in that panel and deleting it would take them.
Cowork now opens with History folded to its 18px strip. "Tất cả project…" opens
it to 240px, and either choice is remembered — leaving the screen and coming
back no longer re-folds a panel you just opened, or re-opens one you just shut.
check_cowork_screen requires the fold on arrival and the strip to open on
request; restoring the old auto-expand fails it.
23/23 checkers pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
I read "chevron gập pane Lịch sử — giữ" as "keep it as a strip" and left an 18px
sliver down the side. The drawing has nothing there: the body row is two columns,
transcript and files, and the relocation table settles it — History (lịch sử
chat), was "pane giữa, chỉ ở tab Cowork", now "Sidebar ▸ RECENTS", verdict "giữ,
dễ tới hơn". The line about the chevron belongs to a tally of the app's eleven
collapsible spots, not to this screen's layout.
So the panel is not on Cowork at all. "Tất cả project…" in RECENTS brings it in
at full width — it has to stay reachable, because search, filters, pin, rename
and multi-select delete exist nowhere else — and its chevron puts it away again.
Either choice survives leaving the screen and coming back.
check_cowork_screen now requires it absent on arrival and present after the
request; showing it unconditionally fails.
23/23 checkers pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Moving it onto the title row put it on the wrong side of a boundary: that row
lives on WorkspaceTab, above the sub-tabs, so it is drawn over Cowork, Co4E,
Folder and GraphRAG too — a button offering to create a project in the corner of
a chat. The title label beside it already followed the sub-tab; the button did
not, because I added it to the row without adding it to that rule.
Both follow it now. check_project_screen walks the other four sub-tabs and fails
if the button shows on any of them.
23/23 checkers pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The drawing reads it left to right —
Agent: qwen2.5-coder · Định tuyến: Tắt · ↓292.8K ↑102.7K · $0.31 · 📁 folder
— and the app had it split down the middle: usage and folder on the left, Agent
and routing away on the right. They are one run on the left now, in that order.
Nén and Tự chạy stay on the right, where the control inventory marks them
"giữ nguyên tại chỗ".
The figures were also missing. _usage_total_lbl was written in exactly one
place, at the end of a turn, so a thread opened from History showed an empty
strip however much it had already spent — the numbers only appeared once you
sent another message. refresh_usage() reads the same per-conversation events
_show_usage does and now runs wherever the thread changes: opening one, starting
a new one, or deriving a title from the first turn. Opening a seeded thread now
reads ↓103.5k ↑44.6k ▤201.0k $0.1187 straight away.
check_cowork_screen asserts the strip is non-empty for a thread with recorded
usage and that the four parts run left to right in the drawn order.
23/23 checkers pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Measured what a visit costs: 140ms to construct the QWebEngineView the first
time, 485ms to walk the folder and lay the graph out, and a setHtml of the whole
D3 page to show it. The setHtml is the flash — the page really is reloaded.
Once built it is not repeated: three round trips in and out rescanned nothing.
What did repeat was project switching. _refresh_project_combo scanned
immediately on any change, and the rail's picker put that one click away from
every screen — so the graph was rebuilt again and again for a tab usually not on
screen, and the visit after a switch always paid for a reload.
It now marks _needs_scan and lets auto_scan_and_fit() pick it up when GraphRAG
is actually opened: two switches from Cowork cost 0 scans, and opening it after
costs exactly 1.
The first visit per run still builds the browser view and scans, which is the
lazy construction the code chose deliberately to keep startup light. That one is
a trade, not a bug; say if you would rather pay it at startup.
check_graphrag_rescan covers all three: no rebuild on an unchanged re-entry, no
scan while off-screen, exactly one on opening.
24/24 checkers pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two causes, both dealt with.
A fresh QWebEngineView paints white, and _ensure_web put it on screen empty —
so on a dark theme that white rectangle sat there for the whole first scan.
It is now blanked to the app's own background colour the moment it is created,
before it is ever shown.
And the work itself moved off the click. MainWindow warms GraphRAG up 3s after
the window appears — building the browser view (~140ms) and the first graph
(~485ms) while nothing is waiting on them. Clicking GraphRAG then costs 78ms
with zero scans and zero setHtml calls, where before it was ~625ms of empty
view.
This spends the memory the lazy construction was saving, a few seconds after
startup rather than never — which is the trade you asked for. Startup itself is
untouched: the checker asserts neither the view nor the graph exists at the
moment the window opens.
check_graphrag_rescan now covers the warm-up too, and fails if it stops running.
24/24 checkers pass; check_combo_popup took a teardown segfault in the suite and
passed 3/3 standalone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Its two entries were "Thu nhỏ", which the − button immediately to its left
already does, and "Ẩn vào cạnh phải". A drop-list whose real content is one
action, half of it duplicating its neighbour, is chrome — removed at the user's
request.
Nothing became unreachable: collapsing is the − button and the dot itself,
hiding is a right-click on the dot (already there, and named in its tooltip) or
now on the open panel's header too.
This is a deliberate deviation from the audit page, which asks for "'Ẩn trợ lý'
dời vào menu ⋯". check_design_parity records it as that rather than quietly
scoring it done — the item is relabelled and its detail says where the action
went. check_help_dock stopped reading the menu's contents and now exercises the
routes: − collapses, the dot carries a context menu, hide reaches the edge.
24/24 checkers pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
26 → 52px, 14 → 28px for the sparkle. The audit page draws 26 ("26×26 · không
chữ, không chevron"); doubled at the user's request. Still half the area of the
84×64 button it replaced: 2704 vs 5376px².
Four checks pinned the old number. Three now read the size the design actually
claims rather than a literal — square, at least 24px to hit, and smaller than
what it replaced — and check_design_parity records the deviation in its item
label instead of scoring 52px against a 26px expectation.
The fourth attempt was worse than the literal: I had check_layout_geometry
import _DOT and compare against it, which makes it unfailable — change the
constant and the expectation moves too. check_probes_bite caught that, reporting
that doubling _DOT again left the checker green. It now bounds the range
(24..64, square), which the mutation does break.
24/24 checkers pass; check_multi_screen took the teardown segfault once in three
standalone runs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ran the suite against the pulled commit. Two failures, both mine.
check_design_parity read the Overview column as findChildren(QScrollArea)[0].
e6adfd9 gives each event table a detail panel, which is also a scroll area and
is built first — so the probe was inspecting a detail panel and reporting the
pricing table as missing from a column it had never looked at. It now picks the
scroll area that actually contains ov_usage_group.
check_controls_alive flagged self.refresh_btn as vanished. It did, and on
purpose: the single Refresh in the Monitoring header became one per table
(title_refresh_btn on Bảo mật / MCP / Hành động / Agent). Tổng quan has none
because it auto-refreshes every 3s on _REFRESH_MS, and Icon has nothing to
refresh — I checked both before recording it in MOVED rather than assuming.
Nothing in e6adfd9 needed changing. 24/24 pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- co4e_config_panel: StepConfigPanel reorganized into four collapsible,
borderless sections (Co ban / Model & Quyen / Skills & Tep / Agent song
song) instead of one long form. Agent song song is its own section,
shown only for parallel-variant steps. Fixes a QVBoxLayout without a
trailing stretch that was handing every section an equal share of the
scroll area's leftover height instead of sizing to content.
- schedule_task_tab / theme: Kanban board lane gutters and per-lane
margins trimmed so the seven lanes read their card content more
clearly on a laptop-width window.
- chat_panel / widgets: the Output Files list in Cowork/Code no longer
stops at a fixed height with empty panel below it - it now fills the
column down to the composer.
- composer: Attach/Send/Stop buttons match the input box's height and
align to its top/bottom edge instead of floating centered beside it.
- app.py: the sidebar's "All projects" link reads as a link (italic,
accent color) instead of a plain row.
- i18n: labels for the three renamed Co4E section headers (VI/EN/JA).
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
What changed and why?
Change Type
Related Work
Cowork Task:
Core Repo: http://34.143.229.138/gitea-admin/fsg-ai-core-assets
Core AI Issue:
Core Task:
Related PR:
Scope
What is intentionally included?
What is intentionally NOT included?
Validation
Commands / evidence:
Security Impact
Permission / credential / network / customer data impact:
Compatibility
Reviewer Notes
Anything Cowork reviewers should pay attention to.
Implements the redesign from docs/ui-audit.html. Rearrangement only — no feature was removed; every control that moved kept its handler, and the gates that used to hide things now grey them out instead. Navigation * The rail is one flat list: the five Workspace sub-views sit at the top level instead of behind an accordion, with Dashboard/Monitoring pinned at the foot and Settings below them. * Cowork and GraphRAG stay listed and greyed while no project is selected, rather than vanishing and resizing the menu under the user. * Monitoring keeps its eight sub-views in its own tab strip (unhidden) instead of doubling the rail's length. * _goto now moves the highlight itself, fixing a long-standing bug where programmatic navigation left the rail pointing at the previous screen. * Rail header gained the project picker and "New chat"; RECENTS lists the active project's threads. Both are second views of existing state — the Cowork toolbar button and the full History panel are untouched. * Provider / language / theme moved from the top bar to an account row at the foot of the rail (same widgets, same signals). Screens * Co4E: the flow tab strip is gone (per the design); Flow Status became a toolbar toggle with its own way back, and the three icon-only tabs became four labelled, foldable sections in one column. One flow open at a time is the one capability this costs; background runs are unaffected. * Dashboard: header split into two rows; cost promoted to a hero card. * Monitoring Overview: one scrolling column of titled sections; the model price table got its own full-width section instead of sharing a row with the CPU meters. * Settings and Task editor gained a section index down the left. * Help dock: 84x64 launcher + chevron became one 26px dot that expands to a labelled pill on hover; "hide to the edge" moved into the panel's menu. Layout * The window's minimum width dropped from 1453px to 768px. The main cause was a QTabWidget taking its minimum from the widest page even when that page is hidden, so Co4E was forcing Project and Cowork wide. * Secondary panes fold themselves on a narrow window and restore when it grows, never overriding a fold the user made. * The long dialogs no longer scroll sideways at any font size. Verification: tools/check_*.py build a real MainWindow offscreen against a copy of ~/.cowork_local with the schedulers no-oped. check_design_parity.py reads its checklist straight from the audit page's own proposals. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>check_design_parity.py reads its checklist from the audit page's own proposals; it now reports every one of the 31 as implemented, with no deliberate divergences left. * Schedule: the Kanban/Calendar drop-list became a pair of tabs, and the Running lane is outlined while it holds anything — dropping a card there starts the task for real, so it should not look like the other six. * Cowork: agent / routing / usage / folder moved out of the typing box into their own status strip beneath it, styled as status rather than a second toolbar. All of them stay interactive; the design's read-only strip would have cost features. * Folder: the path is written as the screen's title instead of sitting in a read-only text box that looked editable and cost a row. * GraphRAG: the second toolbar row is gone (Export joined the first), and the one button that relabelled itself became Đồ thị | Tin nhắn tabs, so the view you are NOT in is named too. * Settings gained the theme picker, so language / provider / theme are all reachable there as well as on the rail's account row. * Task editor: the five group boxes are grouped into three step tabs (Nội dung → Lịch chạy → Liên kết). All 22 fields verified present after the move; only the old section index is gone, replaced by the tabs. * The assistant dot now clears a screen's own bottom bar (Cowork's composer), measured from the composer's top edge in window coordinates. Also adds .gitattributes: without it a Windows checkout records CRLF and every file reads as fully rewritten to a Linux CI runner. Verification: 7 check_*.py suites green, no screen clipped at 1920/1366/1280, and no dialog scrolls sideways at 9/11/14pt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>check_design_parity read its checklist from build_audit_page.ANALYSIS. Eight sections of the audit page are hand-written, and for those the generator's text is NOT what the page says — so Settings and the Task editor were reported as matching a design they had never been compared against. The Task editor was in fact built backwards. * The checker now reads the "Thay đổi" bullets straight out of docs/ui-audit.html, and reports how many bullets on the page still have no probe (57 on the page, 32 probed) instead of implying full coverage. * Task editor: reverted from three step tabs to a left list + right panel, five rows matching the five real group boxes — which is what the page asks for, in as many words ("thay vì chia tab"), for consistency with Settings. * Settings now switches panels rather than scrolling, so both dialogs are navigated identically and neither is a long scroll any more. * Settings field presentation, as the page's second bullet asks: the six checkboxes became switches, and Language/Theme became segmented controls. ToggleSwitch subclasses QCheckBox and SegmentedControl exposes the slice of the QComboBox API this dialog uses, so no save/load path changed — verified by round-tripping language/theme/tray through _save(). All 22 task-editor fields and 14 settings fields verified present after the move; 7 suites green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>Five rounds, each looking from an angle the previous one cannot: 1. text — every "Thay đổi" bullet on the page vs a probe (existing) 2. geometry — check_layout_geometry.py: reading order of the rail, section order down Monitoring, which side each pane is on, and the size relationships the design names (hero card, 26px dot) 3. inventory— check_controls_alive.py: every control in controls.json still present in the built app, attributed to its owning CLASS via the baseline commit (a file holds several classes, so a per-file check reported eight dialog controls as missing) 4. eyes — rendered screens read against the wireframes 5. adversarial — check_probes_bite.py: break one feature at a time and fail if the matching check still passes What rounds 4 and 5 caught, which 1-3 could not: * Schedule showed six of seven lanes; the seventh needed a horizontal scroll. The design says "giữ đủ 7 lane, thu hẹp cho vừa một màn". Lane minimum width 190 → 150, so 7 × 150 + gaps fits a 1280 window. Round 2 now measures this instead of relying on someone noticing. * check_design_parity ALWAYS returned 0. It was a report, not a check: every probe in it was incapable of failing, so a regression would print on screen and still exit green. It now exits non-zero when anything is CHUA — which is what let round 5 detect the two mutations it had been sleeping through. Also fixed in the harness itself: it rewrote line endings while restoring mutated files (read_text/write_text translate both ways), and it compared the tree against "clean" rather than against its own starting state. All ten checkers green by exit code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>Two things differ between machines and only one of them is width: a 4K panel has more pixels, while a 125%/150% display has the same logical pixels holding LESS, because every label and margin is taller. Breakpoints written as raw pixels only hold on the machine they were tuned on. * ui_scale() derives a factor from font height (1.0 at the 15px line the layouts were measured against) and every narrow-guard threshold is multiplied by it, so panes fold when the content is cramped rather than when a number is crossed. * The window takes a share of the available screen (80% × 85%) with the old 1180×760 as the floor, instead of opening at that size on any monitor. * Moving the window to another screen re-pins the assistant and re-decides the fold, since the new screen's work area and scaling may differ. Found by tools/check_multi_screen.py, which walks 5 window sizes × 3 font scales: * At 150%, Schedule was clipped on 1280 and 1366 screens and the window's own minimum grew to 1459px — wider than a 1280 laptop, so the app could not fit at all. The cause was not the lanes: the one-line lane-count summary in the header reported a sizeHint wide enough to set the minimum width of the entire window. It now yields first (its text stays in the tooltip); the window minimum drops 1459 → 752 and holds there at every scale. Also: these checkers exited 0xC0000409 from a Qt teardown crash AFTER printing their verdict. check_probes_bite decides whether a probe caught its mutation by reading exit codes, so a crash would have counted as "caught" — the round could have passed while proving nothing. They now flush and os._exit with the real verdict, and round 5 still catches all six mutations. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>Two problems, both only visible at the window size this machine actually opens (1024×571 on a 1280×672 work area): * The rail drew the "GẦN ĐÂY" heading on top of the last nav row. nav and the bottom group have fixed heights, so a short window put the entire squeeze on RECENTS; once that hit zero the layout had nowhere left to give. The destinations and RECENTS now scroll together inside their own area, while the bottom group, Settings and the account row stay pinned. * Overview did not look like the drawing. Fixed, in the order the page draws them: sections are quiet caps headings with flat content instead of six bordered boxes; the four resource metrics sit ACROSS one row rather than stacked four deep; and "Hoạt động gần đây" moved from second place to the bottom beside the audit log, so the reading order is cost → machine → permissions → prices → what happened. At 1280×760 the page now reaches the price table on first screen; before it showed one and a half sections. Two deliberate differences from the drawing, both because matching it would remove information: the wireframe shows four KPI tiles where the app has five real metrics plus the budget entry card, and it writes resources as one text line where the app keeps the CPU/RAM gauges. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>* "+ Mới" / "Quản lý skill…" were full buttons, ~40px tall each — the loudest thing in a 230px sidebar, and three rows of height spent on them. The wireframe writes these as small accent text beside the heading; they are now 17px. * "+ Mới" looked broken. It was not: it replaces the canvas with a fresh empty flow, verified by loading a saved 3-node flow and pressing it (3 nodes → 0). But pressing it while the canvas ALREADY held an empty untitled flow produced an identical empty untitled flow — right, and invisible. It now focuses and selects the name field, and says "Flow mới — đặt tên rồi kéo agent vào canvas" in the status bar, so the press always has a visible result. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>Section 10 (Sự kiện bảo mật) — the one item on that page that was in scope: the "Kết quả" column always showed ✕, because every security_block is recorded with ok=False, so the column distinguished nothing. It is replaced by an "Hành động" column tinted by which rule actually fired (dangerous_command red, path_outside_sandbox green, network_blocked blue, secret_in_output amber). The other three items on that page are the ones marked "vượt phạm vi" — KPI tiles, three filter drop-downs, pagination and the sliding detail panel — and two of their four action types have no real log source yet. Section 9 (Tổng quan) — the KPI row is four tiles as drawn: Tổng token · Tổng chi phí · Lượt gọi · Ngân sách. Input/Output/Cache are NOT dropped: their figures moved onto the token tile's sub-line ("Input 1.52M · Output 513.2K · Cache 641.9K"), so the screen still carries every number it did before while the row reads as the drawing does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>The KPI row already matched the wireframe, but the two sections under it did not, which is what still read as "unchanged": * TÀI NGUYÊN was four stretched columns — on a 1900px screen each value sat at the far right of its quarter, with a 700px progress bar drawn as a rule between. It is now one line: CPU 0% · Bộ nhớ 141 MB · Disk I/O 1 MB/s · Mạng 1 KB/s, packed left. 130px → 57px. The bars are kept and still updated, just not drawn. * SANDBOX and QUYỀN were two four-row columns side by side. The wireframe draws ONE section with one line, so they are merged: the summary line carries file system · network · process · resource limits, and both the sandbox rows (ID, created, uptime) and the permission rows sit behind a "Chi tiết" fold. Nothing is dropped — 168px → 80px closed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>Reading the page's Overview line by line against the app turned up more than layout: * Tab strip: "Sự kiện bảo mật / Lịch sử gọi MCP / Nhật ký hành động / Trạng thái Agent" → "Bảo mật / MCP / Hành động / Agent", as drawn. The long names are also what pushed "Icon" off a 1024px window behind scroll arrows; all eight now fit. * Headings: "SỬ DỤNG TOKEN & CHI PHÍ" → "TOKEN & CHI PHÍ", "SỬ DỤNG TÀI NGUYÊN" → "TÀI NGUYÊN", "CHI TIẾT SANDBOX" → "SANDBOX & QUYỀN", "HOẠT ĐỘNG GẦN ĐÂY" → "NHẬT KÝ GẦN ĐÂY". * Tiles put the number FIRST and its name under it, which is how the page draws them; "Tổng chi phí" → "Chi phí" and Budget → "Ngân sách" in Vietnamese. Applies to the Dashboard's cards too, which share the widget. * Resources showed I/O and network RATES; the drawing shows capacity — memory as used/total (138 MB/15 GB) and the workspace drive's free space (15 GB trống). The rates are still measured and kept. * "SANDBOX & QUYỀN" rendered as "SANDBOX _QUYỀN": a group-box title treats "&" as a mnemonic. Same bug as the usage title, now fixed in both. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>Settings sat 7px further in than the Dashboard/Giám sát rows above it — its QSS gave it a 6px side margin where those rows start at the rail edge. At the collapsed 54px width that put its icon near the middle of the rail, which is what "thu gọn menu lại ra giữa" was describing. Icon and label now start on the same x as the rows, open and collapsed, in both themes. The help panel's transcript is rendered HTML, so switching language re-labelled the chrome but left the greeting — and the "AI Assistant" speaker label — in whatever language the panel was built in. retranslate() now rewrites the greeting (matched by identity, so a real reply is never touched) and re-renders. The sparkle is #FDBE59, sampled from the audit page's own render. Its CSS says .spark{color:#0F9B8A}, but the glyph is the ✨ emoji and a colour emoji ignores CSS colour, so the page has always drawn a gold star. Behind all three: MainWindow does not style itself — run() calls app.setStyleSheet — so 12 of 13 checkers were measuring a window with no padding, margins or borders. Every QSS-driven layout bug was invisible to them, and an unstyled window reported an icon drift that does not exist. Added _apply_theme() and wired it through. Two checker repairs that followed: · check_no_hscroll flagged the 9pt dialogs on sizeHintForColumn(0), which returns 182px at 9pt, 11pt and 14pt alike. Nothing was clipped. It now compares the painted text against the width actually on screen, and fails on a squeezed list (24 combos) where the old test passed. · the checkers print Vietnamese and died mid-report on a cp932 console. New: check_rail_align (icons hold one line, both themes, both states) and check_help_i18n (transcript follows the language). Both verified to fail without their fix. 15/15 checkers pass. check_nav and check_design_parity segfault in Qt teardown roughly one run in three — pre-existing, after the verdict prints, and it happens with or without the theme change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>Re-read both drawings element by element rather than by eye. Five things were still wrong. Cowork headed itself "Cowork" — the screen's own name, which the rail already shows — where the drawing puts the THREAD's title. The title now follows the conversation, falling back to the screen name for a chat that has none yet; ChatPanel notifies on load, on reset and when a title is first derived from the opening turn. Its files panel read "Tệp đầu ra"; every section heading in the drawings is caps, so it matches the rail and Monitoring now. Project kept its three-line explanation above the panes on every visit. The drawing heads a populated screen with the title alone and gives the text to the EMPTY state instead, which is where it is actually needed — so it shows only when there is no project yet. The workspace path sat as grey caption text where the drawing shows a field. A read-only line edit looks like one and lets the path be selected and copied. And Lưu project sat directly under the folder row; the drawing floats it at the foot of the panel, so a stretch went in above it. Checked against the control inventory, not just the picture: "Nén" and "Thư mục Local…" are marked giữ nguyên tại chỗ, so they stay in the status strip even though the drawing's status line is text only. New check_cowork_screen; check_project_screen gains the three points. Both mutation-tested — pinning the title back to tr("cowork.title") and making the hint unconditional each fail. 23/23 checkers pass. The Qt teardown segfault is still around: check_no_hscroll took it 1 run in 3, after printing its verdict. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>You are right that the new Cowork should not carry a History pane. The page lists it under "Thành phần bị dời chỗ": History, pane giữa, chỉ ở tab Cowork → Sidebar ▸ RECENTS. Its own complaint about this screen is that three panes squeezed the conversation under 60% of the width, and the wireframe draws two columns — transcript and files. But the same page also keeps the pane's chevron ("Pane Lịch sử: chevron trên đầu History, gập thành dải mỏng — giữ"), and RECENTS ends in "Tất cả project…", which opens it. So it is folded, not gone: search, filters, pin, rename and multi-select delete only exist in that panel and deleting it would take them. Cowork now opens with History folded to its 18px strip. "Tất cả project…" opens it to 240px, and either choice is remembered — leaving the screen and coming back no longer re-folds a panel you just opened, or re-opens one you just shut. check_cowork_screen requires the fold on arrival and the strip to open on request; restoring the old auto-expand fails it. 23/23 checkers pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>26 → 52px, 14 → 28px for the sparkle. The audit page draws 26 ("26×26 · không chữ, không chevron"); doubled at the user's request. Still half the area of the 84×64 button it replaced: 2704 vs 5376px². Four checks pinned the old number. Three now read the size the design actually claims rather than a literal — square, at least 24px to hit, and smaller than what it replaced — and check_design_parity records the deviation in its item label instead of scoring 52px against a 26px expectation. The fourth attempt was worse than the literal: I had check_layout_geometry import _DOT and compare against it, which makes it unfailable — change the constant and the expectation moves too. check_probes_bite caught that, reporting that doubling _DOT again left the checker green. It now bounds the range (24..64, square), which the mutation does break. 24/24 checkers pass; check_multi_screen took the teardown segfault once in three standalone runs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>e6adfd92ea20a8548