Skip to content

refactor: remove obsolete fixtures and JSONL import - #2656

Merged
jackwener merged 1 commit into
mainfrom
codex/trim-low-value-fixtures
Aug 10, 2026
Merged

refactor: remove obsolete fixtures and JSONL import#2656
jackwener merged 1 commit into
mainfrom
codex/trim-low-value-fixtures

Conversation

@jackwener

Copy link
Copy Markdown
Member

Summary

  • reduce the Desktop E2E fixture registry from about 50 historical scenarios to the 13 scenarios consumed by current E2E, alignment audit, or smoke entry points
  • remove orphaned seed generators, renderer-only fixture controls, and the 1,361-line self-referential fixture test
  • remove the retired legacy JSONL session importer, its automatic SessionStore startup wiring, tests, and 11 transcript fixtures
  • preserve process/crash/lock/race fixtures and external Codex JSONL adapters because they cover active boundaries

Impact

  • 32 files changed
  • 5,194 lines removed, 141 lines added (net -5,053)
  • fixture-related tracked files reduced from 75 to 62

Validation

  • git diff --check
  • Biome lint on all changed source files
  • TypeScript: @maka/core, @maka/storage, and Desktop renderer
  • node --test scripts/fixture-env.test.mjs

Desktop main typecheck still reports the existing scheduled-task API/dist mismatch on origin/main; no errors point to this change.

@jackwener
jackwener merged commit 90bdb30 into mainAug 10, 2026
10 checks passed
@jackwener
jackwener deleted the codex/trim-low-value-fixtures branch August 10, 2026 19:21
ARE404 added a commit to ARE404/maka-agent that referenced this pull request Aug 13, 2026
apache#2580 moved the rail's tick onto Astryx's Button. The bar the tick draws
was a direct child of the flex tick and got blockified; the Button wraps
its children in a label span, so the bar went back to normal flow as an
inline box. An inline box takes no width or height, so every bar computed
to 0x0 and the rail shipped invisible in 0.1.9 and 0.1.10 — present in the
DOM, painting nothing.
`display: block` on the bar restores it. Measured on the new fixture at
1280x800: the rail's box goes from 8px wide (its own padding, ticks
contributing nothing) back to the designed 22px.
This is the third time the rail has failed by rendering and not painting
— apache#2161 pinned it against a containing block as tall as the conversation,
apache#2338 parked it under macOS's overlay scrollbar — and the second time it
reached a release. The e2e coverage that would have caught all three was
deleted in apache#2462, and the multi-prompt fixtures it ran on in apache#2656, so
this adds back the smallest thing that closes the gap:
- `chat-prompt-rail`, a plain 8-prompt conversation. The rail hides itself
below three prompts, so the shipped single-prompt fixture cannot show it
at all.
- `prompt-rail.spec.ts` with one test per past failure: bars have a real
box, the rail stays inside the scrollport at both scroll extremes, and a
tick is what the pointer lands on. Three tests where the deleted suite
had nine.
Verified the first test fails on the unfixed renderer with "Expected: > 0,
Received: 0" and passes with the fix. Neither a static CSS read nor a
jsdom unit test can see any of this: jsdom has no layout engine.
Astro-Han pushed a commit that referenced this pull request Aug 13, 2026
…2923)
* fix(ui): give the prompt rail's tick bars a box again
#2580 moved the rail's tick onto Astryx's Button. The bar the tick draws
was a direct child of the flex tick and got blockified; the Button wraps
its children in a label span, so the bar went back to normal flow as an
inline box. An inline box takes no width or height, so every bar computed
to 0x0 and the rail shipped invisible in 0.1.9 and 0.1.10 — present in the
DOM, painting nothing.
`display: block` on the bar restores it. Measured on the new fixture at
1280x800: the rail's box goes from 8px wide (its own padding, ticks
contributing nothing) back to the designed 22px.
This is the third time the rail has failed by rendering and not painting
— #2161 pinned it against a containing block as tall as the conversation,
#2338 parked it under macOS's overlay scrollbar — and the second time it
reached a release. The e2e coverage that would have caught all three was
deleted in #2462, and the multi-prompt fixtures it ran on in #2656, so
this adds back the smallest thing that closes the gap:
- `chat-prompt-rail`, a plain 8-prompt conversation. The rail hides itself
below three prompts, so the shipped single-prompt fixture cannot show it
at all.
- `prompt-rail.spec.ts` with one test per past failure: bars have a real
box, the rail stays inside the scrollport at both scroll extremes, and a
tick is what the pointer lands on. Three tests where the deleted suite
had nine.
Verified the first test fails on the unfixed renderer with "Expected: > 0,
Received: 0" and passes with the fix. Neither a static CSS read nor a
jsdom unit test can see any of this: jsdom has no layout engine.
* fix(ui): make the prompt rail's hover and jump behave
Four things the rail got wrong once it was visible again, found by using it:
- A 4px gap between ticks was a band where the pointer was over the rail
and over no tick, so the dock-style hover falloff dropped out and picked
up again every few pixels of travel. The rail's `gap` moves into the
ticks' own `padding-block`: same pitch, hit boxes now tile.
- The hover preview waited 300ms before opening — Astryx's HoverCard
default, meant for a pointer crossing a wide row on its way somewhere
else. A tick is 22px of rail that nothing is on the way to, and the wait
is the one part of this hover with no motion in it. Now 120ms.
- The highlight glided 280ms to wherever a click landed, so crossing
twenty prompts read as the bar flying off across the rail. A click now
owns the highlight until its scroll settles: no glide, and the scroll
no longer walks the highlight through every prompt it passes.
- The first click into a session did nothing until the reader scrolled by
hand. See below.
That last one is a collision between Astryx's auto-follow lock and the
progressive transcript mount, and neither side is wrong on its own.
`useChatStreamScroll` unlocks on a scroll up, detected by comparing
scrollTop between events — but it ignores any scroll event that arrives
with a changed scrollHeight or offsetHeight, because Chrome fires those
when content resizes and they are not the reader moving. A jump into an
unmounted turn mounts it and the fill that follows changes scrollHeight
for several frames, so the jump's own scroll is invisible to the lock: it
stays on, and `scrollIfLocked` pulls the transcript back to the bottom.
Only a wheel gesture broke it, which takes a separate path in Astryx.
`holdJumpDestination` re-aims at the target on each height change until
the fill stops. The last of those scrolls lands with a stable height,
which is the one the lock finally reads as a scroll up. Measured on the
30-prompt fixture: clicking the first tick went to scrollTop 7042 (the
bottom) and now goes to 24 and holds.
The fixture grows from 8 prompts to 30 because the progressive mount's
initial window is 10 — at 8 the head of the transcript is already mounted
and the jump-into-unmounted-turns path never runs at all.
Coverage note: the e2e case for the first click is an end-to-end check,
not a guard. Whether the lock wins depends on which frame the fill lands
on relative to a smooth scroll still in flight, and it goes green against
the unfixed renderer often enough to be worthless as one. The guard is
the `holdJumpDestination` unit test, which drives the frames itself.
* fix(ui): own a rail jump through the mount instead of racing it
Review of #2923 found the jump's ownership bound to a clock rather than to
the navigation, and the e2e case that was supposed to guard it asserting
almost nothing. Both hold.
Jump ownership:
- A second click during a jump only replaced the target; the first click's
700ms timer still governed, and could clear the second jump mid-flight.
Each click now carries its own sequence and starts its own hold.
- The fixed window is gone. A hold runs until the progressive mount reports
the transcript filled AND nothing has moved for a few frames, so a long
transcript is never released mid-fill, and it ends the moment the reader
touches the transcript (wheel, touch, pointer, key) rather than outliving
their interest in it.
Chasing the "just release auto-follow" direction the review preferred found
that ChatLayout publishes no such seam, so this adds one — `unlockAutoFollow`
on `ChatLayoutContextValue`, exposing the scroll hook's existing `unlock`
(patch hunk + patches/README entry). It is necessary and it is not
sufficient, which the earlier framing got wrong:
- Astryx re-locks on any `scrollend` that settles near the bottom, and a
session that opens at the bottom produces exactly that while the mount is
still catching up. Releasing once at the click is undone before the jump
goes anywhere — traced: released at the click, landed at 154ms, dragged
back to the bottom by 166ms. The release is now re-asserted for the life
of the hold.
- Auto-follow is not the only thing moving the transcript. The progressive
mount's own scroll compensation holds the reader's position across each
fill step, and mounting the turn a jump asked for IS a fill step, so it
lands after the jump and restores the position the jump just left. That
one no seam can fix; it is what the hold is for.
Jumps also scroll instantly now, whatever the app's scroll-motion policy
says. A jump is a teleport the reader asked for, and an animated one does
not survive this surface: traced on the 30-prompt fixture, the smooth scroll
was cancelled by the mount's compensation and by the follow spring and
stalled two pixels from where it started.
Coverage:
- The first-click e2e case named the wrong turn (`[data-turn-id]` is the
first MOUNTED turn, whose top is already negative at the opening scroll
position, so an upper-bound-only check passed without the jump doing
anything). It now names `turn-prompt-rail-1`, bounds it on both sides, and
asserts that tick's `aria-current`.
- `emulateMedia` could not put that case on the production scroll path:
`resolveScrollMotionBehavior` collapses motion for ANY fixture, keyed on
`data-maka-e2e-fixture` rather than on the media query. Fixtures can now
ask for a behavior back (`scrollMotion`, per launch — it costs seconds of
settling per window, so only the case that needs it pays), with unit
coverage for the precedence: a fixture request never outranks a stated
preference for less motion.
- `holdJumpDestination`'s unit tests grew the two cases its rewrite is
about: it must not settle while the transcript is still filling, and it
must hand the transcript back the moment the reader touches it.
Verified 5/5 on the smooth-scroll fixture, where the previous revision lost
1 in 4. `quote-selection.spec.ts` flakes on this machine (1 in 4) at
upstream/main as well, unchanged by this branch.
Astro-Han added a commit to Astro-Han/maka-agent that referenced this pull request Aug 15, 2026
Deleting `SessionStatusTone` was right; replacing it with a private
`SessionStatus -> StatusDotVariant` table was not. `status-vocabulary.ts`
already owns "the one place a status word becomes a colour", so the
private table made a second authority, and the two disagreed: a task
waiting on a permission prompt drew `error` in the rail while the
permission centre drew `attention` for the identical condition.
Map the session enum to `StatusSemantic` and let `dotForStatus` pick the
colour. `blocked` and `waiting_for_user` are both `attention`, which is
what that semantic is defined as -- both are waiting on a person. Giving
`blocked` `error` to tell them apart used colour for a distinction colour
cannot carry; their labels and `describeBlockedReason` do that.
Restore `review` and `done`. They have no writer in current source, but
`SESSION_STATUSES` is read back out of storage, and narrowing it is a
data migration rather than a cleanup: `resolveLegacyStatus` in the JSONL
importer (removed in apache#2656) passed both values through into real SQLite
stores verbatim, and `normalizeSessionHeader` throws for the WHOLE header
on an unrecognised status, so one stored row carrying `done` fails an
entire catalog page. The migration is its own change.
`archived` and `aborted` get their dots back. They were `muted` before,
`muted` resolved to a real `neutral` dot, and dropping them to `undefined`
was a behaviour change I described as a consequence of collapsing the
layer.
Refs apache#2984
Generated-by: Claude Code
Astro-Han added a commit to Astro-Han/maka-agent that referenced this pull request Aug 15, 2026
Deleting `SessionStatusTone` was right; replacing it with a private
`SessionStatus -> StatusDotVariant` table was not. `status-vocabulary.ts`
already owns "the one place a status word becomes a colour", so the
private table made a second authority, and the two disagreed: a task
waiting on a permission prompt drew `error` in the rail while the
permission centre drew `attention` for the identical condition.
Map the session enum to `StatusSemantic` and let `dotForStatus` pick the
colour. `blocked` and `waiting_for_user` are both `attention`, which is
what that semantic is defined as -- both are waiting on a person. Giving
`blocked` `error` to tell them apart used colour for a distinction colour
cannot carry; their labels and `describeBlockedReason` do that.
Restore `review` and `done`. They have no writer in current source, but
`SESSION_STATUSES` is read back out of storage, and narrowing it is a
data migration rather than a cleanup: `resolveLegacyStatus` in the JSONL
importer (removed in apache#2656) passed both values through into real SQLite
stores verbatim, and `normalizeSessionHeader` throws for the WHOLE header
on an unrecognised status, so one stored row carrying `done` fails an
entire catalog page. The migration is its own change.
`archived` and `aborted` get their dots back. They were `muted` before,
`muted` resolved to a real `neutral` dot, and dropping them to `undefined`
was a behaviour change I described as a consequence of collapsing the
layer.
Refs apache#2984
Generated-by: Claude Code
Astro-Han added a commit that referenced this pull request Aug 15, 2026
…ary (#3033)
* fix(ui): give the task rail's status dot one meaning
The dot the rail draws for a session passed through two lossy hops and
three disagreeing sources, so it could not say what it meant.
Collapse the mapping. `SessionStatus -> SessionStatusTone ->
StatusDotVariant` becomes one table from status to Astryx's variant. Two
of the seven tones had no distinct outcome at the end of that chain, and
the collapse is what let `waiting_for_user` and `blocked` share
`warning`: blocked now maps to `error`, so "cannot proceed until you fix
a connection" and "holding a question for you" stop rendering alike.
Read the running authority. `runningTurnIds` is the runtime's projection
of the runs it holds; `session.ts` states why a persisted `status` cannot
serve that purpose, and `settledSessionTransientIds` already reads it
first over the same list. The row read neither -- a renderer-local
streaming set, then the stored `status` -- so a task running under a bot
channel or a second window read as idle. The streaming set stays below
it, for the gap between this renderer sending a turn and the host
reporting it back.
Delete what nothing wrote. `review` and `done` were never written by
anything, in any version, so no stored record can carry them and no
reader of them was reachable; they leave `SESSION_STATUSES`, the wire
enum, and the copy tables. `SessionLifecycleStatus` now aliases
`SessionStatus` instead of restating it -- three hand-written copies of
one enum is how those two values survived in two of them after the last
writer went away.
Delete the tautological filter. `SessionFilter` had one value left:
`archived` moved to Settings in #2985 and `flagged` was never selected,
which left a control whose answer is always the same answer. The 「会话」
row that selected it goes with it, as does the branch in
`sessionMatchesNavSelection`. A stored filter is dropped rather than
validated, which is the migration.
Also gone: `SessionStatusPresentation.interactive`, which had no reader,
and a doc comment naming a `SessionStatusIcon` and a chat-header badge as
the tone matrix's consumers -- neither exists.
`status: 'archived'` still duplicates `isArchived`. Consolidating those
two rewrites stored rows, so it is #2984's PR 3, not this one.
Refs #2984
Generated-by: Claude Code
* refactor(ui): rebuild the task row on two signal slots
Every task row in the rail now carries exactly two signal slots. Slot 1 is
a single status dot, reserved as an 8px gutter even when there is nothing to
show so titles stay aligned down the list. Slot 2 holds a compact timestamp
at rest and swaps to the ⋯ menu on hover or keyboard focus, so the menu is
no longer permanently mounted next to every title.
The dot's meaning is resolved once, in priority order: live run (accent,
pulsing) › persisted status › unread (accent, inactive rows only). The live
run reads `runningTurnIds` first, matching `settledSessionTransientIds` over
the same session list — a persisted `running` status can be left behind by a
crash, so it must not outrank the authoritative projection.
The stale pill is gone: it duplicated the timestamp it sat next to. The
worktree mark falls back to the row tooltip rather than competing for a slot.
`RelativeTime` gains `variant="compact"` instead of a second component; a
260px rail row cannot fit the medium-date-plus-time past-horizon fallback.
Generated-by: Claude Code
* refactor(ui): give the rail one top region and one grouping switch
The rail's top region now ends with the grouping switch, and the history
below it starts directly at its first group.
The list heading is gone. "会话" labelled a list that is the only thing under
it, inside a landmark that already names the panel — and it existed mainly to
give the grouping switch somewhere to sit. The switch moves into the sticky
top region instead, where it stays reachable: hung off a heading it scrolled
away with the very list it switches.
It is also full width with text labels now. Two icon-only segments asked the
rail to teach a clock and a folder, and the rail has nowhere to teach them;
按时间 / 按项目 is the entire vocabulary and fits inside the minimum 180px
width with room to spare. Collapsed at 48px it does not render at all — no
segment label fits, and the list it governs is not rendered either.
The group headers lose their ➕. It was the 新任务 row's handler under a
second name, three rows below the row itself, and it needed that second name
precisely because two controls cannot share one accessible name.
Generated-by: Claude Code
* feat(desktop): move task import out of the rail into settings
导入 was a rail row that opened a modal over the conversation. Import is not
navigation: it is a rare setup errand, it needs a source, an archived filter
and a paged directory of someone else's conversations to work through, and
none of that belongs in a 260px column of the tasks you are working on. It
becomes 设置 › 活动 › 导入任务, next to 已归档任务 — both are about the task
catalog rather than the task in front of you.
As a page the flow loses its selection state. A dialog needed one because its
single primary action sat in a footer far from the rows; a page puts 导入 on
the row itself, the way 已归档任务 puts 恢复 on its rows. That also retires
`ExternalSessionImportLifecycle`, whose whole job was refusing to close a
dialog mid-import — there is no dialog left to hold open, and an import that
outlives the page is guarded by the mounted ref instead, so it can still
finish but can no longer steer a shell the user has navigated away from.
The source switch now appears only when there is more than one source. Codex
is the only adapter that ships, so in practice every user was being shown a
segmented control with a single segment — a control nobody can operate. With
one source the section names it instead.
Deliberately not carried over: keeping an imported task in sync with its
source. The importer is a one-shot conversion and nothing behind it watches
the source, so that control would be a promise no coordinator can keep.
Generated-by: Claude Code
* refactor: name the thing a user works on 任务 / task everywhere
The product called the same object 会话, 对话, session and conversation
depending on which file you landed in. Code, protocol and storage keep
`session` — that is a durable contract with a wire format behind it — but
every string a user reads now says 任务 / task.
The rule is what the noun REFERS TO, not the word:
- The Maka entity a user opens, renames, archives and searches → 任务 / task.
- Conversing as a behavior stays 对话 / conversation: "与 Maka 对话" over a
bot, "可用于对话的模型", "对话记录" as a message log.
- Another agent's stored conversations stay 对话 / conversation. 导入任务
lists Codex's conversations and produces Maka tasks; collapsing both onto
one word would erase exactly the distinction that page is about.
- 侧边对话 stays: it is a chat beside a task, not a task in the rail.
Two collisions had to be resolved rather than renamed. The per-task ledger
was 会话任务, which would have become 任务任务 — its items are 待办 / to-dos,
which is what they always were. And the automations row read "定时任务,N 个
未完成任务", two different meanings of 任务 in one label; the count now says
"N 条进行中".
The keyboard sheet loses its ←/→ row. It documented cycling between
会话/已标记/已归档, a filter this PR removed along with `SessionFilter`.
Generated-by: Claude Code
* test(ui): make the narrow-rail story actually narrow
`LongTitlesAndNarrow` framed the panel at 176px but let SideNav keep its own
260px `resizable` width, so the story clipped a wide rail instead of showing
a narrow one — the timestamps it exists to check were outside the frame. It
now drives the rail's own width, at 180px, which is the panel's `minWidth`
and therefore the narrowest state a user can reach.
Generated-by: Claude Code
* fix(ui): keep the rail's 任务 row as the way back to the list
bb39b33 deleted `SessionFilter` and took the 「会话」 row with it, on the
reading that the row only selected a filter whose answer was always the
same answer. Half right: its `isSelected` read the dead filter, but its
`onClick` selected the SECTION, and it was the only control that did.
Without it the rail has three sections and two rows. Collapsed at 48px —
the default state (`readSessionListCollapsed`) — the list is not rendered
at all, so 扩展 and 定时任务 became one-way doors: the only way back to a
running task was 新任务, which answers "show me my tasks" by starting
another one. `streaming-remount.spec.ts` walks exactly that path and had
been rewritten to click a task row that a collapsed rail does not have.
Restore the row as 任务, selected on `section === 'sessions'` and
selecting the section with no filter. The filter deletion stands.
Refs #2984
Generated-by: Claude Code
* fix(ui): route task status through the shared semantic layer
Deleting `SessionStatusTone` was right; replacing it with a private
`SessionStatus -> StatusDotVariant` table was not. `status-vocabulary.ts`
already owns "the one place a status word becomes a colour", so the
private table made a second authority, and the two disagreed: a task
waiting on a permission prompt drew `error` in the rail while the
permission centre drew `attention` for the identical condition.
Map the session enum to `StatusSemantic` and let `dotForStatus` pick the
colour. `blocked` and `waiting_for_user` are both `attention`, which is
what that semantic is defined as -- both are waiting on a person. Giving
`blocked` `error` to tell them apart used colour for a distinction colour
cannot carry; their labels and `describeBlockedReason` do that.
Restore `review` and `done`. They have no writer in current source, but
`SESSION_STATUSES` is read back out of storage, and narrowing it is a
data migration rather than a cleanup: `resolveLegacyStatus` in the JSONL
importer (removed in #2656) passed both values through into real SQLite
stores verbatim, and `normalizeSessionHeader` throws for the WHOLE header
on an unrecognised status, so one stored row carrying `done` fails an
entire catalog page. The migration is its own change.
`archived` and `aborted` get their dots back. They were `muted` before,
`muted` resolved to a real `neutral` dot, and dropping them to `undefined`
was a behaviour change I described as a consequence of collapsing the
layer.
Refs #2984
Generated-by: Claude Code
* fix(ui): keep the task row's facts when it collapses to two slots
The row draws one dot and one trailing slot. That was implemented by
collapsing to a single signal inside the resolver, which made the visual
element the only carrier of the fact -- so removing the trailing `Badge`
removed "stale" itself, not just its pill. `staleTitle` and
`staleAriaLabel` were left with no reader at all, the surviving `opacity`
cue is cancelled on the selected row by design, and opacity announces
nothing. Worktree went the same way: an `aria-label`ed icon inside the
button became a `title` on a non-interactive ancestor, which contributes
to neither the button's name nor its description.
Resolve a LIST of signals in priority order, draw `signals[0]`, and put
the rest -- plus worktree and the absolute timestamp -- in one visually
hidden span inside the button. Two slots, same pixels, and the facts stop
depending on which of them had room. Signals also stop eating each other:
`aborted` now keeps its own dot instead of falling through to the unread
branch, where an aborted task with unread text drew the same accent dot
as a running one.
Stale joins the list as `attention`, so a stale task finally has a signal
that survives being selected. It is resolved here rather than in
`presentSessionStatus` because it is a renderer-derived fact, not a
persisted `SessionStatus`.
Drop the `runningTurnIds` read. No `SessionSummary` reaching Desktop
carries that field: Runtime Host's catalog reads storage directly, the
wire projection does not list it, and Desktop Main's converter copies
persisted fields only -- the identifier appears nowhere in
`packages/runtime-host/src` or `apps/desktop/src/main` outside a test
fixture. It was a read of something nothing populates, described as
reading the authority. `streaming` is what the rail actually has, with
the limit that it only knows this renderer's turns; a real live-run
projection from the Host is its own change.
Also: `pointer-events: none` on the resting ⋯, which sat invisible at
`z-index: 1` over the trailing slot and swallowed clicks; and the inner
list no longer repeats the rail's own accessible name.
Refs #2984
Generated-by: Claude Code
* fix(desktop): refresh the catalog when an import's outcome is unknown
`external-sessions:import` emits `sessions:changed` on success, so a task
imported while the user walks away from Settings still reaches the rail
on its own -- the comment in `app-shell.tsx` claiming nothing notifies
the shell was wrong, and is corrected here.
The gap is the other branch. `commit_outcome_unknown` means the Host
cannot say whether the task was committed, and that path returned a
result without emitting anything. The only trace was the page's own
banner, and 导入任务 is a Settings page: leaving it unmounts the banner,
which is exactly when someone comes back and imports the same
conversation again. Emit there too, with no id, because not knowing which
task landed is what the code means.
Also return the import promise from `clickAction` instead of `void`-ing
it. Astryx's Button awaits it and drops repeat clicks until it settles;
discarding it left double-submit to `importingId` alone, one render
behind the second click.
Refs #2984
Generated-by: Claude Code
* fix(desktop): re-point the structural gates the surface move broke
Deleting the import dialog left exact-string readers behind. `scripts/
check-astryx-alignment.mjs` read the dialog and its stylesheet by path,
so running it exited 1 with ENOENT; the surface inventory still listed
both files and had no row for the page that replaced them.
The dialog's Item-row guards are not migrated, they are dropped. They
checked that a SELECTED row stayed keyboard-reachable -- no parent role
stealing Item's native button, no selected-only tabIndex trapping focus.
设置 › 活动 › 导入任务 has no selection: 导入 sits on each row the way 恢复
does on the archived page. Asserting those smells against the new page
would guard a shape it does not have. The button guard and a ListItem
import check follow the surface to the page.
Inventory rows for the new page and for #2985's archived-tasks page,
which was never added. Three runtime-host entries stay missing; they
predate this branch.
Also read the Daily Review model label from its copy table instead of a
literal. The story matched '跟随对话默认', which the rename retired, so it
silently found nothing -- and `storybook-visual-smoke.mjs` disables every
`play` function, so CI could not report it.
Refs #2984
Generated-by: Claude Code
* refactor: finish the 任务 rename where the rule missed
The rename was applied by rule and reviewed by hand, which is the process
that produces both halves of this: strings that kept the old noun, and
strings that took the new one where the old was right.
Same-surface contradictions, which are what a user actually notices:
- The to-do panel's error banner said 任务载入失败 next to a button
labelled 重新载入待办 -- one `Banner`, two vocabularies.
- `chat-readiness` said 当前任务 and then 新建会话 in one sentence.
- The bot's help text offered to 清空当前对话开新会话 while the reply to
that same command already said 任务已重置.
- 定时任务 described itself as 安排定时任务,并回顾本机任务, and its
delivery option read Agent 任务执行 -- two meanings of 任务 in one form.
- `settlementFailed` called the side chat a 任务 and then a 侧边对话;
`forkSetupFailed` called it 追问任务, a third name for one object. Both
are 侧边对话 now, matching the en twins.
zh/en divergence, where the catalogs share keys but stopped sharing
meaning: Daily Review counted 任务 in zh and conversations in en (and its
en empty state said conversations on one branch, tasks on the other); the
terminal panel said "task" and then "the session's terminal".
Missed outright: `tool-activity/copy.ts` (由源会话管理 on every terminal
result chip), `connection-error-copy.ts`, and the CLI's own prose
(`allow for session`, `Give this session full access`, `--continue`).
Kept deliberately: 和 Maka 对话 as a behaviour, Codex's stored 对话 on the
import page, 侧边对话, 对话框 for dialog, and `<session-id>` as a CLI
contract. The palette's `nav:sessions` keywords get 会话 and 对话 back --
collapsing both onto 任务 left a duplicate entry and dropped the words a
long-time user would still type.
Refs #2984
Generated-by: Claude Code
* fix(ui): route the task row's last two dots through the shared vocabulary
`streaming` and `hasUnread` still wrote `'accent'` by hand while the status
and stale signals resolved through `dotForStatus`. Half a function deciding
its own colours is the drift this change set out to close -- the literal is
what let the rail and Settings disagree about the same fact before. Both are
`active`: streaming is the system working on it, and unread text is
"something happened here" rather than a question waiting on the user, which
is the line the two semantics exist to draw. `dotForStatus('active')` is
`'accent'`, so nothing moves on screen.
Also drops the `copy` binding `SessionHistoryList` no longer reads. It fed
the list's own aria-label, and the outer SideNav took that name over when the
rail was rebuilt.
Refs #2984
Generated-by: Claude Code
* docs: say what each session-status-presentation file actually owns
Desktop's copy claimed the status-to-dot mapping was "re-exported below" and
that `describeBlockedReason` was defined there. Both moved to `@maka/ui` in
this branch and neither is in the file; what is left is Desktop-only judgment
-- which blocked reasons are worth acting on, and what to offer after a turn
fails -- so the header says that instead.
The contract the old header carried is real and had nowhere to live after the
move: a UI label must never show a raw `SessionBlockedReason`, and a new
reason has to extend the core enum and the copy matrix together or it reads as
`unknown`. It now sits on `describeBlockedReason` in `@maka/ui`, where the
matrix it constrains is.
Refs #2984
Generated-by: Claude Code
* test: make the restored-task and import fixtures answer like their sources
Both fixtures modelled states their real source cannot produce, which makes
them evidence for nothing.
The purge tests built a restored task by flipping `isArchived` alone, leaving
`status: 'archived'` behind. `SessionStore.unarchive` writes both fields
together, so no stored row looks like that. The sweep only reads `isArchived`
today, so no assertion changes -- the point is that the next assertion written
against these rows would be checking a state that cannot happen. A `restored`
helper names the pair once.
The 导入任务 story's `list` ignored `includeArchived` and `cursor`: it rendered
the archived conversation while the filter was off and handed 加载更多 the same
first page forever, under a comment claiming it demonstrated both controls.
It now filters and pages, with a fourth conversation so the default view is a
short first page rather than the whole list.
Refs #2984
Generated-by: Claude Code
* fix(desktop): tidy the two places the 任务 rename passed through twice
`fake_backend` reaches the user down two paths -- the reason table, and the
header check in `assertSessionCanSend`, which never gets far enough to look a
connection up. They said the same sentence in two copies, so renaming 会话 to
任务 had to be done twice. One `FAKE_BACKEND_MESSAGE` now.
The English workspace help came out of the rename as "Any task can switch next
to the input box", which makes the task the actor and never names what it
switches. It says the project.
Refs #2984
Generated-by: Claude Code
* chore: regenerate the Astryx surface inventory
The Markdown half still listed `external-session-import-dialog.tsx` and
`external-session-import.css` after this branch deleted them, because the two
halves were edited by hand and `check-astryx-surface-inventory.mjs` only
verifies that on-disk files appear in the inventory -- never that inventory
rows still exist on disk. It is also not wired into CI, so the drift was
invisible from both directions.
Running the generator also picks up three files that arrived from main and
were never listed: `runtime-host-profiles-section.tsx`,
`runtime-host-ssh-terminal-dialog.tsx`, and `settings/runtime-host.css`. They
are unrelated to this branch, but a generated file regenerated in halves is
how it drifted in the first place.
Refs #2984
Generated-by: Claude Code
* test(ui): show every task status in the story that exists to show them
Two gaps, both found by shooting the story for a before/after comparison.
`review` and `done` were dropped from `statusSessions` when this branch briefly
deleted them from `SESSION_STATUSES`. The enum values and their labels came
back; the fixture did not, so the story that covers every status covered six of
eight -- and the two missing ones are exactly the two whose colours this change
decided on purpose, `attention` and `success`.
`StoryFrame` also defaulted to 240px while `SessionListPanel`'s rail defaults to
260, so every story that did not pass a width was clipping the rail by 20px.
That lands on the trailing slot, which is where this change puts the timestamp
-- the stories could not show whether it fits. Stories that want a narrow rail
still pass the width to both, which is what the note on `panelProps` is about.
Refs #2984
Generated-by: Claude Code
* refactor(ui): drop the rail's 任务 row
Expanded, the row selected a section whose content was already on screen
directly under it -- the same redundancy as the 会话 list heading this change
deleted one row down, and clicking any task row does the same selection.
It was kept for the collapsed rail, where the list is not rendered, on the
argument that 扩展 and 定时任务 were otherwise one-way doors. That was wrong.
Collapsed, the rail cannot switch tasks at all, so getting back to any task
already means widening it; the titlebar's 展开侧边栏 toggle does that
unconditionally (app-shell-chrome-actions.tsx renders `ChromeColumnToggle` with
no guard), and `activeId` survives a section change, so the widened rail comes
back with the task you left already marked. The row bought one click in a state
the user is leaving regardless, and charged a permanent slot for it.
`sessions` now has no control of its own on the rail. It is where you are
unless you went somewhere, which is why the other two sections light up and
this one has nothing to light.
`streaming-remount` returns the way the product now offers -- widen, then click
the row carrying `aria-current="page"` -- which is a better assertion than the
old one anyway: it checks that the task survives the trip, not just that a
button exists.
Refs #2984
Generated-by: Claude Code
* fix(runtime-host): coalesce a repeat import while the first is still running
Moving 导入任务 from a dialog into a Settings page removed the only thing
stopping one intent from becoming two tasks, and this branch is where that
happened. `ExternalSessionImportLifecycle` refused to close the dialog while an
import was active; retiring it was described here as dropping a guard against
nothing. It was not. The page it was replaced by is one the user may leave
mid-import -- the import continues in Desktop Main by design -- and the page's
`importingId` dies with it, so coming back and pressing 导入 again issues a
second request against a source that is still importing. Nothing downstream
deduplicates: `treats repeats as independent copies` is a pinned behaviour, so
the second request lands a second task the user now has to tell apart.
The guard belongs to the Host, not to the page that asked. Import is a Host
operation and the Host is the only party that knows one is running; a client
knows about its own requests, which is why a second window or the CLI would
have reproduced this with the page's state intact.
Concurrent repeats collapse onto the first attempt's promise and both callers
get its outcome, success or failure, because it is one operation. Sequential
repeats are untouched -- the entry is gone by the time the first settles -- so
importing the same conversation again on purpose still makes an independent
copy, which the existing test continues to pin.
Reported by @M4n5ter in review of #3033.
Refs #2984
Generated-by: Claude Code
Sign up for freeto 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

@jackwener
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
refactor: remove obsolete fixtures and JSONL import by jackwener · Pull Request #2656 · apache/maka · GitHub
Skip to content

refactor: remove obsolete fixtures and JSONL import - #2656

Merged
jackwener merged 1 commit into
mainfrom
codex/trim-low-value-fixtures
Aug 10, 2026
Merged

refactor: remove obsolete fixtures and JSONL import#2656
jackwener merged 1 commit into
mainfrom
codex/trim-low-value-fixtures

Conversation

@jackwener

Copy link
Copy Markdown
Member

Summary

  • reduce the Desktop E2E fixture registry from about 50 historical scenarios to the 13 scenarios consumed by current E2E, alignment audit, or smoke entry points
  • remove orphaned seed generators, renderer-only fixture controls, and the 1,361-line self-referential fixture test
  • remove the retired legacy JSONL session importer, its automatic SessionStore startup wiring, tests, and 11 transcript fixtures
  • preserve process/crash/lock/race fixtures and external Codex JSONL adapters because they cover active boundaries

Impact

  • 32 files changed
  • 5,194 lines removed, 141 lines added (net -5,053)
  • fixture-related tracked files reduced from 75 to 62

Validation

  • git diff --check
  • Biome lint on all changed source files
  • TypeScript: @maka/core, @maka/storage, and Desktop renderer
  • node --test scripts/fixture-env.test.mjs

Desktop main typecheck still reports the existing scheduled-task API/dist mismatch on origin/main; no errors point to this change.

@jackwener
jackwener merged commit 90bdb30 into mainAug 10, 2026
10 checks passed
@jackwener
jackwener deleted the codex/trim-low-value-fixtures branch August 10, 2026 19:21
ARE404 added a commit to ARE404/maka-agent that referenced this pull request Aug 13, 2026
apache#2580 moved the rail's tick onto Astryx's Button. The bar the tick draws
was a direct child of the flex tick and got blockified; the Button wraps
its children in a label span, so the bar went back to normal flow as an
inline box. An inline box takes no width or height, so every bar computed
to 0x0 and the rail shipped invisible in 0.1.9 and 0.1.10 — present in the
DOM, painting nothing.
`display: block` on the bar restores it. Measured on the new fixture at
1280x800: the rail's box goes from 8px wide (its own padding, ticks
contributing nothing) back to the designed 22px.
This is the third time the rail has failed by rendering and not painting
— apache#2161 pinned it against a containing block as tall as the conversation,
apache#2338 parked it under macOS's overlay scrollbar — and the second time it
reached a release. The e2e coverage that would have caught all three was
deleted in apache#2462, and the multi-prompt fixtures it ran on in apache#2656, so
this adds back the smallest thing that closes the gap:
- `chat-prompt-rail`, a plain 8-prompt conversation. The rail hides itself
below three prompts, so the shipped single-prompt fixture cannot show it
at all.
- `prompt-rail.spec.ts` with one test per past failure: bars have a real
box, the rail stays inside the scrollport at both scroll extremes, and a
tick is what the pointer lands on. Three tests where the deleted suite
had nine.
Verified the first test fails on the unfixed renderer with "Expected: > 0,
Received: 0" and passes with the fix. Neither a static CSS read nor a
jsdom unit test can see any of this: jsdom has no layout engine.
Astro-Han pushed a commit that referenced this pull request Aug 13, 2026
…2923)
* fix(ui): give the prompt rail's tick bars a box again
#2580 moved the rail's tick onto Astryx's Button. The bar the tick draws
was a direct child of the flex tick and got blockified; the Button wraps
its children in a label span, so the bar went back to normal flow as an
inline box. An inline box takes no width or height, so every bar computed
to 0x0 and the rail shipped invisible in 0.1.9 and 0.1.10 — present in the
DOM, painting nothing.
`display: block` on the bar restores it. Measured on the new fixture at
1280x800: the rail's box goes from 8px wide (its own padding, ticks
contributing nothing) back to the designed 22px.
This is the third time the rail has failed by rendering and not painting
— #2161 pinned it against a containing block as tall as the conversation,
#2338 parked it under macOS's overlay scrollbar — and the second time it
reached a release. The e2e coverage that would have caught all three was
deleted in #2462, and the multi-prompt fixtures it ran on in #2656, so
this adds back the smallest thing that closes the gap:
- `chat-prompt-rail`, a plain 8-prompt conversation. The rail hides itself
below three prompts, so the shipped single-prompt fixture cannot show it
at all.
- `prompt-rail.spec.ts` with one test per past failure: bars have a real
box, the rail stays inside the scrollport at both scroll extremes, and a
tick is what the pointer lands on. Three tests where the deleted suite
had nine.
Verified the first test fails on the unfixed renderer with "Expected: > 0,
Received: 0" and passes with the fix. Neither a static CSS read nor a
jsdom unit test can see any of this: jsdom has no layout engine.
* fix(ui): make the prompt rail's hover and jump behave
Four things the rail got wrong once it was visible again, found by using it:
- A 4px gap between ticks was a band where the pointer was over the rail
and over no tick, so the dock-style hover falloff dropped out and picked
up again every few pixels of travel. The rail's `gap` moves into the
ticks' own `padding-block`: same pitch, hit boxes now tile.
- The hover preview waited 300ms before opening — Astryx's HoverCard
default, meant for a pointer crossing a wide row on its way somewhere
else. A tick is 22px of rail that nothing is on the way to, and the wait
is the one part of this hover with no motion in it. Now 120ms.
- The highlight glided 280ms to wherever a click landed, so crossing
twenty prompts read as the bar flying off across the rail. A click now
owns the highlight until its scroll settles: no glide, and the scroll
no longer walks the highlight through every prompt it passes.
- The first click into a session did nothing until the reader scrolled by
hand. See below.
That last one is a collision between Astryx's auto-follow lock and the
progressive transcript mount, and neither side is wrong on its own.
`useChatStreamScroll` unlocks on a scroll up, detected by comparing
scrollTop between events — but it ignores any scroll event that arrives
with a changed scrollHeight or offsetHeight, because Chrome fires those
when content resizes and they are not the reader moving. A jump into an
unmounted turn mounts it and the fill that follows changes scrollHeight
for several frames, so the jump's own scroll is invisible to the lock: it
stays on, and `scrollIfLocked` pulls the transcript back to the bottom.
Only a wheel gesture broke it, which takes a separate path in Astryx.
`holdJumpDestination` re-aims at the target on each height change until
the fill stops. The last of those scrolls lands with a stable height,
which is the one the lock finally reads as a scroll up. Measured on the
30-prompt fixture: clicking the first tick went to scrollTop 7042 (the
bottom) and now goes to 24 and holds.
The fixture grows from 8 prompts to 30 because the progressive mount's
initial window is 10 — at 8 the head of the transcript is already mounted
and the jump-into-unmounted-turns path never runs at all.
Coverage note: the e2e case for the first click is an end-to-end check,
not a guard. Whether the lock wins depends on which frame the fill lands
on relative to a smooth scroll still in flight, and it goes green against
the unfixed renderer often enough to be worthless as one. The guard is
the `holdJumpDestination` unit test, which drives the frames itself.
* fix(ui): own a rail jump through the mount instead of racing it
Review of #2923 found the jump's ownership bound to a clock rather than to
the navigation, and the e2e case that was supposed to guard it asserting
almost nothing. Both hold.
Jump ownership:
- A second click during a jump only replaced the target; the first click's
700ms timer still governed, and could clear the second jump mid-flight.
Each click now carries its own sequence and starts its own hold.
- The fixed window is gone. A hold runs until the progressive mount reports
the transcript filled AND nothing has moved for a few frames, so a long
transcript is never released mid-fill, and it ends the moment the reader
touches the transcript (wheel, touch, pointer, key) rather than outliving
their interest in it.
Chasing the "just release auto-follow" direction the review preferred found
that ChatLayout publishes no such seam, so this adds one — `unlockAutoFollow`
on `ChatLayoutContextValue`, exposing the scroll hook's existing `unlock`
(patch hunk + patches/README entry). It is necessary and it is not
sufficient, which the earlier framing got wrong:
- Astryx re-locks on any `scrollend` that settles near the bottom, and a
session that opens at the bottom produces exactly that while the mount is
still catching up. Releasing once at the click is undone before the jump
goes anywhere — traced: released at the click, landed at 154ms, dragged
back to the bottom by 166ms. The release is now re-asserted for the life
of the hold.
- Auto-follow is not the only thing moving the transcript. The progressive
mount's own scroll compensation holds the reader's position across each
fill step, and mounting the turn a jump asked for IS a fill step, so it
lands after the jump and restores the position the jump just left. That
one no seam can fix; it is what the hold is for.
Jumps also scroll instantly now, whatever the app's scroll-motion policy
says. A jump is a teleport the reader asked for, and an animated one does
not survive this surface: traced on the 30-prompt fixture, the smooth scroll
was cancelled by the mount's compensation and by the follow spring and
stalled two pixels from where it started.
Coverage:
- The first-click e2e case named the wrong turn (`[data-turn-id]` is the
first MOUNTED turn, whose top is already negative at the opening scroll
position, so an upper-bound-only check passed without the jump doing
anything). It now names `turn-prompt-rail-1`, bounds it on both sides, and
asserts that tick's `aria-current`.
- `emulateMedia` could not put that case on the production scroll path:
`resolveScrollMotionBehavior` collapses motion for ANY fixture, keyed on
`data-maka-e2e-fixture` rather than on the media query. Fixtures can now
ask for a behavior back (`scrollMotion`, per launch — it costs seconds of
settling per window, so only the case that needs it pays), with unit
coverage for the precedence: a fixture request never outranks a stated
preference for less motion.
- `holdJumpDestination`'s unit tests grew the two cases its rewrite is
about: it must not settle while the transcript is still filling, and it
must hand the transcript back the moment the reader touches it.
Verified 5/5 on the smooth-scroll fixture, where the previous revision lost
1 in 4. `quote-selection.spec.ts` flakes on this machine (1 in 4) at
upstream/main as well, unchanged by this branch.
Astro-Han added a commit to Astro-Han/maka-agent that referenced this pull request Aug 15, 2026
Deleting `SessionStatusTone` was right; replacing it with a private
`SessionStatus -> StatusDotVariant` table was not. `status-vocabulary.ts`
already owns "the one place a status word becomes a colour", so the
private table made a second authority, and the two disagreed: a task
waiting on a permission prompt drew `error` in the rail while the
permission centre drew `attention` for the identical condition.
Map the session enum to `StatusSemantic` and let `dotForStatus` pick the
colour. `blocked` and `waiting_for_user` are both `attention`, which is
what that semantic is defined as -- both are waiting on a person. Giving
`blocked` `error` to tell them apart used colour for a distinction colour
cannot carry; their labels and `describeBlockedReason` do that.
Restore `review` and `done`. They have no writer in current source, but
`SESSION_STATUSES` is read back out of storage, and narrowing it is a
data migration rather than a cleanup: `resolveLegacyStatus` in the JSONL
importer (removed in apache#2656) passed both values through into real SQLite
stores verbatim, and `normalizeSessionHeader` throws for the WHOLE header
on an unrecognised status, so one stored row carrying `done` fails an
entire catalog page. The migration is its own change.
`archived` and `aborted` get their dots back. They were `muted` before,
`muted` resolved to a real `neutral` dot, and dropping them to `undefined`
was a behaviour change I described as a consequence of collapsing the
layer.
Refs apache#2984
Generated-by: Claude Code
Astro-Han added a commit to Astro-Han/maka-agent that referenced this pull request Aug 15, 2026
Deleting `SessionStatusTone` was right; replacing it with a private
`SessionStatus -> StatusDotVariant` table was not. `status-vocabulary.ts`
already owns "the one place a status word becomes a colour", so the
private table made a second authority, and the two disagreed: a task
waiting on a permission prompt drew `error` in the rail while the
permission centre drew `attention` for the identical condition.
Map the session enum to `StatusSemantic` and let `dotForStatus` pick the
colour. `blocked` and `waiting_for_user` are both `attention`, which is
what that semantic is defined as -- both are waiting on a person. Giving
`blocked` `error` to tell them apart used colour for a distinction colour
cannot carry; their labels and `describeBlockedReason` do that.
Restore `review` and `done`. They have no writer in current source, but
`SESSION_STATUSES` is read back out of storage, and narrowing it is a
data migration rather than a cleanup: `resolveLegacyStatus` in the JSONL
importer (removed in apache#2656) passed both values through into real SQLite
stores verbatim, and `normalizeSessionHeader` throws for the WHOLE header
on an unrecognised status, so one stored row carrying `done` fails an
entire catalog page. The migration is its own change.
`archived` and `aborted` get their dots back. They were `muted` before,
`muted` resolved to a real `neutral` dot, and dropping them to `undefined`
was a behaviour change I described as a consequence of collapsing the
layer.
Refs apache#2984
Generated-by: Claude Code
Astro-Han added a commit that referenced this pull request Aug 15, 2026
…ary (#3033)
* fix(ui): give the task rail's status dot one meaning
The dot the rail draws for a session passed through two lossy hops and
three disagreeing sources, so it could not say what it meant.
Collapse the mapping. `SessionStatus -> SessionStatusTone ->
StatusDotVariant` becomes one table from status to Astryx's variant. Two
of the seven tones had no distinct outcome at the end of that chain, and
the collapse is what let `waiting_for_user` and `blocked` share
`warning`: blocked now maps to `error`, so "cannot proceed until you fix
a connection" and "holding a question for you" stop rendering alike.
Read the running authority. `runningTurnIds` is the runtime's projection
of the runs it holds; `session.ts` states why a persisted `status` cannot
serve that purpose, and `settledSessionTransientIds` already reads it
first over the same list. The row read neither -- a renderer-local
streaming set, then the stored `status` -- so a task running under a bot
channel or a second window read as idle. The streaming set stays below
it, for the gap between this renderer sending a turn and the host
reporting it back.
Delete what nothing wrote. `review` and `done` were never written by
anything, in any version, so no stored record can carry them and no
reader of them was reachable; they leave `SESSION_STATUSES`, the wire
enum, and the copy tables. `SessionLifecycleStatus` now aliases
`SessionStatus` instead of restating it -- three hand-written copies of
one enum is how those two values survived in two of them after the last
writer went away.
Delete the tautological filter. `SessionFilter` had one value left:
`archived` moved to Settings in #2985 and `flagged` was never selected,
which left a control whose answer is always the same answer. The 「会话」
row that selected it goes with it, as does the branch in
`sessionMatchesNavSelection`. A stored filter is dropped rather than
validated, which is the migration.
Also gone: `SessionStatusPresentation.interactive`, which had no reader,
and a doc comment naming a `SessionStatusIcon` and a chat-header badge as
the tone matrix's consumers -- neither exists.
`status: 'archived'` still duplicates `isArchived`. Consolidating those
two rewrites stored rows, so it is #2984's PR 3, not this one.
Refs #2984
Generated-by: Claude Code
* refactor(ui): rebuild the task row on two signal slots
Every task row in the rail now carries exactly two signal slots. Slot 1 is
a single status dot, reserved as an 8px gutter even when there is nothing to
show so titles stay aligned down the list. Slot 2 holds a compact timestamp
at rest and swaps to the ⋯ menu on hover or keyboard focus, so the menu is
no longer permanently mounted next to every title.
The dot's meaning is resolved once, in priority order: live run (accent,
pulsing) › persisted status › unread (accent, inactive rows only). The live
run reads `runningTurnIds` first, matching `settledSessionTransientIds` over
the same session list — a persisted `running` status can be left behind by a
crash, so it must not outrank the authoritative projection.
The stale pill is gone: it duplicated the timestamp it sat next to. The
worktree mark falls back to the row tooltip rather than competing for a slot.
`RelativeTime` gains `variant="compact"` instead of a second component; a
260px rail row cannot fit the medium-date-plus-time past-horizon fallback.
Generated-by: Claude Code
* refactor(ui): give the rail one top region and one grouping switch
The rail's top region now ends with the grouping switch, and the history
below it starts directly at its first group.
The list heading is gone. "会话" labelled a list that is the only thing under
it, inside a landmark that already names the panel — and it existed mainly to
give the grouping switch somewhere to sit. The switch moves into the sticky
top region instead, where it stays reachable: hung off a heading it scrolled
away with the very list it switches.
It is also full width with text labels now. Two icon-only segments asked the
rail to teach a clock and a folder, and the rail has nowhere to teach them;
按时间 / 按项目 is the entire vocabulary and fits inside the minimum 180px
width with room to spare. Collapsed at 48px it does not render at all — no
segment label fits, and the list it governs is not rendered either.
The group headers lose their ➕. It was the 新任务 row's handler under a
second name, three rows below the row itself, and it needed that second name
precisely because two controls cannot share one accessible name.
Generated-by: Claude Code
* feat(desktop): move task import out of the rail into settings
导入 was a rail row that opened a modal over the conversation. Import is not
navigation: it is a rare setup errand, it needs a source, an archived filter
and a paged directory of someone else's conversations to work through, and
none of that belongs in a 260px column of the tasks you are working on. It
becomes 设置 › 活动 › 导入任务, next to 已归档任务 — both are about the task
catalog rather than the task in front of you.
As a page the flow loses its selection state. A dialog needed one because its
single primary action sat in a footer far from the rows; a page puts 导入 on
the row itself, the way 已归档任务 puts 恢复 on its rows. That also retires
`ExternalSessionImportLifecycle`, whose whole job was refusing to close a
dialog mid-import — there is no dialog left to hold open, and an import that
outlives the page is guarded by the mounted ref instead, so it can still
finish but can no longer steer a shell the user has navigated away from.
The source switch now appears only when there is more than one source. Codex
is the only adapter that ships, so in practice every user was being shown a
segmented control with a single segment — a control nobody can operate. With
one source the section names it instead.
Deliberately not carried over: keeping an imported task in sync with its
source. The importer is a one-shot conversion and nothing behind it watches
the source, so that control would be a promise no coordinator can keep.
Generated-by: Claude Code
* refactor: name the thing a user works on 任务 / task everywhere
The product called the same object 会话, 对话, session and conversation
depending on which file you landed in. Code, protocol and storage keep
`session` — that is a durable contract with a wire format behind it — but
every string a user reads now says 任务 / task.
The rule is what the noun REFERS TO, not the word:
- The Maka entity a user opens, renames, archives and searches → 任务 / task.
- Conversing as a behavior stays 对话 / conversation: "与 Maka 对话" over a
bot, "可用于对话的模型", "对话记录" as a message log.
- Another agent's stored conversations stay 对话 / conversation. 导入任务
lists Codex's conversations and produces Maka tasks; collapsing both onto
one word would erase exactly the distinction that page is about.
- 侧边对话 stays: it is a chat beside a task, not a task in the rail.
Two collisions had to be resolved rather than renamed. The per-task ledger
was 会话任务, which would have become 任务任务 — its items are 待办 / to-dos,
which is what they always were. And the automations row read "定时任务,N 个
未完成任务", two different meanings of 任务 in one label; the count now says
"N 条进行中".
The keyboard sheet loses its ←/→ row. It documented cycling between
会话/已标记/已归档, a filter this PR removed along with `SessionFilter`.
Generated-by: Claude Code
* test(ui): make the narrow-rail story actually narrow
`LongTitlesAndNarrow` framed the panel at 176px but let SideNav keep its own
260px `resizable` width, so the story clipped a wide rail instead of showing
a narrow one — the timestamps it exists to check were outside the frame. It
now drives the rail's own width, at 180px, which is the panel's `minWidth`
and therefore the narrowest state a user can reach.
Generated-by: Claude Code
* fix(ui): keep the rail's 任务 row as the way back to the list
bb39b33 deleted `SessionFilter` and took the 「会话」 row with it, on the
reading that the row only selected a filter whose answer was always the
same answer. Half right: its `isSelected` read the dead filter, but its
`onClick` selected the SECTION, and it was the only control that did.
Without it the rail has three sections and two rows. Collapsed at 48px —
the default state (`readSessionListCollapsed`) — the list is not rendered
at all, so 扩展 and 定时任务 became one-way doors: the only way back to a
running task was 新任务, which answers "show me my tasks" by starting
another one. `streaming-remount.spec.ts` walks exactly that path and had
been rewritten to click a task row that a collapsed rail does not have.
Restore the row as 任务, selected on `section === 'sessions'` and
selecting the section with no filter. The filter deletion stands.
Refs #2984
Generated-by: Claude Code
* fix(ui): route task status through the shared semantic layer
Deleting `SessionStatusTone` was right; replacing it with a private
`SessionStatus -> StatusDotVariant` table was not. `status-vocabulary.ts`
already owns "the one place a status word becomes a colour", so the
private table made a second authority, and the two disagreed: a task
waiting on a permission prompt drew `error` in the rail while the
permission centre drew `attention` for the identical condition.
Map the session enum to `StatusSemantic` and let `dotForStatus` pick the
colour. `blocked` and `waiting_for_user` are both `attention`, which is
what that semantic is defined as -- both are waiting on a person. Giving
`blocked` `error` to tell them apart used colour for a distinction colour
cannot carry; their labels and `describeBlockedReason` do that.
Restore `review` and `done`. They have no writer in current source, but
`SESSION_STATUSES` is read back out of storage, and narrowing it is a
data migration rather than a cleanup: `resolveLegacyStatus` in the JSONL
importer (removed in #2656) passed both values through into real SQLite
stores verbatim, and `normalizeSessionHeader` throws for the WHOLE header
on an unrecognised status, so one stored row carrying `done` fails an
entire catalog page. The migration is its own change.
`archived` and `aborted` get their dots back. They were `muted` before,
`muted` resolved to a real `neutral` dot, and dropping them to `undefined`
was a behaviour change I described as a consequence of collapsing the
layer.
Refs #2984
Generated-by: Claude Code
* fix(ui): keep the task row's facts when it collapses to two slots
The row draws one dot and one trailing slot. That was implemented by
collapsing to a single signal inside the resolver, which made the visual
element the only carrier of the fact -- so removing the trailing `Badge`
removed "stale" itself, not just its pill. `staleTitle` and
`staleAriaLabel` were left with no reader at all, the surviving `opacity`
cue is cancelled on the selected row by design, and opacity announces
nothing. Worktree went the same way: an `aria-label`ed icon inside the
button became a `title` on a non-interactive ancestor, which contributes
to neither the button's name nor its description.
Resolve a LIST of signals in priority order, draw `signals[0]`, and put
the rest -- plus worktree and the absolute timestamp -- in one visually
hidden span inside the button. Two slots, same pixels, and the facts stop
depending on which of them had room. Signals also stop eating each other:
`aborted` now keeps its own dot instead of falling through to the unread
branch, where an aborted task with unread text drew the same accent dot
as a running one.
Stale joins the list as `attention`, so a stale task finally has a signal
that survives being selected. It is resolved here rather than in
`presentSessionStatus` because it is a renderer-derived fact, not a
persisted `SessionStatus`.
Drop the `runningTurnIds` read. No `SessionSummary` reaching Desktop
carries that field: Runtime Host's catalog reads storage directly, the
wire projection does not list it, and Desktop Main's converter copies
persisted fields only -- the identifier appears nowhere in
`packages/runtime-host/src` or `apps/desktop/src/main` outside a test
fixture. It was a read of something nothing populates, described as
reading the authority. `streaming` is what the rail actually has, with
the limit that it only knows this renderer's turns; a real live-run
projection from the Host is its own change.
Also: `pointer-events: none` on the resting ⋯, which sat invisible at
`z-index: 1` over the trailing slot and swallowed clicks; and the inner
list no longer repeats the rail's own accessible name.
Refs #2984
Generated-by: Claude Code
* fix(desktop): refresh the catalog when an import's outcome is unknown
`external-sessions:import` emits `sessions:changed` on success, so a task
imported while the user walks away from Settings still reaches the rail
on its own -- the comment in `app-shell.tsx` claiming nothing notifies
the shell was wrong, and is corrected here.
The gap is the other branch. `commit_outcome_unknown` means the Host
cannot say whether the task was committed, and that path returned a
result without emitting anything. The only trace was the page's own
banner, and 导入任务 is a Settings page: leaving it unmounts the banner,
which is exactly when someone comes back and imports the same
conversation again. Emit there too, with no id, because not knowing which
task landed is what the code means.
Also return the import promise from `clickAction` instead of `void`-ing
it. Astryx's Button awaits it and drops repeat clicks until it settles;
discarding it left double-submit to `importingId` alone, one render
behind the second click.
Refs #2984
Generated-by: Claude Code
* fix(desktop): re-point the structural gates the surface move broke
Deleting the import dialog left exact-string readers behind. `scripts/
check-astryx-alignment.mjs` read the dialog and its stylesheet by path,
so running it exited 1 with ENOENT; the surface inventory still listed
both files and had no row for the page that replaced them.
The dialog's Item-row guards are not migrated, they are dropped. They
checked that a SELECTED row stayed keyboard-reachable -- no parent role
stealing Item's native button, no selected-only tabIndex trapping focus.
设置 › 活动 › 导入任务 has no selection: 导入 sits on each row the way 恢复
does on the archived page. Asserting those smells against the new page
would guard a shape it does not have. The button guard and a ListItem
import check follow the surface to the page.
Inventory rows for the new page and for #2985's archived-tasks page,
which was never added. Three runtime-host entries stay missing; they
predate this branch.
Also read the Daily Review model label from its copy table instead of a
literal. The story matched '跟随对话默认', which the rename retired, so it
silently found nothing -- and `storybook-visual-smoke.mjs` disables every
`play` function, so CI could not report it.
Refs #2984
Generated-by: Claude Code
* refactor: finish the 任务 rename where the rule missed
The rename was applied by rule and reviewed by hand, which is the process
that produces both halves of this: strings that kept the old noun, and
strings that took the new one where the old was right.
Same-surface contradictions, which are what a user actually notices:
- The to-do panel's error banner said 任务载入失败 next to a button
labelled 重新载入待办 -- one `Banner`, two vocabularies.
- `chat-readiness` said 当前任务 and then 新建会话 in one sentence.
- The bot's help text offered to 清空当前对话开新会话 while the reply to
that same command already said 任务已重置.
- 定时任务 described itself as 安排定时任务,并回顾本机任务, and its
delivery option read Agent 任务执行 -- two meanings of 任务 in one form.
- `settlementFailed` called the side chat a 任务 and then a 侧边对话;
`forkSetupFailed` called it 追问任务, a third name for one object. Both
are 侧边对话 now, matching the en twins.
zh/en divergence, where the catalogs share keys but stopped sharing
meaning: Daily Review counted 任务 in zh and conversations in en (and its
en empty state said conversations on one branch, tasks on the other); the
terminal panel said "task" and then "the session's terminal".
Missed outright: `tool-activity/copy.ts` (由源会话管理 on every terminal
result chip), `connection-error-copy.ts`, and the CLI's own prose
(`allow for session`, `Give this session full access`, `--continue`).
Kept deliberately: 和 Maka 对话 as a behaviour, Codex's stored 对话 on the
import page, 侧边对话, 对话框 for dialog, and `<session-id>` as a CLI
contract. The palette's `nav:sessions` keywords get 会话 and 对话 back --
collapsing both onto 任务 left a duplicate entry and dropped the words a
long-time user would still type.
Refs #2984
Generated-by: Claude Code
* fix(ui): route the task row's last two dots through the shared vocabulary
`streaming` and `hasUnread` still wrote `'accent'` by hand while the status
and stale signals resolved through `dotForStatus`. Half a function deciding
its own colours is the drift this change set out to close -- the literal is
what let the rail and Settings disagree about the same fact before. Both are
`active`: streaming is the system working on it, and unread text is
"something happened here" rather than a question waiting on the user, which
is the line the two semantics exist to draw. `dotForStatus('active')` is
`'accent'`, so nothing moves on screen.
Also drops the `copy` binding `SessionHistoryList` no longer reads. It fed
the list's own aria-label, and the outer SideNav took that name over when the
rail was rebuilt.
Refs #2984
Generated-by: Claude Code
* docs: say what each session-status-presentation file actually owns
Desktop's copy claimed the status-to-dot mapping was "re-exported below" and
that `describeBlockedReason` was defined there. Both moved to `@maka/ui` in
this branch and neither is in the file; what is left is Desktop-only judgment
-- which blocked reasons are worth acting on, and what to offer after a turn
fails -- so the header says that instead.
The contract the old header carried is real and had nowhere to live after the
move: a UI label must never show a raw `SessionBlockedReason`, and a new
reason has to extend the core enum and the copy matrix together or it reads as
`unknown`. It now sits on `describeBlockedReason` in `@maka/ui`, where the
matrix it constrains is.
Refs #2984
Generated-by: Claude Code
* test: make the restored-task and import fixtures answer like their sources
Both fixtures modelled states their real source cannot produce, which makes
them evidence for nothing.
The purge tests built a restored task by flipping `isArchived` alone, leaving
`status: 'archived'` behind. `SessionStore.unarchive` writes both fields
together, so no stored row looks like that. The sweep only reads `isArchived`
today, so no assertion changes -- the point is that the next assertion written
against these rows would be checking a state that cannot happen. A `restored`
helper names the pair once.
The 导入任务 story's `list` ignored `includeArchived` and `cursor`: it rendered
the archived conversation while the filter was off and handed 加载更多 the same
first page forever, under a comment claiming it demonstrated both controls.
It now filters and pages, with a fourth conversation so the default view is a
short first page rather than the whole list.
Refs #2984
Generated-by: Claude Code
* fix(desktop): tidy the two places the 任务 rename passed through twice
`fake_backend` reaches the user down two paths -- the reason table, and the
header check in `assertSessionCanSend`, which never gets far enough to look a
connection up. They said the same sentence in two copies, so renaming 会话 to
任务 had to be done twice. One `FAKE_BACKEND_MESSAGE` now.
The English workspace help came out of the rename as "Any task can switch next
to the input box", which makes the task the actor and never names what it
switches. It says the project.
Refs #2984
Generated-by: Claude Code
* chore: regenerate the Astryx surface inventory
The Markdown half still listed `external-session-import-dialog.tsx` and
`external-session-import.css` after this branch deleted them, because the two
halves were edited by hand and `check-astryx-surface-inventory.mjs` only
verifies that on-disk files appear in the inventory -- never that inventory
rows still exist on disk. It is also not wired into CI, so the drift was
invisible from both directions.
Running the generator also picks up three files that arrived from main and
were never listed: `runtime-host-profiles-section.tsx`,
`runtime-host-ssh-terminal-dialog.tsx`, and `settings/runtime-host.css`. They
are unrelated to this branch, but a generated file regenerated in halves is
how it drifted in the first place.
Refs #2984
Generated-by: Claude Code
* test(ui): show every task status in the story that exists to show them
Two gaps, both found by shooting the story for a before/after comparison.
`review` and `done` were dropped from `statusSessions` when this branch briefly
deleted them from `SESSION_STATUSES`. The enum values and their labels came
back; the fixture did not, so the story that covers every status covered six of
eight -- and the two missing ones are exactly the two whose colours this change
decided on purpose, `attention` and `success`.
`StoryFrame` also defaulted to 240px while `SessionListPanel`'s rail defaults to
260, so every story that did not pass a width was clipping the rail by 20px.
That lands on the trailing slot, which is where this change puts the timestamp
-- the stories could not show whether it fits. Stories that want a narrow rail
still pass the width to both, which is what the note on `panelProps` is about.
Refs #2984
Generated-by: Claude Code
* refactor(ui): drop the rail's 任务 row
Expanded, the row selected a section whose content was already on screen
directly under it -- the same redundancy as the 会话 list heading this change
deleted one row down, and clicking any task row does the same selection.
It was kept for the collapsed rail, where the list is not rendered, on the
argument that 扩展 and 定时任务 were otherwise one-way doors. That was wrong.
Collapsed, the rail cannot switch tasks at all, so getting back to any task
already means widening it; the titlebar's 展开侧边栏 toggle does that
unconditionally (app-shell-chrome-actions.tsx renders `ChromeColumnToggle` with
no guard), and `activeId` survives a section change, so the widened rail comes
back with the task you left already marked. The row bought one click in a state
the user is leaving regardless, and charged a permanent slot for it.
`sessions` now has no control of its own on the rail. It is where you are
unless you went somewhere, which is why the other two sections light up and
this one has nothing to light.
`streaming-remount` returns the way the product now offers -- widen, then click
the row carrying `aria-current="page"` -- which is a better assertion than the
old one anyway: it checks that the task survives the trip, not just that a
button exists.
Refs #2984
Generated-by: Claude Code
* fix(runtime-host): coalesce a repeat import while the first is still running
Moving 导入任务 from a dialog into a Settings page removed the only thing
stopping one intent from becoming two tasks, and this branch is where that
happened. `ExternalSessionImportLifecycle` refused to close the dialog while an
import was active; retiring it was described here as dropping a guard against
nothing. It was not. The page it was replaced by is one the user may leave
mid-import -- the import continues in Desktop Main by design -- and the page's
`importingId` dies with it, so coming back and pressing 导入 again issues a
second request against a source that is still importing. Nothing downstream
deduplicates: `treats repeats as independent copies` is a pinned behaviour, so
the second request lands a second task the user now has to tell apart.
The guard belongs to the Host, not to the page that asked. Import is a Host
operation and the Host is the only party that knows one is running; a client
knows about its own requests, which is why a second window or the CLI would
have reproduced this with the page's state intact.
Concurrent repeats collapse onto the first attempt's promise and both callers
get its outcome, success or failure, because it is one operation. Sequential
repeats are untouched -- the entry is gone by the time the first settles -- so
importing the same conversation again on purpose still makes an independent
copy, which the existing test continues to pin.
Reported by @M4n5ter in review of #3033.
Refs #2984
Generated-by: Claude Code
Sign up for freeto 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

@jackwener
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' refactor: remove obsolete fixtures and JSONL import by jackwener · Pull Request #2656 · apache/maka · GitHub
Skip to content

refactor: remove obsolete fixtures and JSONL import - #2656

Merged
jackwener merged 1 commit into
mainfrom
codex/trim-low-value-fixtures
Aug 10, 2026
Merged

refactor: remove obsolete fixtures and JSONL import#2656
jackwener merged 1 commit into
mainfrom
codex/trim-low-value-fixtures

Conversation

@jackwener

Copy link
Copy Markdown
Member

Summary

  • reduce the Desktop E2E fixture registry from about 50 historical scenarios to the 13 scenarios consumed by current E2E, alignment audit, or smoke entry points
  • remove orphaned seed generators, renderer-only fixture controls, and the 1,361-line self-referential fixture test
  • remove the retired legacy JSONL session importer, its automatic SessionStore startup wiring, tests, and 11 transcript fixtures
  • preserve process/crash/lock/race fixtures and external Codex JSONL adapters because they cover active boundaries

Impact

  • 32 files changed
  • 5,194 lines removed, 141 lines added (net -5,053)
  • fixture-related tracked files reduced from 75 to 62

Validation

  • git diff --check
  • Biome lint on all changed source files
  • TypeScript: @maka/core, @maka/storage, and Desktop renderer
  • node --test scripts/fixture-env.test.mjs

Desktop main typecheck still reports the existing scheduled-task API/dist mismatch on origin/main; no errors point to this change.

@jackwener
jackwener merged commit 90bdb30 into mainAug 10, 2026
10 checks passed
@jackwener
jackwener deleted the codex/trim-low-value-fixtures branch August 10, 2026 19:21
ARE404 added a commit to ARE404/maka-agent that referenced this pull request Aug 13, 2026
apache#2580 moved the rail's tick onto Astryx's Button. The bar the tick draws
was a direct child of the flex tick and got blockified; the Button wraps
its children in a label span, so the bar went back to normal flow as an
inline box. An inline box takes no width or height, so every bar computed
to 0x0 and the rail shipped invisible in 0.1.9 and 0.1.10 — present in the
DOM, painting nothing.
`display: block` on the bar restores it. Measured on the new fixture at
1280x800: the rail's box goes from 8px wide (its own padding, ticks
contributing nothing) back to the designed 22px.
This is the third time the rail has failed by rendering and not painting
— apache#2161 pinned it against a containing block as tall as the conversation,
apache#2338 parked it under macOS's overlay scrollbar — and the second time it
reached a release. The e2e coverage that would have caught all three was
deleted in apache#2462, and the multi-prompt fixtures it ran on in apache#2656, so
this adds back the smallest thing that closes the gap:
- `chat-prompt-rail`, a plain 8-prompt conversation. The rail hides itself
below three prompts, so the shipped single-prompt fixture cannot show it
at all.
- `prompt-rail.spec.ts` with one test per past failure: bars have a real
box, the rail stays inside the scrollport at both scroll extremes, and a
tick is what the pointer lands on. Three tests where the deleted suite
had nine.
Verified the first test fails on the unfixed renderer with "Expected: > 0,
Received: 0" and passes with the fix. Neither a static CSS read nor a
jsdom unit test can see any of this: jsdom has no layout engine.
Astro-Han pushed a commit that referenced this pull request Aug 13, 2026
…2923)
* fix(ui): give the prompt rail's tick bars a box again
#2580 moved the rail's tick onto Astryx's Button. The bar the tick draws
was a direct child of the flex tick and got blockified; the Button wraps
its children in a label span, so the bar went back to normal flow as an
inline box. An inline box takes no width or height, so every bar computed
to 0x0 and the rail shipped invisible in 0.1.9 and 0.1.10 — present in the
DOM, painting nothing.
`display: block` on the bar restores it. Measured on the new fixture at
1280x800: the rail's box goes from 8px wide (its own padding, ticks
contributing nothing) back to the designed 22px.
This is the third time the rail has failed by rendering and not painting
— #2161 pinned it against a containing block as tall as the conversation,
#2338 parked it under macOS's overlay scrollbar — and the second time it
reached a release. The e2e coverage that would have caught all three was
deleted in #2462, and the multi-prompt fixtures it ran on in #2656, so
this adds back the smallest thing that closes the gap:
- `chat-prompt-rail`, a plain 8-prompt conversation. The rail hides itself
below three prompts, so the shipped single-prompt fixture cannot show it
at all.
- `prompt-rail.spec.ts` with one test per past failure: bars have a real
box, the rail stays inside the scrollport at both scroll extremes, and a
tick is what the pointer lands on. Three tests where the deleted suite
had nine.
Verified the first test fails on the unfixed renderer with "Expected: > 0,
Received: 0" and passes with the fix. Neither a static CSS read nor a
jsdom unit test can see any of this: jsdom has no layout engine.
* fix(ui): make the prompt rail's hover and jump behave
Four things the rail got wrong once it was visible again, found by using it:
- A 4px gap between ticks was a band where the pointer was over the rail
and over no tick, so the dock-style hover falloff dropped out and picked
up again every few pixels of travel. The rail's `gap` moves into the
ticks' own `padding-block`: same pitch, hit boxes now tile.
- The hover preview waited 300ms before opening — Astryx's HoverCard
default, meant for a pointer crossing a wide row on its way somewhere
else. A tick is 22px of rail that nothing is on the way to, and the wait
is the one part of this hover with no motion in it. Now 120ms.
- The highlight glided 280ms to wherever a click landed, so crossing
twenty prompts read as the bar flying off across the rail. A click now
owns the highlight until its scroll settles: no glide, and the scroll
no longer walks the highlight through every prompt it passes.
- The first click into a session did nothing until the reader scrolled by
hand. See below.
That last one is a collision between Astryx's auto-follow lock and the
progressive transcript mount, and neither side is wrong on its own.
`useChatStreamScroll` unlocks on a scroll up, detected by comparing
scrollTop between events — but it ignores any scroll event that arrives
with a changed scrollHeight or offsetHeight, because Chrome fires those
when content resizes and they are not the reader moving. A jump into an
unmounted turn mounts it and the fill that follows changes scrollHeight
for several frames, so the jump's own scroll is invisible to the lock: it
stays on, and `scrollIfLocked` pulls the transcript back to the bottom.
Only a wheel gesture broke it, which takes a separate path in Astryx.
`holdJumpDestination` re-aims at the target on each height change until
the fill stops. The last of those scrolls lands with a stable height,
which is the one the lock finally reads as a scroll up. Measured on the
30-prompt fixture: clicking the first tick went to scrollTop 7042 (the
bottom) and now goes to 24 and holds.
The fixture grows from 8 prompts to 30 because the progressive mount's
initial window is 10 — at 8 the head of the transcript is already mounted
and the jump-into-unmounted-turns path never runs at all.
Coverage note: the e2e case for the first click is an end-to-end check,
not a guard. Whether the lock wins depends on which frame the fill lands
on relative to a smooth scroll still in flight, and it goes green against
the unfixed renderer often enough to be worthless as one. The guard is
the `holdJumpDestination` unit test, which drives the frames itself.
* fix(ui): own a rail jump through the mount instead of racing it
Review of #2923 found the jump's ownership bound to a clock rather than to
the navigation, and the e2e case that was supposed to guard it asserting
almost nothing. Both hold.
Jump ownership:
- A second click during a jump only replaced the target; the first click's
700ms timer still governed, and could clear the second jump mid-flight.
Each click now carries its own sequence and starts its own hold.
- The fixed window is gone. A hold runs until the progressive mount reports
the transcript filled AND nothing has moved for a few frames, so a long
transcript is never released mid-fill, and it ends the moment the reader
touches the transcript (wheel, touch, pointer, key) rather than outliving
their interest in it.
Chasing the "just release auto-follow" direction the review preferred found
that ChatLayout publishes no such seam, so this adds one — `unlockAutoFollow`
on `ChatLayoutContextValue`, exposing the scroll hook's existing `unlock`
(patch hunk + patches/README entry). It is necessary and it is not
sufficient, which the earlier framing got wrong:
- Astryx re-locks on any `scrollend` that settles near the bottom, and a
session that opens at the bottom produces exactly that while the mount is
still catching up. Releasing once at the click is undone before the jump
goes anywhere — traced: released at the click, landed at 154ms, dragged
back to the bottom by 166ms. The release is now re-asserted for the life
of the hold.
- Auto-follow is not the only thing moving the transcript. The progressive
mount's own scroll compensation holds the reader's position across each
fill step, and mounting the turn a jump asked for IS a fill step, so it
lands after the jump and restores the position the jump just left. That
one no seam can fix; it is what the hold is for.
Jumps also scroll instantly now, whatever the app's scroll-motion policy
says. A jump is a teleport the reader asked for, and an animated one does
not survive this surface: traced on the 30-prompt fixture, the smooth scroll
was cancelled by the mount's compensation and by the follow spring and
stalled two pixels from where it started.
Coverage:
- The first-click e2e case named the wrong turn (`[data-turn-id]` is the
first MOUNTED turn, whose top is already negative at the opening scroll
position, so an upper-bound-only check passed without the jump doing
anything). It now names `turn-prompt-rail-1`, bounds it on both sides, and
asserts that tick's `aria-current`.
- `emulateMedia` could not put that case on the production scroll path:
`resolveScrollMotionBehavior` collapses motion for ANY fixture, keyed on
`data-maka-e2e-fixture` rather than on the media query. Fixtures can now
ask for a behavior back (`scrollMotion`, per launch — it costs seconds of
settling per window, so only the case that needs it pays), with unit
coverage for the precedence: a fixture request never outranks a stated
preference for less motion.
- `holdJumpDestination`'s unit tests grew the two cases its rewrite is
about: it must not settle while the transcript is still filling, and it
must hand the transcript back the moment the reader touches it.
Verified 5/5 on the smooth-scroll fixture, where the previous revision lost
1 in 4. `quote-selection.spec.ts` flakes on this machine (1 in 4) at
upstream/main as well, unchanged by this branch.
Astro-Han added a commit to Astro-Han/maka-agent that referenced this pull request Aug 15, 2026
Deleting `SessionStatusTone` was right; replacing it with a private
`SessionStatus -> StatusDotVariant` table was not. `status-vocabulary.ts`
already owns "the one place a status word becomes a colour", so the
private table made a second authority, and the two disagreed: a task
waiting on a permission prompt drew `error` in the rail while the
permission centre drew `attention` for the identical condition.
Map the session enum to `StatusSemantic` and let `dotForStatus` pick the
colour. `blocked` and `waiting_for_user` are both `attention`, which is
what that semantic is defined as -- both are waiting on a person. Giving
`blocked` `error` to tell them apart used colour for a distinction colour
cannot carry; their labels and `describeBlockedReason` do that.
Restore `review` and `done`. They have no writer in current source, but
`SESSION_STATUSES` is read back out of storage, and narrowing it is a
data migration rather than a cleanup: `resolveLegacyStatus` in the JSONL
importer (removed in apache#2656) passed both values through into real SQLite
stores verbatim, and `normalizeSessionHeader` throws for the WHOLE header
on an unrecognised status, so one stored row carrying `done` fails an
entire catalog page. The migration is its own change.
`archived` and `aborted` get their dots back. They were `muted` before,
`muted` resolved to a real `neutral` dot, and dropping them to `undefined`
was a behaviour change I described as a consequence of collapsing the
layer.
Refs apache#2984
Generated-by: Claude Code
Astro-Han added a commit to Astro-Han/maka-agent that referenced this pull request Aug 15, 2026
Deleting `SessionStatusTone` was right; replacing it with a private
`SessionStatus -> StatusDotVariant` table was not. `status-vocabulary.ts`
already owns "the one place a status word becomes a colour", so the
private table made a second authority, and the two disagreed: a task
waiting on a permission prompt drew `error` in the rail while the
permission centre drew `attention` for the identical condition.
Map the session enum to `StatusSemantic` and let `dotForStatus` pick the
colour. `blocked` and `waiting_for_user` are both `attention`, which is
what that semantic is defined as -- both are waiting on a person. Giving
`blocked` `error` to tell them apart used colour for a distinction colour
cannot carry; their labels and `describeBlockedReason` do that.
Restore `review` and `done`. They have no writer in current source, but
`SESSION_STATUSES` is read back out of storage, and narrowing it is a
data migration rather than a cleanup: `resolveLegacyStatus` in the JSONL
importer (removed in apache#2656) passed both values through into real SQLite
stores verbatim, and `normalizeSessionHeader` throws for the WHOLE header
on an unrecognised status, so one stored row carrying `done` fails an
entire catalog page. The migration is its own change.
`archived` and `aborted` get their dots back. They were `muted` before,
`muted` resolved to a real `neutral` dot, and dropping them to `undefined`
was a behaviour change I described as a consequence of collapsing the
layer.
Refs apache#2984
Generated-by: Claude Code
Astro-Han added a commit that referenced this pull request Aug 15, 2026
…ary (#3033)
* fix(ui): give the task rail's status dot one meaning
The dot the rail draws for a session passed through two lossy hops and
three disagreeing sources, so it could not say what it meant.
Collapse the mapping. `SessionStatus -> SessionStatusTone ->
StatusDotVariant` becomes one table from status to Astryx's variant. Two
of the seven tones had no distinct outcome at the end of that chain, and
the collapse is what let `waiting_for_user` and `blocked` share
`warning`: blocked now maps to `error`, so "cannot proceed until you fix
a connection" and "holding a question for you" stop rendering alike.
Read the running authority. `runningTurnIds` is the runtime's projection
of the runs it holds; `session.ts` states why a persisted `status` cannot
serve that purpose, and `settledSessionTransientIds` already reads it
first over the same list. The row read neither -- a renderer-local
streaming set, then the stored `status` -- so a task running under a bot
channel or a second window read as idle. The streaming set stays below
it, for the gap between this renderer sending a turn and the host
reporting it back.
Delete what nothing wrote. `review` and `done` were never written by
anything, in any version, so no stored record can carry them and no
reader of them was reachable; they leave `SESSION_STATUSES`, the wire
enum, and the copy tables. `SessionLifecycleStatus` now aliases
`SessionStatus` instead of restating it -- three hand-written copies of
one enum is how those two values survived in two of them after the last
writer went away.
Delete the tautological filter. `SessionFilter` had one value left:
`archived` moved to Settings in #2985 and `flagged` was never selected,
which left a control whose answer is always the same answer. The 「会话」
row that selected it goes with it, as does the branch in
`sessionMatchesNavSelection`. A stored filter is dropped rather than
validated, which is the migration.
Also gone: `SessionStatusPresentation.interactive`, which had no reader,
and a doc comment naming a `SessionStatusIcon` and a chat-header badge as
the tone matrix's consumers -- neither exists.
`status: 'archived'` still duplicates `isArchived`. Consolidating those
two rewrites stored rows, so it is #2984's PR 3, not this one.
Refs #2984
Generated-by: Claude Code
* refactor(ui): rebuild the task row on two signal slots
Every task row in the rail now carries exactly two signal slots. Slot 1 is
a single status dot, reserved as an 8px gutter even when there is nothing to
show so titles stay aligned down the list. Slot 2 holds a compact timestamp
at rest and swaps to the ⋯ menu on hover or keyboard focus, so the menu is
no longer permanently mounted next to every title.
The dot's meaning is resolved once, in priority order: live run (accent,
pulsing) › persisted status › unread (accent, inactive rows only). The live
run reads `runningTurnIds` first, matching `settledSessionTransientIds` over
the same session list — a persisted `running` status can be left behind by a
crash, so it must not outrank the authoritative projection.
The stale pill is gone: it duplicated the timestamp it sat next to. The
worktree mark falls back to the row tooltip rather than competing for a slot.
`RelativeTime` gains `variant="compact"` instead of a second component; a
260px rail row cannot fit the medium-date-plus-time past-horizon fallback.
Generated-by: Claude Code
* refactor(ui): give the rail one top region and one grouping switch
The rail's top region now ends with the grouping switch, and the history
below it starts directly at its first group.
The list heading is gone. "会话" labelled a list that is the only thing under
it, inside a landmark that already names the panel — and it existed mainly to
give the grouping switch somewhere to sit. The switch moves into the sticky
top region instead, where it stays reachable: hung off a heading it scrolled
away with the very list it switches.
It is also full width with text labels now. Two icon-only segments asked the
rail to teach a clock and a folder, and the rail has nowhere to teach them;
按时间 / 按项目 is the entire vocabulary and fits inside the minimum 180px
width with room to spare. Collapsed at 48px it does not render at all — no
segment label fits, and the list it governs is not rendered either.
The group headers lose their ➕. It was the 新任务 row's handler under a
second name, three rows below the row itself, and it needed that second name
precisely because two controls cannot share one accessible name.
Generated-by: Claude Code
* feat(desktop): move task import out of the rail into settings
导入 was a rail row that opened a modal over the conversation. Import is not
navigation: it is a rare setup errand, it needs a source, an archived filter
and a paged directory of someone else's conversations to work through, and
none of that belongs in a 260px column of the tasks you are working on. It
becomes 设置 › 活动 › 导入任务, next to 已归档任务 — both are about the task
catalog rather than the task in front of you.
As a page the flow loses its selection state. A dialog needed one because its
single primary action sat in a footer far from the rows; a page puts 导入 on
the row itself, the way 已归档任务 puts 恢复 on its rows. That also retires
`ExternalSessionImportLifecycle`, whose whole job was refusing to close a
dialog mid-import — there is no dialog left to hold open, and an import that
outlives the page is guarded by the mounted ref instead, so it can still
finish but can no longer steer a shell the user has navigated away from.
The source switch now appears only when there is more than one source. Codex
is the only adapter that ships, so in practice every user was being shown a
segmented control with a single segment — a control nobody can operate. With
one source the section names it instead.
Deliberately not carried over: keeping an imported task in sync with its
source. The importer is a one-shot conversion and nothing behind it watches
the source, so that control would be a promise no coordinator can keep.
Generated-by: Claude Code
* refactor: name the thing a user works on 任务 / task everywhere
The product called the same object 会话, 对话, session and conversation
depending on which file you landed in. Code, protocol and storage keep
`session` — that is a durable contract with a wire format behind it — but
every string a user reads now says 任务 / task.
The rule is what the noun REFERS TO, not the word:
- The Maka entity a user opens, renames, archives and searches → 任务 / task.
- Conversing as a behavior stays 对话 / conversation: "与 Maka 对话" over a
bot, "可用于对话的模型", "对话记录" as a message log.
- Another agent's stored conversations stay 对话 / conversation. 导入任务
lists Codex's conversations and produces Maka tasks; collapsing both onto
one word would erase exactly the distinction that page is about.
- 侧边对话 stays: it is a chat beside a task, not a task in the rail.
Two collisions had to be resolved rather than renamed. The per-task ledger
was 会话任务, which would have become 任务任务 — its items are 待办 / to-dos,
which is what they always were. And the automations row read "定时任务,N 个
未完成任务", two different meanings of 任务 in one label; the count now says
"N 条进行中".
The keyboard sheet loses its ←/→ row. It documented cycling between
会话/已标记/已归档, a filter this PR removed along with `SessionFilter`.
Generated-by: Claude Code
* test(ui): make the narrow-rail story actually narrow
`LongTitlesAndNarrow` framed the panel at 176px but let SideNav keep its own
260px `resizable` width, so the story clipped a wide rail instead of showing
a narrow one — the timestamps it exists to check were outside the frame. It
now drives the rail's own width, at 180px, which is the panel's `minWidth`
and therefore the narrowest state a user can reach.
Generated-by: Claude Code
* fix(ui): keep the rail's 任务 row as the way back to the list
bb39b33 deleted `SessionFilter` and took the 「会话」 row with it, on the
reading that the row only selected a filter whose answer was always the
same answer. Half right: its `isSelected` read the dead filter, but its
`onClick` selected the SECTION, and it was the only control that did.
Without it the rail has three sections and two rows. Collapsed at 48px —
the default state (`readSessionListCollapsed`) — the list is not rendered
at all, so 扩展 and 定时任务 became one-way doors: the only way back to a
running task was 新任务, which answers "show me my tasks" by starting
another one. `streaming-remount.spec.ts` walks exactly that path and had
been rewritten to click a task row that a collapsed rail does not have.
Restore the row as 任务, selected on `section === 'sessions'` and
selecting the section with no filter. The filter deletion stands.
Refs #2984
Generated-by: Claude Code
* fix(ui): route task status through the shared semantic layer
Deleting `SessionStatusTone` was right; replacing it with a private
`SessionStatus -> StatusDotVariant` table was not. `status-vocabulary.ts`
already owns "the one place a status word becomes a colour", so the
private table made a second authority, and the two disagreed: a task
waiting on a permission prompt drew `error` in the rail while the
permission centre drew `attention` for the identical condition.
Map the session enum to `StatusSemantic` and let `dotForStatus` pick the
colour. `blocked` and `waiting_for_user` are both `attention`, which is
what that semantic is defined as -- both are waiting on a person. Giving
`blocked` `error` to tell them apart used colour for a distinction colour
cannot carry; their labels and `describeBlockedReason` do that.
Restore `review` and `done`. They have no writer in current source, but
`SESSION_STATUSES` is read back out of storage, and narrowing it is a
data migration rather than a cleanup: `resolveLegacyStatus` in the JSONL
importer (removed in #2656) passed both values through into real SQLite
stores verbatim, and `normalizeSessionHeader` throws for the WHOLE header
on an unrecognised status, so one stored row carrying `done` fails an
entire catalog page. The migration is its own change.
`archived` and `aborted` get their dots back. They were `muted` before,
`muted` resolved to a real `neutral` dot, and dropping them to `undefined`
was a behaviour change I described as a consequence of collapsing the
layer.
Refs #2984
Generated-by: Claude Code
* fix(ui): keep the task row's facts when it collapses to two slots
The row draws one dot and one trailing slot. That was implemented by
collapsing to a single signal inside the resolver, which made the visual
element the only carrier of the fact -- so removing the trailing `Badge`
removed "stale" itself, not just its pill. `staleTitle` and
`staleAriaLabel` were left with no reader at all, the surviving `opacity`
cue is cancelled on the selected row by design, and opacity announces
nothing. Worktree went the same way: an `aria-label`ed icon inside the
button became a `title` on a non-interactive ancestor, which contributes
to neither the button's name nor its description.
Resolve a LIST of signals in priority order, draw `signals[0]`, and put
the rest -- plus worktree and the absolute timestamp -- in one visually
hidden span inside the button. Two slots, same pixels, and the facts stop
depending on which of them had room. Signals also stop eating each other:
`aborted` now keeps its own dot instead of falling through to the unread
branch, where an aborted task with unread text drew the same accent dot
as a running one.
Stale joins the list as `attention`, so a stale task finally has a signal
that survives being selected. It is resolved here rather than in
`presentSessionStatus` because it is a renderer-derived fact, not a
persisted `SessionStatus`.
Drop the `runningTurnIds` read. No `SessionSummary` reaching Desktop
carries that field: Runtime Host's catalog reads storage directly, the
wire projection does not list it, and Desktop Main's converter copies
persisted fields only -- the identifier appears nowhere in
`packages/runtime-host/src` or `apps/desktop/src/main` outside a test
fixture. It was a read of something nothing populates, described as
reading the authority. `streaming` is what the rail actually has, with
the limit that it only knows this renderer's turns; a real live-run
projection from the Host is its own change.
Also: `pointer-events: none` on the resting ⋯, which sat invisible at
`z-index: 1` over the trailing slot and swallowed clicks; and the inner
list no longer repeats the rail's own accessible name.
Refs #2984
Generated-by: Claude Code
* fix(desktop): refresh the catalog when an import's outcome is unknown
`external-sessions:import` emits `sessions:changed` on success, so a task
imported while the user walks away from Settings still reaches the rail
on its own -- the comment in `app-shell.tsx` claiming nothing notifies
the shell was wrong, and is corrected here.
The gap is the other branch. `commit_outcome_unknown` means the Host
cannot say whether the task was committed, and that path returned a
result without emitting anything. The only trace was the page's own
banner, and 导入任务 is a Settings page: leaving it unmounts the banner,
which is exactly when someone comes back and imports the same
conversation again. Emit there too, with no id, because not knowing which
task landed is what the code means.
Also return the import promise from `clickAction` instead of `void`-ing
it. Astryx's Button awaits it and drops repeat clicks until it settles;
discarding it left double-submit to `importingId` alone, one render
behind the second click.
Refs #2984
Generated-by: Claude Code
* fix(desktop): re-point the structural gates the surface move broke
Deleting the import dialog left exact-string readers behind. `scripts/
check-astryx-alignment.mjs` read the dialog and its stylesheet by path,
so running it exited 1 with ENOENT; the surface inventory still listed
both files and had no row for the page that replaced them.
The dialog's Item-row guards are not migrated, they are dropped. They
checked that a SELECTED row stayed keyboard-reachable -- no parent role
stealing Item's native button, no selected-only tabIndex trapping focus.
设置 › 活动 › 导入任务 has no selection: 导入 sits on each row the way 恢复
does on the archived page. Asserting those smells against the new page
would guard a shape it does not have. The button guard and a ListItem
import check follow the surface to the page.
Inventory rows for the new page and for #2985's archived-tasks page,
which was never added. Three runtime-host entries stay missing; they
predate this branch.
Also read the Daily Review model label from its copy table instead of a
literal. The story matched '跟随对话默认', which the rename retired, so it
silently found nothing -- and `storybook-visual-smoke.mjs` disables every
`play` function, so CI could not report it.
Refs #2984
Generated-by: Claude Code
* refactor: finish the 任务 rename where the rule missed
The rename was applied by rule and reviewed by hand, which is the process
that produces both halves of this: strings that kept the old noun, and
strings that took the new one where the old was right.
Same-surface contradictions, which are what a user actually notices:
- The to-do panel's error banner said 任务载入失败 next to a button
labelled 重新载入待办 -- one `Banner`, two vocabularies.
- `chat-readiness` said 当前任务 and then 新建会话 in one sentence.
- The bot's help text offered to 清空当前对话开新会话 while the reply to
that same command already said 任务已重置.
- 定时任务 described itself as 安排定时任务,并回顾本机任务, and its
delivery option read Agent 任务执行 -- two meanings of 任务 in one form.
- `settlementFailed` called the side chat a 任务 and then a 侧边对话;
`forkSetupFailed` called it 追问任务, a third name for one object. Both
are 侧边对话 now, matching the en twins.
zh/en divergence, where the catalogs share keys but stopped sharing
meaning: Daily Review counted 任务 in zh and conversations in en (and its
en empty state said conversations on one branch, tasks on the other); the
terminal panel said "task" and then "the session's terminal".
Missed outright: `tool-activity/copy.ts` (由源会话管理 on every terminal
result chip), `connection-error-copy.ts`, and the CLI's own prose
(`allow for session`, `Give this session full access`, `--continue`).
Kept deliberately: 和 Maka 对话 as a behaviour, Codex's stored 对话 on the
import page, 侧边对话, 对话框 for dialog, and `<session-id>` as a CLI
contract. The palette's `nav:sessions` keywords get 会话 and 对话 back --
collapsing both onto 任务 left a duplicate entry and dropped the words a
long-time user would still type.
Refs #2984
Generated-by: Claude Code
* fix(ui): route the task row's last two dots through the shared vocabulary
`streaming` and `hasUnread` still wrote `'accent'` by hand while the status
and stale signals resolved through `dotForStatus`. Half a function deciding
its own colours is the drift this change set out to close -- the literal is
what let the rail and Settings disagree about the same fact before. Both are
`active`: streaming is the system working on it, and unread text is
"something happened here" rather than a question waiting on the user, which
is the line the two semantics exist to draw. `dotForStatus('active')` is
`'accent'`, so nothing moves on screen.
Also drops the `copy` binding `SessionHistoryList` no longer reads. It fed
the list's own aria-label, and the outer SideNav took that name over when the
rail was rebuilt.
Refs #2984
Generated-by: Claude Code
* docs: say what each session-status-presentation file actually owns
Desktop's copy claimed the status-to-dot mapping was "re-exported below" and
that `describeBlockedReason` was defined there. Both moved to `@maka/ui` in
this branch and neither is in the file; what is left is Desktop-only judgment
-- which blocked reasons are worth acting on, and what to offer after a turn
fails -- so the header says that instead.
The contract the old header carried is real and had nowhere to live after the
move: a UI label must never show a raw `SessionBlockedReason`, and a new
reason has to extend the core enum and the copy matrix together or it reads as
`unknown`. It now sits on `describeBlockedReason` in `@maka/ui`, where the
matrix it constrains is.
Refs #2984
Generated-by: Claude Code
* test: make the restored-task and import fixtures answer like their sources
Both fixtures modelled states their real source cannot produce, which makes
them evidence for nothing.
The purge tests built a restored task by flipping `isArchived` alone, leaving
`status: 'archived'` behind. `SessionStore.unarchive` writes both fields
together, so no stored row looks like that. The sweep only reads `isArchived`
today, so no assertion changes -- the point is that the next assertion written
against these rows would be checking a state that cannot happen. A `restored`
helper names the pair once.
The 导入任务 story's `list` ignored `includeArchived` and `cursor`: it rendered
the archived conversation while the filter was off and handed 加载更多 the same
first page forever, under a comment claiming it demonstrated both controls.
It now filters and pages, with a fourth conversation so the default view is a
short first page rather than the whole list.
Refs #2984
Generated-by: Claude Code
* fix(desktop): tidy the two places the 任务 rename passed through twice
`fake_backend` reaches the user down two paths -- the reason table, and the
header check in `assertSessionCanSend`, which never gets far enough to look a
connection up. They said the same sentence in two copies, so renaming 会话 to
任务 had to be done twice. One `FAKE_BACKEND_MESSAGE` now.
The English workspace help came out of the rename as "Any task can switch next
to the input box", which makes the task the actor and never names what it
switches. It says the project.
Refs #2984
Generated-by: Claude Code
* chore: regenerate the Astryx surface inventory
The Markdown half still listed `external-session-import-dialog.tsx` and
`external-session-import.css` after this branch deleted them, because the two
halves were edited by hand and `check-astryx-surface-inventory.mjs` only
verifies that on-disk files appear in the inventory -- never that inventory
rows still exist on disk. It is also not wired into CI, so the drift was
invisible from both directions.
Running the generator also picks up three files that arrived from main and
were never listed: `runtime-host-profiles-section.tsx`,
`runtime-host-ssh-terminal-dialog.tsx`, and `settings/runtime-host.css`. They
are unrelated to this branch, but a generated file regenerated in halves is
how it drifted in the first place.
Refs #2984
Generated-by: Claude Code
* test(ui): show every task status in the story that exists to show them
Two gaps, both found by shooting the story for a before/after comparison.
`review` and `done` were dropped from `statusSessions` when this branch briefly
deleted them from `SESSION_STATUSES`. The enum values and their labels came
back; the fixture did not, so the story that covers every status covered six of
eight -- and the two missing ones are exactly the two whose colours this change
decided on purpose, `attention` and `success`.
`StoryFrame` also defaulted to 240px while `SessionListPanel`'s rail defaults to
260, so every story that did not pass a width was clipping the rail by 20px.
That lands on the trailing slot, which is where this change puts the timestamp
-- the stories could not show whether it fits. Stories that want a narrow rail
still pass the width to both, which is what the note on `panelProps` is about.
Refs #2984
Generated-by: Claude Code
* refactor(ui): drop the rail's 任务 row
Expanded, the row selected a section whose content was already on screen
directly under it -- the same redundancy as the 会话 list heading this change
deleted one row down, and clicking any task row does the same selection.
It was kept for the collapsed rail, where the list is not rendered, on the
argument that 扩展 and 定时任务 were otherwise one-way doors. That was wrong.
Collapsed, the rail cannot switch tasks at all, so getting back to any task
already means widening it; the titlebar's 展开侧边栏 toggle does that
unconditionally (app-shell-chrome-actions.tsx renders `ChromeColumnToggle` with
no guard), and `activeId` survives a section change, so the widened rail comes
back with the task you left already marked. The row bought one click in a state
the user is leaving regardless, and charged a permanent slot for it.
`sessions` now has no control of its own on the rail. It is where you are
unless you went somewhere, which is why the other two sections light up and
this one has nothing to light.
`streaming-remount` returns the way the product now offers -- widen, then click
the row carrying `aria-current="page"` -- which is a better assertion than the
old one anyway: it checks that the task survives the trip, not just that a
button exists.
Refs #2984
Generated-by: Claude Code
* fix(runtime-host): coalesce a repeat import while the first is still running
Moving 导入任务 from a dialog into a Settings page removed the only thing
stopping one intent from becoming two tasks, and this branch is where that
happened. `ExternalSessionImportLifecycle` refused to close the dialog while an
import was active; retiring it was described here as dropping a guard against
nothing. It was not. The page it was replaced by is one the user may leave
mid-import -- the import continues in Desktop Main by design -- and the page's
`importingId` dies with it, so coming back and pressing 导入 again issues a
second request against a source that is still importing. Nothing downstream
deduplicates: `treats repeats as independent copies` is a pinned behaviour, so
the second request lands a second task the user now has to tell apart.
The guard belongs to the Host, not to the page that asked. Import is a Host
operation and the Host is the only party that knows one is running; a client
knows about its own requests, which is why a second window or the CLI would
have reproduced this with the page's state intact.
Concurrent repeats collapse onto the first attempt's promise and both callers
get its outcome, success or failure, because it is one operation. Sequential
repeats are untouched -- the entry is gone by the time the first settles -- so
importing the same conversation again on purpose still makes an independent
copy, which the existing test continues to pin.
Reported by @M4n5ter in review of #3033.
Refs #2984
Generated-by: Claude Code
Sign up for freeto 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

@jackwener
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' refactor: remove obsolete fixtures and JSONL import by jackwener · Pull Request #2656 · apache/maka · GitHub
Skip to content

refactor: remove obsolete fixtures and JSONL import - #2656

Merged
jackwener merged 1 commit into
mainfrom
codex/trim-low-value-fixtures
Aug 10, 2026
Merged

refactor: remove obsolete fixtures and JSONL import#2656
jackwener merged 1 commit into
mainfrom
codex/trim-low-value-fixtures

Conversation

@jackwener

Copy link
Copy Markdown
Member

Summary

  • reduce the Desktop E2E fixture registry from about 50 historical scenarios to the 13 scenarios consumed by current E2E, alignment audit, or smoke entry points
  • remove orphaned seed generators, renderer-only fixture controls, and the 1,361-line self-referential fixture test
  • remove the retired legacy JSONL session importer, its automatic SessionStore startup wiring, tests, and 11 transcript fixtures
  • preserve process/crash/lock/race fixtures and external Codex JSONL adapters because they cover active boundaries

Impact

  • 32 files changed
  • 5,194 lines removed, 141 lines added (net -5,053)
  • fixture-related tracked files reduced from 75 to 62

Validation

  • git diff --check
  • Biome lint on all changed source files
  • TypeScript: @maka/core, @maka/storage, and Desktop renderer
  • node --test scripts/fixture-env.test.mjs

Desktop main typecheck still reports the existing scheduled-task API/dist mismatch on origin/main; no errors point to this change.

@jackwener
jackwener merged commit 90bdb30 into mainAug 10, 2026
10 checks passed
@jackwener
jackwener deleted the codex/trim-low-value-fixtures branch August 10, 2026 19:21
ARE404 added a commit to ARE404/maka-agent that referenced this pull request Aug 13, 2026
apache#2580 moved the rail's tick onto Astryx's Button. The bar the tick draws
was a direct child of the flex tick and got blockified; the Button wraps
its children in a label span, so the bar went back to normal flow as an
inline box. An inline box takes no width or height, so every bar computed
to 0x0 and the rail shipped invisible in 0.1.9 and 0.1.10 — present in the
DOM, painting nothing.
`display: block` on the bar restores it. Measured on the new fixture at
1280x800: the rail's box goes from 8px wide (its own padding, ticks
contributing nothing) back to the designed 22px.
This is the third time the rail has failed by rendering and not painting
— apache#2161 pinned it against a containing block as tall as the conversation,
apache#2338 parked it under macOS's overlay scrollbar — and the second time it
reached a release. The e2e coverage that would have caught all three was
deleted in apache#2462, and the multi-prompt fixtures it ran on in apache#2656, so
this adds back the smallest thing that closes the gap:
- `chat-prompt-rail`, a plain 8-prompt conversation. The rail hides itself
below three prompts, so the shipped single-prompt fixture cannot show it
at all.
- `prompt-rail.spec.ts` with one test per past failure: bars have a real
box, the rail stays inside the scrollport at both scroll extremes, and a
tick is what the pointer lands on. Three tests where the deleted suite
had nine.
Verified the first test fails on the unfixed renderer with "Expected: > 0,
Received: 0" and passes with the fix. Neither a static CSS read nor a
jsdom unit test can see any of this: jsdom has no layout engine.
Astro-Han pushed a commit that referenced this pull request Aug 13, 2026
…2923)
* fix(ui): give the prompt rail's tick bars a box again
#2580 moved the rail's tick onto Astryx's Button. The bar the tick draws
was a direct child of the flex tick and got blockified; the Button wraps
its children in a label span, so the bar went back to normal flow as an
inline box. An inline box takes no width or height, so every bar computed
to 0x0 and the rail shipped invisible in 0.1.9 and 0.1.10 — present in the
DOM, painting nothing.
`display: block` on the bar restores it. Measured on the new fixture at
1280x800: the rail's box goes from 8px wide (its own padding, ticks
contributing nothing) back to the designed 22px.
This is the third time the rail has failed by rendering and not painting
— #2161 pinned it against a containing block as tall as the conversation,
#2338 parked it under macOS's overlay scrollbar — and the second time it
reached a release. The e2e coverage that would have caught all three was
deleted in #2462, and the multi-prompt fixtures it ran on in #2656, so
this adds back the smallest thing that closes the gap:
- `chat-prompt-rail`, a plain 8-prompt conversation. The rail hides itself
below three prompts, so the shipped single-prompt fixture cannot show it
at all.
- `prompt-rail.spec.ts` with one test per past failure: bars have a real
box, the rail stays inside the scrollport at both scroll extremes, and a
tick is what the pointer lands on. Three tests where the deleted suite
had nine.
Verified the first test fails on the unfixed renderer with "Expected: > 0,
Received: 0" and passes with the fix. Neither a static CSS read nor a
jsdom unit test can see any of this: jsdom has no layout engine.
* fix(ui): make the prompt rail's hover and jump behave
Four things the rail got wrong once it was visible again, found by using it:
- A 4px gap between ticks was a band where the pointer was over the rail
and over no tick, so the dock-style hover falloff dropped out and picked
up again every few pixels of travel. The rail's `gap` moves into the
ticks' own `padding-block`: same pitch, hit boxes now tile.
- The hover preview waited 300ms before opening — Astryx's HoverCard
default, meant for a pointer crossing a wide row on its way somewhere
else. A tick is 22px of rail that nothing is on the way to, and the wait
is the one part of this hover with no motion in it. Now 120ms.
- The highlight glided 280ms to wherever a click landed, so crossing
twenty prompts read as the bar flying off across the rail. A click now
owns the highlight until its scroll settles: no glide, and the scroll
no longer walks the highlight through every prompt it passes.
- The first click into a session did nothing until the reader scrolled by
hand. See below.
That last one is a collision between Astryx's auto-follow lock and the
progressive transcript mount, and neither side is wrong on its own.
`useChatStreamScroll` unlocks on a scroll up, detected by comparing
scrollTop between events — but it ignores any scroll event that arrives
with a changed scrollHeight or offsetHeight, because Chrome fires those
when content resizes and they are not the reader moving. A jump into an
unmounted turn mounts it and the fill that follows changes scrollHeight
for several frames, so the jump's own scroll is invisible to the lock: it
stays on, and `scrollIfLocked` pulls the transcript back to the bottom.
Only a wheel gesture broke it, which takes a separate path in Astryx.
`holdJumpDestination` re-aims at the target on each height change until
the fill stops. The last of those scrolls lands with a stable height,
which is the one the lock finally reads as a scroll up. Measured on the
30-prompt fixture: clicking the first tick went to scrollTop 7042 (the
bottom) and now goes to 24 and holds.
The fixture grows from 8 prompts to 30 because the progressive mount's
initial window is 10 — at 8 the head of the transcript is already mounted
and the jump-into-unmounted-turns path never runs at all.
Coverage note: the e2e case for the first click is an end-to-end check,
not a guard. Whether the lock wins depends on which frame the fill lands
on relative to a smooth scroll still in flight, and it goes green against
the unfixed renderer often enough to be worthless as one. The guard is
the `holdJumpDestination` unit test, which drives the frames itself.
* fix(ui): own a rail jump through the mount instead of racing it
Review of #2923 found the jump's ownership bound to a clock rather than to
the navigation, and the e2e case that was supposed to guard it asserting
almost nothing. Both hold.
Jump ownership:
- A second click during a jump only replaced the target; the first click's
700ms timer still governed, and could clear the second jump mid-flight.
Each click now carries its own sequence and starts its own hold.
- The fixed window is gone. A hold runs until the progressive mount reports
the transcript filled AND nothing has moved for a few frames, so a long
transcript is never released mid-fill, and it ends the moment the reader
touches the transcript (wheel, touch, pointer, key) rather than outliving
their interest in it.
Chasing the "just release auto-follow" direction the review preferred found
that ChatLayout publishes no such seam, so this adds one — `unlockAutoFollow`
on `ChatLayoutContextValue`, exposing the scroll hook's existing `unlock`
(patch hunk + patches/README entry). It is necessary and it is not
sufficient, which the earlier framing got wrong:
- Astryx re-locks on any `scrollend` that settles near the bottom, and a
session that opens at the bottom produces exactly that while the mount is
still catching up. Releasing once at the click is undone before the jump
goes anywhere — traced: released at the click, landed at 154ms, dragged
back to the bottom by 166ms. The release is now re-asserted for the life
of the hold.
- Auto-follow is not the only thing moving the transcript. The progressive
mount's own scroll compensation holds the reader's position across each
fill step, and mounting the turn a jump asked for IS a fill step, so it
lands after the jump and restores the position the jump just left. That
one no seam can fix; it is what the hold is for.
Jumps also scroll instantly now, whatever the app's scroll-motion policy
says. A jump is a teleport the reader asked for, and an animated one does
not survive this surface: traced on the 30-prompt fixture, the smooth scroll
was cancelled by the mount's compensation and by the follow spring and
stalled two pixels from where it started.
Coverage:
- The first-click e2e case named the wrong turn (`[data-turn-id]` is the
first MOUNTED turn, whose top is already negative at the opening scroll
position, so an upper-bound-only check passed without the jump doing
anything). It now names `turn-prompt-rail-1`, bounds it on both sides, and
asserts that tick's `aria-current`.
- `emulateMedia` could not put that case on the production scroll path:
`resolveScrollMotionBehavior` collapses motion for ANY fixture, keyed on
`data-maka-e2e-fixture` rather than on the media query. Fixtures can now
ask for a behavior back (`scrollMotion`, per launch — it costs seconds of
settling per window, so only the case that needs it pays), with unit
coverage for the precedence: a fixture request never outranks a stated
preference for less motion.
- `holdJumpDestination`'s unit tests grew the two cases its rewrite is
about: it must not settle while the transcript is still filling, and it
must hand the transcript back the moment the reader touches it.
Verified 5/5 on the smooth-scroll fixture, where the previous revision lost
1 in 4. `quote-selection.spec.ts` flakes on this machine (1 in 4) at
upstream/main as well, unchanged by this branch.
Astro-Han added a commit to Astro-Han/maka-agent that referenced this pull request Aug 15, 2026
Deleting `SessionStatusTone` was right; replacing it with a private
`SessionStatus -> StatusDotVariant` table was not. `status-vocabulary.ts`
already owns "the one place a status word becomes a colour", so the
private table made a second authority, and the two disagreed: a task
waiting on a permission prompt drew `error` in the rail while the
permission centre drew `attention` for the identical condition.
Map the session enum to `StatusSemantic` and let `dotForStatus` pick the
colour. `blocked` and `waiting_for_user` are both `attention`, which is
what that semantic is defined as -- both are waiting on a person. Giving
`blocked` `error` to tell them apart used colour for a distinction colour
cannot carry; their labels and `describeBlockedReason` do that.
Restore `review` and `done`. They have no writer in current source, but
`SESSION_STATUSES` is read back out of storage, and narrowing it is a
data migration rather than a cleanup: `resolveLegacyStatus` in the JSONL
importer (removed in apache#2656) passed both values through into real SQLite
stores verbatim, and `normalizeSessionHeader` throws for the WHOLE header
on an unrecognised status, so one stored row carrying `done` fails an
entire catalog page. The migration is its own change.
`archived` and `aborted` get their dots back. They were `muted` before,
`muted` resolved to a real `neutral` dot, and dropping them to `undefined`
was a behaviour change I described as a consequence of collapsing the
layer.
Refs apache#2984
Generated-by: Claude Code
Astro-Han added a commit to Astro-Han/maka-agent that referenced this pull request Aug 15, 2026
Deleting `SessionStatusTone` was right; replacing it with a private
`SessionStatus -> StatusDotVariant` table was not. `status-vocabulary.ts`
already owns "the one place a status word becomes a colour", so the
private table made a second authority, and the two disagreed: a task
waiting on a permission prompt drew `error` in the rail while the
permission centre drew `attention` for the identical condition.
Map the session enum to `StatusSemantic` and let `dotForStatus` pick the
colour. `blocked` and `waiting_for_user` are both `attention`, which is
what that semantic is defined as -- both are waiting on a person. Giving
`blocked` `error` to tell them apart used colour for a distinction colour
cannot carry; their labels and `describeBlockedReason` do that.
Restore `review` and `done`. They have no writer in current source, but
`SESSION_STATUSES` is read back out of storage, and narrowing it is a
data migration rather than a cleanup: `resolveLegacyStatus` in the JSONL
importer (removed in apache#2656) passed both values through into real SQLite
stores verbatim, and `normalizeSessionHeader` throws for the WHOLE header
on an unrecognised status, so one stored row carrying `done` fails an
entire catalog page. The migration is its own change.
`archived` and `aborted` get their dots back. They were `muted` before,
`muted` resolved to a real `neutral` dot, and dropping them to `undefined`
was a behaviour change I described as a consequence of collapsing the
layer.
Refs apache#2984
Generated-by: Claude Code
Astro-Han added a commit that referenced this pull request Aug 15, 2026
…ary (#3033)
* fix(ui): give the task rail's status dot one meaning
The dot the rail draws for a session passed through two lossy hops and
three disagreeing sources, so it could not say what it meant.
Collapse the mapping. `SessionStatus -> SessionStatusTone ->
StatusDotVariant` becomes one table from status to Astryx's variant. Two
of the seven tones had no distinct outcome at the end of that chain, and
the collapse is what let `waiting_for_user` and `blocked` share
`warning`: blocked now maps to `error`, so "cannot proceed until you fix
a connection" and "holding a question for you" stop rendering alike.
Read the running authority. `runningTurnIds` is the runtime's projection
of the runs it holds; `session.ts` states why a persisted `status` cannot
serve that purpose, and `settledSessionTransientIds` already reads it
first over the same list. The row read neither -- a renderer-local
streaming set, then the stored `status` -- so a task running under a bot
channel or a second window read as idle. The streaming set stays below
it, for the gap between this renderer sending a turn and the host
reporting it back.
Delete what nothing wrote. `review` and `done` were never written by
anything, in any version, so no stored record can carry them and no
reader of them was reachable; they leave `SESSION_STATUSES`, the wire
enum, and the copy tables. `SessionLifecycleStatus` now aliases
`SessionStatus` instead of restating it -- three hand-written copies of
one enum is how those two values survived in two of them after the last
writer went away.
Delete the tautological filter. `SessionFilter` had one value left:
`archived` moved to Settings in #2985 and `flagged` was never selected,
which left a control whose answer is always the same answer. The 「会话」
row that selected it goes with it, as does the branch in
`sessionMatchesNavSelection`. A stored filter is dropped rather than
validated, which is the migration.
Also gone: `SessionStatusPresentation.interactive`, which had no reader,
and a doc comment naming a `SessionStatusIcon` and a chat-header badge as
the tone matrix's consumers -- neither exists.
`status: 'archived'` still duplicates `isArchived`. Consolidating those
two rewrites stored rows, so it is #2984's PR 3, not this one.
Refs #2984
Generated-by: Claude Code
* refactor(ui): rebuild the task row on two signal slots
Every task row in the rail now carries exactly two signal slots. Slot 1 is
a single status dot, reserved as an 8px gutter even when there is nothing to
show so titles stay aligned down the list. Slot 2 holds a compact timestamp
at rest and swaps to the ⋯ menu on hover or keyboard focus, so the menu is
no longer permanently mounted next to every title.
The dot's meaning is resolved once, in priority order: live run (accent,
pulsing) › persisted status › unread (accent, inactive rows only). The live
run reads `runningTurnIds` first, matching `settledSessionTransientIds` over
the same session list — a persisted `running` status can be left behind by a
crash, so it must not outrank the authoritative projection.
The stale pill is gone: it duplicated the timestamp it sat next to. The
worktree mark falls back to the row tooltip rather than competing for a slot.
`RelativeTime` gains `variant="compact"` instead of a second component; a
260px rail row cannot fit the medium-date-plus-time past-horizon fallback.
Generated-by: Claude Code
* refactor(ui): give the rail one top region and one grouping switch
The rail's top region now ends with the grouping switch, and the history
below it starts directly at its first group.
The list heading is gone. "会话" labelled a list that is the only thing under
it, inside a landmark that already names the panel — and it existed mainly to
give the grouping switch somewhere to sit. The switch moves into the sticky
top region instead, where it stays reachable: hung off a heading it scrolled
away with the very list it switches.
It is also full width with text labels now. Two icon-only segments asked the
rail to teach a clock and a folder, and the rail has nowhere to teach them;
按时间 / 按项目 is the entire vocabulary and fits inside the minimum 180px
width with room to spare. Collapsed at 48px it does not render at all — no
segment label fits, and the list it governs is not rendered either.
The group headers lose their ➕. It was the 新任务 row's handler under a
second name, three rows below the row itself, and it needed that second name
precisely because two controls cannot share one accessible name.
Generated-by: Claude Code
* feat(desktop): move task import out of the rail into settings
导入 was a rail row that opened a modal over the conversation. Import is not
navigation: it is a rare setup errand, it needs a source, an archived filter
and a paged directory of someone else's conversations to work through, and
none of that belongs in a 260px column of the tasks you are working on. It
becomes 设置 › 活动 › 导入任务, next to 已归档任务 — both are about the task
catalog rather than the task in front of you.
As a page the flow loses its selection state. A dialog needed one because its
single primary action sat in a footer far from the rows; a page puts 导入 on
the row itself, the way 已归档任务 puts 恢复 on its rows. That also retires
`ExternalSessionImportLifecycle`, whose whole job was refusing to close a
dialog mid-import — there is no dialog left to hold open, and an import that
outlives the page is guarded by the mounted ref instead, so it can still
finish but can no longer steer a shell the user has navigated away from.
The source switch now appears only when there is more than one source. Codex
is the only adapter that ships, so in practice every user was being shown a
segmented control with a single segment — a control nobody can operate. With
one source the section names it instead.
Deliberately not carried over: keeping an imported task in sync with its
source. The importer is a one-shot conversion and nothing behind it watches
the source, so that control would be a promise no coordinator can keep.
Generated-by: Claude Code
* refactor: name the thing a user works on 任务 / task everywhere
The product called the same object 会话, 对话, session and conversation
depending on which file you landed in. Code, protocol and storage keep
`session` — that is a durable contract with a wire format behind it — but
every string a user reads now says 任务 / task.
The rule is what the noun REFERS TO, not the word:
- The Maka entity a user opens, renames, archives and searches → 任务 / task.
- Conversing as a behavior stays 对话 / conversation: "与 Maka 对话" over a
bot, "可用于对话的模型", "对话记录" as a message log.
- Another agent's stored conversations stay 对话 / conversation. 导入任务
lists Codex's conversations and produces Maka tasks; collapsing both onto
one word would erase exactly the distinction that page is about.
- 侧边对话 stays: it is a chat beside a task, not a task in the rail.
Two collisions had to be resolved rather than renamed. The per-task ledger
was 会话任务, which would have become 任务任务 — its items are 待办 / to-dos,
which is what they always were. And the automations row read "定时任务,N 个
未完成任务", two different meanings of 任务 in one label; the count now says
"N 条进行中".
The keyboard sheet loses its ←/→ row. It documented cycling between
会话/已标记/已归档, a filter this PR removed along with `SessionFilter`.
Generated-by: Claude Code
* test(ui): make the narrow-rail story actually narrow
`LongTitlesAndNarrow` framed the panel at 176px but let SideNav keep its own
260px `resizable` width, so the story clipped a wide rail instead of showing
a narrow one — the timestamps it exists to check were outside the frame. It
now drives the rail's own width, at 180px, which is the panel's `minWidth`
and therefore the narrowest state a user can reach.
Generated-by: Claude Code
* fix(ui): keep the rail's 任务 row as the way back to the list
bb39b33 deleted `SessionFilter` and took the 「会话」 row with it, on the
reading that the row only selected a filter whose answer was always the
same answer. Half right: its `isSelected` read the dead filter, but its
`onClick` selected the SECTION, and it was the only control that did.
Without it the rail has three sections and two rows. Collapsed at 48px —
the default state (`readSessionListCollapsed`) — the list is not rendered
at all, so 扩展 and 定时任务 became one-way doors: the only way back to a
running task was 新任务, which answers "show me my tasks" by starting
another one. `streaming-remount.spec.ts` walks exactly that path and had
been rewritten to click a task row that a collapsed rail does not have.
Restore the row as 任务, selected on `section === 'sessions'` and
selecting the section with no filter. The filter deletion stands.
Refs #2984
Generated-by: Claude Code
* fix(ui): route task status through the shared semantic layer
Deleting `SessionStatusTone` was right; replacing it with a private
`SessionStatus -> StatusDotVariant` table was not. `status-vocabulary.ts`
already owns "the one place a status word becomes a colour", so the
private table made a second authority, and the two disagreed: a task
waiting on a permission prompt drew `error` in the rail while the
permission centre drew `attention` for the identical condition.
Map the session enum to `StatusSemantic` and let `dotForStatus` pick the
colour. `blocked` and `waiting_for_user` are both `attention`, which is
what that semantic is defined as -- both are waiting on a person. Giving
`blocked` `error` to tell them apart used colour for a distinction colour
cannot carry; their labels and `describeBlockedReason` do that.
Restore `review` and `done`. They have no writer in current source, but
`SESSION_STATUSES` is read back out of storage, and narrowing it is a
data migration rather than a cleanup: `resolveLegacyStatus` in the JSONL
importer (removed in #2656) passed both values through into real SQLite
stores verbatim, and `normalizeSessionHeader` throws for the WHOLE header
on an unrecognised status, so one stored row carrying `done` fails an
entire catalog page. The migration is its own change.
`archived` and `aborted` get their dots back. They were `muted` before,
`muted` resolved to a real `neutral` dot, and dropping them to `undefined`
was a behaviour change I described as a consequence of collapsing the
layer.
Refs #2984
Generated-by: Claude Code
* fix(ui): keep the task row's facts when it collapses to two slots
The row draws one dot and one trailing slot. That was implemented by
collapsing to a single signal inside the resolver, which made the visual
element the only carrier of the fact -- so removing the trailing `Badge`
removed "stale" itself, not just its pill. `staleTitle` and
`staleAriaLabel` were left with no reader at all, the surviving `opacity`
cue is cancelled on the selected row by design, and opacity announces
nothing. Worktree went the same way: an `aria-label`ed icon inside the
button became a `title` on a non-interactive ancestor, which contributes
to neither the button's name nor its description.
Resolve a LIST of signals in priority order, draw `signals[0]`, and put
the rest -- plus worktree and the absolute timestamp -- in one visually
hidden span inside the button. Two slots, same pixels, and the facts stop
depending on which of them had room. Signals also stop eating each other:
`aborted` now keeps its own dot instead of falling through to the unread
branch, where an aborted task with unread text drew the same accent dot
as a running one.
Stale joins the list as `attention`, so a stale task finally has a signal
that survives being selected. It is resolved here rather than in
`presentSessionStatus` because it is a renderer-derived fact, not a
persisted `SessionStatus`.
Drop the `runningTurnIds` read. No `SessionSummary` reaching Desktop
carries that field: Runtime Host's catalog reads storage directly, the
wire projection does not list it, and Desktop Main's converter copies
persisted fields only -- the identifier appears nowhere in
`packages/runtime-host/src` or `apps/desktop/src/main` outside a test
fixture. It was a read of something nothing populates, described as
reading the authority. `streaming` is what the rail actually has, with
the limit that it only knows this renderer's turns; a real live-run
projection from the Host is its own change.
Also: `pointer-events: none` on the resting ⋯, which sat invisible at
`z-index: 1` over the trailing slot and swallowed clicks; and the inner
list no longer repeats the rail's own accessible name.
Refs #2984
Generated-by: Claude Code
* fix(desktop): refresh the catalog when an import's outcome is unknown
`external-sessions:import` emits `sessions:changed` on success, so a task
imported while the user walks away from Settings still reaches the rail
on its own -- the comment in `app-shell.tsx` claiming nothing notifies
the shell was wrong, and is corrected here.
The gap is the other branch. `commit_outcome_unknown` means the Host
cannot say whether the task was committed, and that path returned a
result without emitting anything. The only trace was the page's own
banner, and 导入任务 is a Settings page: leaving it unmounts the banner,
which is exactly when someone comes back and imports the same
conversation again. Emit there too, with no id, because not knowing which
task landed is what the code means.
Also return the import promise from `clickAction` instead of `void`-ing
it. Astryx's Button awaits it and drops repeat clicks until it settles;
discarding it left double-submit to `importingId` alone, one render
behind the second click.
Refs #2984
Generated-by: Claude Code
* fix(desktop): re-point the structural gates the surface move broke
Deleting the import dialog left exact-string readers behind. `scripts/
check-astryx-alignment.mjs` read the dialog and its stylesheet by path,
so running it exited 1 with ENOENT; the surface inventory still listed
both files and had no row for the page that replaced them.
The dialog's Item-row guards are not migrated, they are dropped. They
checked that a SELECTED row stayed keyboard-reachable -- no parent role
stealing Item's native button, no selected-only tabIndex trapping focus.
设置 › 活动 › 导入任务 has no selection: 导入 sits on each row the way 恢复
does on the archived page. Asserting those smells against the new page
would guard a shape it does not have. The button guard and a ListItem
import check follow the surface to the page.
Inventory rows for the new page and for #2985's archived-tasks page,
which was never added. Three runtime-host entries stay missing; they
predate this branch.
Also read the Daily Review model label from its copy table instead of a
literal. The story matched '跟随对话默认', which the rename retired, so it
silently found nothing -- and `storybook-visual-smoke.mjs` disables every
`play` function, so CI could not report it.
Refs #2984
Generated-by: Claude Code
* refactor: finish the 任务 rename where the rule missed
The rename was applied by rule and reviewed by hand, which is the process
that produces both halves of this: strings that kept the old noun, and
strings that took the new one where the old was right.
Same-surface contradictions, which are what a user actually notices:
- The to-do panel's error banner said 任务载入失败 next to a button
labelled 重新载入待办 -- one `Banner`, two vocabularies.
- `chat-readiness` said 当前任务 and then 新建会话 in one sentence.
- The bot's help text offered to 清空当前对话开新会话 while the reply to
that same command already said 任务已重置.
- 定时任务 described itself as 安排定时任务,并回顾本机任务, and its
delivery option read Agent 任务执行 -- two meanings of 任务 in one form.
- `settlementFailed` called the side chat a 任务 and then a 侧边对话;
`forkSetupFailed` called it 追问任务, a third name for one object. Both
are 侧边对话 now, matching the en twins.
zh/en divergence, where the catalogs share keys but stopped sharing
meaning: Daily Review counted 任务 in zh and conversations in en (and its
en empty state said conversations on one branch, tasks on the other); the
terminal panel said "task" and then "the session's terminal".
Missed outright: `tool-activity/copy.ts` (由源会话管理 on every terminal
result chip), `connection-error-copy.ts`, and the CLI's own prose
(`allow for session`, `Give this session full access`, `--continue`).
Kept deliberately: 和 Maka 对话 as a behaviour, Codex's stored 对话 on the
import page, 侧边对话, 对话框 for dialog, and `<session-id>` as a CLI
contract. The palette's `nav:sessions` keywords get 会话 and 对话 back --
collapsing both onto 任务 left a duplicate entry and dropped the words a
long-time user would still type.
Refs #2984
Generated-by: Claude Code
* fix(ui): route the task row's last two dots through the shared vocabulary
`streaming` and `hasUnread` still wrote `'accent'` by hand while the status
and stale signals resolved through `dotForStatus`. Half a function deciding
its own colours is the drift this change set out to close -- the literal is
what let the rail and Settings disagree about the same fact before. Both are
`active`: streaming is the system working on it, and unread text is
"something happened here" rather than a question waiting on the user, which
is the line the two semantics exist to draw. `dotForStatus('active')` is
`'accent'`, so nothing moves on screen.
Also drops the `copy` binding `SessionHistoryList` no longer reads. It fed
the list's own aria-label, and the outer SideNav took that name over when the
rail was rebuilt.
Refs #2984
Generated-by: Claude Code
* docs: say what each session-status-presentation file actually owns
Desktop's copy claimed the status-to-dot mapping was "re-exported below" and
that `describeBlockedReason` was defined there. Both moved to `@maka/ui` in
this branch and neither is in the file; what is left is Desktop-only judgment
-- which blocked reasons are worth acting on, and what to offer after a turn
fails -- so the header says that instead.
The contract the old header carried is real and had nowhere to live after the
move: a UI label must never show a raw `SessionBlockedReason`, and a new
reason has to extend the core enum and the copy matrix together or it reads as
`unknown`. It now sits on `describeBlockedReason` in `@maka/ui`, where the
matrix it constrains is.
Refs #2984
Generated-by: Claude Code
* test: make the restored-task and import fixtures answer like their sources
Both fixtures modelled states their real source cannot produce, which makes
them evidence for nothing.
The purge tests built a restored task by flipping `isArchived` alone, leaving
`status: 'archived'` behind. `SessionStore.unarchive` writes both fields
together, so no stored row looks like that. The sweep only reads `isArchived`
today, so no assertion changes -- the point is that the next assertion written
against these rows would be checking a state that cannot happen. A `restored`
helper names the pair once.
The 导入任务 story's `list` ignored `includeArchived` and `cursor`: it rendered
the archived conversation while the filter was off and handed 加载更多 the same
first page forever, under a comment claiming it demonstrated both controls.
It now filters and pages, with a fourth conversation so the default view is a
short first page rather than the whole list.
Refs #2984
Generated-by: Claude Code
* fix(desktop): tidy the two places the 任务 rename passed through twice
`fake_backend` reaches the user down two paths -- the reason table, and the
header check in `assertSessionCanSend`, which never gets far enough to look a
connection up. They said the same sentence in two copies, so renaming 会话 to
任务 had to be done twice. One `FAKE_BACKEND_MESSAGE` now.
The English workspace help came out of the rename as "Any task can switch next
to the input box", which makes the task the actor and never names what it
switches. It says the project.
Refs #2984
Generated-by: Claude Code
* chore: regenerate the Astryx surface inventory
The Markdown half still listed `external-session-import-dialog.tsx` and
`external-session-import.css` after this branch deleted them, because the two
halves were edited by hand and `check-astryx-surface-inventory.mjs` only
verifies that on-disk files appear in the inventory -- never that inventory
rows still exist on disk. It is also not wired into CI, so the drift was
invisible from both directions.
Running the generator also picks up three files that arrived from main and
were never listed: `runtime-host-profiles-section.tsx`,
`runtime-host-ssh-terminal-dialog.tsx`, and `settings/runtime-host.css`. They
are unrelated to this branch, but a generated file regenerated in halves is
how it drifted in the first place.
Refs #2984
Generated-by: Claude Code
* test(ui): show every task status in the story that exists to show them
Two gaps, both found by shooting the story for a before/after comparison.
`review` and `done` were dropped from `statusSessions` when this branch briefly
deleted them from `SESSION_STATUSES`. The enum values and their labels came
back; the fixture did not, so the story that covers every status covered six of
eight -- and the two missing ones are exactly the two whose colours this change
decided on purpose, `attention` and `success`.
`StoryFrame` also defaulted to 240px while `SessionListPanel`'s rail defaults to
260, so every story that did not pass a width was clipping the rail by 20px.
That lands on the trailing slot, which is where this change puts the timestamp
-- the stories could not show whether it fits. Stories that want a narrow rail
still pass the width to both, which is what the note on `panelProps` is about.
Refs #2984
Generated-by: Claude Code
* refactor(ui): drop the rail's 任务 row
Expanded, the row selected a section whose content was already on screen
directly under it -- the same redundancy as the 会话 list heading this change
deleted one row down, and clicking any task row does the same selection.
It was kept for the collapsed rail, where the list is not rendered, on the
argument that 扩展 and 定时任务 were otherwise one-way doors. That was wrong.
Collapsed, the rail cannot switch tasks at all, so getting back to any task
already means widening it; the titlebar's 展开侧边栏 toggle does that
unconditionally (app-shell-chrome-actions.tsx renders `ChromeColumnToggle` with
no guard), and `activeId` survives a section change, so the widened rail comes
back with the task you left already marked. The row bought one click in a state
the user is leaving regardless, and charged a permanent slot for it.
`sessions` now has no control of its own on the rail. It is where you are
unless you went somewhere, which is why the other two sections light up and
this one has nothing to light.
`streaming-remount` returns the way the product now offers -- widen, then click
the row carrying `aria-current="page"` -- which is a better assertion than the
old one anyway: it checks that the task survives the trip, not just that a
button exists.
Refs #2984
Generated-by: Claude Code
* fix(runtime-host): coalesce a repeat import while the first is still running
Moving 导入任务 from a dialog into a Settings page removed the only thing
stopping one intent from becoming two tasks, and this branch is where that
happened. `ExternalSessionImportLifecycle` refused to close the dialog while an
import was active; retiring it was described here as dropping a guard against
nothing. It was not. The page it was replaced by is one the user may leave
mid-import -- the import continues in Desktop Main by design -- and the page's
`importingId` dies with it, so coming back and pressing 导入 again issues a
second request against a source that is still importing. Nothing downstream
deduplicates: `treats repeats as independent copies` is a pinned behaviour, so
the second request lands a second task the user now has to tell apart.
The guard belongs to the Host, not to the page that asked. Import is a Host
operation and the Host is the only party that knows one is running; a client
knows about its own requests, which is why a second window or the CLI would
have reproduced this with the page's state intact.
Concurrent repeats collapse onto the first attempt's promise and both callers
get its outcome, success or failure, because it is one operation. Sequential
repeats are untouched -- the entry is gone by the time the first settles -- so
importing the same conversation again on purpose still makes an independent
copy, which the existing test continues to pin.
Reported by @M4n5ter in review of #3033.
Refs #2984
Generated-by: Claude Code
Sign up for freeto 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

@jackwener
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' refactor: remove obsolete fixtures and JSONL import by jackwener · Pull Request #2656 · apache/maka · GitHub
Skip to content

refactor: remove obsolete fixtures and JSONL import - #2656

Merged
jackwener merged 1 commit into
mainfrom
codex/trim-low-value-fixtures
Aug 10, 2026
Merged

refactor: remove obsolete fixtures and JSONL import#2656
jackwener merged 1 commit into
mainfrom
codex/trim-low-value-fixtures

Conversation

@jackwener

Copy link
Copy Markdown
Member

Summary

  • reduce the Desktop E2E fixture registry from about 50 historical scenarios to the 13 scenarios consumed by current E2E, alignment audit, or smoke entry points
  • remove orphaned seed generators, renderer-only fixture controls, and the 1,361-line self-referential fixture test
  • remove the retired legacy JSONL session importer, its automatic SessionStore startup wiring, tests, and 11 transcript fixtures
  • preserve process/crash/lock/race fixtures and external Codex JSONL adapters because they cover active boundaries

Impact

  • 32 files changed
  • 5,194 lines removed, 141 lines added (net -5,053)
  • fixture-related tracked files reduced from 75 to 62

Validation

  • git diff --check
  • Biome lint on all changed source files
  • TypeScript: @maka/core, @maka/storage, and Desktop renderer
  • node --test scripts/fixture-env.test.mjs

Desktop main typecheck still reports the existing scheduled-task API/dist mismatch on origin/main; no errors point to this change.

@jackwener
jackwener merged commit 90bdb30 into mainAug 10, 2026
10 checks passed
@jackwener
jackwener deleted the codex/trim-low-value-fixtures branch August 10, 2026 19:21
ARE404 added a commit to ARE404/maka-agent that referenced this pull request Aug 13, 2026
apache#2580 moved the rail's tick onto Astryx's Button. The bar the tick draws
was a direct child of the flex tick and got blockified; the Button wraps
its children in a label span, so the bar went back to normal flow as an
inline box. An inline box takes no width or height, so every bar computed
to 0x0 and the rail shipped invisible in 0.1.9 and 0.1.10 — present in the
DOM, painting nothing.
`display: block` on the bar restores it. Measured on the new fixture at
1280x800: the rail's box goes from 8px wide (its own padding, ticks
contributing nothing) back to the designed 22px.
This is the third time the rail has failed by rendering and not painting
— apache#2161 pinned it against a containing block as tall as the conversation,
apache#2338 parked it under macOS's overlay scrollbar — and the second time it
reached a release. The e2e coverage that would have caught all three was
deleted in apache#2462, and the multi-prompt fixtures it ran on in apache#2656, so
this adds back the smallest thing that closes the gap:
- `chat-prompt-rail`, a plain 8-prompt conversation. The rail hides itself
below three prompts, so the shipped single-prompt fixture cannot show it
at all.
- `prompt-rail.spec.ts` with one test per past failure: bars have a real
box, the rail stays inside the scrollport at both scroll extremes, and a
tick is what the pointer lands on. Three tests where the deleted suite
had nine.
Verified the first test fails on the unfixed renderer with "Expected: > 0,
Received: 0" and passes with the fix. Neither a static CSS read nor a
jsdom unit test can see any of this: jsdom has no layout engine.
Astro-Han pushed a commit that referenced this pull request Aug 13, 2026
…2923)
* fix(ui): give the prompt rail's tick bars a box again
#2580 moved the rail's tick onto Astryx's Button. The bar the tick draws
was a direct child of the flex tick and got blockified; the Button wraps
its children in a label span, so the bar went back to normal flow as an
inline box. An inline box takes no width or height, so every bar computed
to 0x0 and the rail shipped invisible in 0.1.9 and 0.1.10 — present in the
DOM, painting nothing.
`display: block` on the bar restores it. Measured on the new fixture at
1280x800: the rail's box goes from 8px wide (its own padding, ticks
contributing nothing) back to the designed 22px.
This is the third time the rail has failed by rendering and not painting
— #2161 pinned it against a containing block as tall as the conversation,
#2338 parked it under macOS's overlay scrollbar — and the second time it
reached a release. The e2e coverage that would have caught all three was
deleted in #2462, and the multi-prompt fixtures it ran on in #2656, so
this adds back the smallest thing that closes the gap:
- `chat-prompt-rail`, a plain 8-prompt conversation. The rail hides itself
below three prompts, so the shipped single-prompt fixture cannot show it
at all.
- `prompt-rail.spec.ts` with one test per past failure: bars have a real
box, the rail stays inside the scrollport at both scroll extremes, and a
tick is what the pointer lands on. Three tests where the deleted suite
had nine.
Verified the first test fails on the unfixed renderer with "Expected: > 0,
Received: 0" and passes with the fix. Neither a static CSS read nor a
jsdom unit test can see any of this: jsdom has no layout engine.
* fix(ui): make the prompt rail's hover and jump behave
Four things the rail got wrong once it was visible again, found by using it:
- A 4px gap between ticks was a band where the pointer was over the rail
and over no tick, so the dock-style hover falloff dropped out and picked
up again every few pixels of travel. The rail's `gap` moves into the
ticks' own `padding-block`: same pitch, hit boxes now tile.
- The hover preview waited 300ms before opening — Astryx's HoverCard
default, meant for a pointer crossing a wide row on its way somewhere
else. A tick is 22px of rail that nothing is on the way to, and the wait
is the one part of this hover with no motion in it. Now 120ms.
- The highlight glided 280ms to wherever a click landed, so crossing
twenty prompts read as the bar flying off across the rail. A click now
owns the highlight until its scroll settles: no glide, and the scroll
no longer walks the highlight through every prompt it passes.
- The first click into a session did nothing until the reader scrolled by
hand. See below.
That last one is a collision between Astryx's auto-follow lock and the
progressive transcript mount, and neither side is wrong on its own.
`useChatStreamScroll` unlocks on a scroll up, detected by comparing
scrollTop between events — but it ignores any scroll event that arrives
with a changed scrollHeight or offsetHeight, because Chrome fires those
when content resizes and they are not the reader moving. A jump into an
unmounted turn mounts it and the fill that follows changes scrollHeight
for several frames, so the jump's own scroll is invisible to the lock: it
stays on, and `scrollIfLocked` pulls the transcript back to the bottom.
Only a wheel gesture broke it, which takes a separate path in Astryx.
`holdJumpDestination` re-aims at the target on each height change until
the fill stops. The last of those scrolls lands with a stable height,
which is the one the lock finally reads as a scroll up. Measured on the
30-prompt fixture: clicking the first tick went to scrollTop 7042 (the
bottom) and now goes to 24 and holds.
The fixture grows from 8 prompts to 30 because the progressive mount's
initial window is 10 — at 8 the head of the transcript is already mounted
and the jump-into-unmounted-turns path never runs at all.
Coverage note: the e2e case for the first click is an end-to-end check,
not a guard. Whether the lock wins depends on which frame the fill lands
on relative to a smooth scroll still in flight, and it goes green against
the unfixed renderer often enough to be worthless as one. The guard is
the `holdJumpDestination` unit test, which drives the frames itself.
* fix(ui): own a rail jump through the mount instead of racing it
Review of #2923 found the jump's ownership bound to a clock rather than to
the navigation, and the e2e case that was supposed to guard it asserting
almost nothing. Both hold.
Jump ownership:
- A second click during a jump only replaced the target; the first click's
700ms timer still governed, and could clear the second jump mid-flight.
Each click now carries its own sequence and starts its own hold.
- The fixed window is gone. A hold runs until the progressive mount reports
the transcript filled AND nothing has moved for a few frames, so a long
transcript is never released mid-fill, and it ends the moment the reader
touches the transcript (wheel, touch, pointer, key) rather than outliving
their interest in it.
Chasing the "just release auto-follow" direction the review preferred found
that ChatLayout publishes no such seam, so this adds one — `unlockAutoFollow`
on `ChatLayoutContextValue`, exposing the scroll hook's existing `unlock`
(patch hunk + patches/README entry). It is necessary and it is not
sufficient, which the earlier framing got wrong:
- Astryx re-locks on any `scrollend` that settles near the bottom, and a
session that opens at the bottom produces exactly that while the mount is
still catching up. Releasing once at the click is undone before the jump
goes anywhere — traced: released at the click, landed at 154ms, dragged
back to the bottom by 166ms. The release is now re-asserted for the life
of the hold.
- Auto-follow is not the only thing moving the transcript. The progressive
mount's own scroll compensation holds the reader's position across each
fill step, and mounting the turn a jump asked for IS a fill step, so it
lands after the jump and restores the position the jump just left. That
one no seam can fix; it is what the hold is for.
Jumps also scroll instantly now, whatever the app's scroll-motion policy
says. A jump is a teleport the reader asked for, and an animated one does
not survive this surface: traced on the 30-prompt fixture, the smooth scroll
was cancelled by the mount's compensation and by the follow spring and
stalled two pixels from where it started.
Coverage:
- The first-click e2e case named the wrong turn (`[data-turn-id]` is the
first MOUNTED turn, whose top is already negative at the opening scroll
position, so an upper-bound-only check passed without the jump doing
anything). It now names `turn-prompt-rail-1`, bounds it on both sides, and
asserts that tick's `aria-current`.
- `emulateMedia` could not put that case on the production scroll path:
`resolveScrollMotionBehavior` collapses motion for ANY fixture, keyed on
`data-maka-e2e-fixture` rather than on the media query. Fixtures can now
ask for a behavior back (`scrollMotion`, per launch — it costs seconds of
settling per window, so only the case that needs it pays), with unit
coverage for the precedence: a fixture request never outranks a stated
preference for less motion.
- `holdJumpDestination`'s unit tests grew the two cases its rewrite is
about: it must not settle while the transcript is still filling, and it
must hand the transcript back the moment the reader touches it.
Verified 5/5 on the smooth-scroll fixture, where the previous revision lost
1 in 4. `quote-selection.spec.ts` flakes on this machine (1 in 4) at
upstream/main as well, unchanged by this branch.
Astro-Han added a commit to Astro-Han/maka-agent that referenced this pull request Aug 15, 2026
Deleting `SessionStatusTone` was right; replacing it with a private
`SessionStatus -> StatusDotVariant` table was not. `status-vocabulary.ts`
already owns "the one place a status word becomes a colour", so the
private table made a second authority, and the two disagreed: a task
waiting on a permission prompt drew `error` in the rail while the
permission centre drew `attention` for the identical condition.
Map the session enum to `StatusSemantic` and let `dotForStatus` pick the
colour. `blocked` and `waiting_for_user` are both `attention`, which is
what that semantic is defined as -- both are waiting on a person. Giving
`blocked` `error` to tell them apart used colour for a distinction colour
cannot carry; their labels and `describeBlockedReason` do that.
Restore `review` and `done`. They have no writer in current source, but
`SESSION_STATUSES` is read back out of storage, and narrowing it is a
data migration rather than a cleanup: `resolveLegacyStatus` in the JSONL
importer (removed in apache#2656) passed both values through into real SQLite
stores verbatim, and `normalizeSessionHeader` throws for the WHOLE header
on an unrecognised status, so one stored row carrying `done` fails an
entire catalog page. The migration is its own change.
`archived` and `aborted` get their dots back. They were `muted` before,
`muted` resolved to a real `neutral` dot, and dropping them to `undefined`
was a behaviour change I described as a consequence of collapsing the
layer.
Refs apache#2984
Generated-by: Claude Code
Astro-Han added a commit to Astro-Han/maka-agent that referenced this pull request Aug 15, 2026
Deleting `SessionStatusTone` was right; replacing it with a private
`SessionStatus -> StatusDotVariant` table was not. `status-vocabulary.ts`
already owns "the one place a status word becomes a colour", so the
private table made a second authority, and the two disagreed: a task
waiting on a permission prompt drew `error` in the rail while the
permission centre drew `attention` for the identical condition.
Map the session enum to `StatusSemantic` and let `dotForStatus` pick the
colour. `blocked` and `waiting_for_user` are both `attention`, which is
what that semantic is defined as -- both are waiting on a person. Giving
`blocked` `error` to tell them apart used colour for a distinction colour
cannot carry; their labels and `describeBlockedReason` do that.
Restore `review` and `done`. They have no writer in current source, but
`SESSION_STATUSES` is read back out of storage, and narrowing it is a
data migration rather than a cleanup: `resolveLegacyStatus` in the JSONL
importer (removed in apache#2656) passed both values through into real SQLite
stores verbatim, and `normalizeSessionHeader` throws for the WHOLE header
on an unrecognised status, so one stored row carrying `done` fails an
entire catalog page. The migration is its own change.
`archived` and `aborted` get their dots back. They were `muted` before,
`muted` resolved to a real `neutral` dot, and dropping them to `undefined`
was a behaviour change I described as a consequence of collapsing the
layer.
Refs apache#2984
Generated-by: Claude Code
Astro-Han added a commit that referenced this pull request Aug 15, 2026
…ary (#3033)
* fix(ui): give the task rail's status dot one meaning
The dot the rail draws for a session passed through two lossy hops and
three disagreeing sources, so it could not say what it meant.
Collapse the mapping. `SessionStatus -> SessionStatusTone ->
StatusDotVariant` becomes one table from status to Astryx's variant. Two
of the seven tones had no distinct outcome at the end of that chain, and
the collapse is what let `waiting_for_user` and `blocked` share
`warning`: blocked now maps to `error`, so "cannot proceed until you fix
a connection" and "holding a question for you" stop rendering alike.
Read the running authority. `runningTurnIds` is the runtime's projection
of the runs it holds; `session.ts` states why a persisted `status` cannot
serve that purpose, and `settledSessionTransientIds` already reads it
first over the same list. The row read neither -- a renderer-local
streaming set, then the stored `status` -- so a task running under a bot
channel or a second window read as idle. The streaming set stays below
it, for the gap between this renderer sending a turn and the host
reporting it back.
Delete what nothing wrote. `review` and `done` were never written by
anything, in any version, so no stored record can carry them and no
reader of them was reachable; they leave `SESSION_STATUSES`, the wire
enum, and the copy tables. `SessionLifecycleStatus` now aliases
`SessionStatus` instead of restating it -- three hand-written copies of
one enum is how those two values survived in two of them after the last
writer went away.
Delete the tautological filter. `SessionFilter` had one value left:
`archived` moved to Settings in #2985 and `flagged` was never selected,
which left a control whose answer is always the same answer. The 「会话」
row that selected it goes with it, as does the branch in
`sessionMatchesNavSelection`. A stored filter is dropped rather than
validated, which is the migration.
Also gone: `SessionStatusPresentation.interactive`, which had no reader,
and a doc comment naming a `SessionStatusIcon` and a chat-header badge as
the tone matrix's consumers -- neither exists.
`status: 'archived'` still duplicates `isArchived`. Consolidating those
two rewrites stored rows, so it is #2984's PR 3, not this one.
Refs #2984
Generated-by: Claude Code
* refactor(ui): rebuild the task row on two signal slots
Every task row in the rail now carries exactly two signal slots. Slot 1 is
a single status dot, reserved as an 8px gutter even when there is nothing to
show so titles stay aligned down the list. Slot 2 holds a compact timestamp
at rest and swaps to the ⋯ menu on hover or keyboard focus, so the menu is
no longer permanently mounted next to every title.
The dot's meaning is resolved once, in priority order: live run (accent,
pulsing) › persisted status › unread (accent, inactive rows only). The live
run reads `runningTurnIds` first, matching `settledSessionTransientIds` over
the same session list — a persisted `running` status can be left behind by a
crash, so it must not outrank the authoritative projection.
The stale pill is gone: it duplicated the timestamp it sat next to. The
worktree mark falls back to the row tooltip rather than competing for a slot.
`RelativeTime` gains `variant="compact"` instead of a second component; a
260px rail row cannot fit the medium-date-plus-time past-horizon fallback.
Generated-by: Claude Code
* refactor(ui): give the rail one top region and one grouping switch
The rail's top region now ends with the grouping switch, and the history
below it starts directly at its first group.
The list heading is gone. "会话" labelled a list that is the only thing under
it, inside a landmark that already names the panel — and it existed mainly to
give the grouping switch somewhere to sit. The switch moves into the sticky
top region instead, where it stays reachable: hung off a heading it scrolled
away with the very list it switches.
It is also full width with text labels now. Two icon-only segments asked the
rail to teach a clock and a folder, and the rail has nowhere to teach them;
按时间 / 按项目 is the entire vocabulary and fits inside the minimum 180px
width with room to spare. Collapsed at 48px it does not render at all — no
segment label fits, and the list it governs is not rendered either.
The group headers lose their ➕. It was the 新任务 row's handler under a
second name, three rows below the row itself, and it needed that second name
precisely because two controls cannot share one accessible name.
Generated-by: Claude Code
* feat(desktop): move task import out of the rail into settings
导入 was a rail row that opened a modal over the conversation. Import is not
navigation: it is a rare setup errand, it needs a source, an archived filter
and a paged directory of someone else's conversations to work through, and
none of that belongs in a 260px column of the tasks you are working on. It
becomes 设置 › 活动 › 导入任务, next to 已归档任务 — both are about the task
catalog rather than the task in front of you.
As a page the flow loses its selection state. A dialog needed one because its
single primary action sat in a footer far from the rows; a page puts 导入 on
the row itself, the way 已归档任务 puts 恢复 on its rows. That also retires
`ExternalSessionImportLifecycle`, whose whole job was refusing to close a
dialog mid-import — there is no dialog left to hold open, and an import that
outlives the page is guarded by the mounted ref instead, so it can still
finish but can no longer steer a shell the user has navigated away from.
The source switch now appears only when there is more than one source. Codex
is the only adapter that ships, so in practice every user was being shown a
segmented control with a single segment — a control nobody can operate. With
one source the section names it instead.
Deliberately not carried over: keeping an imported task in sync with its
source. The importer is a one-shot conversion and nothing behind it watches
the source, so that control would be a promise no coordinator can keep.
Generated-by: Claude Code
* refactor: name the thing a user works on 任务 / task everywhere
The product called the same object 会话, 对话, session and conversation
depending on which file you landed in. Code, protocol and storage keep
`session` — that is a durable contract with a wire format behind it — but
every string a user reads now says 任务 / task.
The rule is what the noun REFERS TO, not the word:
- The Maka entity a user opens, renames, archives and searches → 任务 / task.
- Conversing as a behavior stays 对话 / conversation: "与 Maka 对话" over a
bot, "可用于对话的模型", "对话记录" as a message log.
- Another agent's stored conversations stay 对话 / conversation. 导入任务
lists Codex's conversations and produces Maka tasks; collapsing both onto
one word would erase exactly the distinction that page is about.
- 侧边对话 stays: it is a chat beside a task, not a task in the rail.
Two collisions had to be resolved rather than renamed. The per-task ledger
was 会话任务, which would have become 任务任务 — its items are 待办 / to-dos,
which is what they always were. And the automations row read "定时任务,N 个
未完成任务", two different meanings of 任务 in one label; the count now says
"N 条进行中".
The keyboard sheet loses its ←/→ row. It documented cycling between
会话/已标记/已归档, a filter this PR removed along with `SessionFilter`.
Generated-by: Claude Code
* test(ui): make the narrow-rail story actually narrow
`LongTitlesAndNarrow` framed the panel at 176px but let SideNav keep its own
260px `resizable` width, so the story clipped a wide rail instead of showing
a narrow one — the timestamps it exists to check were outside the frame. It
now drives the rail's own width, at 180px, which is the panel's `minWidth`
and therefore the narrowest state a user can reach.
Generated-by: Claude Code
* fix(ui): keep the rail's 任务 row as the way back to the list
bb39b33 deleted `SessionFilter` and took the 「会话」 row with it, on the
reading that the row only selected a filter whose answer was always the
same answer. Half right: its `isSelected` read the dead filter, but its
`onClick` selected the SECTION, and it was the only control that did.
Without it the rail has three sections and two rows. Collapsed at 48px —
the default state (`readSessionListCollapsed`) — the list is not rendered
at all, so 扩展 and 定时任务 became one-way doors: the only way back to a
running task was 新任务, which answers "show me my tasks" by starting
another one. `streaming-remount.spec.ts` walks exactly that path and had
been rewritten to click a task row that a collapsed rail does not have.
Restore the row as 任务, selected on `section === 'sessions'` and
selecting the section with no filter. The filter deletion stands.
Refs #2984
Generated-by: Claude Code
* fix(ui): route task status through the shared semantic layer
Deleting `SessionStatusTone` was right; replacing it with a private
`SessionStatus -> StatusDotVariant` table was not. `status-vocabulary.ts`
already owns "the one place a status word becomes a colour", so the
private table made a second authority, and the two disagreed: a task
waiting on a permission prompt drew `error` in the rail while the
permission centre drew `attention` for the identical condition.
Map the session enum to `StatusSemantic` and let `dotForStatus` pick the
colour. `blocked` and `waiting_for_user` are both `attention`, which is
what that semantic is defined as -- both are waiting on a person. Giving
`blocked` `error` to tell them apart used colour for a distinction colour
cannot carry; their labels and `describeBlockedReason` do that.
Restore `review` and `done`. They have no writer in current source, but
`SESSION_STATUSES` is read back out of storage, and narrowing it is a
data migration rather than a cleanup: `resolveLegacyStatus` in the JSONL
importer (removed in #2656) passed both values through into real SQLite
stores verbatim, and `normalizeSessionHeader` throws for the WHOLE header
on an unrecognised status, so one stored row carrying `done` fails an
entire catalog page. The migration is its own change.
`archived` and `aborted` get their dots back. They were `muted` before,
`muted` resolved to a real `neutral` dot, and dropping them to `undefined`
was a behaviour change I described as a consequence of collapsing the
layer.
Refs #2984
Generated-by: Claude Code
* fix(ui): keep the task row's facts when it collapses to two slots
The row draws one dot and one trailing slot. That was implemented by
collapsing to a single signal inside the resolver, which made the visual
element the only carrier of the fact -- so removing the trailing `Badge`
removed "stale" itself, not just its pill. `staleTitle` and
`staleAriaLabel` were left with no reader at all, the surviving `opacity`
cue is cancelled on the selected row by design, and opacity announces
nothing. Worktree went the same way: an `aria-label`ed icon inside the
button became a `title` on a non-interactive ancestor, which contributes
to neither the button's name nor its description.
Resolve a LIST of signals in priority order, draw `signals[0]`, and put
the rest -- plus worktree and the absolute timestamp -- in one visually
hidden span inside the button. Two slots, same pixels, and the facts stop
depending on which of them had room. Signals also stop eating each other:
`aborted` now keeps its own dot instead of falling through to the unread
branch, where an aborted task with unread text drew the same accent dot
as a running one.
Stale joins the list as `attention`, so a stale task finally has a signal
that survives being selected. It is resolved here rather than in
`presentSessionStatus` because it is a renderer-derived fact, not a
persisted `SessionStatus`.
Drop the `runningTurnIds` read. No `SessionSummary` reaching Desktop
carries that field: Runtime Host's catalog reads storage directly, the
wire projection does not list it, and Desktop Main's converter copies
persisted fields only -- the identifier appears nowhere in
`packages/runtime-host/src` or `apps/desktop/src/main` outside a test
fixture. It was a read of something nothing populates, described as
reading the authority. `streaming` is what the rail actually has, with
the limit that it only knows this renderer's turns; a real live-run
projection from the Host is its own change.
Also: `pointer-events: none` on the resting ⋯, which sat invisible at
`z-index: 1` over the trailing slot and swallowed clicks; and the inner
list no longer repeats the rail's own accessible name.
Refs #2984
Generated-by: Claude Code
* fix(desktop): refresh the catalog when an import's outcome is unknown
`external-sessions:import` emits `sessions:changed` on success, so a task
imported while the user walks away from Settings still reaches the rail
on its own -- the comment in `app-shell.tsx` claiming nothing notifies
the shell was wrong, and is corrected here.
The gap is the other branch. `commit_outcome_unknown` means the Host
cannot say whether the task was committed, and that path returned a
result without emitting anything. The only trace was the page's own
banner, and 导入任务 is a Settings page: leaving it unmounts the banner,
which is exactly when someone comes back and imports the same
conversation again. Emit there too, with no id, because not knowing which
task landed is what the code means.
Also return the import promise from `clickAction` instead of `void`-ing
it. Astryx's Button awaits it and drops repeat clicks until it settles;
discarding it left double-submit to `importingId` alone, one render
behind the second click.
Refs #2984
Generated-by: Claude Code
* fix(desktop): re-point the structural gates the surface move broke
Deleting the import dialog left exact-string readers behind. `scripts/
check-astryx-alignment.mjs` read the dialog and its stylesheet by path,
so running it exited 1 with ENOENT; the surface inventory still listed
both files and had no row for the page that replaced them.
The dialog's Item-row guards are not migrated, they are dropped. They
checked that a SELECTED row stayed keyboard-reachable -- no parent role
stealing Item's native button, no selected-only tabIndex trapping focus.
设置 › 活动 › 导入任务 has no selection: 导入 sits on each row the way 恢复
does on the archived page. Asserting those smells against the new page
would guard a shape it does not have. The button guard and a ListItem
import check follow the surface to the page.
Inventory rows for the new page and for #2985's archived-tasks page,
which was never added. Three runtime-host entries stay missing; they
predate this branch.
Also read the Daily Review model label from its copy table instead of a
literal. The story matched '跟随对话默认', which the rename retired, so it
silently found nothing -- and `storybook-visual-smoke.mjs` disables every
`play` function, so CI could not report it.
Refs #2984
Generated-by: Claude Code
* refactor: finish the 任务 rename where the rule missed
The rename was applied by rule and reviewed by hand, which is the process
that produces both halves of this: strings that kept the old noun, and
strings that took the new one where the old was right.
Same-surface contradictions, which are what a user actually notices:
- The to-do panel's error banner said 任务载入失败 next to a button
labelled 重新载入待办 -- one `Banner`, two vocabularies.
- `chat-readiness` said 当前任务 and then 新建会话 in one sentence.
- The bot's help text offered to 清空当前对话开新会话 while the reply to
that same command already said 任务已重置.
- 定时任务 described itself as 安排定时任务,并回顾本机任务, and its
delivery option read Agent 任务执行 -- two meanings of 任务 in one form.
- `settlementFailed` called the side chat a 任务 and then a 侧边对话;
`forkSetupFailed` called it 追问任务, a third name for one object. Both
are 侧边对话 now, matching the en twins.
zh/en divergence, where the catalogs share keys but stopped sharing
meaning: Daily Review counted 任务 in zh and conversations in en (and its
en empty state said conversations on one branch, tasks on the other); the
terminal panel said "task" and then "the session's terminal".
Missed outright: `tool-activity/copy.ts` (由源会话管理 on every terminal
result chip), `connection-error-copy.ts`, and the CLI's own prose
(`allow for session`, `Give this session full access`, `--continue`).
Kept deliberately: 和 Maka 对话 as a behaviour, Codex's stored 对话 on the
import page, 侧边对话, 对话框 for dialog, and `<session-id>` as a CLI
contract. The palette's `nav:sessions` keywords get 会话 and 对话 back --
collapsing both onto 任务 left a duplicate entry and dropped the words a
long-time user would still type.
Refs #2984
Generated-by: Claude Code
* fix(ui): route the task row's last two dots through the shared vocabulary
`streaming` and `hasUnread` still wrote `'accent'` by hand while the status
and stale signals resolved through `dotForStatus`. Half a function deciding
its own colours is the drift this change set out to close -- the literal is
what let the rail and Settings disagree about the same fact before. Both are
`active`: streaming is the system working on it, and unread text is
"something happened here" rather than a question waiting on the user, which
is the line the two semantics exist to draw. `dotForStatus('active')` is
`'accent'`, so nothing moves on screen.
Also drops the `copy` binding `SessionHistoryList` no longer reads. It fed
the list's own aria-label, and the outer SideNav took that name over when the
rail was rebuilt.
Refs #2984
Generated-by: Claude Code
* docs: say what each session-status-presentation file actually owns
Desktop's copy claimed the status-to-dot mapping was "re-exported below" and
that `describeBlockedReason` was defined there. Both moved to `@maka/ui` in
this branch and neither is in the file; what is left is Desktop-only judgment
-- which blocked reasons are worth acting on, and what to offer after a turn
fails -- so the header says that instead.
The contract the old header carried is real and had nowhere to live after the
move: a UI label must never show a raw `SessionBlockedReason`, and a new
reason has to extend the core enum and the copy matrix together or it reads as
`unknown`. It now sits on `describeBlockedReason` in `@maka/ui`, where the
matrix it constrains is.
Refs #2984
Generated-by: Claude Code
* test: make the restored-task and import fixtures answer like their sources
Both fixtures modelled states their real source cannot produce, which makes
them evidence for nothing.
The purge tests built a restored task by flipping `isArchived` alone, leaving
`status: 'archived'` behind. `SessionStore.unarchive` writes both fields
together, so no stored row looks like that. The sweep only reads `isArchived`
today, so no assertion changes -- the point is that the next assertion written
against these rows would be checking a state that cannot happen. A `restored`
helper names the pair once.
The 导入任务 story's `list` ignored `includeArchived` and `cursor`: it rendered
the archived conversation while the filter was off and handed 加载更多 the same
first page forever, under a comment claiming it demonstrated both controls.
It now filters and pages, with a fourth conversation so the default view is a
short first page rather than the whole list.
Refs #2984
Generated-by: Claude Code
* fix(desktop): tidy the two places the 任务 rename passed through twice
`fake_backend` reaches the user down two paths -- the reason table, and the
header check in `assertSessionCanSend`, which never gets far enough to look a
connection up. They said the same sentence in two copies, so renaming 会话 to
任务 had to be done twice. One `FAKE_BACKEND_MESSAGE` now.
The English workspace help came out of the rename as "Any task can switch next
to the input box", which makes the task the actor and never names what it
switches. It says the project.
Refs #2984
Generated-by: Claude Code
* chore: regenerate the Astryx surface inventory
The Markdown half still listed `external-session-import-dialog.tsx` and
`external-session-import.css` after this branch deleted them, because the two
halves were edited by hand and `check-astryx-surface-inventory.mjs` only
verifies that on-disk files appear in the inventory -- never that inventory
rows still exist on disk. It is also not wired into CI, so the drift was
invisible from both directions.
Running the generator also picks up three files that arrived from main and
were never listed: `runtime-host-profiles-section.tsx`,
`runtime-host-ssh-terminal-dialog.tsx`, and `settings/runtime-host.css`. They
are unrelated to this branch, but a generated file regenerated in halves is
how it drifted in the first place.
Refs #2984
Generated-by: Claude Code
* test(ui): show every task status in the story that exists to show them
Two gaps, both found by shooting the story for a before/after comparison.
`review` and `done` were dropped from `statusSessions` when this branch briefly
deleted them from `SESSION_STATUSES`. The enum values and their labels came
back; the fixture did not, so the story that covers every status covered six of
eight -- and the two missing ones are exactly the two whose colours this change
decided on purpose, `attention` and `success`.
`StoryFrame` also defaulted to 240px while `SessionListPanel`'s rail defaults to
260, so every story that did not pass a width was clipping the rail by 20px.
That lands on the trailing slot, which is where this change puts the timestamp
-- the stories could not show whether it fits. Stories that want a narrow rail
still pass the width to both, which is what the note on `panelProps` is about.
Refs #2984
Generated-by: Claude Code
* refactor(ui): drop the rail's 任务 row
Expanded, the row selected a section whose content was already on screen
directly under it -- the same redundancy as the 会话 list heading this change
deleted one row down, and clicking any task row does the same selection.
It was kept for the collapsed rail, where the list is not rendered, on the
argument that 扩展 and 定时任务 were otherwise one-way doors. That was wrong.
Collapsed, the rail cannot switch tasks at all, so getting back to any task
already means widening it; the titlebar's 展开侧边栏 toggle does that
unconditionally (app-shell-chrome-actions.tsx renders `ChromeColumnToggle` with
no guard), and `activeId` survives a section change, so the widened rail comes
back with the task you left already marked. The row bought one click in a state
the user is leaving regardless, and charged a permanent slot for it.
`sessions` now has no control of its own on the rail. It is where you are
unless you went somewhere, which is why the other two sections light up and
this one has nothing to light.
`streaming-remount` returns the way the product now offers -- widen, then click
the row carrying `aria-current="page"` -- which is a better assertion than the
old one anyway: it checks that the task survives the trip, not just that a
button exists.
Refs #2984
Generated-by: Claude Code
* fix(runtime-host): coalesce a repeat import while the first is still running
Moving 导入任务 from a dialog into a Settings page removed the only thing
stopping one intent from becoming two tasks, and this branch is where that
happened. `ExternalSessionImportLifecycle` refused to close the dialog while an
import was active; retiring it was described here as dropping a guard against
nothing. It was not. The page it was replaced by is one the user may leave
mid-import -- the import continues in Desktop Main by design -- and the page's
`importingId` dies with it, so coming back and pressing 导入 again issues a
second request against a source that is still importing. Nothing downstream
deduplicates: `treats repeats as independent copies` is a pinned behaviour, so
the second request lands a second task the user now has to tell apart.
The guard belongs to the Host, not to the page that asked. Import is a Host
operation and the Host is the only party that knows one is running; a client
knows about its own requests, which is why a second window or the CLI would
have reproduced this with the page's state intact.
Concurrent repeats collapse onto the first attempt's promise and both callers
get its outcome, success or failure, because it is one operation. Sequential
repeats are untouched -- the entry is gone by the time the first settles -- so
importing the same conversation again on purpose still makes an independent
copy, which the existing test continues to pin.
Reported by @M4n5ter in review of #3033.
Refs #2984
Generated-by: Claude Code
Sign up for freeto 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

@jackwener
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' refactor: remove obsolete fixtures and JSONL import by jackwener · Pull Request #2656 · apache/maka · GitHub
Skip to content

refactor: remove obsolete fixtures and JSONL import - #2656

Merged
jackwener merged 1 commit into
mainfrom
codex/trim-low-value-fixtures
Aug 10, 2026
Merged

refactor: remove obsolete fixtures and JSONL import#2656
jackwener merged 1 commit into
mainfrom
codex/trim-low-value-fixtures

Conversation

@jackwener

Copy link
Copy Markdown
Member

Summary

  • reduce the Desktop E2E fixture registry from about 50 historical scenarios to the 13 scenarios consumed by current E2E, alignment audit, or smoke entry points
  • remove orphaned seed generators, renderer-only fixture controls, and the 1,361-line self-referential fixture test
  • remove the retired legacy JSONL session importer, its automatic SessionStore startup wiring, tests, and 11 transcript fixtures
  • preserve process/crash/lock/race fixtures and external Codex JSONL adapters because they cover active boundaries

Impact

  • 32 files changed
  • 5,194 lines removed, 141 lines added (net -5,053)
  • fixture-related tracked files reduced from 75 to 62

Validation

  • git diff --check
  • Biome lint on all changed source files
  • TypeScript: @maka/core, @maka/storage, and Desktop renderer
  • node --test scripts/fixture-env.test.mjs

Desktop main typecheck still reports the existing scheduled-task API/dist mismatch on origin/main; no errors point to this change.

@jackwener
jackwener merged commit 90bdb30 into mainAug 10, 2026
10 checks passed
@jackwener
jackwener deleted the codex/trim-low-value-fixtures branch August 10, 2026 19:21
ARE404 added a commit to ARE404/maka-agent that referenced this pull request Aug 13, 2026
apache#2580 moved the rail's tick onto Astryx's Button. The bar the tick draws
was a direct child of the flex tick and got blockified; the Button wraps
its children in a label span, so the bar went back to normal flow as an
inline box. An inline box takes no width or height, so every bar computed
to 0x0 and the rail shipped invisible in 0.1.9 and 0.1.10 — present in the
DOM, painting nothing.
`display: block` on the bar restores it. Measured on the new fixture at
1280x800: the rail's box goes from 8px wide (its own padding, ticks
contributing nothing) back to the designed 22px.
This is the third time the rail has failed by rendering and not painting
— apache#2161 pinned it against a containing block as tall as the conversation,
apache#2338 parked it under macOS's overlay scrollbar — and the second time it
reached a release. The e2e coverage that would have caught all three was
deleted in apache#2462, and the multi-prompt fixtures it ran on in apache#2656, so
this adds back the smallest thing that closes the gap:
- `chat-prompt-rail`, a plain 8-prompt conversation. The rail hides itself
below three prompts, so the shipped single-prompt fixture cannot show it
at all.
- `prompt-rail.spec.ts` with one test per past failure: bars have a real
box, the rail stays inside the scrollport at both scroll extremes, and a
tick is what the pointer lands on. Three tests where the deleted suite
had nine.
Verified the first test fails on the unfixed renderer with "Expected: > 0,
Received: 0" and passes with the fix. Neither a static CSS read nor a
jsdom unit test can see any of this: jsdom has no layout engine.
Astro-Han pushed a commit that referenced this pull request Aug 13, 2026
…2923)
* fix(ui): give the prompt rail's tick bars a box again
#2580 moved the rail's tick onto Astryx's Button. The bar the tick draws
was a direct child of the flex tick and got blockified; the Button wraps
its children in a label span, so the bar went back to normal flow as an
inline box. An inline box takes no width or height, so every bar computed
to 0x0 and the rail shipped invisible in 0.1.9 and 0.1.10 — present in the
DOM, painting nothing.
`display: block` on the bar restores it. Measured on the new fixture at
1280x800: the rail's box goes from 8px wide (its own padding, ticks
contributing nothing) back to the designed 22px.
This is the third time the rail has failed by rendering and not painting
— #2161 pinned it against a containing block as tall as the conversation,
#2338 parked it under macOS's overlay scrollbar — and the second time it
reached a release. The e2e coverage that would have caught all three was
deleted in #2462, and the multi-prompt fixtures it ran on in #2656, so
this adds back the smallest thing that closes the gap:
- `chat-prompt-rail`, a plain 8-prompt conversation. The rail hides itself
below three prompts, so the shipped single-prompt fixture cannot show it
at all.
- `prompt-rail.spec.ts` with one test per past failure: bars have a real
box, the rail stays inside the scrollport at both scroll extremes, and a
tick is what the pointer lands on. Three tests where the deleted suite
had nine.
Verified the first test fails on the unfixed renderer with "Expected: > 0,
Received: 0" and passes with the fix. Neither a static CSS read nor a
jsdom unit test can see any of this: jsdom has no layout engine.
* fix(ui): make the prompt rail's hover and jump behave
Four things the rail got wrong once it was visible again, found by using it:
- A 4px gap between ticks was a band where the pointer was over the rail
and over no tick, so the dock-style hover falloff dropped out and picked
up again every few pixels of travel. The rail's `gap` moves into the
ticks' own `padding-block`: same pitch, hit boxes now tile.
- The hover preview waited 300ms before opening — Astryx's HoverCard
default, meant for a pointer crossing a wide row on its way somewhere
else. A tick is 22px of rail that nothing is on the way to, and the wait
is the one part of this hover with no motion in it. Now 120ms.
- The highlight glided 280ms to wherever a click landed, so crossing
twenty prompts read as the bar flying off across the rail. A click now
owns the highlight until its scroll settles: no glide, and the scroll
no longer walks the highlight through every prompt it passes.
- The first click into a session did nothing until the reader scrolled by
hand. See below.
That last one is a collision between Astryx's auto-follow lock and the
progressive transcript mount, and neither side is wrong on its own.
`useChatStreamScroll` unlocks on a scroll up, detected by comparing
scrollTop between events — but it ignores any scroll event that arrives
with a changed scrollHeight or offsetHeight, because Chrome fires those
when content resizes and they are not the reader moving. A jump into an
unmounted turn mounts it and the fill that follows changes scrollHeight
for several frames, so the jump's own scroll is invisible to the lock: it
stays on, and `scrollIfLocked` pulls the transcript back to the bottom.
Only a wheel gesture broke it, which takes a separate path in Astryx.
`holdJumpDestination` re-aims at the target on each height change until
the fill stops. The last of those scrolls lands with a stable height,
which is the one the lock finally reads as a scroll up. Measured on the
30-prompt fixture: clicking the first tick went to scrollTop 7042 (the
bottom) and now goes to 24 and holds.
The fixture grows from 8 prompts to 30 because the progressive mount's
initial window is 10 — at 8 the head of the transcript is already mounted
and the jump-into-unmounted-turns path never runs at all.
Coverage note: the e2e case for the first click is an end-to-end check,
not a guard. Whether the lock wins depends on which frame the fill lands
on relative to a smooth scroll still in flight, and it goes green against
the unfixed renderer often enough to be worthless as one. The guard is
the `holdJumpDestination` unit test, which drives the frames itself.
* fix(ui): own a rail jump through the mount instead of racing it
Review of #2923 found the jump's ownership bound to a clock rather than to
the navigation, and the e2e case that was supposed to guard it asserting
almost nothing. Both hold.
Jump ownership:
- A second click during a jump only replaced the target; the first click's
700ms timer still governed, and could clear the second jump mid-flight.
Each click now carries its own sequence and starts its own hold.
- The fixed window is gone. A hold runs until the progressive mount reports
the transcript filled AND nothing has moved for a few frames, so a long
transcript is never released mid-fill, and it ends the moment the reader
touches the transcript (wheel, touch, pointer, key) rather than outliving
their interest in it.
Chasing the "just release auto-follow" direction the review preferred found
that ChatLayout publishes no such seam, so this adds one — `unlockAutoFollow`
on `ChatLayoutContextValue`, exposing the scroll hook's existing `unlock`
(patch hunk + patches/README entry). It is necessary and it is not
sufficient, which the earlier framing got wrong:
- Astryx re-locks on any `scrollend` that settles near the bottom, and a
session that opens at the bottom produces exactly that while the mount is
still catching up. Releasing once at the click is undone before the jump
goes anywhere — traced: released at the click, landed at 154ms, dragged
back to the bottom by 166ms. The release is now re-asserted for the life
of the hold.
- Auto-follow is not the only thing moving the transcript. The progressive
mount's own scroll compensation holds the reader's position across each
fill step, and mounting the turn a jump asked for IS a fill step, so it
lands after the jump and restores the position the jump just left. That
one no seam can fix; it is what the hold is for.
Jumps also scroll instantly now, whatever the app's scroll-motion policy
says. A jump is a teleport the reader asked for, and an animated one does
not survive this surface: traced on the 30-prompt fixture, the smooth scroll
was cancelled by the mount's compensation and by the follow spring and
stalled two pixels from where it started.
Coverage:
- The first-click e2e case named the wrong turn (`[data-turn-id]` is the
first MOUNTED turn, whose top is already negative at the opening scroll
position, so an upper-bound-only check passed without the jump doing
anything). It now names `turn-prompt-rail-1`, bounds it on both sides, and
asserts that tick's `aria-current`.
- `emulateMedia` could not put that case on the production scroll path:
`resolveScrollMotionBehavior` collapses motion for ANY fixture, keyed on
`data-maka-e2e-fixture` rather than on the media query. Fixtures can now
ask for a behavior back (`scrollMotion`, per launch — it costs seconds of
settling per window, so only the case that needs it pays), with unit
coverage for the precedence: a fixture request never outranks a stated
preference for less motion.
- `holdJumpDestination`'s unit tests grew the two cases its rewrite is
about: it must not settle while the transcript is still filling, and it
must hand the transcript back the moment the reader touches it.
Verified 5/5 on the smooth-scroll fixture, where the previous revision lost
1 in 4. `quote-selection.spec.ts` flakes on this machine (1 in 4) at
upstream/main as well, unchanged by this branch.
Astro-Han added a commit to Astro-Han/maka-agent that referenced this pull request Aug 15, 2026
Deleting `SessionStatusTone` was right; replacing it with a private
`SessionStatus -> StatusDotVariant` table was not. `status-vocabulary.ts`
already owns "the one place a status word becomes a colour", so the
private table made a second authority, and the two disagreed: a task
waiting on a permission prompt drew `error` in the rail while the
permission centre drew `attention` for the identical condition.
Map the session enum to `StatusSemantic` and let `dotForStatus` pick the
colour. `blocked` and `waiting_for_user` are both `attention`, which is
what that semantic is defined as -- both are waiting on a person. Giving
`blocked` `error` to tell them apart used colour for a distinction colour
cannot carry; their labels and `describeBlockedReason` do that.
Restore `review` and `done`. They have no writer in current source, but
`SESSION_STATUSES` is read back out of storage, and narrowing it is a
data migration rather than a cleanup: `resolveLegacyStatus` in the JSONL
importer (removed in apache#2656) passed both values through into real SQLite
stores verbatim, and `normalizeSessionHeader` throws for the WHOLE header
on an unrecognised status, so one stored row carrying `done` fails an
entire catalog page. The migration is its own change.
`archived` and `aborted` get their dots back. They were `muted` before,
`muted` resolved to a real `neutral` dot, and dropping them to `undefined`
was a behaviour change I described as a consequence of collapsing the
layer.
Refs apache#2984
Generated-by: Claude Code
Astro-Han added a commit to Astro-Han/maka-agent that referenced this pull request Aug 15, 2026
Deleting `SessionStatusTone` was right; replacing it with a private
`SessionStatus -> StatusDotVariant` table was not. `status-vocabulary.ts`
already owns "the one place a status word becomes a colour", so the
private table made a second authority, and the two disagreed: a task
waiting on a permission prompt drew `error` in the rail while the
permission centre drew `attention` for the identical condition.
Map the session enum to `StatusSemantic` and let `dotForStatus` pick the
colour. `blocked` and `waiting_for_user` are both `attention`, which is
what that semantic is defined as -- both are waiting on a person. Giving
`blocked` `error` to tell them apart used colour for a distinction colour
cannot carry; their labels and `describeBlockedReason` do that.
Restore `review` and `done`. They have no writer in current source, but
`SESSION_STATUSES` is read back out of storage, and narrowing it is a
data migration rather than a cleanup: `resolveLegacyStatus` in the JSONL
importer (removed in apache#2656) passed both values through into real SQLite
stores verbatim, and `normalizeSessionHeader` throws for the WHOLE header
on an unrecognised status, so one stored row carrying `done` fails an
entire catalog page. The migration is its own change.
`archived` and `aborted` get their dots back. They were `muted` before,
`muted` resolved to a real `neutral` dot, and dropping them to `undefined`
was a behaviour change I described as a consequence of collapsing the
layer.
Refs apache#2984
Generated-by: Claude Code
Astro-Han added a commit that referenced this pull request Aug 15, 2026
…ary (#3033)
* fix(ui): give the task rail's status dot one meaning
The dot the rail draws for a session passed through two lossy hops and
three disagreeing sources, so it could not say what it meant.
Collapse the mapping. `SessionStatus -> SessionStatusTone ->
StatusDotVariant` becomes one table from status to Astryx's variant. Two
of the seven tones had no distinct outcome at the end of that chain, and
the collapse is what let `waiting_for_user` and `blocked` share
`warning`: blocked now maps to `error`, so "cannot proceed until you fix
a connection" and "holding a question for you" stop rendering alike.
Read the running authority. `runningTurnIds` is the runtime's projection
of the runs it holds; `session.ts` states why a persisted `status` cannot
serve that purpose, and `settledSessionTransientIds` already reads it
first over the same list. The row read neither -- a renderer-local
streaming set, then the stored `status` -- so a task running under a bot
channel or a second window read as idle. The streaming set stays below
it, for the gap between this renderer sending a turn and the host
reporting it back.
Delete what nothing wrote. `review` and `done` were never written by
anything, in any version, so no stored record can carry them and no
reader of them was reachable; they leave `SESSION_STATUSES`, the wire
enum, and the copy tables. `SessionLifecycleStatus` now aliases
`SessionStatus` instead of restating it -- three hand-written copies of
one enum is how those two values survived in two of them after the last
writer went away.
Delete the tautological filter. `SessionFilter` had one value left:
`archived` moved to Settings in #2985 and `flagged` was never selected,
which left a control whose answer is always the same answer. The 「会话」
row that selected it goes with it, as does the branch in
`sessionMatchesNavSelection`. A stored filter is dropped rather than
validated, which is the migration.
Also gone: `SessionStatusPresentation.interactive`, which had no reader,
and a doc comment naming a `SessionStatusIcon` and a chat-header badge as
the tone matrix's consumers -- neither exists.
`status: 'archived'` still duplicates `isArchived`. Consolidating those
two rewrites stored rows, so it is #2984's PR 3, not this one.
Refs #2984
Generated-by: Claude Code
* refactor(ui): rebuild the task row on two signal slots
Every task row in the rail now carries exactly two signal slots. Slot 1 is
a single status dot, reserved as an 8px gutter even when there is nothing to
show so titles stay aligned down the list. Slot 2 holds a compact timestamp
at rest and swaps to the ⋯ menu on hover or keyboard focus, so the menu is
no longer permanently mounted next to every title.
The dot's meaning is resolved once, in priority order: live run (accent,
pulsing) › persisted status › unread (accent, inactive rows only). The live
run reads `runningTurnIds` first, matching `settledSessionTransientIds` over
the same session list — a persisted `running` status can be left behind by a
crash, so it must not outrank the authoritative projection.
The stale pill is gone: it duplicated the timestamp it sat next to. The
worktree mark falls back to the row tooltip rather than competing for a slot.
`RelativeTime` gains `variant="compact"` instead of a second component; a
260px rail row cannot fit the medium-date-plus-time past-horizon fallback.
Generated-by: Claude Code
* refactor(ui): give the rail one top region and one grouping switch
The rail's top region now ends with the grouping switch, and the history
below it starts directly at its first group.
The list heading is gone. "会话" labelled a list that is the only thing under
it, inside a landmark that already names the panel — and it existed mainly to
give the grouping switch somewhere to sit. The switch moves into the sticky
top region instead, where it stays reachable: hung off a heading it scrolled
away with the very list it switches.
It is also full width with text labels now. Two icon-only segments asked the
rail to teach a clock and a folder, and the rail has nowhere to teach them;
按时间 / 按项目 is the entire vocabulary and fits inside the minimum 180px
width with room to spare. Collapsed at 48px it does not render at all — no
segment label fits, and the list it governs is not rendered either.
The group headers lose their ➕. It was the 新任务 row's handler under a
second name, three rows below the row itself, and it needed that second name
precisely because two controls cannot share one accessible name.
Generated-by: Claude Code
* feat(desktop): move task import out of the rail into settings
导入 was a rail row that opened a modal over the conversation. Import is not
navigation: it is a rare setup errand, it needs a source, an archived filter
and a paged directory of someone else's conversations to work through, and
none of that belongs in a 260px column of the tasks you are working on. It
becomes 设置 › 活动 › 导入任务, next to 已归档任务 — both are about the task
catalog rather than the task in front of you.
As a page the flow loses its selection state. A dialog needed one because its
single primary action sat in a footer far from the rows; a page puts 导入 on
the row itself, the way 已归档任务 puts 恢复 on its rows. That also retires
`ExternalSessionImportLifecycle`, whose whole job was refusing to close a
dialog mid-import — there is no dialog left to hold open, and an import that
outlives the page is guarded by the mounted ref instead, so it can still
finish but can no longer steer a shell the user has navigated away from.
The source switch now appears only when there is more than one source. Codex
is the only adapter that ships, so in practice every user was being shown a
segmented control with a single segment — a control nobody can operate. With
one source the section names it instead.
Deliberately not carried over: keeping an imported task in sync with its
source. The importer is a one-shot conversion and nothing behind it watches
the source, so that control would be a promise no coordinator can keep.
Generated-by: Claude Code
* refactor: name the thing a user works on 任务 / task everywhere
The product called the same object 会话, 对话, session and conversation
depending on which file you landed in. Code, protocol and storage keep
`session` — that is a durable contract with a wire format behind it — but
every string a user reads now says 任务 / task.
The rule is what the noun REFERS TO, not the word:
- The Maka entity a user opens, renames, archives and searches → 任务 / task.
- Conversing as a behavior stays 对话 / conversation: "与 Maka 对话" over a
bot, "可用于对话的模型", "对话记录" as a message log.
- Another agent's stored conversations stay 对话 / conversation. 导入任务
lists Codex's conversations and produces Maka tasks; collapsing both onto
one word would erase exactly the distinction that page is about.
- 侧边对话 stays: it is a chat beside a task, not a task in the rail.
Two collisions had to be resolved rather than renamed. The per-task ledger
was 会话任务, which would have become 任务任务 — its items are 待办 / to-dos,
which is what they always were. And the automations row read "定时任务,N 个
未完成任务", two different meanings of 任务 in one label; the count now says
"N 条进行中".
The keyboard sheet loses its ←/→ row. It documented cycling between
会话/已标记/已归档, a filter this PR removed along with `SessionFilter`.
Generated-by: Claude Code
* test(ui): make the narrow-rail story actually narrow
`LongTitlesAndNarrow` framed the panel at 176px but let SideNav keep its own
260px `resizable` width, so the story clipped a wide rail instead of showing
a narrow one — the timestamps it exists to check were outside the frame. It
now drives the rail's own width, at 180px, which is the panel's `minWidth`
and therefore the narrowest state a user can reach.
Generated-by: Claude Code
* fix(ui): keep the rail's 任务 row as the way back to the list
bb39b33 deleted `SessionFilter` and took the 「会话」 row with it, on the
reading that the row only selected a filter whose answer was always the
same answer. Half right: its `isSelected` read the dead filter, but its
`onClick` selected the SECTION, and it was the only control that did.
Without it the rail has three sections and two rows. Collapsed at 48px —
the default state (`readSessionListCollapsed`) — the list is not rendered
at all, so 扩展 and 定时任务 became one-way doors: the only way back to a
running task was 新任务, which answers "show me my tasks" by starting
another one. `streaming-remount.spec.ts` walks exactly that path and had
been rewritten to click a task row that a collapsed rail does not have.
Restore the row as 任务, selected on `section === 'sessions'` and
selecting the section with no filter. The filter deletion stands.
Refs #2984
Generated-by: Claude Code
* fix(ui): route task status through the shared semantic layer
Deleting `SessionStatusTone` was right; replacing it with a private
`SessionStatus -> StatusDotVariant` table was not. `status-vocabulary.ts`
already owns "the one place a status word becomes a colour", so the
private table made a second authority, and the two disagreed: a task
waiting on a permission prompt drew `error` in the rail while the
permission centre drew `attention` for the identical condition.
Map the session enum to `StatusSemantic` and let `dotForStatus` pick the
colour. `blocked` and `waiting_for_user` are both `attention`, which is
what that semantic is defined as -- both are waiting on a person. Giving
`blocked` `error` to tell them apart used colour for a distinction colour
cannot carry; their labels and `describeBlockedReason` do that.
Restore `review` and `done`. They have no writer in current source, but
`SESSION_STATUSES` is read back out of storage, and narrowing it is a
data migration rather than a cleanup: `resolveLegacyStatus` in the JSONL
importer (removed in #2656) passed both values through into real SQLite
stores verbatim, and `normalizeSessionHeader` throws for the WHOLE header
on an unrecognised status, so one stored row carrying `done` fails an
entire catalog page. The migration is its own change.
`archived` and `aborted` get their dots back. They were `muted` before,
`muted` resolved to a real `neutral` dot, and dropping them to `undefined`
was a behaviour change I described as a consequence of collapsing the
layer.
Refs #2984
Generated-by: Claude Code
* fix(ui): keep the task row's facts when it collapses to two slots
The row draws one dot and one trailing slot. That was implemented by
collapsing to a single signal inside the resolver, which made the visual
element the only carrier of the fact -- so removing the trailing `Badge`
removed "stale" itself, not just its pill. `staleTitle` and
`staleAriaLabel` were left with no reader at all, the surviving `opacity`
cue is cancelled on the selected row by design, and opacity announces
nothing. Worktree went the same way: an `aria-label`ed icon inside the
button became a `title` on a non-interactive ancestor, which contributes
to neither the button's name nor its description.
Resolve a LIST of signals in priority order, draw `signals[0]`, and put
the rest -- plus worktree and the absolute timestamp -- in one visually
hidden span inside the button. Two slots, same pixels, and the facts stop
depending on which of them had room. Signals also stop eating each other:
`aborted` now keeps its own dot instead of falling through to the unread
branch, where an aborted task with unread text drew the same accent dot
as a running one.
Stale joins the list as `attention`, so a stale task finally has a signal
that survives being selected. It is resolved here rather than in
`presentSessionStatus` because it is a renderer-derived fact, not a
persisted `SessionStatus`.
Drop the `runningTurnIds` read. No `SessionSummary` reaching Desktop
carries that field: Runtime Host's catalog reads storage directly, the
wire projection does not list it, and Desktop Main's converter copies
persisted fields only -- the identifier appears nowhere in
`packages/runtime-host/src` or `apps/desktop/src/main` outside a test
fixture. It was a read of something nothing populates, described as
reading the authority. `streaming` is what the rail actually has, with
the limit that it only knows this renderer's turns; a real live-run
projection from the Host is its own change.
Also: `pointer-events: none` on the resting ⋯, which sat invisible at
`z-index: 1` over the trailing slot and swallowed clicks; and the inner
list no longer repeats the rail's own accessible name.
Refs #2984
Generated-by: Claude Code
* fix(desktop): refresh the catalog when an import's outcome is unknown
`external-sessions:import` emits `sessions:changed` on success, so a task
imported while the user walks away from Settings still reaches the rail
on its own -- the comment in `app-shell.tsx` claiming nothing notifies
the shell was wrong, and is corrected here.
The gap is the other branch. `commit_outcome_unknown` means the Host
cannot say whether the task was committed, and that path returned a
result without emitting anything. The only trace was the page's own
banner, and 导入任务 is a Settings page: leaving it unmounts the banner,
which is exactly when someone comes back and imports the same
conversation again. Emit there too, with no id, because not knowing which
task landed is what the code means.
Also return the import promise from `clickAction` instead of `void`-ing
it. Astryx's Button awaits it and drops repeat clicks until it settles;
discarding it left double-submit to `importingId` alone, one render
behind the second click.
Refs #2984
Generated-by: Claude Code
* fix(desktop): re-point the structural gates the surface move broke
Deleting the import dialog left exact-string readers behind. `scripts/
check-astryx-alignment.mjs` read the dialog and its stylesheet by path,
so running it exited 1 with ENOENT; the surface inventory still listed
both files and had no row for the page that replaced them.
The dialog's Item-row guards are not migrated, they are dropped. They
checked that a SELECTED row stayed keyboard-reachable -- no parent role
stealing Item's native button, no selected-only tabIndex trapping focus.
设置 › 活动 › 导入任务 has no selection: 导入 sits on each row the way 恢复
does on the archived page. Asserting those smells against the new page
would guard a shape it does not have. The button guard and a ListItem
import check follow the surface to the page.
Inventory rows for the new page and for #2985's archived-tasks page,
which was never added. Three runtime-host entries stay missing; they
predate this branch.
Also read the Daily Review model label from its copy table instead of a
literal. The story matched '跟随对话默认', which the rename retired, so it
silently found nothing -- and `storybook-visual-smoke.mjs` disables every
`play` function, so CI could not report it.
Refs #2984
Generated-by: Claude Code
* refactor: finish the 任务 rename where the rule missed
The rename was applied by rule and reviewed by hand, which is the process
that produces both halves of this: strings that kept the old noun, and
strings that took the new one where the old was right.
Same-surface contradictions, which are what a user actually notices:
- The to-do panel's error banner said 任务载入失败 next to a button
labelled 重新载入待办 -- one `Banner`, two vocabularies.
- `chat-readiness` said 当前任务 and then 新建会话 in one sentence.
- The bot's help text offered to 清空当前对话开新会话 while the reply to
that same command already said 任务已重置.
- 定时任务 described itself as 安排定时任务,并回顾本机任务, and its
delivery option read Agent 任务执行 -- two meanings of 任务 in one form.
- `settlementFailed` called the side chat a 任务 and then a 侧边对话;
`forkSetupFailed` called it 追问任务, a third name for one object. Both
are 侧边对话 now, matching the en twins.
zh/en divergence, where the catalogs share keys but stopped sharing
meaning: Daily Review counted 任务 in zh and conversations in en (and its
en empty state said conversations on one branch, tasks on the other); the
terminal panel said "task" and then "the session's terminal".
Missed outright: `tool-activity/copy.ts` (由源会话管理 on every terminal
result chip), `connection-error-copy.ts`, and the CLI's own prose
(`allow for session`, `Give this session full access`, `--continue`).
Kept deliberately: 和 Maka 对话 as a behaviour, Codex's stored 对话 on the
import page, 侧边对话, 对话框 for dialog, and `<session-id>` as a CLI
contract. The palette's `nav:sessions` keywords get 会话 and 对话 back --
collapsing both onto 任务 left a duplicate entry and dropped the words a
long-time user would still type.
Refs #2984
Generated-by: Claude Code
* fix(ui): route the task row's last two dots through the shared vocabulary
`streaming` and `hasUnread` still wrote `'accent'` by hand while the status
and stale signals resolved through `dotForStatus`. Half a function deciding
its own colours is the drift this change set out to close -- the literal is
what let the rail and Settings disagree about the same fact before. Both are
`active`: streaming is the system working on it, and unread text is
"something happened here" rather than a question waiting on the user, which
is the line the two semantics exist to draw. `dotForStatus('active')` is
`'accent'`, so nothing moves on screen.
Also drops the `copy` binding `SessionHistoryList` no longer reads. It fed
the list's own aria-label, and the outer SideNav took that name over when the
rail was rebuilt.
Refs #2984
Generated-by: Claude Code
* docs: say what each session-status-presentation file actually owns
Desktop's copy claimed the status-to-dot mapping was "re-exported below" and
that `describeBlockedReason` was defined there. Both moved to `@maka/ui` in
this branch and neither is in the file; what is left is Desktop-only judgment
-- which blocked reasons are worth acting on, and what to offer after a turn
fails -- so the header says that instead.
The contract the old header carried is real and had nowhere to live after the
move: a UI label must never show a raw `SessionBlockedReason`, and a new
reason has to extend the core enum and the copy matrix together or it reads as
`unknown`. It now sits on `describeBlockedReason` in `@maka/ui`, where the
matrix it constrains is.
Refs #2984
Generated-by: Claude Code
* test: make the restored-task and import fixtures answer like their sources
Both fixtures modelled states their real source cannot produce, which makes
them evidence for nothing.
The purge tests built a restored task by flipping `isArchived` alone, leaving
`status: 'archived'` behind. `SessionStore.unarchive` writes both fields
together, so no stored row looks like that. The sweep only reads `isArchived`
today, so no assertion changes -- the point is that the next assertion written
against these rows would be checking a state that cannot happen. A `restored`
helper names the pair once.
The 导入任务 story's `list` ignored `includeArchived` and `cursor`: it rendered
the archived conversation while the filter was off and handed 加载更多 the same
first page forever, under a comment claiming it demonstrated both controls.
It now filters and pages, with a fourth conversation so the default view is a
short first page rather than the whole list.
Refs #2984
Generated-by: Claude Code
* fix(desktop): tidy the two places the 任务 rename passed through twice
`fake_backend` reaches the user down two paths -- the reason table, and the
header check in `assertSessionCanSend`, which never gets far enough to look a
connection up. They said the same sentence in two copies, so renaming 会话 to
任务 had to be done twice. One `FAKE_BACKEND_MESSAGE` now.
The English workspace help came out of the rename as "Any task can switch next
to the input box", which makes the task the actor and never names what it
switches. It says the project.
Refs #2984
Generated-by: Claude Code
* chore: regenerate the Astryx surface inventory
The Markdown half still listed `external-session-import-dialog.tsx` and
`external-session-import.css` after this branch deleted them, because the two
halves were edited by hand and `check-astryx-surface-inventory.mjs` only
verifies that on-disk files appear in the inventory -- never that inventory
rows still exist on disk. It is also not wired into CI, so the drift was
invisible from both directions.
Running the generator also picks up three files that arrived from main and
were never listed: `runtime-host-profiles-section.tsx`,
`runtime-host-ssh-terminal-dialog.tsx`, and `settings/runtime-host.css`. They
are unrelated to this branch, but a generated file regenerated in halves is
how it drifted in the first place.
Refs #2984
Generated-by: Claude Code
* test(ui): show every task status in the story that exists to show them
Two gaps, both found by shooting the story for a before/after comparison.
`review` and `done` were dropped from `statusSessions` when this branch briefly
deleted them from `SESSION_STATUSES`. The enum values and their labels came
back; the fixture did not, so the story that covers every status covered six of
eight -- and the two missing ones are exactly the two whose colours this change
decided on purpose, `attention` and `success`.
`StoryFrame` also defaulted to 240px while `SessionListPanel`'s rail defaults to
260, so every story that did not pass a width was clipping the rail by 20px.
That lands on the trailing slot, which is where this change puts the timestamp
-- the stories could not show whether it fits. Stories that want a narrow rail
still pass the width to both, which is what the note on `panelProps` is about.
Refs #2984
Generated-by: Claude Code
* refactor(ui): drop the rail's 任务 row
Expanded, the row selected a section whose content was already on screen
directly under it -- the same redundancy as the 会话 list heading this change
deleted one row down, and clicking any task row does the same selection.
It was kept for the collapsed rail, where the list is not rendered, on the
argument that 扩展 and 定时任务 were otherwise one-way doors. That was wrong.
Collapsed, the rail cannot switch tasks at all, so getting back to any task
already means widening it; the titlebar's 展开侧边栏 toggle does that
unconditionally (app-shell-chrome-actions.tsx renders `ChromeColumnToggle` with
no guard), and `activeId` survives a section change, so the widened rail comes
back with the task you left already marked. The row bought one click in a state
the user is leaving regardless, and charged a permanent slot for it.
`sessions` now has no control of its own on the rail. It is where you are
unless you went somewhere, which is why the other two sections light up and
this one has nothing to light.
`streaming-remount` returns the way the product now offers -- widen, then click
the row carrying `aria-current="page"` -- which is a better assertion than the
old one anyway: it checks that the task survives the trip, not just that a
button exists.
Refs #2984
Generated-by: Claude Code
* fix(runtime-host): coalesce a repeat import while the first is still running
Moving 导入任务 from a dialog into a Settings page removed the only thing
stopping one intent from becoming two tasks, and this branch is where that
happened. `ExternalSessionImportLifecycle` refused to close the dialog while an
import was active; retiring it was described here as dropping a guard against
nothing. It was not. The page it was replaced by is one the user may leave
mid-import -- the import continues in Desktop Main by design -- and the page's
`importingId` dies with it, so coming back and pressing 导入 again issues a
second request against a source that is still importing. Nothing downstream
deduplicates: `treats repeats as independent copies` is a pinned behaviour, so
the second request lands a second task the user now has to tell apart.
The guard belongs to the Host, not to the page that asked. Import is a Host
operation and the Host is the only party that knows one is running; a client
knows about its own requests, which is why a second window or the CLI would
have reproduced this with the page's state intact.
Concurrent repeats collapse onto the first attempt's promise and both callers
get its outcome, success or failure, because it is one operation. Sequential
repeats are untouched -- the entry is gone by the time the first settles -- so
importing the same conversation again on purpose still makes an independent
copy, which the existing test continues to pin.
Reported by @M4n5ter in review of #3033.
Refs #2984
Generated-by: Claude Code
Sign up for freeto 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

@jackwener
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' refactor: remove obsolete fixtures and JSONL import by jackwener · Pull Request #2656 · apache/maka · GitHub
Skip to content

refactor: remove obsolete fixtures and JSONL import - #2656

Merged
jackwener merged 1 commit into
mainfrom
codex/trim-low-value-fixtures
Aug 10, 2026
Merged

refactor: remove obsolete fixtures and JSONL import#2656
jackwener merged 1 commit into
mainfrom
codex/trim-low-value-fixtures

Conversation

@jackwener

Copy link
Copy Markdown
Member

Summary

  • reduce the Desktop E2E fixture registry from about 50 historical scenarios to the 13 scenarios consumed by current E2E, alignment audit, or smoke entry points
  • remove orphaned seed generators, renderer-only fixture controls, and the 1,361-line self-referential fixture test
  • remove the retired legacy JSONL session importer, its automatic SessionStore startup wiring, tests, and 11 transcript fixtures
  • preserve process/crash/lock/race fixtures and external Codex JSONL adapters because they cover active boundaries

Impact

  • 32 files changed
  • 5,194 lines removed, 141 lines added (net -5,053)
  • fixture-related tracked files reduced from 75 to 62

Validation

  • git diff --check
  • Biome lint on all changed source files
  • TypeScript: @maka/core, @maka/storage, and Desktop renderer
  • node --test scripts/fixture-env.test.mjs

Desktop main typecheck still reports the existing scheduled-task API/dist mismatch on origin/main; no errors point to this change.

@jackwener
jackwener merged commit 90bdb30 into mainAug 10, 2026
10 checks passed
@jackwener
jackwener deleted the codex/trim-low-value-fixtures branch August 10, 2026 19:21
ARE404 added a commit to ARE404/maka-agent that referenced this pull request Aug 13, 2026
apache#2580 moved the rail's tick onto Astryx's Button. The bar the tick draws
was a direct child of the flex tick and got blockified; the Button wraps
its children in a label span, so the bar went back to normal flow as an
inline box. An inline box takes no width or height, so every bar computed
to 0x0 and the rail shipped invisible in 0.1.9 and 0.1.10 — present in the
DOM, painting nothing.
`display: block` on the bar restores it. Measured on the new fixture at
1280x800: the rail's box goes from 8px wide (its own padding, ticks
contributing nothing) back to the designed 22px.
This is the third time the rail has failed by rendering and not painting
— apache#2161 pinned it against a containing block as tall as the conversation,
apache#2338 parked it under macOS's overlay scrollbar — and the second time it
reached a release. The e2e coverage that would have caught all three was
deleted in apache#2462, and the multi-prompt fixtures it ran on in apache#2656, so
this adds back the smallest thing that closes the gap:
- `chat-prompt-rail`, a plain 8-prompt conversation. The rail hides itself
below three prompts, so the shipped single-prompt fixture cannot show it
at all.
- `prompt-rail.spec.ts` with one test per past failure: bars have a real
box, the rail stays inside the scrollport at both scroll extremes, and a
tick is what the pointer lands on. Three tests where the deleted suite
had nine.
Verified the first test fails on the unfixed renderer with "Expected: > 0,
Received: 0" and passes with the fix. Neither a static CSS read nor a
jsdom unit test can see any of this: jsdom has no layout engine.
Astro-Han pushed a commit that referenced this pull request Aug 13, 2026
…2923)
* fix(ui): give the prompt rail's tick bars a box again
#2580 moved the rail's tick onto Astryx's Button. The bar the tick draws
was a direct child of the flex tick and got blockified; the Button wraps
its children in a label span, so the bar went back to normal flow as an
inline box. An inline box takes no width or height, so every bar computed
to 0x0 and the rail shipped invisible in 0.1.9 and 0.1.10 — present in the
DOM, painting nothing.
`display: block` on the bar restores it. Measured on the new fixture at
1280x800: the rail's box goes from 8px wide (its own padding, ticks
contributing nothing) back to the designed 22px.
This is the third time the rail has failed by rendering and not painting
— #2161 pinned it against a containing block as tall as the conversation,
#2338 parked it under macOS's overlay scrollbar — and the second time it
reached a release. The e2e coverage that would have caught all three was
deleted in #2462, and the multi-prompt fixtures it ran on in #2656, so
this adds back the smallest thing that closes the gap:
- `chat-prompt-rail`, a plain 8-prompt conversation. The rail hides itself
below three prompts, so the shipped single-prompt fixture cannot show it
at all.
- `prompt-rail.spec.ts` with one test per past failure: bars have a real
box, the rail stays inside the scrollport at both scroll extremes, and a
tick is what the pointer lands on. Three tests where the deleted suite
had nine.
Verified the first test fails on the unfixed renderer with "Expected: > 0,
Received: 0" and passes with the fix. Neither a static CSS read nor a
jsdom unit test can see any of this: jsdom has no layout engine.
* fix(ui): make the prompt rail's hover and jump behave
Four things the rail got wrong once it was visible again, found by using it:
- A 4px gap between ticks was a band where the pointer was over the rail
and over no tick, so the dock-style hover falloff dropped out and picked
up again every few pixels of travel. The rail's `gap` moves into the
ticks' own `padding-block`: same pitch, hit boxes now tile.
- The hover preview waited 300ms before opening — Astryx's HoverCard
default, meant for a pointer crossing a wide row on its way somewhere
else. A tick is 22px of rail that nothing is on the way to, and the wait
is the one part of this hover with no motion in it. Now 120ms.
- The highlight glided 280ms to wherever a click landed, so crossing
twenty prompts read as the bar flying off across the rail. A click now
owns the highlight until its scroll settles: no glide, and the scroll
no longer walks the highlight through every prompt it passes.
- The first click into a session did nothing until the reader scrolled by
hand. See below.
That last one is a collision between Astryx's auto-follow lock and the
progressive transcript mount, and neither side is wrong on its own.
`useChatStreamScroll` unlocks on a scroll up, detected by comparing
scrollTop between events — but it ignores any scroll event that arrives
with a changed scrollHeight or offsetHeight, because Chrome fires those
when content resizes and they are not the reader moving. A jump into an
unmounted turn mounts it and the fill that follows changes scrollHeight
for several frames, so the jump's own scroll is invisible to the lock: it
stays on, and `scrollIfLocked` pulls the transcript back to the bottom.
Only a wheel gesture broke it, which takes a separate path in Astryx.
`holdJumpDestination` re-aims at the target on each height change until
the fill stops. The last of those scrolls lands with a stable height,
which is the one the lock finally reads as a scroll up. Measured on the
30-prompt fixture: clicking the first tick went to scrollTop 7042 (the
bottom) and now goes to 24 and holds.
The fixture grows from 8 prompts to 30 because the progressive mount's
initial window is 10 — at 8 the head of the transcript is already mounted
and the jump-into-unmounted-turns path never runs at all.
Coverage note: the e2e case for the first click is an end-to-end check,
not a guard. Whether the lock wins depends on which frame the fill lands
on relative to a smooth scroll still in flight, and it goes green against
the unfixed renderer often enough to be worthless as one. The guard is
the `holdJumpDestination` unit test, which drives the frames itself.
* fix(ui): own a rail jump through the mount instead of racing it
Review of #2923 found the jump's ownership bound to a clock rather than to
the navigation, and the e2e case that was supposed to guard it asserting
almost nothing. Both hold.
Jump ownership:
- A second click during a jump only replaced the target; the first click's
700ms timer still governed, and could clear the second jump mid-flight.
Each click now carries its own sequence and starts its own hold.
- The fixed window is gone. A hold runs until the progressive mount reports
the transcript filled AND nothing has moved for a few frames, so a long
transcript is never released mid-fill, and it ends the moment the reader
touches the transcript (wheel, touch, pointer, key) rather than outliving
their interest in it.
Chasing the "just release auto-follow" direction the review preferred found
that ChatLayout publishes no such seam, so this adds one — `unlockAutoFollow`
on `ChatLayoutContextValue`, exposing the scroll hook's existing `unlock`
(patch hunk + patches/README entry). It is necessary and it is not
sufficient, which the earlier framing got wrong:
- Astryx re-locks on any `scrollend` that settles near the bottom, and a
session that opens at the bottom produces exactly that while the mount is
still catching up. Releasing once at the click is undone before the jump
goes anywhere — traced: released at the click, landed at 154ms, dragged
back to the bottom by 166ms. The release is now re-asserted for the life
of the hold.
- Auto-follow is not the only thing moving the transcript. The progressive
mount's own scroll compensation holds the reader's position across each
fill step, and mounting the turn a jump asked for IS a fill step, so it
lands after the jump and restores the position the jump just left. That
one no seam can fix; it is what the hold is for.
Jumps also scroll instantly now, whatever the app's scroll-motion policy
says. A jump is a teleport the reader asked for, and an animated one does
not survive this surface: traced on the 30-prompt fixture, the smooth scroll
was cancelled by the mount's compensation and by the follow spring and
stalled two pixels from where it started.
Coverage:
- The first-click e2e case named the wrong turn (`[data-turn-id]` is the
first MOUNTED turn, whose top is already negative at the opening scroll
position, so an upper-bound-only check passed without the jump doing
anything). It now names `turn-prompt-rail-1`, bounds it on both sides, and
asserts that tick's `aria-current`.
- `emulateMedia` could not put that case on the production scroll path:
`resolveScrollMotionBehavior` collapses motion for ANY fixture, keyed on
`data-maka-e2e-fixture` rather than on the media query. Fixtures can now
ask for a behavior back (`scrollMotion`, per launch — it costs seconds of
settling per window, so only the case that needs it pays), with unit
coverage for the precedence: a fixture request never outranks a stated
preference for less motion.
- `holdJumpDestination`'s unit tests grew the two cases its rewrite is
about: it must not settle while the transcript is still filling, and it
must hand the transcript back the moment the reader touches it.
Verified 5/5 on the smooth-scroll fixture, where the previous revision lost
1 in 4. `quote-selection.spec.ts` flakes on this machine (1 in 4) at
upstream/main as well, unchanged by this branch.
Astro-Han added a commit to Astro-Han/maka-agent that referenced this pull request Aug 15, 2026
Deleting `SessionStatusTone` was right; replacing it with a private
`SessionStatus -> StatusDotVariant` table was not. `status-vocabulary.ts`
already owns "the one place a status word becomes a colour", so the
private table made a second authority, and the two disagreed: a task
waiting on a permission prompt drew `error` in the rail while the
permission centre drew `attention` for the identical condition.
Map the session enum to `StatusSemantic` and let `dotForStatus` pick the
colour. `blocked` and `waiting_for_user` are both `attention`, which is
what that semantic is defined as -- both are waiting on a person. Giving
`blocked` `error` to tell them apart used colour for a distinction colour
cannot carry; their labels and `describeBlockedReason` do that.
Restore `review` and `done`. They have no writer in current source, but
`SESSION_STATUSES` is read back out of storage, and narrowing it is a
data migration rather than a cleanup: `resolveLegacyStatus` in the JSONL
importer (removed in apache#2656) passed both values through into real SQLite
stores verbatim, and `normalizeSessionHeader` throws for the WHOLE header
on an unrecognised status, so one stored row carrying `done` fails an
entire catalog page. The migration is its own change.
`archived` and `aborted` get their dots back. They were `muted` before,
`muted` resolved to a real `neutral` dot, and dropping them to `undefined`
was a behaviour change I described as a consequence of collapsing the
layer.
Refs apache#2984
Generated-by: Claude Code
Astro-Han added a commit to Astro-Han/maka-agent that referenced this pull request Aug 15, 2026
Deleting `SessionStatusTone` was right; replacing it with a private
`SessionStatus -> StatusDotVariant` table was not. `status-vocabulary.ts`
already owns "the one place a status word becomes a colour", so the
private table made a second authority, and the two disagreed: a task
waiting on a permission prompt drew `error` in the rail while the
permission centre drew `attention` for the identical condition.
Map the session enum to `StatusSemantic` and let `dotForStatus` pick the
colour. `blocked` and `waiting_for_user` are both `attention`, which is
what that semantic is defined as -- both are waiting on a person. Giving
`blocked` `error` to tell them apart used colour for a distinction colour
cannot carry; their labels and `describeBlockedReason` do that.
Restore `review` and `done`. They have no writer in current source, but
`SESSION_STATUSES` is read back out of storage, and narrowing it is a
data migration rather than a cleanup: `resolveLegacyStatus` in the JSONL
importer (removed in apache#2656) passed both values through into real SQLite
stores verbatim, and `normalizeSessionHeader` throws for the WHOLE header
on an unrecognised status, so one stored row carrying `done` fails an
entire catalog page. The migration is its own change.
`archived` and `aborted` get their dots back. They were `muted` before,
`muted` resolved to a real `neutral` dot, and dropping them to `undefined`
was a behaviour change I described as a consequence of collapsing the
layer.
Refs apache#2984
Generated-by: Claude Code
Astro-Han added a commit that referenced this pull request Aug 15, 2026
…ary (#3033)
* fix(ui): give the task rail's status dot one meaning
The dot the rail draws for a session passed through two lossy hops and
three disagreeing sources, so it could not say what it meant.
Collapse the mapping. `SessionStatus -> SessionStatusTone ->
StatusDotVariant` becomes one table from status to Astryx's variant. Two
of the seven tones had no distinct outcome at the end of that chain, and
the collapse is what let `waiting_for_user` and `blocked` share
`warning`: blocked now maps to `error`, so "cannot proceed until you fix
a connection" and "holding a question for you" stop rendering alike.
Read the running authority. `runningTurnIds` is the runtime's projection
of the runs it holds; `session.ts` states why a persisted `status` cannot
serve that purpose, and `settledSessionTransientIds` already reads it
first over the same list. The row read neither -- a renderer-local
streaming set, then the stored `status` -- so a task running under a bot
channel or a second window read as idle. The streaming set stays below
it, for the gap between this renderer sending a turn and the host
reporting it back.
Delete what nothing wrote. `review` and `done` were never written by
anything, in any version, so no stored record can carry them and no
reader of them was reachable; they leave `SESSION_STATUSES`, the wire
enum, and the copy tables. `SessionLifecycleStatus` now aliases
`SessionStatus` instead of restating it -- three hand-written copies of
one enum is how those two values survived in two of them after the last
writer went away.
Delete the tautological filter. `SessionFilter` had one value left:
`archived` moved to Settings in #2985 and `flagged` was never selected,
which left a control whose answer is always the same answer. The 「会话」
row that selected it goes with it, as does the branch in
`sessionMatchesNavSelection`. A stored filter is dropped rather than
validated, which is the migration.
Also gone: `SessionStatusPresentation.interactive`, which had no reader,
and a doc comment naming a `SessionStatusIcon` and a chat-header badge as
the tone matrix's consumers -- neither exists.
`status: 'archived'` still duplicates `isArchived`. Consolidating those
two rewrites stored rows, so it is #2984's PR 3, not this one.
Refs #2984
Generated-by: Claude Code
* refactor(ui): rebuild the task row on two signal slots
Every task row in the rail now carries exactly two signal slots. Slot 1 is
a single status dot, reserved as an 8px gutter even when there is nothing to
show so titles stay aligned down the list. Slot 2 holds a compact timestamp
at rest and swaps to the ⋯ menu on hover or keyboard focus, so the menu is
no longer permanently mounted next to every title.
The dot's meaning is resolved once, in priority order: live run (accent,
pulsing) › persisted status › unread (accent, inactive rows only). The live
run reads `runningTurnIds` first, matching `settledSessionTransientIds` over
the same session list — a persisted `running` status can be left behind by a
crash, so it must not outrank the authoritative projection.
The stale pill is gone: it duplicated the timestamp it sat next to. The
worktree mark falls back to the row tooltip rather than competing for a slot.
`RelativeTime` gains `variant="compact"` instead of a second component; a
260px rail row cannot fit the medium-date-plus-time past-horizon fallback.
Generated-by: Claude Code
* refactor(ui): give the rail one top region and one grouping switch
The rail's top region now ends with the grouping switch, and the history
below it starts directly at its first group.
The list heading is gone. "会话" labelled a list that is the only thing under
it, inside a landmark that already names the panel — and it existed mainly to
give the grouping switch somewhere to sit. The switch moves into the sticky
top region instead, where it stays reachable: hung off a heading it scrolled
away with the very list it switches.
It is also full width with text labels now. Two icon-only segments asked the
rail to teach a clock and a folder, and the rail has nowhere to teach them;
按时间 / 按项目 is the entire vocabulary and fits inside the minimum 180px
width with room to spare. Collapsed at 48px it does not render at all — no
segment label fits, and the list it governs is not rendered either.
The group headers lose their ➕. It was the 新任务 row's handler under a
second name, three rows below the row itself, and it needed that second name
precisely because two controls cannot share one accessible name.
Generated-by: Claude Code
* feat(desktop): move task import out of the rail into settings
导入 was a rail row that opened a modal over the conversation. Import is not
navigation: it is a rare setup errand, it needs a source, an archived filter
and a paged directory of someone else's conversations to work through, and
none of that belongs in a 260px column of the tasks you are working on. It
becomes 设置 › 活动 › 导入任务, next to 已归档任务 — both are about the task
catalog rather than the task in front of you.
As a page the flow loses its selection state. A dialog needed one because its
single primary action sat in a footer far from the rows; a page puts 导入 on
the row itself, the way 已归档任务 puts 恢复 on its rows. That also retires
`ExternalSessionImportLifecycle`, whose whole job was refusing to close a
dialog mid-import — there is no dialog left to hold open, and an import that
outlives the page is guarded by the mounted ref instead, so it can still
finish but can no longer steer a shell the user has navigated away from.
The source switch now appears only when there is more than one source. Codex
is the only adapter that ships, so in practice every user was being shown a
segmented control with a single segment — a control nobody can operate. With
one source the section names it instead.
Deliberately not carried over: keeping an imported task in sync with its
source. The importer is a one-shot conversion and nothing behind it watches
the source, so that control would be a promise no coordinator can keep.
Generated-by: Claude Code
* refactor: name the thing a user works on 任务 / task everywhere
The product called the same object 会话, 对话, session and conversation
depending on which file you landed in. Code, protocol and storage keep
`session` — that is a durable contract with a wire format behind it — but
every string a user reads now says 任务 / task.
The rule is what the noun REFERS TO, not the word:
- The Maka entity a user opens, renames, archives and searches → 任务 / task.
- Conversing as a behavior stays 对话 / conversation: "与 Maka 对话" over a
bot, "可用于对话的模型", "对话记录" as a message log.
- Another agent's stored conversations stay 对话 / conversation. 导入任务
lists Codex's conversations and produces Maka tasks; collapsing both onto
one word would erase exactly the distinction that page is about.
- 侧边对话 stays: it is a chat beside a task, not a task in the rail.
Two collisions had to be resolved rather than renamed. The per-task ledger
was 会话任务, which would have become 任务任务 — its items are 待办 / to-dos,
which is what they always were. And the automations row read "定时任务,N 个
未完成任务", two different meanings of 任务 in one label; the count now says
"N 条进行中".
The keyboard sheet loses its ←/→ row. It documented cycling between
会话/已标记/已归档, a filter this PR removed along with `SessionFilter`.
Generated-by: Claude Code
* test(ui): make the narrow-rail story actually narrow
`LongTitlesAndNarrow` framed the panel at 176px but let SideNav keep its own
260px `resizable` width, so the story clipped a wide rail instead of showing
a narrow one — the timestamps it exists to check were outside the frame. It
now drives the rail's own width, at 180px, which is the panel's `minWidth`
and therefore the narrowest state a user can reach.
Generated-by: Claude Code
* fix(ui): keep the rail's 任务 row as the way back to the list
bb39b33 deleted `SessionFilter` and took the 「会话」 row with it, on the
reading that the row only selected a filter whose answer was always the
same answer. Half right: its `isSelected` read the dead filter, but its
`onClick` selected the SECTION, and it was the only control that did.
Without it the rail has three sections and two rows. Collapsed at 48px —
the default state (`readSessionListCollapsed`) — the list is not rendered
at all, so 扩展 and 定时任务 became one-way doors: the only way back to a
running task was 新任务, which answers "show me my tasks" by starting
another one. `streaming-remount.spec.ts` walks exactly that path and had
been rewritten to click a task row that a collapsed rail does not have.
Restore the row as 任务, selected on `section === 'sessions'` and
selecting the section with no filter. The filter deletion stands.
Refs #2984
Generated-by: Claude Code
* fix(ui): route task status through the shared semantic layer
Deleting `SessionStatusTone` was right; replacing it with a private
`SessionStatus -> StatusDotVariant` table was not. `status-vocabulary.ts`
already owns "the one place a status word becomes a colour", so the
private table made a second authority, and the two disagreed: a task
waiting on a permission prompt drew `error` in the rail while the
permission centre drew `attention` for the identical condition.
Map the session enum to `StatusSemantic` and let `dotForStatus` pick the
colour. `blocked` and `waiting_for_user` are both `attention`, which is
what that semantic is defined as -- both are waiting on a person. Giving
`blocked` `error` to tell them apart used colour for a distinction colour
cannot carry; their labels and `describeBlockedReason` do that.
Restore `review` and `done`. They have no writer in current source, but
`SESSION_STATUSES` is read back out of storage, and narrowing it is a
data migration rather than a cleanup: `resolveLegacyStatus` in the JSONL
importer (removed in #2656) passed both values through into real SQLite
stores verbatim, and `normalizeSessionHeader` throws for the WHOLE header
on an unrecognised status, so one stored row carrying `done` fails an
entire catalog page. The migration is its own change.
`archived` and `aborted` get their dots back. They were `muted` before,
`muted` resolved to a real `neutral` dot, and dropping them to `undefined`
was a behaviour change I described as a consequence of collapsing the
layer.
Refs #2984
Generated-by: Claude Code
* fix(ui): keep the task row's facts when it collapses to two slots
The row draws one dot and one trailing slot. That was implemented by
collapsing to a single signal inside the resolver, which made the visual
element the only carrier of the fact -- so removing the trailing `Badge`
removed "stale" itself, not just its pill. `staleTitle` and
`staleAriaLabel` were left with no reader at all, the surviving `opacity`
cue is cancelled on the selected row by design, and opacity announces
nothing. Worktree went the same way: an `aria-label`ed icon inside the
button became a `title` on a non-interactive ancestor, which contributes
to neither the button's name nor its description.
Resolve a LIST of signals in priority order, draw `signals[0]`, and put
the rest -- plus worktree and the absolute timestamp -- in one visually
hidden span inside the button. Two slots, same pixels, and the facts stop
depending on which of them had room. Signals also stop eating each other:
`aborted` now keeps its own dot instead of falling through to the unread
branch, where an aborted task with unread text drew the same accent dot
as a running one.
Stale joins the list as `attention`, so a stale task finally has a signal
that survives being selected. It is resolved here rather than in
`presentSessionStatus` because it is a renderer-derived fact, not a
persisted `SessionStatus`.
Drop the `runningTurnIds` read. No `SessionSummary` reaching Desktop
carries that field: Runtime Host's catalog reads storage directly, the
wire projection does not list it, and Desktop Main's converter copies
persisted fields only -- the identifier appears nowhere in
`packages/runtime-host/src` or `apps/desktop/src/main` outside a test
fixture. It was a read of something nothing populates, described as
reading the authority. `streaming` is what the rail actually has, with
the limit that it only knows this renderer's turns; a real live-run
projection from the Host is its own change.
Also: `pointer-events: none` on the resting ⋯, which sat invisible at
`z-index: 1` over the trailing slot and swallowed clicks; and the inner
list no longer repeats the rail's own accessible name.
Refs #2984
Generated-by: Claude Code
* fix(desktop): refresh the catalog when an import's outcome is unknown
`external-sessions:import` emits `sessions:changed` on success, so a task
imported while the user walks away from Settings still reaches the rail
on its own -- the comment in `app-shell.tsx` claiming nothing notifies
the shell was wrong, and is corrected here.
The gap is the other branch. `commit_outcome_unknown` means the Host
cannot say whether the task was committed, and that path returned a
result without emitting anything. The only trace was the page's own
banner, and 导入任务 is a Settings page: leaving it unmounts the banner,
which is exactly when someone comes back and imports the same
conversation again. Emit there too, with no id, because not knowing which
task landed is what the code means.
Also return the import promise from `clickAction` instead of `void`-ing
it. Astryx's Button awaits it and drops repeat clicks until it settles;
discarding it left double-submit to `importingId` alone, one render
behind the second click.
Refs #2984
Generated-by: Claude Code
* fix(desktop): re-point the structural gates the surface move broke
Deleting the import dialog left exact-string readers behind. `scripts/
check-astryx-alignment.mjs` read the dialog and its stylesheet by path,
so running it exited 1 with ENOENT; the surface inventory still listed
both files and had no row for the page that replaced them.
The dialog's Item-row guards are not migrated, they are dropped. They
checked that a SELECTED row stayed keyboard-reachable -- no parent role
stealing Item's native button, no selected-only tabIndex trapping focus.
设置 › 活动 › 导入任务 has no selection: 导入 sits on each row the way 恢复
does on the archived page. Asserting those smells against the new page
would guard a shape it does not have. The button guard and a ListItem
import check follow the surface to the page.
Inventory rows for the new page and for #2985's archived-tasks page,
which was never added. Three runtime-host entries stay missing; they
predate this branch.
Also read the Daily Review model label from its copy table instead of a
literal. The story matched '跟随对话默认', which the rename retired, so it
silently found nothing -- and `storybook-visual-smoke.mjs` disables every
`play` function, so CI could not report it.
Refs #2984
Generated-by: Claude Code
* refactor: finish the 任务 rename where the rule missed
The rename was applied by rule and reviewed by hand, which is the process
that produces both halves of this: strings that kept the old noun, and
strings that took the new one where the old was right.
Same-surface contradictions, which are what a user actually notices:
- The to-do panel's error banner said 任务载入失败 next to a button
labelled 重新载入待办 -- one `Banner`, two vocabularies.
- `chat-readiness` said 当前任务 and then 新建会话 in one sentence.
- The bot's help text offered to 清空当前对话开新会话 while the reply to
that same command already said 任务已重置.
- 定时任务 described itself as 安排定时任务,并回顾本机任务, and its
delivery option read Agent 任务执行 -- two meanings of 任务 in one form.
- `settlementFailed` called the side chat a 任务 and then a 侧边对话;
`forkSetupFailed` called it 追问任务, a third name for one object. Both
are 侧边对话 now, matching the en twins.
zh/en divergence, where the catalogs share keys but stopped sharing
meaning: Daily Review counted 任务 in zh and conversations in en (and its
en empty state said conversations on one branch, tasks on the other); the
terminal panel said "task" and then "the session's terminal".
Missed outright: `tool-activity/copy.ts` (由源会话管理 on every terminal
result chip), `connection-error-copy.ts`, and the CLI's own prose
(`allow for session`, `Give this session full access`, `--continue`).
Kept deliberately: 和 Maka 对话 as a behaviour, Codex's stored 对话 on the
import page, 侧边对话, 对话框 for dialog, and `<session-id>` as a CLI
contract. The palette's `nav:sessions` keywords get 会话 and 对话 back --
collapsing both onto 任务 left a duplicate entry and dropped the words a
long-time user would still type.
Refs #2984
Generated-by: Claude Code
* fix(ui): route the task row's last two dots through the shared vocabulary
`streaming` and `hasUnread` still wrote `'accent'` by hand while the status
and stale signals resolved through `dotForStatus`. Half a function deciding
its own colours is the drift this change set out to close -- the literal is
what let the rail and Settings disagree about the same fact before. Both are
`active`: streaming is the system working on it, and unread text is
"something happened here" rather than a question waiting on the user, which
is the line the two semantics exist to draw. `dotForStatus('active')` is
`'accent'`, so nothing moves on screen.
Also drops the `copy` binding `SessionHistoryList` no longer reads. It fed
the list's own aria-label, and the outer SideNav took that name over when the
rail was rebuilt.
Refs #2984
Generated-by: Claude Code
* docs: say what each session-status-presentation file actually owns
Desktop's copy claimed the status-to-dot mapping was "re-exported below" and
that `describeBlockedReason` was defined there. Both moved to `@maka/ui` in
this branch and neither is in the file; what is left is Desktop-only judgment
-- which blocked reasons are worth acting on, and what to offer after a turn
fails -- so the header says that instead.
The contract the old header carried is real and had nowhere to live after the
move: a UI label must never show a raw `SessionBlockedReason`, and a new
reason has to extend the core enum and the copy matrix together or it reads as
`unknown`. It now sits on `describeBlockedReason` in `@maka/ui`, where the
matrix it constrains is.
Refs #2984
Generated-by: Claude Code
* test: make the restored-task and import fixtures answer like their sources
Both fixtures modelled states their real source cannot produce, which makes
them evidence for nothing.
The purge tests built a restored task by flipping `isArchived` alone, leaving
`status: 'archived'` behind. `SessionStore.unarchive` writes both fields
together, so no stored row looks like that. The sweep only reads `isArchived`
today, so no assertion changes -- the point is that the next assertion written
against these rows would be checking a state that cannot happen. A `restored`
helper names the pair once.
The 导入任务 story's `list` ignored `includeArchived` and `cursor`: it rendered
the archived conversation while the filter was off and handed 加载更多 the same
first page forever, under a comment claiming it demonstrated both controls.
It now filters and pages, with a fourth conversation so the default view is a
short first page rather than the whole list.
Refs #2984
Generated-by: Claude Code
* fix(desktop): tidy the two places the 任务 rename passed through twice
`fake_backend` reaches the user down two paths -- the reason table, and the
header check in `assertSessionCanSend`, which never gets far enough to look a
connection up. They said the same sentence in two copies, so renaming 会话 to
任务 had to be done twice. One `FAKE_BACKEND_MESSAGE` now.
The English workspace help came out of the rename as "Any task can switch next
to the input box", which makes the task the actor and never names what it
switches. It says the project.
Refs #2984
Generated-by: Claude Code
* chore: regenerate the Astryx surface inventory
The Markdown half still listed `external-session-import-dialog.tsx` and
`external-session-import.css` after this branch deleted them, because the two
halves were edited by hand and `check-astryx-surface-inventory.mjs` only
verifies that on-disk files appear in the inventory -- never that inventory
rows still exist on disk. It is also not wired into CI, so the drift was
invisible from both directions.
Running the generator also picks up three files that arrived from main and
were never listed: `runtime-host-profiles-section.tsx`,
`runtime-host-ssh-terminal-dialog.tsx`, and `settings/runtime-host.css`. They
are unrelated to this branch, but a generated file regenerated in halves is
how it drifted in the first place.
Refs #2984
Generated-by: Claude Code
* test(ui): show every task status in the story that exists to show them
Two gaps, both found by shooting the story for a before/after comparison.
`review` and `done` were dropped from `statusSessions` when this branch briefly
deleted them from `SESSION_STATUSES`. The enum values and their labels came
back; the fixture did not, so the story that covers every status covered six of
eight -- and the two missing ones are exactly the two whose colours this change
decided on purpose, `attention` and `success`.
`StoryFrame` also defaulted to 240px while `SessionListPanel`'s rail defaults to
260, so every story that did not pass a width was clipping the rail by 20px.
That lands on the trailing slot, which is where this change puts the timestamp
-- the stories could not show whether it fits. Stories that want a narrow rail
still pass the width to both, which is what the note on `panelProps` is about.
Refs #2984
Generated-by: Claude Code
* refactor(ui): drop the rail's 任务 row
Expanded, the row selected a section whose content was already on screen
directly under it -- the same redundancy as the 会话 list heading this change
deleted one row down, and clicking any task row does the same selection.
It was kept for the collapsed rail, where the list is not rendered, on the
argument that 扩展 and 定时任务 were otherwise one-way doors. That was wrong.
Collapsed, the rail cannot switch tasks at all, so getting back to any task
already means widening it; the titlebar's 展开侧边栏 toggle does that
unconditionally (app-shell-chrome-actions.tsx renders `ChromeColumnToggle` with
no guard), and `activeId` survives a section change, so the widened rail comes
back with the task you left already marked. The row bought one click in a state
the user is leaving regardless, and charged a permanent slot for it.
`sessions` now has no control of its own on the rail. It is where you are
unless you went somewhere, which is why the other two sections light up and
this one has nothing to light.
`streaming-remount` returns the way the product now offers -- widen, then click
the row carrying `aria-current="page"` -- which is a better assertion than the
old one anyway: it checks that the task survives the trip, not just that a
button exists.
Refs #2984
Generated-by: Claude Code
* fix(runtime-host): coalesce a repeat import while the first is still running
Moving 导入任务 from a dialog into a Settings page removed the only thing
stopping one intent from becoming two tasks, and this branch is where that
happened. `ExternalSessionImportLifecycle` refused to close the dialog while an
import was active; retiring it was described here as dropping a guard against
nothing. It was not. The page it was replaced by is one the user may leave
mid-import -- the import continues in Desktop Main by design -- and the page's
`importingId` dies with it, so coming back and pressing 导入 again issues a
second request against a source that is still importing. Nothing downstream
deduplicates: `treats repeats as independent copies` is a pinned behaviour, so
the second request lands a second task the user now has to tell apart.
The guard belongs to the Host, not to the page that asked. Import is a Host
operation and the Host is the only party that knows one is running; a client
knows about its own requests, which is why a second window or the CLI would
have reproduced this with the page's state intact.
Concurrent repeats collapse onto the first attempt's promise and both callers
get its outcome, success or failure, because it is one operation. Sequential
repeats are untouched -- the entry is gone by the time the first settles -- so
importing the same conversation again on purpose still makes an independent
copy, which the existing test continues to pin.
Reported by @M4n5ter in review of #3033.
Refs #2984
Generated-by: Claude Code
Sign up for freeto 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

@jackwener
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); refactor: remove obsolete fixtures and JSONL import by jackwener · Pull Request #2656 · apache/maka · GitHub
Skip to content

refactor: remove obsolete fixtures and JSONL import - #2656

Merged
jackwener merged 1 commit into
mainfrom
codex/trim-low-value-fixtures
Aug 10, 2026
Merged

refactor: remove obsolete fixtures and JSONL import#2656
jackwener merged 1 commit into
mainfrom
codex/trim-low-value-fixtures

Conversation

@jackwener

Copy link
Copy Markdown
Member

Summary

  • reduce the Desktop E2E fixture registry from about 50 historical scenarios to the 13 scenarios consumed by current E2E, alignment audit, or smoke entry points
  • remove orphaned seed generators, renderer-only fixture controls, and the 1,361-line self-referential fixture test
  • remove the retired legacy JSONL session importer, its automatic SessionStore startup wiring, tests, and 11 transcript fixtures
  • preserve process/crash/lock/race fixtures and external Codex JSONL adapters because they cover active boundaries

Impact

  • 32 files changed
  • 5,194 lines removed, 141 lines added (net -5,053)
  • fixture-related tracked files reduced from 75 to 62

Validation

  • git diff --check
  • Biome lint on all changed source files
  • TypeScript: @maka/core, @maka/storage, and Desktop renderer
  • node --test scripts/fixture-env.test.mjs

Desktop main typecheck still reports the existing scheduled-task API/dist mismatch on origin/main; no errors point to this change.

@jackwener
jackwener merged commit 90bdb30 into mainAug 10, 2026
10 checks passed
@jackwener
jackwener deleted the codex/trim-low-value-fixtures branch August 10, 2026 19:21
ARE404 added a commit to ARE404/maka-agent that referenced this pull request Aug 13, 2026
apache#2580 moved the rail's tick onto Astryx's Button. The bar the tick draws
was a direct child of the flex tick and got blockified; the Button wraps
its children in a label span, so the bar went back to normal flow as an
inline box. An inline box takes no width or height, so every bar computed
to 0x0 and the rail shipped invisible in 0.1.9 and 0.1.10 — present in the
DOM, painting nothing.
`display: block` on the bar restores it. Measured on the new fixture at
1280x800: the rail's box goes from 8px wide (its own padding, ticks
contributing nothing) back to the designed 22px.
This is the third time the rail has failed by rendering and not painting
— apache#2161 pinned it against a containing block as tall as the conversation,
apache#2338 parked it under macOS's overlay scrollbar — and the second time it
reached a release. The e2e coverage that would have caught all three was
deleted in apache#2462, and the multi-prompt fixtures it ran on in apache#2656, so
this adds back the smallest thing that closes the gap:
- `chat-prompt-rail`, a plain 8-prompt conversation. The rail hides itself
below three prompts, so the shipped single-prompt fixture cannot show it
at all.
- `prompt-rail.spec.ts` with one test per past failure: bars have a real
box, the rail stays inside the scrollport at both scroll extremes, and a
tick is what the pointer lands on. Three tests where the deleted suite
had nine.
Verified the first test fails on the unfixed renderer with "Expected: > 0,
Received: 0" and passes with the fix. Neither a static CSS read nor a
jsdom unit test can see any of this: jsdom has no layout engine.
Astro-Han pushed a commit that referenced this pull request Aug 13, 2026
…2923)
* fix(ui): give the prompt rail's tick bars a box again
#2580 moved the rail's tick onto Astryx's Button. The bar the tick draws
was a direct child of the flex tick and got blockified; the Button wraps
its children in a label span, so the bar went back to normal flow as an
inline box. An inline box takes no width or height, so every bar computed
to 0x0 and the rail shipped invisible in 0.1.9 and 0.1.10 — present in the
DOM, painting nothing.
`display: block` on the bar restores it. Measured on the new fixture at
1280x800: the rail's box goes from 8px wide (its own padding, ticks
contributing nothing) back to the designed 22px.
This is the third time the rail has failed by rendering and not painting
— #2161 pinned it against a containing block as tall as the conversation,
#2338 parked it under macOS's overlay scrollbar — and the second time it
reached a release. The e2e coverage that would have caught all three was
deleted in #2462, and the multi-prompt fixtures it ran on in #2656, so
this adds back the smallest thing that closes the gap:
- `chat-prompt-rail`, a plain 8-prompt conversation. The rail hides itself
below three prompts, so the shipped single-prompt fixture cannot show it
at all.
- `prompt-rail.spec.ts` with one test per past failure: bars have a real
box, the rail stays inside the scrollport at both scroll extremes, and a
tick is what the pointer lands on. Three tests where the deleted suite
had nine.
Verified the first test fails on the unfixed renderer with "Expected: > 0,
Received: 0" and passes with the fix. Neither a static CSS read nor a
jsdom unit test can see any of this: jsdom has no layout engine.
* fix(ui): make the prompt rail's hover and jump behave
Four things the rail got wrong once it was visible again, found by using it:
- A 4px gap between ticks was a band where the pointer was over the rail
and over no tick, so the dock-style hover falloff dropped out and picked
up again every few pixels of travel. The rail's `gap` moves into the
ticks' own `padding-block`: same pitch, hit boxes now tile.
- The hover preview waited 300ms before opening — Astryx's HoverCard
default, meant for a pointer crossing a wide row on its way somewhere
else. A tick is 22px of rail that nothing is on the way to, and the wait
is the one part of this hover with no motion in it. Now 120ms.
- The highlight glided 280ms to wherever a click landed, so crossing
twenty prompts read as the bar flying off across the rail. A click now
owns the highlight until its scroll settles: no glide, and the scroll
no longer walks the highlight through every prompt it passes.
- The first click into a session did nothing until the reader scrolled by
hand. See below.
That last one is a collision between Astryx's auto-follow lock and the
progressive transcript mount, and neither side is wrong on its own.
`useChatStreamScroll` unlocks on a scroll up, detected by comparing
scrollTop between events — but it ignores any scroll event that arrives
with a changed scrollHeight or offsetHeight, because Chrome fires those
when content resizes and they are not the reader moving. A jump into an
unmounted turn mounts it and the fill that follows changes scrollHeight
for several frames, so the jump's own scroll is invisible to the lock: it
stays on, and `scrollIfLocked` pulls the transcript back to the bottom.
Only a wheel gesture broke it, which takes a separate path in Astryx.
`holdJumpDestination` re-aims at the target on each height change until
the fill stops. The last of those scrolls lands with a stable height,
which is the one the lock finally reads as a scroll up. Measured on the
30-prompt fixture: clicking the first tick went to scrollTop 7042 (the
bottom) and now goes to 24 and holds.
The fixture grows from 8 prompts to 30 because the progressive mount's
initial window is 10 — at 8 the head of the transcript is already mounted
and the jump-into-unmounted-turns path never runs at all.
Coverage note: the e2e case for the first click is an end-to-end check,
not a guard. Whether the lock wins depends on which frame the fill lands
on relative to a smooth scroll still in flight, and it goes green against
the unfixed renderer often enough to be worthless as one. The guard is
the `holdJumpDestination` unit test, which drives the frames itself.
* fix(ui): own a rail jump through the mount instead of racing it
Review of #2923 found the jump's ownership bound to a clock rather than to
the navigation, and the e2e case that was supposed to guard it asserting
almost nothing. Both hold.
Jump ownership:
- A second click during a jump only replaced the target; the first click's
700ms timer still governed, and could clear the second jump mid-flight.
Each click now carries its own sequence and starts its own hold.
- The fixed window is gone. A hold runs until the progressive mount reports
the transcript filled AND nothing has moved for a few frames, so a long
transcript is never released mid-fill, and it ends the moment the reader
touches the transcript (wheel, touch, pointer, key) rather than outliving
their interest in it.
Chasing the "just release auto-follow" direction the review preferred found
that ChatLayout publishes no such seam, so this adds one — `unlockAutoFollow`
on `ChatLayoutContextValue`, exposing the scroll hook's existing `unlock`
(patch hunk + patches/README entry). It is necessary and it is not
sufficient, which the earlier framing got wrong:
- Astryx re-locks on any `scrollend` that settles near the bottom, and a
session that opens at the bottom produces exactly that while the mount is
still catching up. Releasing once at the click is undone before the jump
goes anywhere — traced: released at the click, landed at 154ms, dragged
back to the bottom by 166ms. The release is now re-asserted for the life
of the hold.
- Auto-follow is not the only thing moving the transcript. The progressive
mount's own scroll compensation holds the reader's position across each
fill step, and mounting the turn a jump asked for IS a fill step, so it
lands after the jump and restores the position the jump just left. That
one no seam can fix; it is what the hold is for.
Jumps also scroll instantly now, whatever the app's scroll-motion policy
says. A jump is a teleport the reader asked for, and an animated one does
not survive this surface: traced on the 30-prompt fixture, the smooth scroll
was cancelled by the mount's compensation and by the follow spring and
stalled two pixels from where it started.
Coverage:
- The first-click e2e case named the wrong turn (`[data-turn-id]` is the
first MOUNTED turn, whose top is already negative at the opening scroll
position, so an upper-bound-only check passed without the jump doing
anything). It now names `turn-prompt-rail-1`, bounds it on both sides, and
asserts that tick's `aria-current`.
- `emulateMedia` could not put that case on the production scroll path:
`resolveScrollMotionBehavior` collapses motion for ANY fixture, keyed on
`data-maka-e2e-fixture` rather than on the media query. Fixtures can now
ask for a behavior back (`scrollMotion`, per launch — it costs seconds of
settling per window, so only the case that needs it pays), with unit
coverage for the precedence: a fixture request never outranks a stated
preference for less motion.
- `holdJumpDestination`'s unit tests grew the two cases its rewrite is
about: it must not settle while the transcript is still filling, and it
must hand the transcript back the moment the reader touches it.
Verified 5/5 on the smooth-scroll fixture, where the previous revision lost
1 in 4. `quote-selection.spec.ts` flakes on this machine (1 in 4) at
upstream/main as well, unchanged by this branch.
Astro-Han added a commit to Astro-Han/maka-agent that referenced this pull request Aug 15, 2026
Deleting `SessionStatusTone` was right; replacing it with a private
`SessionStatus -> StatusDotVariant` table was not. `status-vocabulary.ts`
already owns "the one place a status word becomes a colour", so the
private table made a second authority, and the two disagreed: a task
waiting on a permission prompt drew `error` in the rail while the
permission centre drew `attention` for the identical condition.
Map the session enum to `StatusSemantic` and let `dotForStatus` pick the
colour. `blocked` and `waiting_for_user` are both `attention`, which is
what that semantic is defined as -- both are waiting on a person. Giving
`blocked` `error` to tell them apart used colour for a distinction colour
cannot carry; their labels and `describeBlockedReason` do that.
Restore `review` and `done`. They have no writer in current source, but
`SESSION_STATUSES` is read back out of storage, and narrowing it is a
data migration rather than a cleanup: `resolveLegacyStatus` in the JSONL
importer (removed in apache#2656) passed both values through into real SQLite
stores verbatim, and `normalizeSessionHeader` throws for the WHOLE header
on an unrecognised status, so one stored row carrying `done` fails an
entire catalog page. The migration is its own change.
`archived` and `aborted` get their dots back. They were `muted` before,
`muted` resolved to a real `neutral` dot, and dropping them to `undefined`
was a behaviour change I described as a consequence of collapsing the
layer.
Refs apache#2984
Generated-by: Claude Code
Astro-Han added a commit to Astro-Han/maka-agent that referenced this pull request Aug 15, 2026
Deleting `SessionStatusTone` was right; replacing it with a private
`SessionStatus -> StatusDotVariant` table was not. `status-vocabulary.ts`
already owns "the one place a status word becomes a colour", so the
private table made a second authority, and the two disagreed: a task
waiting on a permission prompt drew `error` in the rail while the
permission centre drew `attention` for the identical condition.
Map the session enum to `StatusSemantic` and let `dotForStatus` pick the
colour. `blocked` and `waiting_for_user` are both `attention`, which is
what that semantic is defined as -- both are waiting on a person. Giving
`blocked` `error` to tell them apart used colour for a distinction colour
cannot carry; their labels and `describeBlockedReason` do that.
Restore `review` and `done`. They have no writer in current source, but
`SESSION_STATUSES` is read back out of storage, and narrowing it is a
data migration rather than a cleanup: `resolveLegacyStatus` in the JSONL
importer (removed in apache#2656) passed both values through into real SQLite
stores verbatim, and `normalizeSessionHeader` throws for the WHOLE header
on an unrecognised status, so one stored row carrying `done` fails an
entire catalog page. The migration is its own change.
`archived` and `aborted` get their dots back. They were `muted` before,
`muted` resolved to a real `neutral` dot, and dropping them to `undefined`
was a behaviour change I described as a consequence of collapsing the
layer.
Refs apache#2984
Generated-by: Claude Code
Astro-Han added a commit that referenced this pull request Aug 15, 2026
…ary (#3033)
* fix(ui): give the task rail's status dot one meaning
The dot the rail draws for a session passed through two lossy hops and
three disagreeing sources, so it could not say what it meant.
Collapse the mapping. `SessionStatus -> SessionStatusTone ->
StatusDotVariant` becomes one table from status to Astryx's variant. Two
of the seven tones had no distinct outcome at the end of that chain, and
the collapse is what let `waiting_for_user` and `blocked` share
`warning`: blocked now maps to `error`, so "cannot proceed until you fix
a connection" and "holding a question for you" stop rendering alike.
Read the running authority. `runningTurnIds` is the runtime's projection
of the runs it holds; `session.ts` states why a persisted `status` cannot
serve that purpose, and `settledSessionTransientIds` already reads it
first over the same list. The row read neither -- a renderer-local
streaming set, then the stored `status` -- so a task running under a bot
channel or a second window read as idle. The streaming set stays below
it, for the gap between this renderer sending a turn and the host
reporting it back.
Delete what nothing wrote. `review` and `done` were never written by
anything, in any version, so no stored record can carry them and no
reader of them was reachable; they leave `SESSION_STATUSES`, the wire
enum, and the copy tables. `SessionLifecycleStatus` now aliases
`SessionStatus` instead of restating it -- three hand-written copies of
one enum is how those two values survived in two of them after the last
writer went away.
Delete the tautological filter. `SessionFilter` had one value left:
`archived` moved to Settings in #2985 and `flagged` was never selected,
which left a control whose answer is always the same answer. The 「会话」
row that selected it goes with it, as does the branch in
`sessionMatchesNavSelection`. A stored filter is dropped rather than
validated, which is the migration.
Also gone: `SessionStatusPresentation.interactive`, which had no reader,
and a doc comment naming a `SessionStatusIcon` and a chat-header badge as
the tone matrix's consumers -- neither exists.
`status: 'archived'` still duplicates `isArchived`. Consolidating those
two rewrites stored rows, so it is #2984's PR 3, not this one.
Refs #2984
Generated-by: Claude Code
* refactor(ui): rebuild the task row on two signal slots
Every task row in the rail now carries exactly two signal slots. Slot 1 is
a single status dot, reserved as an 8px gutter even when there is nothing to
show so titles stay aligned down the list. Slot 2 holds a compact timestamp
at rest and swaps to the ⋯ menu on hover or keyboard focus, so the menu is
no longer permanently mounted next to every title.
The dot's meaning is resolved once, in priority order: live run (accent,
pulsing) › persisted status › unread (accent, inactive rows only). The live
run reads `runningTurnIds` first, matching `settledSessionTransientIds` over
the same session list — a persisted `running` status can be left behind by a
crash, so it must not outrank the authoritative projection.
The stale pill is gone: it duplicated the timestamp it sat next to. The
worktree mark falls back to the row tooltip rather than competing for a slot.
`RelativeTime` gains `variant="compact"` instead of a second component; a
260px rail row cannot fit the medium-date-plus-time past-horizon fallback.
Generated-by: Claude Code
* refactor(ui): give the rail one top region and one grouping switch
The rail's top region now ends with the grouping switch, and the history
below it starts directly at its first group.
The list heading is gone. "会话" labelled a list that is the only thing under
it, inside a landmark that already names the panel — and it existed mainly to
give the grouping switch somewhere to sit. The switch moves into the sticky
top region instead, where it stays reachable: hung off a heading it scrolled
away with the very list it switches.
It is also full width with text labels now. Two icon-only segments asked the
rail to teach a clock and a folder, and the rail has nowhere to teach them;
按时间 / 按项目 is the entire vocabulary and fits inside the minimum 180px
width with room to spare. Collapsed at 48px it does not render at all — no
segment label fits, and the list it governs is not rendered either.
The group headers lose their ➕. It was the 新任务 row's handler under a
second name, three rows below the row itself, and it needed that second name
precisely because two controls cannot share one accessible name.
Generated-by: Claude Code
* feat(desktop): move task import out of the rail into settings
导入 was a rail row that opened a modal over the conversation. Import is not
navigation: it is a rare setup errand, it needs a source, an archived filter
and a paged directory of someone else's conversations to work through, and
none of that belongs in a 260px column of the tasks you are working on. It
becomes 设置 › 活动 › 导入任务, next to 已归档任务 — both are about the task
catalog rather than the task in front of you.
As a page the flow loses its selection state. A dialog needed one because its
single primary action sat in a footer far from the rows; a page puts 导入 on
the row itself, the way 已归档任务 puts 恢复 on its rows. That also retires
`ExternalSessionImportLifecycle`, whose whole job was refusing to close a
dialog mid-import — there is no dialog left to hold open, and an import that
outlives the page is guarded by the mounted ref instead, so it can still
finish but can no longer steer a shell the user has navigated away from.
The source switch now appears only when there is more than one source. Codex
is the only adapter that ships, so in practice every user was being shown a
segmented control with a single segment — a control nobody can operate. With
one source the section names it instead.
Deliberately not carried over: keeping an imported task in sync with its
source. The importer is a one-shot conversion and nothing behind it watches
the source, so that control would be a promise no coordinator can keep.
Generated-by: Claude Code
* refactor: name the thing a user works on 任务 / task everywhere
The product called the same object 会话, 对话, session and conversation
depending on which file you landed in. Code, protocol and storage keep
`session` — that is a durable contract with a wire format behind it — but
every string a user reads now says 任务 / task.
The rule is what the noun REFERS TO, not the word:
- The Maka entity a user opens, renames, archives and searches → 任务 / task.
- Conversing as a behavior stays 对话 / conversation: "与 Maka 对话" over a
bot, "可用于对话的模型", "对话记录" as a message log.
- Another agent's stored conversations stay 对话 / conversation. 导入任务
lists Codex's conversations and produces Maka tasks; collapsing both onto
one word would erase exactly the distinction that page is about.
- 侧边对话 stays: it is a chat beside a task, not a task in the rail.
Two collisions had to be resolved rather than renamed. The per-task ledger
was 会话任务, which would have become 任务任务 — its items are 待办 / to-dos,
which is what they always were. And the automations row read "定时任务,N 个
未完成任务", two different meanings of 任务 in one label; the count now says
"N 条进行中".
The keyboard sheet loses its ←/→ row. It documented cycling between
会话/已标记/已归档, a filter this PR removed along with `SessionFilter`.
Generated-by: Claude Code
* test(ui): make the narrow-rail story actually narrow
`LongTitlesAndNarrow` framed the panel at 176px but let SideNav keep its own
260px `resizable` width, so the story clipped a wide rail instead of showing
a narrow one — the timestamps it exists to check were outside the frame. It
now drives the rail's own width, at 180px, which is the panel's `minWidth`
and therefore the narrowest state a user can reach.
Generated-by: Claude Code
* fix(ui): keep the rail's 任务 row as the way back to the list
bb39b33 deleted `SessionFilter` and took the 「会话」 row with it, on the
reading that the row only selected a filter whose answer was always the
same answer. Half right: its `isSelected` read the dead filter, but its
`onClick` selected the SECTION, and it was the only control that did.
Without it the rail has three sections and two rows. Collapsed at 48px —
the default state (`readSessionListCollapsed`) — the list is not rendered
at all, so 扩展 and 定时任务 became one-way doors: the only way back to a
running task was 新任务, which answers "show me my tasks" by starting
another one. `streaming-remount.spec.ts` walks exactly that path and had
been rewritten to click a task row that a collapsed rail does not have.
Restore the row as 任务, selected on `section === 'sessions'` and
selecting the section with no filter. The filter deletion stands.
Refs #2984
Generated-by: Claude Code
* fix(ui): route task status through the shared semantic layer
Deleting `SessionStatusTone` was right; replacing it with a private
`SessionStatus -> StatusDotVariant` table was not. `status-vocabulary.ts`
already owns "the one place a status word becomes a colour", so the
private table made a second authority, and the two disagreed: a task
waiting on a permission prompt drew `error` in the rail while the
permission centre drew `attention` for the identical condition.
Map the session enum to `StatusSemantic` and let `dotForStatus` pick the
colour. `blocked` and `waiting_for_user` are both `attention`, which is
what that semantic is defined as -- both are waiting on a person. Giving
`blocked` `error` to tell them apart used colour for a distinction colour
cannot carry; their labels and `describeBlockedReason` do that.
Restore `review` and `done`. They have no writer in current source, but
`SESSION_STATUSES` is read back out of storage, and narrowing it is a
data migration rather than a cleanup: `resolveLegacyStatus` in the JSONL
importer (removed in #2656) passed both values through into real SQLite
stores verbatim, and `normalizeSessionHeader` throws for the WHOLE header
on an unrecognised status, so one stored row carrying `done` fails an
entire catalog page. The migration is its own change.
`archived` and `aborted` get their dots back. They were `muted` before,
`muted` resolved to a real `neutral` dot, and dropping them to `undefined`
was a behaviour change I described as a consequence of collapsing the
layer.
Refs #2984
Generated-by: Claude Code
* fix(ui): keep the task row's facts when it collapses to two slots
The row draws one dot and one trailing slot. That was implemented by
collapsing to a single signal inside the resolver, which made the visual
element the only carrier of the fact -- so removing the trailing `Badge`
removed "stale" itself, not just its pill. `staleTitle` and
`staleAriaLabel` were left with no reader at all, the surviving `opacity`
cue is cancelled on the selected row by design, and opacity announces
nothing. Worktree went the same way: an `aria-label`ed icon inside the
button became a `title` on a non-interactive ancestor, which contributes
to neither the button's name nor its description.
Resolve a LIST of signals in priority order, draw `signals[0]`, and put
the rest -- plus worktree and the absolute timestamp -- in one visually
hidden span inside the button. Two slots, same pixels, and the facts stop
depending on which of them had room. Signals also stop eating each other:
`aborted` now keeps its own dot instead of falling through to the unread
branch, where an aborted task with unread text drew the same accent dot
as a running one.
Stale joins the list as `attention`, so a stale task finally has a signal
that survives being selected. It is resolved here rather than in
`presentSessionStatus` because it is a renderer-derived fact, not a
persisted `SessionStatus`.
Drop the `runningTurnIds` read. No `SessionSummary` reaching Desktop
carries that field: Runtime Host's catalog reads storage directly, the
wire projection does not list it, and Desktop Main's converter copies
persisted fields only -- the identifier appears nowhere in
`packages/runtime-host/src` or `apps/desktop/src/main` outside a test
fixture. It was a read of something nothing populates, described as
reading the authority. `streaming` is what the rail actually has, with
the limit that it only knows this renderer's turns; a real live-run
projection from the Host is its own change.
Also: `pointer-events: none` on the resting ⋯, which sat invisible at
`z-index: 1` over the trailing slot and swallowed clicks; and the inner
list no longer repeats the rail's own accessible name.
Refs #2984
Generated-by: Claude Code
* fix(desktop): refresh the catalog when an import's outcome is unknown
`external-sessions:import` emits `sessions:changed` on success, so a task
imported while the user walks away from Settings still reaches the rail
on its own -- the comment in `app-shell.tsx` claiming nothing notifies
the shell was wrong, and is corrected here.
The gap is the other branch. `commit_outcome_unknown` means the Host
cannot say whether the task was committed, and that path returned a
result without emitting anything. The only trace was the page's own
banner, and 导入任务 is a Settings page: leaving it unmounts the banner,
which is exactly when someone comes back and imports the same
conversation again. Emit there too, with no id, because not knowing which
task landed is what the code means.
Also return the import promise from `clickAction` instead of `void`-ing
it. Astryx's Button awaits it and drops repeat clicks until it settles;
discarding it left double-submit to `importingId` alone, one render
behind the second click.
Refs #2984
Generated-by: Claude Code
* fix(desktop): re-point the structural gates the surface move broke
Deleting the import dialog left exact-string readers behind. `scripts/
check-astryx-alignment.mjs` read the dialog and its stylesheet by path,
so running it exited 1 with ENOENT; the surface inventory still listed
both files and had no row for the page that replaced them.
The dialog's Item-row guards are not migrated, they are dropped. They
checked that a SELECTED row stayed keyboard-reachable -- no parent role
stealing Item's native button, no selected-only tabIndex trapping focus.
设置 › 活动 › 导入任务 has no selection: 导入 sits on each row the way 恢复
does on the archived page. Asserting those smells against the new page
would guard a shape it does not have. The button guard and a ListItem
import check follow the surface to the page.
Inventory rows for the new page and for #2985's archived-tasks page,
which was never added. Three runtime-host entries stay missing; they
predate this branch.
Also read the Daily Review model label from its copy table instead of a
literal. The story matched '跟随对话默认', which the rename retired, so it
silently found nothing -- and `storybook-visual-smoke.mjs` disables every
`play` function, so CI could not report it.
Refs #2984
Generated-by: Claude Code
* refactor: finish the 任务 rename where the rule missed
The rename was applied by rule and reviewed by hand, which is the process
that produces both halves of this: strings that kept the old noun, and
strings that took the new one where the old was right.
Same-surface contradictions, which are what a user actually notices:
- The to-do panel's error banner said 任务载入失败 next to a button
labelled 重新载入待办 -- one `Banner`, two vocabularies.
- `chat-readiness` said 当前任务 and then 新建会话 in one sentence.
- The bot's help text offered to 清空当前对话开新会话 while the reply to
that same command already said 任务已重置.
- 定时任务 described itself as 安排定时任务,并回顾本机任务, and its
delivery option read Agent 任务执行 -- two meanings of 任务 in one form.
- `settlementFailed` called the side chat a 任务 and then a 侧边对话;
`forkSetupFailed` called it 追问任务, a third name for one object. Both
are 侧边对话 now, matching the en twins.
zh/en divergence, where the catalogs share keys but stopped sharing
meaning: Daily Review counted 任务 in zh and conversations in en (and its
en empty state said conversations on one branch, tasks on the other); the
terminal panel said "task" and then "the session's terminal".
Missed outright: `tool-activity/copy.ts` (由源会话管理 on every terminal
result chip), `connection-error-copy.ts`, and the CLI's own prose
(`allow for session`, `Give this session full access`, `--continue`).
Kept deliberately: 和 Maka 对话 as a behaviour, Codex's stored 对话 on the
import page, 侧边对话, 对话框 for dialog, and `<session-id>` as a CLI
contract. The palette's `nav:sessions` keywords get 会话 and 对话 back --
collapsing both onto 任务 left a duplicate entry and dropped the words a
long-time user would still type.
Refs #2984
Generated-by: Claude Code
* fix(ui): route the task row's last two dots through the shared vocabulary
`streaming` and `hasUnread` still wrote `'accent'` by hand while the status
and stale signals resolved through `dotForStatus`. Half a function deciding
its own colours is the drift this change set out to close -- the literal is
what let the rail and Settings disagree about the same fact before. Both are
`active`: streaming is the system working on it, and unread text is
"something happened here" rather than a question waiting on the user, which
is the line the two semantics exist to draw. `dotForStatus('active')` is
`'accent'`, so nothing moves on screen.
Also drops the `copy` binding `SessionHistoryList` no longer reads. It fed
the list's own aria-label, and the outer SideNav took that name over when the
rail was rebuilt.
Refs #2984
Generated-by: Claude Code
* docs: say what each session-status-presentation file actually owns
Desktop's copy claimed the status-to-dot mapping was "re-exported below" and
that `describeBlockedReason` was defined there. Both moved to `@maka/ui` in
this branch and neither is in the file; what is left is Desktop-only judgment
-- which blocked reasons are worth acting on, and what to offer after a turn
fails -- so the header says that instead.
The contract the old header carried is real and had nowhere to live after the
move: a UI label must never show a raw `SessionBlockedReason`, and a new
reason has to extend the core enum and the copy matrix together or it reads as
`unknown`. It now sits on `describeBlockedReason` in `@maka/ui`, where the
matrix it constrains is.
Refs #2984
Generated-by: Claude Code
* test: make the restored-task and import fixtures answer like their sources
Both fixtures modelled states their real source cannot produce, which makes
them evidence for nothing.
The purge tests built a restored task by flipping `isArchived` alone, leaving
`status: 'archived'` behind. `SessionStore.unarchive` writes both fields
together, so no stored row looks like that. The sweep only reads `isArchived`
today, so no assertion changes -- the point is that the next assertion written
against these rows would be checking a state that cannot happen. A `restored`
helper names the pair once.
The 导入任务 story's `list` ignored `includeArchived` and `cursor`: it rendered
the archived conversation while the filter was off and handed 加载更多 the same
first page forever, under a comment claiming it demonstrated both controls.
It now filters and pages, with a fourth conversation so the default view is a
short first page rather than the whole list.
Refs #2984
Generated-by: Claude Code
* fix(desktop): tidy the two places the 任务 rename passed through twice
`fake_backend` reaches the user down two paths -- the reason table, and the
header check in `assertSessionCanSend`, which never gets far enough to look a
connection up. They said the same sentence in two copies, so renaming 会话 to
任务 had to be done twice. One `FAKE_BACKEND_MESSAGE` now.
The English workspace help came out of the rename as "Any task can switch next
to the input box", which makes the task the actor and never names what it
switches. It says the project.
Refs #2984
Generated-by: Claude Code
* chore: regenerate the Astryx surface inventory
The Markdown half still listed `external-session-import-dialog.tsx` and
`external-session-import.css` after this branch deleted them, because the two
halves were edited by hand and `check-astryx-surface-inventory.mjs` only
verifies that on-disk files appear in the inventory -- never that inventory
rows still exist on disk. It is also not wired into CI, so the drift was
invisible from both directions.
Running the generator also picks up three files that arrived from main and
were never listed: `runtime-host-profiles-section.tsx`,
`runtime-host-ssh-terminal-dialog.tsx`, and `settings/runtime-host.css`. They
are unrelated to this branch, but a generated file regenerated in halves is
how it drifted in the first place.
Refs #2984
Generated-by: Claude Code
* test(ui): show every task status in the story that exists to show them
Two gaps, both found by shooting the story for a before/after comparison.
`review` and `done` were dropped from `statusSessions` when this branch briefly
deleted them from `SESSION_STATUSES`. The enum values and their labels came
back; the fixture did not, so the story that covers every status covered six of
eight -- and the two missing ones are exactly the two whose colours this change
decided on purpose, `attention` and `success`.
`StoryFrame` also defaulted to 240px while `SessionListPanel`'s rail defaults to
260, so every story that did not pass a width was clipping the rail by 20px.
That lands on the trailing slot, which is where this change puts the timestamp
-- the stories could not show whether it fits. Stories that want a narrow rail
still pass the width to both, which is what the note on `panelProps` is about.
Refs #2984
Generated-by: Claude Code
* refactor(ui): drop the rail's 任务 row
Expanded, the row selected a section whose content was already on screen
directly under it -- the same redundancy as the 会话 list heading this change
deleted one row down, and clicking any task row does the same selection.
It was kept for the collapsed rail, where the list is not rendered, on the
argument that 扩展 and 定时任务 were otherwise one-way doors. That was wrong.
Collapsed, the rail cannot switch tasks at all, so getting back to any task
already means widening it; the titlebar's 展开侧边栏 toggle does that
unconditionally (app-shell-chrome-actions.tsx renders `ChromeColumnToggle` with
no guard), and `activeId` survives a section change, so the widened rail comes
back with the task you left already marked. The row bought one click in a state
the user is leaving regardless, and charged a permanent slot for it.
`sessions` now has no control of its own on the rail. It is where you are
unless you went somewhere, which is why the other two sections light up and
this one has nothing to light.
`streaming-remount` returns the way the product now offers -- widen, then click
the row carrying `aria-current="page"` -- which is a better assertion than the
old one anyway: it checks that the task survives the trip, not just that a
button exists.
Refs #2984
Generated-by: Claude Code
* fix(runtime-host): coalesce a repeat import while the first is still running
Moving 导入任务 from a dialog into a Settings page removed the only thing
stopping one intent from becoming two tasks, and this branch is where that
happened. `ExternalSessionImportLifecycle` refused to close the dialog while an
import was active; retiring it was described here as dropping a guard against
nothing. It was not. The page it was replaced by is one the user may leave
mid-import -- the import continues in Desktop Main by design -- and the page's
`importingId` dies with it, so coming back and pressing 导入 again issues a
second request against a source that is still importing. Nothing downstream
deduplicates: `treats repeats as independent copies` is a pinned behaviour, so
the second request lands a second task the user now has to tell apart.
The guard belongs to the Host, not to the page that asked. Import is a Host
operation and the Host is the only party that knows one is running; a client
knows about its own requests, which is why a second window or the CLI would
have reproduced this with the page's state intact.
Concurrent repeats collapse onto the first attempt's promise and both callers
get its outcome, success or failure, because it is one operation. Sequential
repeats are untouched -- the entry is gone by the time the first settles -- so
importing the same conversation again on purpose still makes an independent
copy, which the existing test continues to pin.
Reported by @M4n5ter in review of #3033.
Refs #2984
Generated-by: Claude Code
Sign up for freeto 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

@jackwener