| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
| |
A ticket glyph now sits on every issue heading and a speech balloon on every comment heading. The glyph rides a display overlay on the space after the leading stars, so it lands ahead of the TODO keyword where a literal text prefix can't go (Org wants the keyword first). The choice is keyed on the heading's own drawer, LINEAR-ID for issues and LINEAR-COMMENT-ID for comments, so it's per-heading-type rather than per outline level.
Display-only: the buffer text is untouched, so parse, sync, and merge are unaffected. It reuses the comment-highlight overlay pattern and rides the same redecoration trigger. It's applied synchronously because the glyph is drawer-driven and shouldn't wait on the viewer lookup. Behind pearl-show-glyphs (default on). Overlays go stale after a manual heading edit until the next refresh.
|
| |
|
|
|
|
|
|
|
|
| |
Mentioning a teammate meant recalling their exact Linear handle, dotted last names and all. Now a picker inserts it for you.
In a comment or description compose buffer, typing @ at the start of a word pops a completing-read over the issue team's members, showing "Full Name (@handle)" so you can find someone by name, and inserts @displayName, the bare handle Linear resolves to a mention (no id or link wrapper). C-; L @ (pearl-mention-user) runs the same picker explicitly and also works inline in an issue buffer, resolving the team from the heading at point. An @ mid-word (an email) stays literal, and cancelling the picker leaves a literal @, so neither case is hijacked.
The compose buffer now carries the issue's team id so the picker knows whom to offer. The @ trigger is bound only when a team is in context. @displayName survives the md/org round-trip unchanged, so it reaches Linear intact.
Not yet confirmed: that writing a body with @displayName through the API fires Linear's mention notification (the web app does, and stored bodies carry the bare handle, so it almost certainly round-trips). Worth one self-mention write-test before relying on it.
|
| |
|
|
|
|
|
|
| |
Pearl maps Linear's five priorities to org cookies (A=Urgent through D=Low, no cookie = None) and tells users to set priority with org's native cookie cycling (C-c , / S-up). But that cycling obeys the user's global org-priority-lowest, which defaults to C, so on a stock Emacs you can't even cycle down to [#D]/Low, and the priority you most want to set is unreachable.
I emit a #+PRIORITIES: A D D line into every rendered file (highest A, lowest D, default D). Org reads it per-file, so the range is bounded to Linear's scale and Low is reachable regardless of the user's global setting. Verified org picks it up: org-lowest-priority becomes ?D in the rendered buffer.
This is the last piece of constraining cookies to Linear's range. The out-of-range guard (a cookie past D errors instead of silently clearing the priority) and the A-to-name legend in the help header already shipped.
|
| |
|
|
|
|
| |
In a grouped view, each issue's Comments subtree rendered at the issue's own level instead of one deeper, so the Comments heading sat as a sibling of its ticket rather than a child.
The grouping feature gave pearl--format-issue-as-org-entry a level parameter (issues render at level 3 under a group heading) but left the comment subtree hardcoded: pearl--format-comments emitted "*** Comments" and pearl--format-comment "**** ". That nests right when the issue is at level 2 (flat) and breaks when it's at level 3 (grouped). I thread the issue's level through, so the Comments heading is level+1 and each comment body level+2: a flat issue stays 2/3/4, a grouped one becomes 3/4/5.
|
| |
|
|
|
|
| |
The assignee @-tag slugs Linear's full @-mention handle, so a firstname.lastname handle still renders long (:@first_last:). For someone who wants a tighter tag line, I added pearl-assignee-tag-short (default off): when on, the tag keeps only the first segment of the handle (before the first dot or space), so :@first_last: becomes :@first: and a single-token handle like alice is unchanged.
It's opt-in because first names collide across teammates, so the default stays the unambiguous full handle.
|
| |
|
|
|
|
| |
The assignee @-tag slugged the user's name field, which for a teammate who never set a display name in Linear is their full email. That produced tags like :@first_last_example_com: that wrap the heading's tag line across several lines.
Linear's displayName is the short @-mention handle (e.g. "alice", "first.last"), consistent across users where name isn't. I normalize it onto the user as :display-name and slug the tag from it, falling back to name when it's absent. So an email-named teammate becomes :@first_last: and a handle-named one :@alice: instead of :@alice_smith:. The :LINEAR-ASSIGNEE-NAME drawer keeps the fuller name. Every issue query already selects displayName on the assignee, so no fetch change.
|
| |
|
|
|
|
|
|
| |
On a team or "by person" view you couldn't tell who owned an issue at a glance: the assignee was captured but folded inside the :LINEAR-ASSIGNEE-NAME: drawer, with nothing in the heading.
I render the assignee as a leading @-tag (:@eric:bug:backend:), behind a pearl-show-assignee defcustom (default on). I picked a tag over a heading suffix because Org strips tags before it hashes the title for sync, so the tag can't corrupt title-sync the way a suffix would. The @ prefix keeps it in its own namespace, separate from label tags: labels are tracked in their drawer, not read back from the heading, so the tag never gets mistaken for a label. The label-edit path preserves any @-tag when it rewrites the heading's tags, and edit-assignee refreshes the @-tag so a reassign doesn't leave a stale name until the next fetch.
Verified live against a by-person view: assigned issues carry their @-tag, unassigned ones stay bare.
|
| |
|
|
|
|
|
|
|
|
| |
A Linear view can group its issues (by status, project, assignee, priority, cycle), and until now pearl ignored that: a "by status" or "by project" view came back as one flat list and didn't look like itself. The sort half of matching the view UI shipped earlier. This is the grouping half.
I read issueGrouping off the view preferences (next to the ordering pearl already fetches) and stamp it on the source as :group. When it's a single-valued dimension, the builder partitions issues into groups in first-appearance order and renders each as a level-2 section heading with its issues at level 3. An issue with no value for the dimension lands in a "No project" / "No assignee" bucket, mirroring Linear. The fold depth tracks where issues render (show3levels grouped, show2levels flat), so the buffer always opens unfolded to the issue level. issueGrouping "none", label-grouping (an issue carries several labels, deferred), and sub-grouping render the flat list.
The group headings carry no LINEAR-ID, so save and merge skip them like the help header. Two merge points needed grouping-awareness: an in-place update now re-renders at the issue's existing heading level rather than flattening to level 2, and a new issue on refresh is placed under its group section (creating the section if it doesn't exist yet) instead of appended at the end. Both entry points share the resolver, so run-linear-view and a favorited view picked in pearl-pick-source group the same way.
Verified live against the workspace: a by-project view renders one section per project plus a "No project" bucket, issues at level 3, opened to show3levels.
|
| |
|
|
|
|
|
|
|
|
| |
A Linear view that hides completed issues still showed done and cancelled issues when run in pearl. The open-only restriction isn't in the view's filterData. It's a separate display preference, viewPreferencesValues.showCompletedIssues, which pearl fetched for sort but never applied. So the customView.issues connection returned every state type, and a "by status" or "open issues" view didn't look like itself.
I now read showCompletedIssues alongside the sort fields and translate it to an IssueFilter passed to the connection, AND-combined with the view's own filter. "all" adds nothing, "none" excludes the closed state types (completed, canceled, duplicate), and a recency window (day/week/month/quarter/year) keeps open issues plus those completed or canceled within the window. I stamp the raw preference on the source so a refresh rebuilds the window relative to the refresh time rather than first-run time.
Both entry points route through a shared pearl--view-node-prefs resolver, so pearl-run-linear-view and a favorited view picked in pearl-pick-source behave the same. That also closes a latent gap where the pick-source path ignored the view's configured sort.
Verified live against the workspace: a "none" view returns only open issues, and a "week" view returns open issues plus those closed within seven days, with none older leaking through.
|
| |
|
|
|
|
| |
The header set #+STARTUP: show3levels, which opened a fresh view with each issue's Comments heading already unfolded. That's a level of detail past what you want when scanning a list. Drop to show2levels so a view opens at the parent plus the issue headings, with comments and bodies folded under each issue.
pearl--restore-page-visibility re-reads #+STARTUP from the buffer after a repopulation, so the in-place rebuild path follows the new depth without any other change.
|
| |
|
|
|
|
| |
test-pearl-build-org-content-no-shared-file-id guards against pearl leaking a hardcoded file-level :ID: drawer. It checked for that two ways: the leaked id literal, and a blanket "no :PROPERTIES: drawer anywhere." The foldable help header (3336442) then started emitting a legitimate heading-level :PROPERTIES: :VISIBILITY: folded drawer, which the blanket check caught, so the test failed on correct output.
Anchor the second assertion on :ID: instead. It still catches an id leak, alongside the unchanged literal check, while letting the help header's :VISIBILITY: drawer through. Test-only change; the rendered output was already correct.
|
| |
|
|
|
|
|
|
| |
A Linear view's issue query passed no orderBy, so issues came back in Linear's API default, createdAt descending. That's neither the order set on the view in Linear nor the updatedAt-desc the rest of pearl uses, and the two unrelated defaults colliding is what made a view's order look random.
Linear exposes a view's configured sort in viewPreferencesValues (viewOrdering plus viewOrderingDirection), and the issues connection accepts an IssueSortInput sort arg that applies it server-side. I read the view's ordering when running it, translate it to that sort arg, and pass it through, so a view arrives in the order you gave it in Linear. The sort is stamped on the recorded source so a refresh keeps the same order. An ordering pearl can't map, or a view with no preference, falls back to updatedAt-desc rather than the createdAt default, so it lines up with every other pearl fetch.
This is the sort half of matching the Linear UI. Grouping (issueGrouping into org sections) is the larger remaining half and is tracked separately.
|
| |
|
|
|
|
|
|
| |
Linear's CustomView type has no url field. Three GraphQL selections requested it (the custom-views list query and both the create and update mutation payloads), so each of those requests returned HTTP 400 and pearl got nil back. Running a Linear view came up with an empty picker, and publishing or updating a local view to Linear failed silently. The test suite stayed green because the view tests mocked a url field the real API never returns.
I dropped url from all three selections. The workspace urlKey is now fetched once and cached, and a view's web URL is rebuilt as https://linear.app/<urlKey>/view/<view-id>, the shape Linear's own favorite URLs use (the full view UUID, not the slug id). pearl-open-current-view-in-linear keeps working, and when the urlKey is unavailable it degrades to nil so the link builder can't throw. That also stops a urlKey hiccup right after a successful publish from aborting the record step and orphaning the new view.
I added regression locks asserting neither the list query nor the mutation payload selection requests url, since the field name is the schema contract that broke.
|
| |
|
|
|
|
|
|
| |
The ~9-line #+ keyword block atop every Linear file is now hidden behind a display overlay showing one line: "Pearl - <view> · <count> · <time>", styled large and goldenrod via the new pearl-preamble-summary face. It leads with "Pearl" so it doubles as the buffer's identity marker, the thing that tells you you're in a Pearl buffer and not just another org file. A pearl-hide-preamble defcustom (default t) turns it off for the raw keywords.
The overlay only governs display: the #+ text stays in the buffer, so #+LINEAR-SOURCE parsing, the merge rewrites of the RUN-AT / COUNT / TRUNCATED lines, and refresh all keep working untouched. The summary is composed from those same #+ values, so it stays accurate across merges. It re-applies idempotently on pearl-mode enable and after every render, refresh, and merge, so reopening a file hides it the same way a fresh fetch does.
With the summary now carrying identity, the folded help heading reverts from "* Pearl" back to the descriptive "* Pearl Help".
|
| |
|
|
|
|
|
|
| |
The preamble carried two raw "# ..." affordance comment lines on every Linear file. Those move into a folded "* Pearl" heading, gated by a new pearl-help-header defcustom (default t). It collapses on open via a :VISIBILITY: folded property and carries the edit-then-save reminder, a note that the section is never sent to Linear, the key cues (derived from pearl-keymap-prefix, falling back to M-x when no prefix is bound), the priority-cookie legend, and how to hide it. Set the defcustom to nil for a minimal preamble of just the machine keywords. The heading also doubles as the "you're in Pearl" marker at the top of the buffer.
The heading has no LINEAR-ID, so save and merge skip it for free: they walk only LINEAR-ID-bearing subtrees. A forward declaration of pearl-keymap-prefix keeps the byte-compiler happy, since its defcustom lives later in the file than the renderer.
This is the human-content half of the preamble work. The machine #+ keyword lines (title, STARTUP, TODO, LINEAR-SOURCE, the provenance fields) are still visible. Hiding those is the remaining part of the task.
|
| |
|
|
|
|
| |
pearl--priority-number-at-point matched only [#A-D], so a cookie outside that range (a hand-typed [#E], say) didn't match and fell through to 0 (None). On save that silently pushed None to Linear and cleared the issue's priority, with no error and no sign anything was wrong.
It now matches any [#A-Z] and, when the letter isn't A-D, signals a user-error naming the valid range (A=Urgent, B=High, C=Medium, D=Low, or no cookie for None), so a stray cookie surfaces before any push rather than quietly wiping the priority. Linear has exactly five priorities, so A-D plus no-cookie is the whole range.
|
| |
|
|
|
|
| |
The C-; L sub-prefixes were labelled +fetch / +views / +edit / +create / +delete / +open / +copy / +workspace, the which-key submenu idiom but terse. They now read fetch... / views... / edit... / create... and so on: drop the +, add an ellipsis so the label reads as "do this, then pick what." Leaf commands and the direct hot keys are unchanged.
These labels are the (LABEL . COMMAND) menu-item strings baked into pearl-prefix-map, so which-key, describe-prefix, and which-key-less lookups all pick them up with no which-key dependency. Pure label strings, no behavior change. The keymap-label test moved to the new wording.
|
| |
|
|
|
|
| |
The keymap's l hot key opens the default view, but the transient could only run literal my-open-issues, so the menu couldn't reach a configured default. Now the Fetch group's l mirrors the keymap and runs pearl-open-default-view. Literal my-open-issues moves to m so it stays reachable from the menu.
Suite holds at 809, compile and lint clean.
|
| |
|
|
|
|
|
|
|
|
| |
Phase 3, the last of the default-view spec: surface the phase 1-2 commands and flip the hot key over.
Top-level l now runs pearl-open-default-view instead of pearl-list-issues, so it opens your default. The literal my-open-issues fetch stays reachable at f o, and l is unchanged until you set a default. v D binds pearl-set-default-view (the slot the keybinding reconciliation reserved), and the transient Views group gets a . suffix for it. D there stays save-locally.
README documents the default view: what l does, how to set and clear it, the Customize persistence, and the per-account scoping under multiple accounts.
Keymap and menu tests updated for the l rebind, the v D binding, and the transient suffix. Suite holds at 809, compile and lint clean.
|
| |
|
|
|
|
|
|
|
|
| |
Phase 2 of the default-view spec: the command that sets the default that phase 1 reads. It's the v D slot the keybinding work reserved (the binding itself lands in phase 3).
From a buffer that's showing a local view, pearl-set-default-view offers that view as the default. Otherwise it prompts over the local-view names with a my-open-issues sentinel on top that clears the default. Quitting leaves it unchanged.
pearl--persist-default-view writes through customize-save-variable so the choice survives a restart. In accounts mode it updates the active account's :default-view and preserves the account's other plist keys, the same metadata-preserving rewrite local-view edits use. In legacy mode it writes the global pearl-default-view.
Eight tests: persist scope-targeting and key preservation in both modes, plus the offer, prompt, clear, and cancel paths. Suite 801 to 809, compile and lint clean.
|
| |
|
|
|
|
|
|
|
|
| |
Phase 1 of the default-view spec. I added the data and dispatch; keymap and the setter come in later phases.
pearl-default-view (defcustom) holds the legacy-mode default: nil for my open issues, or a local-view name. In accounts mode the per-account :default-view key wins instead, so pearl--resolve-account now carries :default-view in its context plist and pearl--resolve-default-view reads from the active account, falling back to the global only in legacy mode.
pearl-open-default-view dispatches the resolved default: nil runs pearl-list-issues, a known local-view name runs pearl-run-local-view, and a name that's since been deleted falls back to pearl-list-issues with a message instead of erroring. pearl-list-issues keeps its contract — it's still literal my-open-issues, the keymap rebind is phase 3.
Nine tests cover the resolver in both modes and the three dispatch paths. Suite 792 to 801, compile and lint clean.
|
| |
|
|
|
|
|
|
|
|
| |
The C-; L keymap and the transient menu had drifted into two different letter schemes for the same commands, the fetch sub-map carried both issue-fetching and view-movement, and pearl-switch-account was bound nowhere.
Give views their own group under v: l/L run local/Linear, c/e/k create/edit/delete, u/U publish local/current, d save a Linear view locally. Fetch (f) is now issue sources only. A new workspace group (w) binds switch-account plus the setup commands, which previously lived only in the transient. v D is reserved, unbound, for the not-yet-built set-default-view.
The transient already had an issue-only fetch group and a dedicated views group, so its changes are additive: switch-account and copy-issue-url were both missing from the menu and are added, and save/save-all/edit-description take s/S/d to match the keymap's hot keys. Matching those three cascades a few view and edit letters because the transient is a flat keyspace where every command needs a unique key.
Suite 789 to 792, compile and lint clean.
|
| |
|
|
|
|
| |
I made the builder's State prompt a completing-read-multiple, the way the Labels prompt already is, so you can pick several workflow states for one filter. The selection maps the way the spec describes: pick one and :state stays a scalar string, pick several and it becomes a list (state.name.in), pick none and there's no state constraint. The "[ Any. ]" sentinel is filtered out of the result like it is for labels.
Phase 2 of docs/multi-state-filter-spec.org. A stubbed-builder test covers the one/many/none mapping. Suite, compile, and lint green.
|
| |
|
|
|
|
| |
I made :state polymorphic, mirroring :state-type. A string still compiles to state.name.eq. A list now compiles to state.name.in, so (:state ("Todo" "In Review")) matches issues in either state. The reverse-compile inverts state.name.in back to a scalar (one name) or a list (several), so copy-down no longer refuses a Linear view filtering on a set of named states. The validator accepts a string or a list of non-empty names, the same shape it already enforces for :labels, and a list-valued :state keeps its precedence over :state-type and :open.
Phase 1 of docs/multi-state-filter-spec.org. Suite, compile, and lint green.
|
| |
|
|
|
|
|
|
| |
I added a test that reads pearl.el and asserts the user-facing "saved query" phrase appears nowhere, so the rename can't silently regress. Internal helpers keep the hyphenated saved-query symbol fragment, which a user never sees. The banned thing is the two-word user-facing phrase in any prompt, message, transient label, or docstring.
This is the cross-cutting net over Phase 8. The per-phase tests already cover the account guard, cross-store dedup and dispatch, reverse-compile round-trips and refusals, and the metadata-preserving writer. The live create, edit, run, publish, save-locally, refuse, and delete end-to-end needs a real Linear workspace and is filed as a manual-verify checklist.
Suite at 777 green, compile and lint clean.
|
| |
|
|
|
|
|
|
|
|
| |
I implemented copy-down, the one direction that needed real engine work. A live probe showed CustomView.filterData is readable but stored as Linear's and/or view-filter tree, not the flat IssueFilter Pearl emits, so pearl--reverse-compile-issue-filter is a normalize-then-match: it flattens the top-level and, unwraps single-branch and/or, reads a one-element in as a scalar, then matches each conjunct to one authoring key. Every helper returns (ok . plist) or (refuse . reason).
The contract follows the spec's dimension table: distinct :state and :state-type, canonical numeric :priority, :open t mapped from the exact open-state nin while generic nin refuses, and a multi-value in on any singular key refuses rather than silently narrow the view. Anything outside Pearl's AND-only model refuses with a structured reason that names the construct.
pearl-save-linear-view-locally lists favorited Linear views, fetches the chosen view's filterData through a small async helper, reverse-compiles it, and saves a forked local view through pearl--save-local-view: no tracking link, :copied-from-view-id provenance only, active account stamped. On a refusal it points the user at pearl-run-linear-view; on success it states the fork's independence. Bound at f d (down) and the transient Views column.
Phase 6 of the views spec. 30 tests cover per-dimension round-trips (build->encode->read bridge), normalize-from-real-JSON trees, the full refusal set, and the command-side. Suite, compile, and lint are green.
|
| |
|
|
|
|
|
|
| |
I extended the account guard from edit and run to the two remote-mutating commands. pearl-publish-local-view now refuses a local view tagged to another account before any customViewCreate or customViewUpdate, and pearl-delete-local-view refuses before the confirm prompt or any customViewDelete. Publishing or deleting under the wrong account would resolve ids against the wrong workspace or target the wrong remote, so the guard fires at the command boundary, before Linear is touched. publish-current-view inherits the guard by delegating to publish-local-view.
This completes the round-6 finding that one guard should cover every read and mutate of a local-view entry, not only edit and copy.
Phase 5 of the views spec. Tests confirm both commands error on a cross-account entry and make no Linear call. Suite, compile, and lint are green.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
dispatch
I reworked pearl--pick-source-candidates for the three Phase 4 behaviors. Labels now read [local] for a private local view, [local -> Linear:SCOPE] for a published one, [linear] for a Linear view favorite, and [KIND] for a non-view favorite, so the label itself shows where a view lives.
Dispatch follows the source-of-truth rule (decision 16): every local view, tracked or not, dispatches as a :type filter and runs its local authoring filter, never the Linear mirror. A tracked entry carries its :linear-view-url so the rendered buffer can still open the mirror. Before this, a published local view ran server-side, which contradicted the editable-local model.
Cross-store dedup: a Linear view favorite whose customView id matches a tracked local view's :linear-view-id is dropped, so the editable local entry is the one shown. A local and a Linear view that merely share a name stay distinct.
Phase 4 of the views spec. Tests cover the labels, the local-filter dispatch, and the dedup and same-name cases. Suite, compile, and lint are green.
|
| |
|
|
|
|
|
|
| |
I put the view operations under the verb a user would reach for. Create, edit, and delete local views are c v, e v, and k v. The fetch group gains run-local-view at f l and keeps run-linear-view at f v, with publish at f u and save-locally reserved at f d. Since f p already runs issues by project, publish and save-locally take u and d, which read as up and down and mirror the copy-up and copy-down lifecycle.
The transient now has a Views column grouping create, edit, run-local, run-linear, publish, and delete, separate from the source-fetching commands. I updated the spec's keymap to the implemented letters so the two stay in sync.
Phase 3 of the views spec. Keymap tests assert the new bindings. Suite, compile, and lint are green.
|
| |
|
|
|
|
|
|
|
|
| |
I added pearl-create-local-view and pearl-edit-local-view, plus the writer and guards the spec calls for. pearl--save-local-view replaces the old rebuild-from-scratch writer. It copies an existing entry and updates only the filter, sort, and order, so a published view keeps its :linear-view-* tracking link and a copied-down view keeps its provenance. A new entry gets the active account stamped under pearl-accounts.
pearl--require-local-view-account is the one guard every read or mutate command shares, and pearl-run-local-view now calls it instead of an inline check. pearl--resolve-local-view-name carries the Replace/Rename/Cancel collision policy for create, edit-rename, and copy-down. The ad-hoc builder's save offer routes through the preserving writer and reads "save this as a local view".
Edit re-runs the builder when you choose to rebuild and otherwise keeps the stored filter. Full per-dimension pre-seeding of the builder is deferred, since it needs the id-to-name reverse resolution that copy-down's reverse-compile introduces. I filed it as a follow-up.
Phase 2 of the views spec. Tests cover metadata preservation, the account guard, the collision policy, and the metadata-preserving rename. Suite, compile, and lint are green.
|
| |
|
|
|
|
|
|
| |
I renamed Pearl's "saved query" surface to the local/Linear view vocabulary, with no obsolete aliases, since Pearl has no users to protect. The defcustom pearl-saved-queries is now pearl-local-views. The commands pearl-run-saved-query, pearl-delete-saved-query, pearl-sync-saved-query-to-linear, pearl-run-view, and pearl-publish-current-source become pearl-run-local-view, pearl-delete-local-view, pearl-publish-local-view, pearl-run-linear-view, and pearl-publish-current-view.
User-facing prompts, messages, and docstrings drop "saved query" for "local view", and the publish command reads "publish" instead of "sync". Internal GraphQL and helper names keep their "query" terms, which a user never sees. A naming-regression test asserts the new symbols exist, the old ones are gone rather than aliased, and no public command exposes "query".
Phase 1 of docs/local-and-linear-views-spec.org. No behavior change. Suite, compile, and lint are green.
|
| |
|
|
| |
The setup check gated on pearl-api-key, which is normally unset once pearl-accounts is configured, so under multi-account it wrongly reported "API key is not set" and skipped the connection test even when the active account resolved a key fine. Under accounts mode it now resolves the active account's context, names it, and runs the test. An unusable setup (no default, missing key) reports the reason instead of signaling. Legacy single-account behavior is unchanged. The resolved key is never printed.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
I added a named-account layer for working more than one Linear workspace from one Emacs.
Before this, everything that identified a workspace was a single global: pearl-api-key, pearl-graphql-url, pearl-org-file-path, and the lookup caches. Nothing stopped a work command from running under personal credentials or a work fetch from landing in the personal file, and switching accounts meant re-customizing the key, team, and file by hand and clearing the cache.
pearl-accounts maps a name to a per-workspace plist (credential source, org file, default team, optional endpoint), and pearl-switch-account makes one active. Account state flows through an explicit context rather than mutating globals. Every request snapshots its account at dispatch through pearl--graphql-request-async and re-establishes it around the callbacks. A switch mid-fetch can't bleed into a request already in flight: the result finishes into the account it was dispatched under. I centralized this in the one request primitive, so the leak surface is a single function instead of every call site.
Rendered files carry a #+LINEAR-ACCOUNT marker, and a buffer guard refuses a command run from one account's file while another is active, naming both, so a work edit can't push under personal credentials. An unmarked legacy file lets reads and refreshes through and acquires its marker on the first refresh. Mutations wait until then. Credentials resolve through auth-source, an env var, or an inline literal, and a resolved key is never persisted through Customize or logged. The active account shows in the mode line. A saved query can carry an :account so it refuses to run under the wrong workspace before any lookup.
With pearl-accounts unset, everything behaves exactly as before, off the legacy globals.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
The previous commit (eb56295) shipped the buggy pre-fix version of pearl--merge-issues-into-buffer along with a too-weak test. The intended fix and the strengthened test were sitting uncommitted in the working tree when I ran git commit, because I forgot to re-stage pearl.el and tests/test-pearl-merge.el after addressing the critical correctness finding from pre-commit review. The test that landed asserts each touched marker leads to some LINEAR-ID heading, which passes against the buggy code (the markers point at the wrong heading, but it's still a valid heading).
This commit is the fix and the real test, which together honor what eb56295's message described.
Code change: pearl--merge-issues-into-buffer captures a type-nil copy of the alist marker BEFORE calling pearl--replace-issue-subtree-at-point, instead of pushing the original type-t marker after the replace. The alist marker is insertion-type t (it needs to advance past replaces of EARLIER subtrees so it stays anchored to its own heading), so reusing it after the current subtree's delete-then-insert captures the post-advance position, which is the NEXT subtree's heading. A type-nil copy made before the replace stays anchored at the deletion start and lands on the new heading after the re-insert.
Test change: the returns-touched-markers test now resolves each marker to its heading's LINEAR-ID and asserts the set matches {a, b, c} exactly (for 2 updated + 1 added), instead of just checking each marker reaches some heading. With the buggy code the IDs would resolve to {b, c, c} (a's marker advanced to b's heading, b's marker advanced to c's added heading). The strengthened assertion fails on the bug.
All 674 ert tests pass. make compile and make lint are clean.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
A merge refresh (the "merge keeping edits" choice on a dirty buffer, or pearl-refresh-current-view on a clean one) used to call pearl--restore-page-visibility at the tail, re-folding the whole buffer to its #+STARTUP overview. That collapsed every subtree the user had expanded, including the comment they were just editing, and disturbed point. The edit-then-merge flow felt jarring.
The fix: pearl--merge-issues-into-buffer now returns a marker for every subtree it re-rendered or appended, alongside the existing counts (:touched-markers (M1 M2 ...)). The merge call sites fold just those subtrees via a new pearl--fold-touched-subtrees helper. Kept (locally-edited) and untouched subtrees stay exactly as the user had them, and point is preserved because the localized fold doesn't touch the rest of the page.
The full-rebuild paths (Branch A no-buffer, Branch B clean-buffer replace, Branch C-discard) still call pearl--restore-page-visibility. They re-render the whole buffer so the global re-fold is the right behavior.
Pre-commit review caught a critical correctness bug in the first draft: the alist marker from pearl--issue-subtree-markers is insertion-type t (it needs to advance past replaces of EARLIER subtrees so it stays anchored to its own heading), and reusing it after pearl--replace-issue-subtree-at-point captured the post-advance position, which is the NEXT subtree's heading, so the fold collapsed the wrong subtree. The fix copies the marker as type-nil BEFORE the replace, so it stays anchored at the deletion start and lands on the new heading after the re-insert. I strengthened the marker test to verify the IDs at the touched markers match the expected set ({a, b, c} for 2 updated + 1 added), not just that they're some valid headings. The original test would have passed against the bug.
Five new tests in test-pearl-merge.el cover: merge returns markers with the correct IDs at their headings, skipped (locally-edited) issues don't add to touched-markers, fold-subtree-at-marker hides body content while leaving the heading visible, fold-touched-subtrees is a no-op when pearl-fold-after-update is nil, and fold-subtree-at-marker tolerates a nil marker silently. All 674 ert tests pass. make compile and make lint are clean.
|
| |
|
|
|
|
|
|
|
|
| |
Pearl was rendering the Comments subtree heading as `*** 💬 Comments [N/M]`. Craig flagged that it looks strange, and the renderer should work cleanly before it gets glyphed. I dropped the emoji prefix so the heading reads `*** Comments [N/M]`. Partly reverts 41a3396. The overlay-glyph idea (a content-aware display overlay on the leading stars) stays tracked as a separate task.
I touched pearl--format-comments (the new-subtree literal) and pearl--append-comment-to-issue (both the new-subtree insert and a couple of stale docstring references). The append-locator regex stays tolerant of the legacy `💬 Comments` heading and of the older pre-2026-05 `Comments 💬 N/N` trailing-glyph layout. Buffers rendered in any of the three states still locate their Comments heading on append.
I added one regression test (test-pearl-append-comment-locates-legacy-emoji-prefixed-heading) that seeds a `*** 💬 Comments 1/1` heading and asserts append still finds it, bumps the count to 2/2, and doesn't add a second Comments heading.
I updated assertions in test-pearl-comments.el, test-pearl-list-comments.el, and test-integration-acceptance.el to expect the no-emoji form. I also updated docstrings on pearl-fetch-comments-in-list, pearl-list-comments-count-cap, pearl--format-comments, pearl--bump-comments-count-marker, and pearl--comment-count-marker. The README's sample buffer block now matches what the renderer emits. All 669 ert tests pass. make compile and make lint are clean.
|
| |
|
|
|
|
|
|
|
|
| |
pearl-sync-saved-query-to-linear used to rebuild the entire scope-candidates list a second time (full sort + 2N row allocations) just to rassoc the chosen scope plist back to its display string for the success message. The picker had thrown away the display string the user picked, so the caller had no choice but to re-derive it.
I changed pearl--sync-saved-query-pick-scope to return (DISPLAY . PLIST) instead of just PLIST. The caller now reads scope-label from (car scope-pair) and scope from (cdr scope-pair). The rassoc is gone, and so is the second pearl--sync-scope-candidates call.
To keep the re-sync path label-consistent with the picker (so a "Synced X (Team: Engineering, visible to the team)" message matches what the picker would have shown for the equivalent first-time sync), I extracted pearl--sync-scope-label as the single source of truth for the label format. The picker, the re-sync branch, and any future caller that needs to describe a scope all go through the helper. Personal scope always renders "[ Personal, only I see it ]" regardless of the shared flag (a Personal + shared combination is meaningless on Linear). Team scopes render "[ Team: NAME, visible to the team ]" or "[ Team: NAME, only I see it ]" per the flag.
I added five tests covering the new behavior: scope-label for personal (ignores shared) and team (both shared and private), pick-scope returns the team pair, pick-scope returns the personal pair, pick-scope returns nil on cancel. All 668 ert tests pass. make compile and make lint are clean.
|
| |
|
|
|
|
|
|
|
|
| |
A saved query with no filter constraints compiled to nil via pearl--build-issue-filter. The sync command then passed (cons "filterData" nil) to customViewCreate, which JSON-encoded as filterData=null. Linear's CustomViewCreateInput.filterData expects a JSON object, so the API rejected with an opaque error message after the user had already clicked through the scope picker.
I added an early refusal in pearl-sync-saved-query-to-linear: when filter-data is nil, signal a user-error naming what's wrong before the scope prompt fires. The message tells the user to add a constraint and re-sync rather than leaving them to puzzle out an opaque API rejection.
I restructured the let* nesting to put the empty-filter check between the bindings (where filter-data is computed) and the scope prompt (where the user is asked to pick a destination). The scope binding moved out of the outer let* into an inner let so it doesn't evaluate until after the filter-data guard runs.
Two tests cover the guard. The first-time-sync test (no :linear-view-id on the entry) asserts neither the scope prompt nor customViewCreate fires. The re-sync test (entry has :linear-view-id) asserts customViewUpdate doesn't fire either. The re-sync test guards against a future refactor accidentally hoisting the check into the create-only branch. All 663 ert tests pass. make compile and make lint are clean.
|
| |
|
|
|
|
|
|
| |
The prior implementation rebuilt the spec from a fixed key set (:filter, :sort, :order, plus the four :linear-view-* sync keys and the optional :linear-view-url) and silently dropped anything else. No keys outside that set live on a saved-query entry today, but the persistence layer was coupled to the current schema. A future :description, a user-added annotation, or any new pearl field would vanish on every re-sync.
I switched to copy-then-plist-put: shallow-copy the existing spec, plist-put the four sync keys on top, and conditionally plist-put :linear-view-url when this call provided one. Arbitrary keys survive untouched. The URL-preservation behavior on a re-sync that omits view-url is unchanged: the prior :linear-view-url is already in the copied spec, so the when-view-url guard skips the put and leaves it in place.
I added one regression test that seeds an entry with :description and :user-tag alongside the standard keys and asserts all of them survive a mark-synced call. All 661 ert tests pass. make compile and make lint are clean.
|
| |
|
|
|
|
|
|
|
|
| |
This is Phase 5 of docs/saved-query-sync-spec.org (§ New: pearl-publish-current-source). When I'm reading a local saved query rendered in pearl and decide it should be a Linear view, I now have a one-chord publish instead of the M-x round-trip through pearl-sync-saved-query-to-linear with the name lookup.
The new command reads the active buffer's #+LINEAR-SOURCE header and dispatches to pearl-sync-saved-query-to-linear with the name pre-filled when the source is a :type filter that matches an entry in pearl-saved-queries. Other shapes refuse with a clear message: no source header at all, the source is already a :type view (no publish needed), the type isn't filter, the source has no usable name (transient ad-hoc filter, or whitespace-only name), or the recorded name doesn't match any local saved query.
I bound the command under C-; L f P in pearl-fetch-map. The transient gets ("U" "upload current source") in the Fetch group because P is taken in the transient's flat namespace, and the verb-prefix shape revisit task tracks reshaping that.
7 new tests cover each refusal branch and the dispatch path, including a regression for the whitespace-only :name case the pre-commit review surfaced. The prior check used string-empty-p, which let " " fall through to the no-matching-saved-query branch with a visibly-blank name in the error message. string-blank-p routes it to the cleaner no-name branch. The keymap and transient menu tests gained coverage for the new binding. 660 ert tests total green. make compile and make lint clean.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
/ [view]
This is Phase 4 of docs/saved-query-sync-spec.org (§ Extended pearl-pick-source candidate label). The picker now reads where each saved query lives at a glance:
- `[saved] Name`: local-only saved query, dispatches as :type filter (unchanged)
- `[saved → Engineering] Name`: synced to a team-scoped Linear view
- `[saved → Personal] Name`: synced as a personal Linear view
- `[saved → ?] Name`: synced, but the team id no longer resolves (deleted/renamed on Linear) or the teams cache wasn't loaded
- `[view] Name`: Linear-authored view favorite (unchanged)
A synced entry's source plist gets :type view, so the existing view-dispatch branch in pearl-pick-source routes through pearl--query-view-async against the linked Linear view. Refresh and pagination honor whatever the view's filter is on Linear now, not the local authoring filter pearl built it from.
I added pearl--saved-query-scope-label to resolve the scope string from (spec, teams) and extended pearl--pick-source-candidates with an optional teams argument. pearl-pick-source fetches pearl--all-teams lazily, only when at least one saved query carries :linear-view-id, and feeds the result in. An all-local configuration pays no extra network cost.
While in the picker code I also closed a latent gap the review surfaced: pearl-open-current-view-in-linear was about to break on synced-saved-query sources because the source plist had no :url. I extended pearl--customview-create-async and -update-async to fetch `url` in the response, pearl--save-query-mark-synced to take an optional VIEW-URL arg and store it as :linear-view-url, and the picker source plist to surface :url when the entry has one. Entries synced before this commit have no stored URL. A re-sync repopulates it. Mark-synced preserves a prior URL when VIEW-URL is omitted, so a re-sync whose API response somehow lacks the URL doesn't erase the working one.
15 new tests across test-pearl-favorites.el (the picker labels for team-scope, Personal, unknown-team, no-teams-arg, URL surfaced + URL absent, mixed local/synced) and test-pearl-saved-query-sync.el (mark-synced stores URL when given, preserves prior URL on re-sync omission). The existing 4 pick-source tests stay green. They pass either no teams arg or nil, so the scope-resolution branch is dead in their cases. 653 ert tests total green. make compile and make lint clean.
Lower-severity findings from the review filed as follow-ups: the `?` label conflates "unknown team" and "no teams data" (could be split). pearl--all-teams cache has no TTL so stale team names persist across renames. The lazy teams fetch reuses the "Fetching favorites..." progress message during its own roundtrip. Synced entries silently drop the local :sort/:order because Linear's view doesn't carry them in v1.
|
| |
|
|
|
|
|
|
|
|
| |
Phase 3 of docs/saved-query-sync-spec.org. pearl-delete-saved-query on a synced entry (one carrying :linear-view-id) now asks a second question after the local-delete confirmation: also delete the linked Linear view? Yes calls customViewDelete, no unlinks only and leaves the Linear view in place. Local-only entries take the unchanged single-prompt path.
If customViewDelete fails on the API side, a fallback yes-or-no prompt asks whether to drop the local entry anyway. Accepting orphans the Linear view, and the success message names the view id explicitly so the user can clean it up by hand. The same view-id-in-message pattern fires on the timeout branch, since a timeout doesn't tell us whether the delete actually completed server-side.
I added pearl--customview-delete-async (the mutation, parallel to the create and update helpers from Phase 2) and pearl--delete-saved-query-local (factors the cl-remove + customize-save-variable that both the local-only and the synced-unlink branches need). The new pearl--delete-saved-query-do-linear-delete carries the API path so the top-level command stays readable.
I added 7 tests in test-pearl-saved-query-sync.el covering the customViewDelete success and failure parses, the synced yes/yes path (API call fires + local removed), the synced yes/no path (unlink only, no API call), the API-failure-then-delete-anyway path (orphan message names the view id), the API-failure-then-keep path (asserts the API call fired so a refactor can't silently route through the unlink branch), and the local-only entry unchanged path. The existing pearl-delete-saved-query tests in test-pearl-adhoc.el stay green. Their fixtures don't carry :linear-view-id, so they take the local-only branch. 638 tests total green. make compile and make lint clean.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
pearl-sync-saved-query-to-linear promotes a local pearl-saved-queries entry to a Linear Custom View via customViewCreate, or updates an already-synced entry in place via customViewUpdate. This is Phase 2 of docs/saved-query-sync-spec.org. Re-syncing keeps the stored scope. First-time sync prompts for the destination (team + visibility), with each candidate spelling out the complete end-state. Same-name collisions in the chosen scope prompt Replace / Rename / Cancel.
A successful sync extends the entry with :linear-view-id, :linear-view-team-id, :linear-view-shared, :linear-view-synced-at. If the customize-save-variable persist fails after a successful API call, the message names the orphan view's id explicitly so the user can re-sync with Replace to reconcile.
I bound the command under C-; L f S in pearl-fetch-map and added ("S" "sync saved query") to the transient's Fetch group. filterData is the existing pearl--build-issue-filter output verbatim. The 2026-05-28 live probe confirmed the shape passes through customView.filterData with no translation needed.
Four related fixes ride along:
- Extended pearl-get-teams-async to fetch `key' alongside `id name'. It shares pearl--cache-teams with pearl--all-teams, so an async-first populate would otherwise leave the cache without `key' and silently break filter-team lookups.
- Normalized the stored :linear-view-shared flag to a strict boolean at the re-sync read site. `:json-false' is truthy in Elisp, so an unnormalized stored false would flip the view to team-shared on re-sync without asking.
- pearl--sync-saved-query-await now returns `(:success nil :timeout t)' on a wait-for elapse, so the caller can distinguish a timeout from a Linear-side rejection and tell the user the request may still complete server-side.
- The success path now emits a single message via pearl--sync-record-or-orphan-error. The prior code clobbered the orphan-recoverable view-id with a second message.
I hoisted the prompt-sentinel block (pearl--filter-cancel etc.) above its first user. The prior order produced a forward-reference compile error the moment any other change touched the file.
I added 31 tests in test-pearl-saved-query-sync.el covering the scope candidates, the customViewCreate/Update parsing, the entry persistence (including the :json-false overwrite case), the orphan-id error path, the timeout sentinel, and the get-teams-async cache parity. The keymap and menu tests gained coverage for the new binding. 631 ert tests total green. make compile and make lint clean.
Lower-severity findings filed as follow-ups: stale pearl--cache-views across a schema upgrade, empty filter encoding to null filterData, the pearl--save-query-mark-synced rebuild dropping unknown plist keys, and the scope-label rassoc rebuilding the candidate list to look up a string the picker already had.
|
| |
|
|
|
|
|
|
|
|
| |
Two UX refinements on the just-shipped prompt surface, both following the principle "the prompt should describe what it's doing and the default should be the most-common choice."
First, the sentinel that was uniformly "[ None. ]" really meant two different things at two kinds of prompt, and the label only matched one of them. Filter-dimension prompts (team, state, project, labels, assignee) treat picking the sentinel as "no constraint on this dimension." Every value matches, which is *any*, not *none*. The saved-query prompts (delete, run) treat picking the sentinel as "don't act." That one is *cancel*, not *none* or *any*. Renamed accordingly: `pearl--filter-any` ("[ Any. ]") for the five filter dimensions and `pearl--filter-cancel` ("[ Cancel. ]") for the two saved-query prompts. The generic helper became `pearl--with-sentinel SENTINEL CANDIDATES` so each call site picks the label that fits its case. The predicate became `pearl--filter-sentinel-value-p` (recognizes either sentinel or empty/nil) so the cancellation logic is unchanged.
Second, three non-destructive yes/no prompts ("Open issues only?", "Save this filter locally...", "Save N fields across M issues?") moved from `y-or-n-p` to a new `pearl--read-yes-no` helper. The helper renders a completing-read over ("yes" "no") with the most-common choice as the default and topmost candidate, so RET takes it without typing. Default is "yes" for all three (each is a do-the-thing-I-asked confirmation), but the helper takes a DEFAULT arg so a future prompt where "no" is more common can opt in. The destructive prompts (delete issue, delete saved query, delete comment) stay as `yes-or-no-p`. Typing "yes" there is a deliberate safety affordance, not friction worth removing.
Tests cover the sentinel-value predicate across both sentinels + empty/nil + real values, the `pearl--with-sentinel` helper, the `pearl--read-yes-no` t/nil return and default-ordering behavior, and the three save-test stubs swapped to mock the new helper.
|
| |
|
|
|
|
| |
`pearl-saved-queries' was write-only from a user's perspective. Save was bound to the builder, run was bound to a key, but removing a stale entry meant `M-x customize-variable RET pearl-saved-queries RET` and a manual delete from the Customize buffer. `pearl-delete-saved-query` under `C-; L k q` (and `q` in the transient's Delete group) closes that gap: completes over the saved query names with `[ None. ]` at the top as a cancel, confirms, removes the entry, and persists via `customize-save-variable`. Linear is untouched. Saved queries are local. Pushing them up to Linear as custom views is a separate spec.
While in the same path, `pearl-run-saved-query` got the same sentinel cancel. Both prompts use the keep-order completion table so `[ None. ]` stays at the top regardless of the user's completion sorter. Both error cleanly when `pearl-saved-queries` is empty rather than offering an empty picker. Six tests cover the new command (delete-on-yes, no-at-confirm, sentinel-cancels, unknown-name-errors, empty-list-errors) plus run's sentinel cancel.
|
| |
|
|
|
|
| |
Wrapping each builder prompt with `pearl--with-none` put the sentinel first in the candidate list, but the completion framework's default sort (vertico-sort-history-length-alpha, prescient, ivy's sorter, etc.) re-sorted alphabetically and any real candidate starting with a letter before "[" pushed the sentinel down. Linear's "Software Engineering" outsorted "[ None. ]", so the new pattern failed at the very first prompt.
The standard Emacs hook for "I sorted these on purpose" is a completion table that returns `(metadata (display-sort-function . identity))` for the `metadata` action. The new `pearl--completion-table-keep-order` helper does exactly that, and the five builder prompts (team, state, project, labels, assignee) now wrap their candidate lists through it. The framework keeps its default sort everywhere else. Only these five prompts opt out, in the place where author-order is meaningful.
|
| |
|
|
|
|
|
|
| |
The builder's state / project / team / labels / assignee prompts each relied on the "empty input means no constraint" convention, with a parenthetical hint in the prompt label ("Team (empty for any):"). The convention is invisible, has to be remembered, and reads as a non-choice rather than a choice. Now every prompt carries `pearl--filter-none` ("[ None. ]") as the first candidate and uses `require-match` so the user picks from the list. Selecting the sentinel is the same logical opt-out as empty input was, but the choice is on screen rather than in the user's head.
Two small helpers (`pearl--with-none`, `pearl--filter-none-value-p`) and one defconst keep the sentinel string in one place. The prompt labels lost their parenthetical hints since the affordance is now in the candidate list. Five unit tests cover the sentinel predicate and the list helper across the normal / boundary / "real value" cases.
The pattern generalizes well past the builder: any time pearl prompts the user to pick between a value and "no value," the no-value option belongs in the list, not hidden behind empty input.
|
| |
|
|
| |
The four-line preamble enumerated `M-x pearl-save-issue` / `M-x pearl-create-comment` / etc., even though pearl-mode binds the whole verb-prefix keymap under `C-; L` and that's what users actually type after the first session. Replaced with two lines: one for inline edits + save, one for the verb-prefix groups (edit, refresh, menu, pick-source). Reads as a key reference rather than a command list. The build-org-content test now matches the affordance line on "save" instead of the literal `pearl-save-issue` so the next rephrase doesn't trip it.
|
| |
|
|
|
|
|
|
|
|
| |
Pearl now treats Linear sources (favorites, custom views, ad-hoc filters, saved queries) as one surface. `pearl-pick-source` is the unified picker: it lists every Linear favorite alongside every locally saved query, and dispatches by kind. View / project / cycle / label / user favorites resolve to runnable filter or view sources; issue / document / dashboard favorites open in the browser since they aren't lists.
The filter compiler grows id-based forms (`:label-id` and `:assignee-id`), so favorites that name a Linear entity (a user, a label) compile to rename-proof queries rather than fragile name matching. The ad-hoc builder gets a matching member-assignee option ("me / member / any"), so a one-off "what's a teammate working on?" doesn't require building a saved query first.
Two pre-existing builder bugs surfaced during verify and got fixed in the same pass. The builder previously passed display names straight to the compiler, but `:team` compiles to `team.key.eq` and `:project` to `project.id.eq`, so Linear rejected every builder run that pinned a team or project. Now the builder resolves the picked name to its key / id before assembling. And `pearl-list-issues-by-project` previously injected `:assignee :me` into the filter, hiding every teammate's work in the project. It now returns the whole project.
The save prompt on the builder was rewritten so the local-only nature is unambiguous, and the README grew a Sources section plus a keymap-table entry for `f s`.
|
| |
|
|
|
|
| |
The keybinding prefixes now each name an action verb: f fetch, e edit, c create, k delete, o open, y copy. The url group is gone, its actions split into open and copy, and the direct lowercase c (new comment) is dropped so c can head the create group. Two commands carried non-verb names, so I renamed them: pearl-new-issue is now pearl-create-issue and pearl-add-comment is now pearl-create-comment. The old sync helper that already held the pearl-create-issue name moved to the private pearl--create-issue, where it belongs as internal plumbing.
I also swept "ticket" to "issue" across the labels, transient, docstrings, and README. Linear's own term is "issue" and the buffer mirrors Linear, so the command names already said "issue" while the labels said "ticket". Now they agree. The transient's per-entry keys still trail the keymap verbs, which stays its own task.
|