1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
|
#+TITLE: Spec: a configurable default view
#+AUTHOR: Craig Jennings
#+DATE: 2026-06-02
* Status
*Ready.* Codex review incorporated (2026-06-02, =Not ready= → resolved); the one blocker (HP1: the transient set-default-view key was unspecified and =D= is taken by save-locally) is settled by binding the transient suffix =.=, and the medium finding (MP1: missing test-surface drop-in task) is folded in. See *Review dispositions*. Triggered by the 2026-06-02 keybindings reconciliation: that work reserved =C-; L v D= for a not-yet-built set-default-view command and flagged the open question of whether the hot key =l= should run a configurable default instead of being hardwired to "my open issues." This spec settles four decisions before code — the value model, per-account vs global, the =l=-rebind question, and persistence — and is deliberately light: the feature is local config plus dispatch, with no external API surface, so no live probe is needed. Closest precedent in size is [[file:multi-state-filter-spec.org][multi-state-filter-spec.org]] (one short review round).
* Problem
Pearl's default fetch is hardwired. =pearl-list-issues= with no project runs =(:assignee :me :open t)= — "my open issues" (=pearl--list-issues-source=, =pearl.el:6100=) — and it's bound to the hot key =l= and to =C-; L f o=. There's no way to say "when I open Pearl, show me /this/ instead" — a particular local view, or a narrower slice than everything assigned to me.
Most people live in one view. For one user it's their open issues; for another it's a curated local view ("My active sprint work"), or a team board they've saved locally. Today the second user runs =C-; L v l= and picks the view by name every time, while =l= — the shortest key — runs a fetch they rarely want.
The goal: let =l= open whatever the user has chosen as their default, defaulting to "my open issues" so nothing changes for anyone who never sets one.
* The four decisions
These are the design calls worth settling on paper. The first is load-bearing (getting it wrong means a defcustom-shape change plus migration); the rest are contained.
** Decision 1 — value model: nil or a local-view name
The default is one of:
- *unset (nil)* — "my open issues", today's behavior.
- *a local-view name* (a string keying =pearl-local-views=) — run that local view.
A Linear Custom View is /not/ a direct default value. To default to a Linear view, save it locally first (=pearl-save-linear-view-locally=, copy-down) and set the resulting local view as the default. This keeps v1 from needing a second identification scheme (Linear views are id-keyed and need a fetch to resolve a name), and it costs the user nothing they can't already do.
The defcustom type mirrors =pearl-default-account= exactly: =(choice (const :tag "My open issues" nil) (string :tag "Local view name"))=.
** Decision 2 — per-account, not global (the load-bearing one)
Local views carry an optional =:account= stamp and refuse to run under a different active account (=pearl--require-local-view-account=, =pearl.el:4349=). A single global default naming an account-tagged local view would therefore /refuse/ whenever a different account is active — the default would be broken half the time in a multi-account setup.
So the default is *per-account when accounts are configured, global otherwise*, resolved through the active-account context the same way =:default-team-id= already is:
- *Accounts mode* (=pearl-accounts= set): the default lives in each account's plist as =:default-view=, alongside the existing =:default-team-id= and =:org-file=. Each account names its own default, so a tagged local view is always run under the account it belongs to — the cross-account refusal can't fire.
- *Legacy mode* (=pearl-accounts= nil): the default lives in a standalone =pearl-default-view= defcustom.
This parallels the existing =pearl-default-account= / per-account split exactly: a durable global preference for the simple case, per-account state for the multi-workspace case. The alternative (one global default that silently falls back to my-open-issues when its view is cross-account) was rejected — it makes =l= mean different things depending on which account is active, which is the confusion the per-account model removes.
** Decision 3 — rebind =l= to the default; keep =pearl-list-issues= literal
A new command =pearl-open-default-view= resolves the active account's default and dispatches: nil → run my-open-issues (=pearl-list-issues= with no project), a name → run that local view (=pearl-run-local-view=). Top-level =l= rebinds from =pearl-list-issues= to =pearl-open-default-view=.
=pearl-list-issues= is *not* overloaded. It keeps its exact contract ("my open issues, assignee=me") because =pearl-list-issues-by-project= calls it with a project id and the literal fetch still needs a home — it stays at =C-; L f o=. So:
- =C-; L l= (hot key) → =pearl-open-default-view= — your default.
- =C-; L f o= → =pearl-list-issues= — literal my-open-issues, unchanged.
Back-compat holds by construction: with the default unset (every current config), =pearl-open-default-view= runs my-open-issues, so =l= does exactly what it does today. Setting a default is the only thing that changes =l='s behavior.
*Transient key for set-default-view.* The prefix keymap reserves =C-; L v D= (D = Default) for =pearl-set-default-view=. The transient =pearl-menu= can't reuse =D= in its Views group: it's a flat keyspace and =D= already runs =pearl-save-linear-view-locally= there. So the transient binds =pearl-set-default-view= to =.= in the Views group ("set the current/default view"). The mismatch is expected and not worth re-keying the just-shipped save-locally suffix to fix: the keybinding reconciliation established that the transient's per-command letters are read off the screen and don't track the keymap's two-key chains. =v D= is the chain you type from memory; =.= is the slot you see in the menu. (The key is a free-slot pick, easy to change at implementation if a better one surfaces.)
** Decision 4 — persistence: durable via customize
A default is a durable preference, so the setter persists it the way =pearl-default-account= is meant to be set: through =customize-save-variable= so it survives a restart. In accounts mode the setter rewrites the active account's entry in =pearl-accounts= (updating its =:default-view= key) and saves; in legacy mode it sets and saves =pearl-default-view=. Session-only setting (a plain =setq= that evaporates on restart) is not the default behavior — a "default" the user has to re-set every session isn't one.
The accounts-mode rewrite is *metadata-preserving*: it updates only =:default-view= on the active account's plist and leaves every other key (=:api-key-source=, =:org-file=, =:default-team-id=, =:url=, and anything the user added) intact, the same way local-view edits preserve unrelated metadata. And =pearl-accounts='s docstring and =:value-type= gain =:default-view= so the new key is visible to a Customize reader rather than silently undocumented.
* Proposed commands
** =pearl-set-default-view= (the reserved =C-; L v D=)
Sets the active account's default view and persists it. Two entry paths:
- *From a Pearl buffer whose source is a local view* — offers that view as the default ("Set 'My active sprint work' as the default view?"). The obvious "make what I'm looking at my default" gesture.
- *Otherwise* — =completing-read= over =pearl-local-views= names, with a =[ My open issues ]= sentinel at the top that clears the default back to nil. Cancelling leaves the default unchanged.
Writes through =customize-save-variable= (Decision 4). In accounts mode it targets the active account's =:default-view=; in legacy mode =pearl-default-view=.
** =pearl-open-default-view= (the hot key =l=)
Resolves the active account's default (Decision 2) and dispatches (Decision 3):
- nil → =pearl-list-issues= (my open issues).
- a local-view name that exists in =pearl-local-views= → =pearl-run-local-view= on it.
- a local-view name that no longer exists (deleted since it was set) → fall back to =pearl-list-issues= with a one-line message ("Default view 'X' no longer exists; showing my open issues"). Never errors — a stale default degrades to the universal default rather than blocking the shortest key in the keymap.
A local view named as a per-account default is, by Decision 2, always tagged to (or shared with) the account it's stored under, so =pearl-run-local-view='s account guard passes by construction.
* Current state
- =pearl--list-issues-source= / =pearl-list-issues= (=pearl.el:6100-6134=) — the hardwired my-open-issues fetch; =l= and =f o= both point here today.
- =pearl-run-local-view= (=pearl.el:3857=) — runs a named local view through the account guard; the dispatch target for a name-valued default.
- =pearl-accounts= (=pearl.el:105=) — per-account plist; gains an optional =:default-view= key. =pearl-default-account= (=pearl.el:124=) is the value-type and persistence precedent.
- =pearl-prefix-map= (=pearl.el:6748=) — top-level =l= rebinds to =pearl-open-default-view=; =v D= (currently reserved/unbound) binds =pearl-set-default-view=; =f o= unchanged.
- The transient =pearl-menu= Views group currently binds =D= to =pearl-save-linear-view-locally= (=pearl-views-map= separately reserves =v D= for set-default-view). The transient gains a new =.= suffix for =pearl-set-default-view= (see Decision 3); the existing =D= suffix is untouched.
*Unaffected:* =pearl-list-issues-by-project= (still calls =pearl-list-issues= with a project id), =pearl-refresh-current-view= (re-runs the buffer's recorded =#+LINEAR-SOURCE=, which already records whichever view was opened — a default-opened view refreshes like any other).
* Migration
None for existing configs. With no =:default-view= on any account and =pearl-default-view= nil, =pearl-open-default-view= runs my-open-issues, so =l= is unchanged. The feature is purely additive: a new defcustom (legacy), a new optional account plist key, and two new commands. No stored data changes shape.
* Acceptance criteria
- With no default set (legacy mode, =pearl-default-view= nil): =pearl-open-default-view= runs my-open-issues — same source plist =pearl-list-issues= produces today.
- With =pearl-default-view= set to a local-view name: =pearl-open-default-view= runs that local view (same path as =pearl-run-local-view=).
- With a default naming a non-existent local view: =pearl-open-default-view= falls back to my-open-issues and messages; it does not error.
- Accounts mode: the default resolves from the *active* account's =:default-view=; switching accounts switches which default =l= opens.
- Accounts mode: a default naming an account-tagged local view runs without tripping the cross-account guard.
- =pearl-set-default-view= from a local-view buffer offers that view; elsewhere it prompts over local-view names with a clear-to-my-open-issues sentinel.
- =pearl-set-default-view= persists through =customize-save-variable= — the choice survives a fresh Emacs (verified by re-reading the saved custom value, not a live =setq=).
- Accounts mode: setting the default rewrites only =:default-view= on the active account and leaves the account's other plist keys (=:api-key-source=, =:org-file=, =:default-team-id=, =:url=, any extras) intact.
- =pearl-accounts='s docstring and =:value-type= document the =:default-view= key.
- =pearl-list-issues= is unchanged in contract and still reachable at =C-; L f o=; =pearl-list-issues-by-project= still works.
- Top-level =C-; L l= resolves to =pearl-open-default-view=; =C-; L v D= resolves to =pearl-set-default-view=; the transient Views group exposes set-default-view under =.= with no duplicate suffix.
- Full ERT suite green, =make lint= and byte-compile clean.
* Implementation phases (commits)
1. *Resolver + open command* — =pearl-default-view= defcustom (legacy) + =:default-view= account-plist key (documented in =pearl-accounts='s docstring and =:value-type=); a =pearl--resolve-default-view= helper that returns the active scope's default (per-account in accounts mode, the defcustom in legacy); =pearl-open-default-view= dispatching nil → my-open-issues, name → local view, stale-name → fallback+message. Unit tests for the resolver (both modes) and the dispatch/fallback. (=feat:=)
2. *Setter* — =pearl-set-default-view= with the from-buffer and prompt paths, persisting via =customize-save-variable= to the right scope. The accounts-mode rewrite preserves unrelated account plist keys (metadata-preserving). Tests for scope targeting, plist-key preservation, and the clear-to-nil sentinel. (=feat:=)
3. *Keymap + transient + docs* — rebind top-level =l= to =pearl-open-default-view=, bind =v D= to =pearl-set-default-view=, add the =.= set-default-view suffix to the transient Views group; keymap and menu tests updated (=v D= and transient =.= resolve, no duplicate suffix); README documents the default view, the =l= behavior, and the per-account scoping. (=feat:= + doc changes ride along)
* Out of scope (vNext)
- A Linear Custom View as a direct default value (save it locally first; see Decision 1).
- Auto-opening the default view on Emacs startup or on visiting the account's org file — v1 is the command + the =l= binding only.
- An ad-hoc filter (not saved as a local view) as a default.
- Per-project or context-sensitive defaults.
* Implementation tasks (drop-in for todo.org)
#+begin_src org
,** TODO [#B] Default view — resolver + open command :feature:
=pearl-default-view= defcustom (legacy) + =:default-view= account-plist key; =pearl--resolve-default-view= returns the active scope's default (per-account in accounts mode, defcustom in legacy); =pearl-open-default-view= dispatches nil → my-open-issues, a local-view name → =pearl-run-local-view=, a stale name → my-open-issues + message (never errors). Spec: [[file:docs/default-view-spec.org]] (phase 1).
,** TODO [#B] Default view — set-default-view command :feature:
=pearl-set-default-view= (the reserved =C-; L v D=): from a local-view buffer offers that view; otherwise prompts over local-view names with a clear-to-my-open-issues sentinel. Persists via =customize-save-variable= to the active account's =:default-view= (accounts mode) or =pearl-default-view= (legacy). Spec: [[file:docs/default-view-spec.org]] (phase 2).
,** TODO [#B] Default view — keymap, transient, docs :feature:
Rebind top-level =C-; L l= to =pearl-open-default-view= (=f o= stays literal my-open-issues); bind =C-; L v D= to =pearl-set-default-view=; add the =.= set-default-view suffix to the transient Views group (=D= stays save-locally). Update keymap + menu tests + README (default view, =l= behavior, per-account scoping). Spec: [[file:docs/default-view-spec.org]] (phase 3).
,** TODO [#B] Default view — test surface :test:
Unit: =pearl--resolve-default-view= returns =pearl-default-view= in legacy mode and the active account's =:default-view= in accounts mode; the accounts-mode setter rewrite updates only =:default-view= and preserves other plist keys; stale-name fallback. Command flow: nil default dispatches to =pearl-list-issues= (no project), an existing local-view name to =pearl-run-local-view=, a stale name to =pearl-list-issues= with the fallback message; setter from a local-view buffer offers that view, from elsewhere prompts with the clear sentinel, cancel leaves state unchanged; persistence calls =customize-save-variable= for the right scope. Surface: =C-; L l= → =pearl-open-default-view=, =C-; L f o= → =pearl-list-issues=, =C-; L v D= → =pearl-set-default-view=, transient Views =.= → set-default-view with no duplicate suffix. E2e/manual-verify: =C-; L l= opens the configured default; switching accounts switches which default opens. Spec: [[file:docs/default-view-spec.org]] (Acceptance criteria).
#+end_src
* Review dispositions
Codex returned =Not ready= with one blocker and one medium; both accepted.
- *HP1 (transient set-default-view key unspecified; =D= taken) — accepted.* The spec now binds the transient suffix =.= for =pearl-set-default-view= in the Views group, leaves the shipped =D= = save-locally suffix untouched, and explains why the transient key differs from the keymap's =v D= (flat keyspace, read off the screen — the same surface-divergence the keybinding reconciliation established). Decision 3, the Current-state bullet, acceptance, phase 3, and the phase-3 drop-in task all name =.= now. The key is a free-slot pick, flagged as easy to change at implementation.
- *MP1 (missing =:test:= drop-in task) — accepted.* Added a fourth drop-in task, =Default view — test surface :test:=, mirroring the acceptance criteria (resolver in both modes, plist-key preservation, stale-name fallback, dispatch, setter paths, persistence, surface, e2e).
Supporting recommendations folded in: the Current-state transient fact corrected to the real binding (=D= = save-locally, =v D= reserved); the metadata-preserving account rewrite (preserve unrelated plist keys) added to Decision 4, acceptance, phase 2, and the test surface; =pearl-accounts='s docstring/=:value-type= documenting =:default-view= added to acceptance and phase 1. The remaining test-strategy points were already covered by acceptance and the new test-surface task. Status raised to Ready.
* Review and iteration history
** 2026-06-02 Tue @ 12:18:21 -0500 — Codex — reviewer
- *What changed or was recommended:* Reviewed the spec against the current local-view, account, issue-list, keymap, transient, README, tests, and task-tracking surfaces. Verdict: =Not ready=. One high-priority blocker: the spec asks the transient Views group to expose =pearl-set-default-view= but does not choose a flat transient key, and =D= is already used by =pearl-save-linear-view-locally=. One medium finding: the drop-in task block needs the required test-surface entry.
- *Why:* The core feature design is coherent, but command-surface keys are visible UX and must not be invented during implementation. The task handoff also needs a dedicated test entry so acceptance coverage survives into =todo.org=.
- *Artifacts:* =default-view-spec-review.org= (consumed and deleted by the response pass below).
** 2026-06-02 Tue @ 12:30:50 -0500 — Claude Code (pearl) — responder
- *What changed:* Accepted both findings. HP1: bound the transient suffix =.= to =pearl-set-default-view= (keeping =D= = save-locally), with Decision 3 explaining the keymap/transient key divergence; updated the Current-state bullet, acceptance, phase 3, and the phase-3 drop-in task to name =.=. MP1: added the fourth =:test:= drop-in task. Folded the supporting recommendations: corrected the Current-state transient fact, added the metadata-preserving account rewrite to Decision 4 / acceptance / phase 2 / test surface, and added =pearl-accounts= docstring/=:value-type= documentation to acceptance and phase 1. Raised status to Ready.
- *Why:* The blocker was a real visible-UX gap — naming the transient key now keeps the implementer from inventing command surface mid-build. The plist-preservation and docstring points prevent a setter that clobbers account config or an undocumented Customize key.
- *Artifacts:* Review file deleted on completion.
|