Skip to content

refactor(ui): decompose composer and plan-reminder panels into focused hooks (#1044) - #1153

Merged
Astro-Han merged 3 commits into
mainfrom
refactor/1044-ui-panel-decomposition
Jul 17, 2026
Merged

refactor(ui): decompose composer and plan-reminder panels into focused hooks (#1044)#1153
Astro-Han merged 3 commits into
mainfrom
refactor/1044-ui-panel-decomposition

Conversation

@Astro-Han

@Astro-Han Astro-Han commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #1044.

Behavior- and visually-neutral decomposition of the two shared-UI panels so each hook/component owns one concern:

Composer (composer.tsx 1121 → 787 lines) — now orchestrates only:

  • use-composer-draft.ts — per-session draft persistence: the bounded draft Map, active key, hasDraftText, saveCurrentDraft/clearDraft, and the draftKey swap effect (pure store ops stay in composer-helpers.ts).
  • use-composer-history.ts — up/down prompt-history navigation: the in-memory history mirror, resetNavigation/rememberSentEntry, and the arrow-key handler that applies navigation to the uncontrolled textarea.
  • use-mention-popup.ts — the @// mention state machine: trigger detection, items, active index, post-insertion suppression, selectionchange listener. Composer keeps only the keydown routing, driving the hook through identically-named bindings.
  • composer-workspace-row.tsx — the workspace + branch picker row, with named ComposerWorkspacePicker/ComposerBranchPicker prop types.

Plan reminder (plan-reminder-panel.tsx 863 → 540 lines) — the panel keeps only list/runs/query state, per-action pending, and refresh:

  • plan-reminder-form-dialog.tsxPlanReminderFormDialog owns ALL create/edit form state (nine fields, editingId), the submitPending single-flight owner, validation, and the submit/close pipeline. The panel opens it with a PlanReminderFormSeed and remounts per open (key={formNonce}), so fields initialize from the seed exactly where the old open-handler setters put them.
  • plan-reminder-select.tsx — the shared PlanReminderSelect used by both surfaces.

Public export surface is unchanged (Composer/ComposerHandle via components.tsx, PlanReminderPanel via plan-reminder-panel.tsx); @maka/ui stays host-agnostic (no new props, no window.maka).

Contract tests that regex-match raw source were co-migrated to pin the same invariants across the new seams — none weakened: composer-send-guard (draft internals now asserted on the hook; send path unchanged), project-context-badge (workspace-row assertions on the row component), plan-reminder-panel-contract (submit owner/close guard asserted on the dialog), command-palette-plan-reminder (title-input marker), renderer-utility-primitives (preset button variants), and the copy-hygiene / stub-vocabulary scans now include the new files.

One deliberate simplification: the card-menu edit/duplicate items drop their submitPending || disabled guard — unreachable, since the menu lives behind the modal dialog whenever a submit can be in flight. Also dropped two imports in composer.tsx that the move made dead (MenuSeparator, FileEdit).

Verification

  • Post-merge polish (pure draft/history helper unit tests + plan-reminder form-seed move into helpers) lands in follow-up test(ui): pin draft/history helpers and relocate form seeds (#1044 follow-up) #1157.
  • npm run test:fast from the worktree root — all workspace tests pass (includes build:test; ui + desktop suites green end to end).
  • Narrow sweeps during development: all composer contract tests (157 tests incl. composer-send-guard, composer-mention, composer-esc-drag-clear, composer-constant-footprint, permission-composer-takeover, SSR hint tests) and all plan-reminder tests (95 tests incl. plan-reminder-panel-contract, tab-spec-499, badge/chip-converge) — green after each commit.
  • npm run typecheck (all workspaces, incl. desktop renderer + storybook tsconfigs) — clean.
  • npm run lint — clean.
  • Not run: e2e / visual smoke / screenshots (no visual or behavior change intended; DOM structure, classes, and aria attributes are preserved verbatim).

Review focus

The riskiest extraction is the draft-persistence hook (use-composer-draft.ts): the draftKey swap effect must remember the outgoing value under the OLD key, swap in the NEW key's draft, and reset history navigation in one pass — including the first-send-new-session re-key, where sendCurrent clears the submitted key via clearDraft(submittedDraftKey) + saveCurrentDraft(''). The send-guard contract pins this path; worth a manual read of the hook's effect against the old inline code.

Pull the per-session draft store (draftStoreRef/activeDraftKeyRef/
hasDraftText + the draftKey swap effect) into useComposerDraft, and the
up/down prompt-history navigation state machine into useComposerHistory.
The pure store/navigation logic stays in composer-helpers.ts; the hooks
are the React seam that applies results to the uncontrolled textarea.

sendCurrent now clears the submitted key via clearDraft(submittedDraftKey)
+ saveCurrentDraft(''); the composer-send-guard contract is co-migrated to
pin the same invariant across the composer/hook seam.
Move the @// mention-popup state machine (trigger detection, items,
active index, post-insertion suppression, selectionchange listener) into
useMentionPopup; Composer keeps only the keydown routing, driving the
hook through identically-named bindings so the mention contract reads
the same source shape.

Move the workspace + branch picker row into ComposerWorkspaceRow with
named picker prop types; project-context-badge is co-migrated to pin the
same invariants across the composer/row seam, and the copy-hygiene +
stub-vocabulary scans follow the moved file. Also drops two imports that
became dead with the move (MenuSeparator, FileEdit).
The create/edit dialog now owns all nine form field states, editingId,
the submitPending single-flight owner, validation, and the close guard;
the panel keeps only list/runs/query state, per-action pending, and
refresh. The panel opens the dialog with a PlanReminderFormSeed and
remounts it per open (key={formNonce}), so fields initialize from the
seed exactly where the old open-handler setters put them. The shared
PlanReminderSelect moves to its own leaf module used by both surfaces.

Co-migrated contracts pin the same invariants across the seam:
plan-reminder-panel-contract (submit owner/close guard now asserted on
the dialog), command-palette (title-input marker), utility-primitives
(preset button variants), and the copy-hygiene/stub-vocabulary scans.

One deliberate simplification: the card-menu edit/duplicate items drop
their submitPending || guard — unreachable, since the menu lives behind
the modal dialog whenever a submit can be in flight.
@Astro-Han
Astro-Han merged commit 78b9003 into main Jul 17, 2026
3 checks passed
@Astro-Han
Astro-Han deleted the refactor/1044-ui-panel-decomposition branch July 17, 2026 00:52
Astro-Han added a commit that referenced this pull request Jul 17, 2026
…llow-up) (#1157)

* test(ui): cover composer draft and history pure helpers

Pin rememberComposerDraft/readComposerDraft (key no-op, blank delete,
120k/32 caps), rememberComposerHistoryEntry (trim/dedup/50 cap), and
navigateComposerHistory (up/down + saved-draft round trip) so the
useComposerDraft/useComposerHistory seams from #1153 stay pure-unit
covered.

* refactor(ui): move plan-reminder form seeds into helpers

PlanReminderFormSeed and the create/template/edit/duplicate seed
builders are pure mappings — relocate them next to the other form
helpers so PlanReminderFormDialog stays the React owner of field state
and submit, matching the #1044 ownership split.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor(ui): decompose composer and plan-reminder panels into focused hooks

1 participant