Skip to content

feat(jira): show and edit story points on the issue detail - #122

Merged
nplez1 merged 18 commits into
nplez1/mainfrom
nplez1/tasks-story-points
Oct 16, 2026
Merged

nplez1 merged 18 commits into
nplez1/mainfrom
nplez1/tasks-story-points

Conversation

@nplez1

@nplez1 nplez1 commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner

ELI5

Story points live in a Jira field that Orca could read but almost nowhere showed. The task list had an optional "Points" column you had to switch on, and no issue view displayed the value at all — the only way to change it was to know it was hiding inside the "Edit fields" dialog. Now the value sits in the issue's own detail, next to status and priority, and you can set or clear it right there.

What Changed

The issue detail now shows story points and edits them in place.

Before: opening a Jira issue (task detail, or the linked-issue pane in the worktree sidebar) showed status, priority, assignee, sprint and labels. Story points were nowhere, and changing them meant knowing they were one of the fields in the "Edit fields" form.

After: the detail's metadata bar carries the points beside the other properties — Points: 8, or "No points" when the issue has none — and clicking it opens a small form that sets or clears the value; Enter saves, Esc backs out. The right-sidebar pane gets the same value as one of its property rows. Where Jira says the field is not writable (or reports no story-points field for that issue at all), the value is still shown but there is nothing to click.

The board's cards show them too. The card in the sprint/backlog board previously showed the key, title, status, assignee, attached workspace and priority — the plan's own number was missing. It now sits in the card's top-right corner beside the status pill as Jira's 3sp shorthand. Nothing is editable from the card (the detail's control is the only writer); a card Jira holds no points for carries no chip, and zero reads as 0sp rather than as an absence. This needed the board's own issue read to ask Jira for the site's points field — it did not, so the chip would have been dead on arrival; that is fixed here and pinned by a host test.

The list shows the Points column by default. The list already rendered story points, but upstream ships every planning column hidden (stablyai#28615), so the number a scrum board is planned by sat behind the Columns picker. Points now joins the default columns: a fresh list — and one whose column choice was never saved — shows it, while anyone who has saved a column choice keeps exactly that choice. The other planning columns (Parent, Sprint, Estimate, Remaining) stay opt-in. This is a deliberate divergence from upstream's default, recorded in jira-list-columns.ts, pinned by the unit test's expectation, and reflected in the e2e spec's own toggling; if upstream later changes its planning-column defaults, this one line is the conflict.

Mechanism, in short. The field id is not hardcoded: it comes from Jira's own editmeta read — the same read the "Edit fields" form is built from, which already lists the story-points custom field — and the write goes through the already-shipped customFields update path. The issue also carries the id of the field its value was read from, so a site with two points fields cannot have one shown and another written. No new RPC parameter or stream message exists, so an older host or a paired client is unaffected rather than silently no-oping. The control carries the value optimistically and puts it back if Jira refuses, keeps Jira's reason beside the field while the form is open (a toast for a refusal that lands after it has gone, so a failure is never reported nowhere), and the shared mutation path re-reads editmeta after a custom-field write so the "Edit fields" form cannot end up showing a value Jira no longer has.

Why

Story points are the one number on a Jira issue that a team plans by, and Orca had it in hand without displaying it: the host already discovers the site's points field for the list column and the agile reads. The narrow gap was the detail view, so that is where the control went, reusing the field-editing plumbing from #98/#112 rather than adding a parallel write path.

Design decisions worth knowing about:

  • Points is on by default in the list, where upstream hides it, and the cost is measured rather than described. The column costs 72px of the row (60px plus its gap), so:

    • the row's minimum is now 830px at the md tier and 1032px at the lg tier, which means a panel just above the 768px and 1024px breakpoints scrolls sideways — up to 63px at 780, 8px at 1037, and nothing from about 843 and 1050;
    • the issue title sits on the grid's own 180px floor at those steps (measured 180 at 860, 185 at 1050) instead of upstream's 200px margin, while wide panels are unaffected (337 at 500, 367 at 1320).

    Measured in the real app, headless, at eleven panel widths; the e2e spec now asserts both the title floor and that the row fits where the default set should. For reference, the original 200px expectation already failed at 780 on this machine with upstream's columns (189px, because a stable scrollbar gutter takes 11px), so that step now measures 860 instead. If the wider minimum matters more than seeing points, the flag is one word in jira-list-columns.ts.

  • The write reuses customFields, with the field id from editmeta, rather than adding a storyPoints field to JiraIssueUpdate. A new update field would need its own capability read: the RPC schema strips fields it does not declare, so an older host would report success while writing nothing — the exact silent no-op feat(task-page): reliable Jira lane drops and custom-field editing #112 had to fix for sprint assignment. customFields is already declared and already keyed to customfield_*, and the editmeta read itself is the capability check (a host predating it returns no editable fields, so no editor is offered).

  • One candidate field beats a hardcoded id. Jira's field catalogue can hold more than one story-points field (a classic float beside a next-gen estimate), so the issue now carries the id of the field its value was read from (storyPointsFieldId, an added optional read field) and the editor writes that one. When Jira does not offer that field as writable on the issue, no editor is offered at all — writing a different points field would leave the value on screen unchanged. A host that predates the field omits it, and the client falls back to matching the value, which is the best a client can do alone.

  • Both detail surfaces, not one. feat(task-page): reliable Jira lane drops and custom-field editing #112 put the field-edit form in the task detail and the sidebar pane; story points follows the same pair, from one shared editor.

  • The card uses Jira's 3sp shorthand, and cannot edit. A board card is a compact, draggable surface with a fixed corner; the number goes there and editing stays in the detail, so a drag can never turn into a field write. The abbreviation is a translated string with the value interpolated, so a locale can spell it out.

  • Clearing is explicit. An empty box submits null rather than omitting the key, because the update path only writes what it is given and a Jira null is what clears a field.

  • The chip says Points: 8. The pane row has a label beside it, the detail bar's chip does not, so the chip names the field instead of showing a bare number with only a tooltip to explain it. A label rather than a pts suffix also keeps one point from reading as "1 pts" in English and needs no plural rules in the other locales.

Linked Issue

Fixes #118

Visual Proof

N/A — no Jira site is configured in this environment, so there is no connected board or issue to capture a before/after against. The control, its failure paths and both detail surfaces are covered by component and integration tests instead (list below). A reviewer with a connected site can see it in ten seconds: open any issue in a scrum project, click the points chip in the metadata bar, type a value, press Enter.

Testing

Run from the worktree, on the final revision.

  • pnpm tc — exit 0. Also run cold, with a fresh --tsBuildInfoFile so no incremental cache could hide a result: tc:web, tc:node, tc:cli each exit 0.
  • pnpm test jira — 185 files, 1616 tests passed (after the base merge). Every Jira test in the repo, which is the whole surface this change touches.
  • pnpm test src/renderer/src/components/task-page src/renderer/src/store src/renderer/src/i18n src/renderer/src/runtime src/renderer/src/components/right-sidebar — 1102 files, 9445 tests passed (1 pre-existing skip): the task views, the store the optimistic patch writes to, the catalogs, the runtime clients, and the pane.
  • pnpm run check:code-quality:changed — passed, 0 findings across 26 changed files, measured against the merged base (including the design-system, React Doctor and type-aware passes).
  • Localization: every new translate key resolves in en.json and its fallback matches (checked with the catalog verifier's own extractor); verify:localization-extraction and audit-localization-coverage --check both pass. The repo-wide verify-localization-catalog reports one pre-existing dotted key, settings.appearance.chat.title, which is present at nplez1/main and unrelated to this change.
  • pnpm lint runs green through repo-wide oxlint, the anti-slop and code-quality audits, the reliability and max-lines ratchets, and then stops at check:ts-nocheck-ratchet on src/main/runtime/workspaces/workspace-process-listing.ts — a file this diff does not touch and which carries the same suppression at nplez1/main. That is a pre-existing red, not a failure of this branch; the gates that cover this change (check:code-quality:changed, its type-aware and design-system passes) are green.
  • New/updated tests:
    • src/renderer/src/components/jira-issue-story-points-control.test.tsx — writes a value, clears one, refuses a draft that is not a number instead of reading it as a clear, blocks a save while another field is being written (and keeps a refused save's focus), shows Jira's refusal beside the field, describes the input with its own message (and with none when there is none), writes the field the issue names, offers no editor when Jira will not write that field, matches the value for a host that does not name it, shows points Jira will not let you write, renders nothing when there is neither a value nor an editor.
    • src/renderer/src/components/JiraIssueWorkspace.story-points.test.tsx — through the real workspace: the write's payload, exactly one editmeta read on open and two after a write, none after a refused write, the rollback, and that a refusal is reported beside the field instead of in a toast.
    • src/renderer/src/components/right-sidebar/jira-story-points-row.test.tsx and a case in .../right-sidebar/JiraLinkedIssuePane.test.tsx — the pane's row and the pane mounting it: value, editor, read-only when Jira will not write it, disabled while another field is pending.
    • src/renderer/src/components/jira-issue-workspace-hydration.test.ts — the optimistic patch is tracked, rolled back, and protected from a stale fetch.
    • src/renderer/src/components/task-page-jira-issue-cells.test.tsx — pins the list's Points and Sprint cells, and src/renderer/src/components/jira-list-columns.test.ts pins the new default set (story points in, the other planning fields out). The e2e spec that exercises the planning columns was updated for the same default.
    • src/renderer/src/components/task-page/jira/Board.test.tsx and src/main/jira/jira-board-queries.test.ts — the card shows 3sp, 0sp for zero and no chip when unset, and the board's issue read asks for the site's discovered points field and maps it onto every issue it returns.
    • src/shared/jira-field-rendering.test.ts and src/main/jira/jira-agile-fields.test.ts — the shared "is this the story-points field" rule, that the mapped issue names the field its points came from, and that an issue whose points are unset still carries both points keys (a refresh is spread onto the issue on screen, so an omitted key would leave a value Jira no longer has).
  • Base sync: nplez1/main was merged in (59ca23f716) — five commits, one of which (feat(task-page): match the Jira board's Me filter on any role field #121, the board's Me filter matching any role field) had rewritten the same board issue read and the same agile-field discovery. The board read now takes one catalog read and uses both halves of it: the role-field ids within the request budget, and the agile ids so a card can show story points. pnpm test jira passes on the merge, cold tc:node/tc:web are clean, and the two Jira e2e specs pass.
  • Rendered UI, headless (ORCA_BACKGROUND_LAUNCH=1 npx playwright test … --project=electron-headless, hidden renderers, no focus stolen): tests/e2e/jira-panel-layout.spec.ts and tests/e2e/jira-tasks-settings-link.spec.ts — 3 passed. The panel-width spec was re-measured in the real app rather than reasoned about, and now asserts the title's floor where the default column set fills the row plus that the row does not overflow at the widths where it should fit (500, 860 and 1320; 1050 sits exactly on the lg minimum, so its fit is left to the floor).
  • Not run: the full pnpm test suite (it has a known runtime trap on this machine, a separate hunt is on it), the full pnpm lint chain past the pre-existing ratchet failure above, and pnpm build (left to CI). typecheck:e2e cannot pass on this machine for reasons outside this diff (the cloud/ workspace is not installed, and 500-odd pre-existing spec errors); the two Jira specs this change touches typecheck with no errors. No Jira site, Windows, Linux or SSH runtime was exercised locally.

AI Disclosure

Implemented by an AI coding agent (pi) using DeepSeek and MiMo models for research and edits, with the review rounds below.

Review

Claude Opus 5.5 (claude/opus, the Claude Code CLI backend) at high reasoning, tool-enabled, adversarial, over the full diff against nplez1/main.

  • Round 1 (54e07b5212) found five issues and returned NOT SHIP-READY: a number input reported an unparseable draft (5e, 1.) as empty and saving cleared the field; a popover save left the "Edit fields" form holding a stale value; the control could show one story-points field and write another; a save during another field's write was refused with an error this form had nowhere to show; and the chip showed a bare number with only a tooltip for a label. All five were fixed, with tests for each.
  • Round 2 (abec452b1c) returned SHIP-READY with three minor findings worth closing anyway: the read/write field pairing still fell back to a different field when nothing held the displayed value, 1 pts had no singular, and the test that pins the editmeta re-read only asserted "more than one call". All three were closed, along with its accessibility notes (the chip's accessible name now contains its visible text, the input is described by its own error, and the input is read-only rather than disabled so a refused save does not drop focus).
  • Round 3 (814f6a6ec6) returned SHIP-READY but flagged one real regression it wanted fixed before the PR: the new mapStoryPoints left the points keys off an issue with no points, where the inline mapping it replaced always set them, so a refresh spread onto the issue on screen could keep showing points Jira no longer had. Fixed in 671fae1095, along with its other notes (Jira's refusal now shows beside the field instead of only in a toast; the chip stays focusable during its own save; three unused exports dropped).
  • Round 4 (671fae1095) returned NOT SHIP-READY: the previous fix had moved Jira's reason inline and turned the toast off, so a save whose popover was dismissed before Jira answered reported the failure on a form nobody could see and then lost it. Fixed in c16512ad66: the reason goes inline while the form is up and to a toast once it is gone, it is dropped as soon as the draft changes, and the Issue pane's row no longer disables itself during its own save.
  • Round 5 (c16512ad66) returned SHIP-READY and asked for one further line: the ref that decides whether a refusal can be shown inline was not cleared when the editor unmounted — the pane's own remount or a worktree switch — so a refusal could still land on a form that no longer existed. Fixed in c1f5e65995.
  • Round 6 (c1f5e65995) returned SHIP-READY with no blocking or major findings and nothing it required to be fixed. Its one note was optional (a test for the unmount path above, which is why that fix has no test of its own); it was left out deliberately so that the reviewed revision is the shipped revision, and it is the first thing to add if that cleanup is ever touched.
  • Round 7 (f9873719bb, the board card) returned NOT SHIP-READY with a BLOCKER: the board's issue read never asked Jira for the points field, so the new chip could never render. The read now resolves the site's field catalogue (the same cached read the list and detail use) and maps the discovered ids, with a host test that fails without the fix (f32014b922), and the chip's shorthand is announced as "3 story points" rather than "3 s p" (e5717ccb0b).
  • Round 9 (2031fe8735, the Points column by default) returned NOT SHIP-READY with a MAJOR: the default column set spends 72px of fixed tracks at the narrow panel steps, so the issue title lands on the grid's 180px floor and the upstream e2e guard that asserted a 200px margin would fail. Fixed in 507c301c55 by measuring the four widths in the real app and asserting the floor where it now applies; the reviewer's predecessor revision could not be verified by running e2e locally at the time.
  • Round 10 (507c301c55) returned SHIP-READY with two MINORs worth fixing before merge: the reworked assertion could not see the real consequence of the wider default row (panels just above the md and lg breakpoints now scroll sideways), and its comment overstated one number and blamed the points column alone for a failure that the stable scrollbar gutter already caused at 780. Both were fixed in 0cf59ae4bd by measuring the app directly and asserting the fit.
  • Round 11 (0cf59ae4bd) returned SHIP-READY with two MINORs, both fixed in 47230e2aa2: several measured numbers in the new step's comment were wrong (the md band that scrolls is 770–844 at up to 73px, the lg band is 1026–1044, and 1050 has 5px to spare rather than none), and the lg tier's fit was not asserted anywhere. The lg step now measures 1060 so every step asserts both the title floor and that the row fits.
  • Round 12 (59ca23f716, the base merge) returned SHIP-READY. It verified the reconciliation point by point: one catalog read per board page taken before the request-queue slot, the request budget applied to the role fields alone, both halves handed to mapJiraIssue in the shape it expects, nothing lost from either side (the once-per-site warning survives), and the deleted test mock having weakened nothing. Its two notes were optional and are recorded here rather than folded in, so the reviewed revision is the shipped one: the branch's board test now covers the backlog scope where it used to cover the sprint scope (the fields list is built before that split, so nothing differs), and mergeJiraIssueHydration takes storyPointsFieldId from the fetched issue rather than the locally edited one, which on a multi-points-field site can only make the editor fall back to matching by value — the field it would still match.
  • Round 8 (e5717ccb0b) returned SHIP-READY with nothing it required to be fixed. It confirmed every board read path (IPC, runtime RPC, web preload, both scopes, Cloud and Server/DC, paging) reaches the fixed read, that the optimistic board move keeps the value, that the field catalogue is shared and cached per site so no board page pays a new request, and that a discovery failure still lets the board load. Its optional notes — a test for the call ordering, an exact rather than substring fields assertion, and an sr-only span instead of aria-label on the chip — are recorded here rather than folded in, so that the reviewed revision is the shipped revision.

Agent skill upstream boundary

  • Not applicable, or this change copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

  • Security: the write is re-validated at the IPC/RPC boundary, where custom-field keys must be customfield_*; Jira still enforces the user's own permissions. The editmeta read is read-only. Nothing is written that Jira did not report as writable for that issue.
  • Remote SSH: no new wire parameter. The editor appears only when the host answered jira.listEditFields, which a host predating feat(task-page): reliable Jira lane drops and custom-field editing #112 reports as empty — the value is then shown read-only rather than offering a write that would silently no-op. The added storyPointsFieldId is an optional read field: an older host omits it and the client falls back to matching the value against the editable fields. One known limit, shared with sprint, parent and assignee: a remote JSON transport drops undefined issue fields, so an issue whose points were cleared on Jira can keep showing the old value until the next full read, locally only.
  • Backwards compatibility: customFields is an existing optional field; storyPointsFieldId is a new optional read field that an older host simply omits. No new stream opcode or RPC parameter.
  • Board reads: the board's issue page now resolves the site's agile field catalogue. That read is shared and cached per site (10 minutes after a success, a 60-second backoff after a failure) by the list, search and detail reads already, so a board page does not pay for a new request; a catalogue read that fails leaves the board loading normally, just without the chip.
  • Performance: no new requests on the issue-open path — the field id comes from the editmeta read that already runs with the issue's side data — plus one editmeta re-read after a custom-field write, which is what keeps the two forms agreeing. One extra request there was chosen over two surfaces disagreeing about the value.
  • Known non-fix, stated rather than hidden: the busy spinner on the chip appears as soon as the write starts rather than after the ~200 ms the style guide suggests for in-flight feedback. The status, priority, assignee and sprint controls in the same bar all do the same, and the guide's own resolution order says to follow the adjacent code; adding the delay only here would make this chip behave differently from its neighbours. Worth doing for the whole bar in a separate change.- Cross-platform: no platform-specific code, paths or shortcuts; the input is a text box with inputMode="decimal", so touch keyboards still get a numeric keypad. The control uses the same primitives and chrome as the neighbouring status, priority, assignee and sprint controls.
  • Mobile: not applicable — desktop renderer only, no mobile surface touched.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm build left to CI; pnpm tc passes locally, with the focused suites and changed-lines gate recorded above

nplez1 added 18 commits October 15, 2026 21:24
The host's field catalogue and the issue detail's edit form both have to answer
"is this field the story-points field", and only the host answered it. Moving the
rule into shared/jira-field-rendering.ts lets the renderer reuse it when it reads
Jira's own editmeta, so a site's field id is found by one rule rather than by a
second, drifting copy. Behaviour is unchanged: Greenhopper's own field type, or
the float custom field a site names "Story points".
Story points were readable — the host resolves the site's points field from the
field catalogue, and the list view offers a Points column — but no issue view
displayed them, so the value only existed behind a column most people never
enable.

The detail's metadata bar now carries the points beside status, priority,
assignee and sprint: the current value, and a popover that sets or clears it.
The right-sidebar Issue pane gets the same value as one of its property rows.
Both share one editor, and both write through the custom-field path the "Edit
fields" form already uses, with the field's own id taken from Jira's editmeta,
so nothing is keyed to one site and Jira still decides what is writable: an
issue whose points are not writable shows them but offers no editor.

Story points join the fields the optimistic-patch bookkeeping protects, so the
value does not snap back to the old one while the write is in flight and returns
to it if Jira refuses.
The list view's planning columns are the list half of "show story points": the
column exists and the host resolves the value, but nothing asserted the cells
render it, so the claim rested on code reading alone. These cover the value, the
unset dash, and the sprint cell beside it.
Five findings from the adversarial review of the control, all of them about a
value that is easy to get subtly wrong:

- The points box was an `<input type="number">`, which reports an empty value for
  text it cannot parse, so a draft of "5e" or "1." read as an empty one and
  saving cleared the field. It is now a text box parsed here: a draft that is not
  a number disables Save and says so, and only an actually empty box clears.
- A save through the popover left the "Edit fields" form holding the value Jira
  no longer had, because that form seeds its drafts from the same editmeta read.
  A custom-field write through the shared mutation path now re-reads that read,
  so the two forms agree whichever one wrote.
- A site can expose more than one story-points field, and the write went to the
  first one editmeta listed while the view showed another. The editor now prefers
  the field holding the value the view displays.
- Save was disabled only for this field's own write, so a save attempted while
  another field was in flight was refused by the mutation guard with an error
  this form has nowhere to show; the whole form is now disabled for any pending
  write, as the row's own popover already was.
- The detail chip named its value (`8 pts`) rather than showing a bare number with
  only a tooltip to say what it was; the labelled Issue pane row keeps the number.

Tests cover the unparseable draft, the blocked save, the two-field choice, that a
save re-reads editmeta, that a refusal rolls the chip back and reaches a toast,
and that the list cells render points.
…ot name

The inferred type of a bare vi.fn() pair needs a vitest-internal Procedure type the
renderer project cannot reference (TS2883), so the spies are typed by their call
signature.
…und-2 notes

The second review round passed the change but left three minor findings and a
handful of accessibility notes, all of which are cheaper to fix than to argue
about:

- The read/write pairing still had a hole. The control preferred the editable
  field holding the value on screen, but fell back to a different points field
  when none held it, so on a site with two points fields a save could land where
  the view never reads and appear to do nothing. The issue read now carries the
  id of the field its points came from (`storyPointsFieldId`), so the editor
  writes that field and offers no editor at all when Jira will not let this user
  write it. A host that predates the field omits it, and the client falls back to
  matching the value.
- The detail chip read "1 pts" on a one-point issue. It names the field instead
  ("Points: 1" / "No points"), which needs no plural rules in any language.
- The test that pins the post-write `editmeta` re-read only asserted "more than
  one call"; it now pins one read on open and exactly two after a write, and that
  a refused write reads nothing.
- Accessibility: the chip's accessible name is now its visible text (it was
  "Story points: 8" beside a visible "8 pts"), the input is described by its own
  error message, and it is read-only rather than disabled while a write is in
  flight so a refusal does not drop the caret.
- A value Jira will not let this user write is now a disabled property row in the
  Issue pane, matching how the pane's own "Fields" row reads when nothing is
  editable.
…cribed-by

The pane mounts the row itself, so the wiring deserved an assertion of its own: a
value Jira will let this user write now shows up in the pane.

The input also points at its message only when there is one, rather than at an
empty element on an issue with no points.
…leared keys

Round 3's findings, one of which is a regression this branch introduced:

- `mapStoryPoints` returned an object without the points keys when an issue had
  none, where the inline mapping it replaced always set them. A refresh is spread
  onto the issue on screen, so an omitted key left points that Jira no longer had
  visible until the next full read. Both keys are now always present. The old
  tests could not catch this: `toEqual` treats a missing key and an undefined one
  as the same, so the mapping has a case that asserts the keys exist.
- Jira's reason for refusing a write appeared only in a toast, which disappears
  while the user is still looking at the value that needs correcting. The form now
  shows it beside the field (and as its `aria-describedby` description), so the
  write is submitted with `reportsInline` and no toast competes with it.
- The trigger was disabled during its own field's write, so pressing Esc while a
  save was in flight left focus with nowhere to go; it is only disabled while
  another field is writing now.
- Three helpers that nothing imports are no longer exported, and the tests cover
  the `aria-describedby` wiring and the inline refusal.
Round 4 found that the previous commit's inline reporting introduced a worse
failure than the one it fixed: with `reportsInline` suppressing the toast, a save
whose popover was dismissed before Jira answered — Esc, a click outside, or the
issue being switched — showed its reason on a form nobody could see, then lost it
on reopen. On the revision before, a toast still reported it.

The editor now tracks whether its form is up: the reason goes inline while it is,
and to the toast when the form has gone. It is also dropped as soon as the draft
changes, so a refusal can no longer mark a value that was never sent.

Two more from that round: the Issue pane's row was still disabled during its own
save, so Escape there had nowhere to put focus (the chip had been fixed), and the
chip's enabled-while-writing behaviour had no test. Both are covered now.
Round 5's remaining case: the popover's open state was only tracked through its
own dismissals, so a component that unmounted mid-write — the Issue pane's
remount, a worktree switch — left the form marked up, and the refusal was written
to a form that no longer existed. Clearing that on unmount sends the reason to a
toast instead, which is the one place still on screen.
The board's cards showed the key, title, status, assignee, attached workspace and
priority, but not the number a scrum board is planned by — the one field the
board is named for. It now sits in the card's top corner beside the status pill as
Jira's own `3sp` shorthand, from a translation with the value interpolated so the
abbreviation travels with the locale.

A card Jira holds no points for carries no chip rather than a dash; zero is a
value and reads as `0sp`. Nothing is editable from the card — the detail's control
is still the only writer.
The card chip was dead on arrival: `listBoardIssues` requested only the standard
issue fields and mapped the page without the site's agile field ids, so every
board issue arrived with `storyPoints` undefined and nothing could ever render.
The read now resolves the field catalogue (shared and cached per site, answering
empty when it cannot be read) and maps the discovered ids, exactly as the list and
detail reads already do.

Found while checking the card's own test against a real board payload rather than
a fixture that set the value directly; the new host test fails without this.
The board-card chip's name is built from the card's text, so a screen reader read
the shorthand as "3 s p"; it now carries its own label while still showing `3sp`.
The "no chip" case queries the chip itself instead of looking for the letters "sp"
anywhere in a card's text.
Story points are what a scrum board is planned by, and this branch now shows them
in the issue detail and on the board's cards — but the list still made every reader
find the Columns picker first, because upstream ships all planning columns hidden.
Points joins the default set; the other planning columns stay opt-in, and anyone
with a saved column choice keeps it, so only a fresh list changes shape.

This is a deliberate divergence from upstream's default in
`jira-list-columns.ts`, recorded in the unit test's name and comment and in the
e2e spec's own toggling so the two do not disagree about what is already visible.
With story points in the default column set, the md and lg panel steps spend 72px
of fixed tracks on it, so the title lands on the grid's own minmax(180px, 1fr)
floor there. The width test asserted the 200px margin it was calibrated on, which
that no longer meets, so it now asserts the floor at those two steps and keeps the
200px margin where there is room.

Measured in the real app, headless, at the four panel widths the test drives:
337px at 500, 180 at 780, 185 at 1050, 367 at 1320. The guard's other halves (the
header's fixed tracks matching the row's, the mobile sort controls, and the
assignee column's tier) are untouched.
The previous commit's threshold change was measured but its reasoning was partly
wrong, and it only asserted the title floor — which the grid's own minmax(180px,
1fr) already guarantees — so it could not see the real cost of the wider default
row: at panel widths just above the md and lg breakpoints the row no longer fits
and scrolls sideways (measured: 63px of overflow at 780, 8px at 1037, none from
843 and 1050).

The md step now measures 860px, still in the md tier and wide enough for the
default set, and the test asserts the row does not overflow at the two ends and
at that step. The comment records the measured numbers, the two bands that do
scroll, and why the original 200px expectation was already wrong here at 780:
with upstream's columns on a machine with a stable scrollbar gutter the title is
189px, not 200.
Round 11 measured every panel width and found the previous comment's numbers wrong:
the md band that scrolls is 770-844 (73px at its worst, not 63), the lg band is
1026-1044, and 1045 upwards fits — 1050 has 5px to spare rather than none, which
was the stated reason for skipping its fit check. The title is 197px at 860, not
on the 180px floor.

The lg step now measures 1060 (still the lg tier, 13px of slack) so the row's fit
is asserted in every tier rather than left to the title floor in one, and the
comment carries the measured bands and slack.
Five commits from the fork's line, one of which (#121, matching the board's Me
filter on any role field) rewrote the same board issue read and agile-field
discovery this branch touches. The reconciliation, by file:

- `jira-agile-fields.ts`: one cached `/field` read per site already answered the
  agile planning fields and now also the user-picker role fields, each reader with
  its own patience. Kept that structure and the shared `isJiraStoryPointsField`
  rule for the planning half; dropped the two field-type constants the shared rule
  replaced and the module-level caches the per-patience readers superseded.
- `jira-board-queries.ts`: the board performs one catalog read and uses both
  halves of it — role ids within the request budget for "Me", the agile ids for
  the story points a card shows — and maps each issue with both.
- `jira-issue-mapping.ts`: `mapJiraIssue` keeps #121's `JiraIssueFieldIds` and its
  `roleAssignees`, with this branch's `mapStoryPoints` helper as its points half.
- `jira-board-queries.test.ts`: #121's tests kept as they were; this branch's
  points case rewritten in the same style, on the real catalog read, so it also
  pins that one read answers both halves.

Verified on the merge: `pnpm test jira` 185 files / 1616 tests, cold `tc:node` and
`tc:web` clean, and the two Jira e2e specs pass headless.
@nplez1
nplez1 merged commit 55e6f53 into nplez1/main Oct 16, 2026
@nplez1
nplez1 deleted the nplez1/tasks-story-points branch October 24, 2026 03:23
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.

Tasks: show and edit story points

1 participant