Skip to content

feat: content search, unified toolbar, full-window tasks (0.10.0) - #29

Merged
anbturki merged 79 commits into
mainfrom
feat/content-search
Jul 23, 2026
Merged

feat: content search, unified toolbar, full-window tasks (0.10.0)#29
anbturki merged 79 commits into
mainfrom
feat/content-search

Conversation

@anbturki

Copy link
Copy Markdown
Owner

What this is

The 0.10.0 feature line: workspace content search, a single window-wide toolbar with a lens rail, full-window tasks (board or grouped list), and a change to how agent writes resolve a workspace. Full detail in CHANGELOG.md; the highlights below.

Breaking change

A write with no workspace argument is now refused unless a workspace covers where the agent is working, instead of silently falling back to the shared personal workspace at ~/notes and creating it on demand. Reads still fall back. Only init_workspace creates a workspace now. Passing workspace explicitly always works. This affects every write tool; see the changelog's Breaking changes section for the full list and the migration note.

macOS 11 Big Sur is now the declared minimum (with Safari 16.4+), correcting an earlier claim of 10.15 Catalina that never actually worked.

Added

  • Search inside document contents - the sidebar search now matches text inside documents, grouped by document with per-match counts, expanding to matching lines in context.
  • One search for the whole sidebar - a single magnifier applies to whichever lens is showing, Tasks included; scope filters (Files / Contents / Tags) across the sidebar and quick open; content matches in quick open beneath the file-name matches; find-in-document with a running count; all shortcuts rebindable.
  • Tasks in the main area - a full-window tab, shown as a board of status columns or a grouped list, chosen from the toolbar; collapsible status groups; priority/label filters in the sidebar header.
  • Four more accents (teal, magenta, bronze, black) and a rebuilt appearance picker.

Changed

  • One toolbar across the window, holding the workspace switcher, that no longer shifts when the sidebar toggles - and, as of the last fix here, whose search holds one position rather than sliding as the breadcrumb length or the open tab's controls change.
  • Lenses moved to a vertical rail; panels inset as a set; editor and status colours now drawn from the theme.

Fixed

Window controls drawn for the wrong appearance; a theme choice overridden at sunset; the whole window scrolling behind the app; task drag-to-advance on macOS; missing dialog overlays; scroll position lost in a split-opened pane; a tab stuck on Loading; a stray scrollbar over the tab titles; duplicate workspace slugs; and more - see the changelog's Fixed section.

Verification

469 frontend tests, the Rust suites, tsc, clippy, fmt and the production build all pass. The toolbar-search fix and the task interactions were verified in the running app, not only in jsdom.

anbturki added 30 commits July 22, 2026 03:35
The sidebar search only ever filtered filenames, titles and tags. The
existing search_docs_core walks the workspace status folders, so it finds
nothing in an arbitrary opened folder and could not be reused.

Add a content search built on the scan traversal:

- Extract collect_markdown_entries from run_scan so the file tree and
  content search share one definition of which files a folder contains.
- Add core::search: rayon-parallel scan per query, matching through the
  regex crate with escaped terms so input stays literal and case folding
  does not shift the offsets used to build snippets.
- Return pre-segmented snippets rather than offsets. Rust byte offsets and
  JavaScript UTF-16 indices disagree on any document containing an accent
  or an emoji, so no index crosses the process boundary.
- Share the field weights between the in-app and MCP scorers so the two
  rank the same corpus identically.
- Cancel superseded queries with a generation counter: a Tauri command
  cannot be aborted once running, so a stale search bails out instead of
  competing for the disk.

Also lands the DOM range engine that will back find-in-document.
Typing a query now searches document contents alongside names, titles and
tags, and the sidebar shows a ranked result list with the matched line and
its surrounding context.

- A non-empty query replaces the lens body with results, so a file that
  matches by both name and content is listed once rather than in two
  places. Clearing the query restores the lens.
- Name matches render immediately from the in-memory list and are
  re-ranked in place when the scored content hits land, so the list fills
  in instead of flashing empty.
- Snippets render from pre-split segments as text nodes, so no markup from
  a document can reach the DOM.
- The footer match count and the search placeholder now reflect that
  contents are searched.

Note: typing a query while the tasks lens is open now shows search results
rather than being silently ignored.
Cmd+F opens a find bar scoped to the focused pane: all matches are
highlighted, Enter and Shift+Enter step through them, the focused match is
centred, and the count reads "3 of 17".

The rendered document is React-managed DOM, so nothing may mutate it. A
rehype pass injecting <mark> would also re-run the whole unified pipeline,
including Shiki's WASM tokenizer, on every keystroke. Matches are therefore
modelled as DOM Ranges and painted outside the document tree:

- On webviews with the CSS Custom Highlight API, ranges are handed to the
  engine and nothing is created at all.
- Older webviews get positioned rects in a container that is a sibling of
  the document. macOS 11 tops out at Safari 16.6.1 and can never have the
  Highlight API, so the fallback is required rather than optional.

Both paths share one match engine and one stylesheet, so they look the
same and only the paint step differs. Live ranges collapse silently when
React replaces nodes, so a MutationObserver rebuilds them, ignoring the
overlay's own writes to avoid a paint loop.
Paint was keyed on matchCount and currentIndex, but the ranges live in a
ref and so cannot drive an effect. Editing a query into one that matched
in the same number of places left the previous ranges on screen: typing
"board" kept painting the "bo" ranges, because both match five times.

Found by running the app; the unit tests missed it because each one moved
between queries with different match counts, so the effect always
happened to re-run. A revision counter now signals every recompute, and
two regression tests cover a query that lengthens and one that shortens
without changing the count.
Adds a scope to the core query so a caller can decide which fields count.
Defined once in Rust and mirrored once in TypeScript, since the pair is a
closed set that would otherwise drift. The names scope matches the whole
workspace-relative path, so a folder name is searchable the way a file
name is.
Search results now live in their own lens rather than hijacking whichever
lens was open, and follow the shape editors have settled on: results
grouped into the files that contain the term, each with its match count,
expandable to every matched line.

- Shift+Cmd+F opens the lens and focuses the query, alongside Cmd+F for
  the open document. The two chords never contend.
- A scope row narrows the search to names, contents or tags, backed by the
  core scope rather than filtered client-side.
- Quick Open stays a separate file-name-only surface across every open
  workspace, which is the split editors already train people on.

Typing a query no longer replaces the tasks board or the file tree.
Instant name matches were merged into the results whatever the scope, so
narrowing to contents or tags still listed files that had only matched by
name and the filter looked inert. The client-side filter spans names,
paths, titles and tags, which is a superset of any single narrower scope,
so it is only sound to seed when searching everything.
Quick open is the most-used way into search, so it now lists matches from
inside documents beneath the file-name matches, each with the line that
matched. Name ranking is untouched and still resolves synchronously, so
the list never lags a keystroke.

Quick open spans every open workspace while the search command took a
single folder. Running one request per folder would have broken
cancellation, since the generation counter is shared and each request
marks the previous one stale, so folders would have cancelled each other.
The command now takes the folders together and every hit reports which
one it came from.

Also renders the real shortcut glyph in the header rather than the raw
setting string.
Adding the Search lens made six tabs, which no longer fit one row at the
default sidebar width. The row had no width constraint, so the last tab
rendered outside the sidebar and painted over the open document.

The row now wraps. Clipping it instead would have hidden a primary entry
point behind a scroll, which is worse than a second line.
Quick open now carries the same scope filter as the search lens, so the
list can be narrowed to files, contents or tags without leaving the
dialog. Narrowing to contents drops the file-name group, narrowing to
files drops the document matches.

The "names" scope is now labelled Files, which reads better than Names on
both surfaces. Only a tag search legitimately returns a hit with no
matched line, so elsewhere those stay out of the "in documents" group
rather than appearing under a heading that misdescribes them.
Quick open's binding was already a stored setting, but find-in-document
and workspace search were hardcoded at their use sites. All three are now
settings with defaults, editable together in Settings, and read live so a
rebind takes effect without a restart.

A cleared or malformed stored binding falls back to its default instead of
leaving the action unreachable.
Search was one line and predated the Search tab, the scope filter, and
content matches in the file switcher, so a reader could not tell the
surfaces apart or find their bindings.
The switcher was a horizontally scrolling row of names: with more than a
few workspaces most were hidden behind the scroll arrows and the active
one was distinguishable only by font weight.

It is now a single trigger showing the active workspace, opening a menu
that lists every workspace with add and remove actions. Remove sits in a
submenu rather than as a per-row control, since removal is unconfirmed
and a trailing button inside a menu item is both mis-clickable and
unreachable under the menu's roving-focus model.
Six lenses no longer fit one row at the sidebar width. The row wrapped,
which made its height depend on the sidebar width and shifted everything
below it whenever the sidebar was resized.

The lenses now sit in a narrow rail down the left edge, each an icon with
its label beneath it, so the control keeps a fixed height and scales to
further lenses without reflowing. The sidebar widens to 20rem to pay for
the rail and leave the content column wider than the whole sidebar was.

The header's hardcoded left offset duplicated the sidebar width; both now
read one declaration.
An agent asked to track work for a project with no workspace could not
work out where it belonged: it inferred from an init refusal that a git
repository blocks workspace creation, then fell back to the shared user
workspace and used a label in place of isolation.

None of that inference was correct. Workspace creation never consults
git, and staging on write only adds the file just written, inside the
workspace folder, never a commit. The onboarding resource now says so,
gives an ordered choice of workspace locations including the sibling
folder for when a project tree must stay untouched, and states that
labels do not separate projects. The init recovery hint no longer reads
as though the folder's location is the blocker.
The top bar spanned only the document area while the sidebar carried its
own header, so the window showed two unrelated strips of chrome at
different heights, and the bar shifted whenever the sidebar toggled.

There is now one toolbar across the full window width, holding the
workspace switcher beside the window controls. It moved inside the
sidebar provider so it can reach the sidebar context and inherit the
shared custom properties, which removes the duplicated width
declaration. Toolbar height is declared once and the sidebar and
document area both offset from it, replacing three copies of the old
literal.

The window-control inset now depends on the platform rather than
applying everywhere, and the platform check lives in one module instead
of being redefined for shortcuts.

The switcher moves next to the toolbar that owns it and drops its
sidebar padding.
…dule

The sidebar width and toolbar height are read by the toolbar, the
sidebar, and the document area. Keeping them in the app root made every
change to either value a change to the root component.
Agents create workspaces without checking what exists, then read the
resulting conflict as a failure and go looking for somewhere else to
write, which is how work ends up scattered. They also pass names like
notes or docs, which is useless the moment a second workspace exists.

The guidance now establishes list, reuse, create as the order and says
plainly that an already-a-workspace answer means the workspace is ready.
The naming rule sits on the creating tool itself, where an agent will
meet it without having read the onboarding resource first. The conflict
recovery hint no longer reads as an obstacle to route around.
The bar returns to its original height now that the switcher shows the
workspace name on one line instead of a two-line identity block, and the
macOS window controls are positioned to sit centred in it rather than
against its top edge.

The switcher gains a surface of its own so it reads as a control, and
sizes to its content instead of reserving a fixed width.

The badge no longer disappears on hover. It was drawn with sidebar
tokens while rendering on a popover, and the menu forces a colour onto
every descendant of a hovered row: in the light theme the forced colour
and the badge background were the same lightness, so the letter vanished
into it. It now uses tokens belonging to the surface it renders on, and
a test asserts the contrast survives the forced colour in both themes.

Menu rows show a pointer cursor.

An unnamed workspace no longer labels itself notes. Managed workspaces
all live in a folder of that name, so every one of them rendered
identically; the label now falls back to the project above it.

The space reserved for the window controls is computed from where they
are configured to sit, and a test holds the two files in agreement.
The sidebar carried a Search lens, an always-visible search field, and a
separate free-text filter on the tasks board: three entry points for one
intent, none of which worked on the others' content.

There is now one search. A magnifier in the sidebar header reveals the
input and its scope filters, dismissing returns the space, and the query
applies to whichever lens is showing, including tasks. The scope filters
become chips and are hidden on the tasks lens, where document scopes
have no meaning. The board keeps its priority and label filters and
loses only the redundant text field.

Search is no longer a place you navigate to, so it leaves the lens rail.
A stored selection naming it already degrades to a valid lens.
Two workspaces could carry the same slug, and every call naming it went
to whichever registered first. No error, no warning: work landed in the
wrong folder and reported success.

A slug the caller chose is now refused when another workspace already
holds it, naming the path it collides with. An agent keeps the slug it
passed and will pass it again, so silently suffixing would hand back a
name that resolves somewhere else; a refusal is the outcome it cannot
miss. A slug derived from the folder name is suffixed instead, through
the same helper the conversion path already uses, and the assigned slug
comes back in the result.

Resolution no longer takes the first match. It collects every candidate
that still exists and reports the ambiguity when more than one remains,
which also closes the case of a registered workspace and a different
unregistered one sharing a slug.

An existing registry is left alone: it loads unchanged, non-duplicated
slugs resolve as before, and only the duplicated slug now fails loudly.
The registry recorded a slug at creation and nothing ever re-read it, so
a folder whose marker was edited kept resolving under the old value and
a folder that had been deleted was still advertised as available. The
duplicate-slug message had to ask the user to repair two files by hand,
because nothing reconciled them.

Listing and resolution now take each slug from the folder's own marker
and drop entries whose folder is gone. This is read-only: the stored
registry is never rewritten, so an existing one keeps working and no
migration runs behind the user's back. A folder reached by two spellings
counts once, compared by identity rather than by text.
Collapsing hid the whole panel, rail included, so every lens became
unreachable and the only way back was a control in the window chrome.
The panel now collapses to the rail: the content column goes, the
lenses stay, and choosing one brings the column back.

The toggle moves out of the toolbar into the rail, where it keeps one
position in both states. Giving it a home only while collapsed made
every lens item shift down the rail as it appeared. Its icon points the
direction it will move the panel.

Rail items share one rule for their width, padding, and centering, so
the toggle and the lenses cannot drift apart, and the rail takes its
width from the shared token rather than a literal.

Also fixes a tab that could sit on Loading forever: a read settling
before React committed the state that started it found a stale
reference, discarded its own result, and had already cleared the
timeout that would have rescued it.
Find-in-document does not match rendered math, and nothing said so.
KaTeX marks its own visible layer aria-hidden, so the existing rule
excluding hidden content already skipped both halves of every formula.
Keeping it that way is deliberate: the visible layer's text is ordered
by CSS rather than by document order, so a fraction serialises with its
denominator first and any highlight would land on the wrong glyphs.

Math stays findable by its LaTeX source, which workspace search reads.
A test built from real rendered output locks the assumption, so an
upstream change that stops hiding the visible layer fails loudly.

Also corrects the sidebar search description, which still described the
tab it had before it became a revealed panel.
The app declared macOS 10.15, which it cannot run on: the interface
needs Safari 16.4, and Catalina's last Safari is 15.6.1. Safari 16.4
shipped for Big Sur, so 11.0 is the lowest version that can reach it.

Big Sur and Monterey do not ship that Safari, they only accept it
through a software update, so the requirement is stated in the README
rather than implied by the number alone. macOS 13.3 is the lowest that
qualifies with no user action; choosing it instead would be a one-line
change and would drop an order of magnitude more machines.

The published cask gains a matching requirement, in the workflow that
generates it rather than in the tap, which is overwritten each release.
The reconciliation doc block restated the three steps its body already
reads plainly, and the two helpers below it already carry the WHYs that
are not obvious. Only the part a reader could not infer survives: that
reconciliation never touches the stored file.

Also corrects a comment written with a triple slash, which in TypeScript
is reserved for compiler directives.
The bar renders inside the element it searches, and nothing excluded it,
so its own text was walked in. Counts included the bar and a highlight
could paint on it.

Worse, a query that appears in the bar's own label never settled.
Searching "no" against a document with no matches showed "No results",
which the walker then found, so the label became "1 of 1", which
contains no match, so it returned to "No results" and round again. The
character-data mutation that drives each cycle targets a text node, and
the guard meant to ignore the find bar's own writes only matched
elements, so it never caught it.

The bar is now marked as chrome, excluded from the walk, and its own
mutations are recognised whether they land on an element or on text.
Range building is capped so a single-character query cannot allocate one
per match across a long document.
The installer branched only on architecture, so it would place the app
on a Mac whose Safari is too old to draw the interface, after a sudo
prompt and with no explanation of what went wrong.

It now reads the system version and stops below Big Sur, and warns below
13.3, which is the first release shipping the required Safari. The lower
bound tests 10.x below 10.16 rather than a major below 11, because a Mac
reporting 10.16 is Big Sur under the old numbering and must not be
turned away.

Release notes now lead with the requirement, since a user already on an
unsupported Mac cannot be reached any other way: the updater has no
concept of a minimum system version, so nothing in a published build can
stop it offering an update that will not launch.
…failed

One abort counter was shared by every search surface, so typing in the
file switcher cancelled the sidebar's in-flight query. The sidebar then
discarded the result, left its spinner running, and kept showing stale
matches with no sign anything had gone wrong. Each surface now has its
own counter, keyed by an identity the hook generates, so the two stop
cancelling each other; a surface superseding its own request also clears
its progress rather than waiting for a result that is not coming.

A root whose search failed was skipped in silence and the command still
reported success, so a workspace whose folder had been deleted read as
'nothing matched'. Failed roots are now carried back, and the error the
results view already knew how to render is raised when every root
failed. Partial failures keep their matches: hiding real results behind
an error costs the reader more than the missing folder does.

Cancellation is now checked while walking the tree and between roots,
not only per file, so an abandoned query stops instead of paying for the
traversal that dominates a large workspace. The number of hits is capped
and reported through the existing truncated flag.

Creating the default workspace on first write now hands back the slug it
was actually assigned, so a caller cannot build an identifier naming a
workspace its file did not go into.
anbturki added 28 commits July 22, 2026 18:50
A write with no workspace argument, from a folder with no workspace
above it, resolved to the user default and created it on the spot. An
agent doing project work had that work absorbed into a shared workspace
named after nothing, with no sign anything had gone wrong.

Such a write now offers the workspaces that exist, and refuses with that
list when the caller cannot be asked. An explicit workspace argument is
still honoured even when it names the user default: that is a choice,
not a drift. Reads are untouched, so a clean session can still look
around.

This retires create-on-first-write, which existed so agents never met a
setup wall. The wall is the point: init_workspace is now the only thing
that creates a workspace, and the refusal names the alternatives.
The lens could only be a board, and the switch would have been another
comparison against the lens name, which the sidebar already does in four
places.

A lens now declares which views it has, and the header renders the switch
from that declaration rather than from a name. Only tasks declares more
than one today; a second lens gaining views needs no change to the shape
that stores the choice.

The board kept everything that was never board-specific: the error line,
the skeleton, the empty state, and the label and count it publishes to
the header. Those moved up to the lens, so both views share them and the
list was a new file plus an entry rather than an edit.
The stylesheet was the only thing told which scheme was showing, but the
close and minimise controls are drawn by the OS from the window's own
appearance. Choosing dark while the system stayed light left them drawn
for a light window over a dark toolbar, which is why they only appeared
on hover.

The choice now reaches the window as well. Following the system passes
no choice at all, rather than resolving it and asserting the answer, so
the OS keeps deciding.
The accent rework, the tasks list view, the single refresh control and
the header count all landed undocumented. The MCP page still promised
that an omitted workspace falls back for writes as well as reads, which
the server no longer does.

Corrects one claim in passing: reads still resolve to the user workspace,
so a session with nothing set up can look around. Only writes refuse.
The default variant carried the badge's link-only hover idiom, which
compiles to a rule matching anchors alone. No hover rule reached a real
button, so every primary button in the app was inert under the pointer.

Reinstalled from the registry rather than hand-edited. Git history shows
the variant was never a deliberate local change.
Measured in a running window for the first time: the controls sat 2px
high, and the gap beside them was 6.5px rather than the 12px intended.

Two wrong numbers. The vertical value is not a top offset: the platform
centres the controls inside a band of button height plus that value, so
it has to be the bar height minus the button height. And the group spans
60px, not the 54 assumed when nobody could measure it.
The first pass asked whether the shared workspace carries a marker,
which is a question about the folder rather than about the caller. Once
it has been set up, and it always has been after first use, every
unplaced write resolved to it as though it had been chosen, so the
original complaint was never actually fixed.

The resolver now asks whether anything covering the caller's location
names a workspace. The shared one counts only when the caller is
standing inside it, which the walk-up cannot report for itself because
it looks one level down.

Standing in the home directory does not count. A shell starts there, so
it says nothing about intent, and admitting it would leave the hole open
for any agent launched from home.

Also fixes the walk-up comparing paths literally, which missed when the
working directory and the home variable spelled the same folder
differently and let the shared workspace be reported as a project.
The page still described the refusal as firing only when the shared
workspace has never been set up. It fires wherever the agent is working
outside a workspace, which is the case that matters.
… bright

Three things the first look at a running window exposed.

Slate was full chroma on a blue hue, so it rendered as a second blue and
the picker showed what looked like a duplicate entry. It is now a cool
near-neutral, shaped like the bronze already in the table.

The accent chips carried a faint rim, so the black one read as an empty
slot on the dark card. The rim now carries the edge, for every chip
rather than that one.

The editor drew its caret from the muted foreground, which is right for
icons and placeholder text and wrong for the one mark that must stand
out more than the text around it. Carets now have their own colour.

Inline code went the same way: the editor coloured it from its own
palette while the reader used the muted chip, so one document had two
looks depending on the mode.
Nothing anchored the page height, so the shell sized itself off the
viewport with a minimum rather than a height. A minimum gives the panes
nothing definite to divide, so a long document grew the page instead of
scrolling inside its own pane: a second scrollbar appeared beside the
document's own, and scrolling it carried the tab bar off the top.

The height now starts at the root and every panel fills its parent.
A draft was seen disappearing once when the theme was toggled with the
editor open, and could not be reproduced. Tracing the path shows it
cannot: applying a theme touches only DOM React does not own, and
nothing on the editor's mounting path derives from anything it changes.

The test drives every scheme and accent change against a live editor
holding unsaved text and asserts the same instance survives. It was
checked against a deliberately keyed editor, which it catches, so it
would fail if the mechanism were ever introduced.
The refusal had tests for the cases it blocks but none for the two it
must let through: a write from inside a project workspace, and reads
from anywhere. Both are the ways the policy could overreach, and both
were only covered by the fix's own reasoning.

Each was checked by breaking the behaviour it pins.
The board lived only in the sidebar, about 260px wide, so three status
columns would have been 80px each and it stayed a stacked list. Giving
it width meant the main area had to hold something that is not a file,
which it had never done: a tab was a path read from disk, and the tabs
it remembered were a list of paths.

A tab now points at a kind and a reference, and the kind says whether
anything is read from disk at all. Loading, watching, editing, checkbox
writes and scroll memory all follow from that one answer rather than
from asking what a tab is. The pane looks its content up by kind, so it
no longer knows what a file is.

Remembered tabs carry the new shape and read the old one, so an upgrade
keeps its tabs. Note the write is one way: a store rewritten by this
version reads as empty to an older build.

The side-by-side board is only reachable in the main area, where the
width exists. The sidebar keeps its own two views and its own switch.
Found by watching the app run rather than by reading it.

The entry point kept a listener on the system scheme for the life of the
window, so a reader who had chosen light watched the app turn dark when
the system did at sunset. Nothing corrected it: the hook that owns the
scheme only reacts to the setting changing, not to the class being
flipped underneath it. The first guess stays, since it is what stops the
window flashing the wrong background before the stored setting loads.

The tab strip asked for sideways scrolling only, but setting one axis
makes the browser compute the other to auto as well, so a stray pixel of
height painted a vertical scrollbar over the tab titles. In a split it
sat on top of the active tab's name.

The window controls were still off centre, now by two the other way. The
top edge tracks the configured value minus nine, measured at two
settings; both earlier values came from reasoning about what the platform
ought to do, and only measuring settled it.
Ten images of an interface that no longer exists: horizontal lens tabs,
the always-on sidebar search box, the file-count footer, refresh in the
window toolbar. No page links to any of them, so their only remaining
use was to be picked up by mistake.

Git history keeps them for anyone who wants to see what changed.
Two effects raced over one flag. The layout effect set it to say the
document had been placed; a plain effect cleared it whenever the tab
changed, and on mount that one always ran second. A pane that mounted
with its document already in hand therefore ended up marked as never
placed, which silently disabled the guard on scroll recording, so the
position was never remembered again. Worse, the restore stayed armed:
the next rewrite of the file, which agents do constantly here, dropped
the reader back at a stale offset.

Toggling the split does exactly that, rebuilding the pane around content
it already has.

The flag is now the path already placed, compared where it is used, so
there is nothing to clear and no second effect to order against it.
The board is already a view of the whole workspace, so the explorer
next to it repeated itself in exactly the width the columns needed.

Each tab kind now says whether it wants the explorer beside it, so the
rule is the kind's own answer rather than a test against its name. The
remembered preference is left alone while such a tab is showing, so an
ordinary document still opens the way the reader left it, and asking for
the explorer there is answered separately: the toggle keeps working,
which matters when the board is the only tab open.
Highlights from a shorter query were seen surviving into a longer one
that extends it. The painting path turns out to clear before every
paint, on both painters, and the symptom would not reproduce at the
painter, the hook, or in either browser engine.

So these tests pin the clearing rather than fix anything. Each asserts
first that the painter it means to exercise actually ran, since the two
are chosen from what the environment offers and a test that silently
exercised the other one would prove nothing.
Every shot predated the sidebar rework. They now show the lens rail, the
single toolbar, the header row and the task board, taken from the
fictional workspace the set has always used rather than from real work.

The hero shot is back on the document that carries all three things its
caption promises: a diagram, a checklist and highlighted code.

Two are dropped rather than retaken: one duplicated the split-view shot,
and the other needed a task left permanently advanced, which would have
put the rest of the set out of step with it.
The previous capture predated the change that hides the explorer next to
a full-page board, so it showed the sidebar task list and the board side
by side: exactly the duplication that change removed.
The window intercepted drag and drop before the page saw it. Its handler
reports every drag as handled, and on macOS that path returns an accepted
operation without deferring to the engine, so a card's drag started and
ended but no column was ever told a drag was over it or dropped on it.

Measured in the engine the app ships on: dragstart and dragend fired,
dragover and drop never arrived. The mechanism is visible in the webview
layer's dragging-destination overrides, which return early rather than
calling through.

The option that turns this off is documented as being needed on Windows.
It is needed here too, and nothing in the app listens for a dropped file,
so the interception was buying nothing.
Three surfaces gave three answers: the sidebar could be a list or a
grouped board and carried the switch, while the full-page tab could only
be columns and could not be switched away from.

A view is now a property of the full-page tab alone. It offers a list or
a board, both filling the content area, chosen from the toolbar and only
while such a tab is showing. The sidebar draws tasks its one compact way
and its header holds no switch, so the map of which-lens-has-which-views
goes with it: one stored value, one place it is chosen.

The columns were named after an industry term nobody outside this trade
uses. They are the board now, in the code as well as on screen, and the
sidebar's grouped rendering stops being a named view at all.

The columns also shared no width, so three of them left a fourth column
of empty space. They divide the pane and fall back to scrolling sideways
only when they genuinely cannot fit.
Every task was a flat two-line card with its details wrapped underneath
the title, so nothing separated one status from another and there was no
column to run an eye down.

Tasks now gather under a status heading that folds, and each task is one
row whose cells line up with a quiet header naming them. Alignment comes
from a single column template both the header and the rows use, so a
column cannot drift between them, and a task missing a detail leaves that
cell empty rather than shifting the row.

Folding reuses the memory the sidebar already keeps per workspace, so a
status folded in one place is folded in the other.

Only fields a task actually carries get a column. The designs this
follows show a due date and a comment count; neither exists here, and
neither was invented to fill the space.
Releases have carried install instructions and nothing else, so what
changed between them lived only in commit history.

The entries are written from what the code does rather than from commit
subjects, and the branch's own churn is left out: a fix to something
introduced on the same branch nets to nothing a reader ever saw.

The refusal to write into a workspace nobody chose is called out three
times over, since an agent relying on the old behaviour will start
getting errors.
The switch moved to the toolbar and the tasks open full-window, but both
pages still sent readers to a control in the sidebar header that is no
longer there, and called the list flat when it now groups by status.

Adds a link to the changelog, and leaves the version in the download
links alone: the release workflow rewrites those.
The search sat between two flex-1 spacers, so its position was the midpoint of
whatever flanked it. Measured in a running window, it slid 143px between a
document tab and a tasks tab: 118px from the breadcrumb shortening and 24px
from the tasks view switch widening the control cluster.

Three grid tracks with equal flex factors are given equal size, so the middle
track holds still whatever the sides do. Measured at 0.0px of movement across
both states. It rests at the centre of the padding box, 37.5px right of the
window centre because the window-control inset pads only the left; that offset
is constant, which is the property that was missing.
Captures the single toolbar, the lens rail, the full-window task board with
the explorer stepped aside, and the corrected window controls.
@anbturki
anbturki merged commit c90b874 into main Jul 23, 2026
3 checks passed
@anbturki
anbturki deleted the feat/content-search branch July 23, 2026 02:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant