aboutsummaryrefslogtreecommitdiff
path: root/pearl.el
diff options
context:
space:
mode:
Diffstat (limited to 'pearl.el')
-rw-r--r--pearl.el347
1 files changed, 304 insertions, 43 deletions
diff --git a/pearl.el b/pearl.el
index 78612a1..7214214 100644
--- a/pearl.el
+++ b/pearl.el
@@ -62,6 +62,7 @@
(require 'org)
(require 'cl-lib)
(require 'transient)
+(require 'auth-source)
;;; Customization and Variables
(defgroup pearl nil
@@ -101,6 +102,34 @@ Defaults to \\='gtd/linear.org\\=' in your `org-directory'."
:type 'file
:group 'pearl)
+(defcustom pearl-accounts nil
+ "Named Linear accounts for multi-workspace use.
+An alist of (NAME . PLIST) where NAME is a string and PLIST carries the
+per-workspace settings:
+
+ :api-key-source how the key is *found* (not the key itself), a tagged
+ list -- one of (:auth-source :host H :user U),
+ (:env \"VAR\"), or (:literal \"lin_...\").
+ :org-file the account's active org file (`~' is expanded).
+ :default-team-id default team for new issues; optional.
+ :url GraphQL endpoint; optional, defaults to
+ `pearl-graphql-url'.
+
+Leave nil for legacy single-account behavior off the standalone
+`pearl-api-key' / `pearl-org-file-path' globals. See
+`pearl-switch-account' and `pearl-default-account'."
+ :type '(alist :key-type string :value-type plist)
+ :group 'pearl)
+
+(defcustom pearl-default-account nil
+ "Name of the account `pearl-active-account' resolves to at first need.
+A durable preference (unlike the runtime `pearl-active-account'). When
+`pearl-accounts' is set and no account is active yet, the first command that
+needs account context resolves this name; if it is nil, that command errors
+asking the user to set it or run `pearl-switch-account'."
+ :type '(choice (const :tag "None" nil) string)
+ :group 'pearl)
+
(defcustom pearl-async-default t
"Use async API calls by default.
When t, all API calls will be asynchronous unless explicitly overridden.
@@ -373,6 +402,190 @@ Backs the comment-edit permission check; clear it to force a refresh.")
(defvar pearl--active-requests 0
"Number of currently active API requests.")
+;;; Multi-Account Support
+
+(defvar pearl-active-account nil
+ "Name of the Linear account currently active, or nil.
+Runtime state, never a persisted defcustom: which account you are on right
+now. `pearl-switch-account' sets it with `setq'; persisting it would make
+the last switch stick across sessions and fight `pearl-default-account'.")
+
+(defun pearl--resolve-api-key-source (source account)
+ "Resolve SOURCE to an API key string for ACCOUNT (named for error messages).
+SOURCE is a tagged list: (:literal KEY), (:env VAR), or
+\(:auth-source :host H :user U). Errors -- naming ACCOUNT, never dumping the
+lookup -- when the key cannot be found or SOURCE is malformed."
+ (pcase source
+ (`(:literal ,key) key)
+ (`(:env ,var)
+ (or (getenv var)
+ (error "No API key for account %s: environment variable %s is unset"
+ account var)))
+ (`(:auth-source . ,plist)
+ (let* ((found (car (apply #'auth-source-search
+ (append plist '(:require (:secret) :max 1)))))
+ (secret (and found (plist-get found :secret))))
+ (cond
+ ((null secret)
+ (error "No API key for account %s: auth-source found no matching entry"
+ account))
+ ((functionp secret) (funcall secret))
+ (t secret))))
+ (_ (error "No API key for account %s: unrecognized :api-key-source form"
+ account))))
+
+(defun pearl--resolve-account (name)
+ "Resolve account NAME from `pearl-accounts' to a context plist.
+The plist is (:name :api-key :org-file :default-team-id :url) with the key
+resolved from `:api-key-source', `:org-file' expanded, and `:url' defaulting
+to `pearl-graphql-url'. Errors when NAME is unknown or its key is missing."
+ (let ((spec (cdr (assoc name pearl-accounts))))
+ (unless spec
+ (error "No Pearl account named %s" name))
+ (let ((org-file (plist-get spec :org-file)))
+ (list :name name
+ :api-key (pearl--resolve-api-key-source
+ (plist-get spec :api-key-source) name)
+ :org-file (and org-file (expand-file-name org-file))
+ :default-team-id (plist-get spec :default-team-id)
+ :url (or (plist-get spec :url) pearl-graphql-url)))))
+
+(defun pearl--current-account-context ()
+ "Return the active account's context plist, or nil in legacy single-account mode.
+Returns nil when `pearl-accounts' is empty (downstream then reads the legacy
+globals unchanged). Otherwise resolves `pearl-active-account', falling back to
+`pearl-default-account' on first need -- which it then records as active.
+Errors when accounts are configured but neither is set."
+ (when pearl-accounts
+ (let ((name (or pearl-active-account
+ pearl-default-account
+ (user-error (concat "Pearl accounts are configured but none is active; "
+ "set `pearl-default-account' or run "
+ "`pearl-switch-account'")))))
+ (unless pearl-active-account
+ (setq pearl-active-account name))
+ (pearl--resolve-account name))))
+
+(defvar pearl--account-context nil
+ "The account context bound for the current dynamic extent, or nil.
+`pearl--with-account-context' binds this so async continuations -- pagination
+and render/mutate callbacks -- re-establish the account that was active when
+the request was dispatched, rather than reading whatever account happens to be
+active when they later run.")
+
+(defmacro pearl--with-account-context (context &rest body)
+ "Evaluate BODY with the legacy globals bound to CONTEXT's account values.
+CONTEXT is a context plist from `pearl--resolve-account'. A nil CONTEXT is a
+passthrough: BODY runs against the current globals (legacy single-account
+mode), so nothing changes when `pearl-accounts' is unset. Also binds
+`pearl--account-context' so a nested async dispatch propagates the same
+account into its own callbacks."
+ (declare (indent 1) (debug (form body)))
+ (let ((ctx (make-symbol "ctx")))
+ `(let ((,ctx ,context))
+ (if (null ,ctx)
+ (progn ,@body)
+ (let ((pearl--account-context ,ctx)
+ (pearl-api-key (plist-get ,ctx :api-key))
+ (pearl-graphql-url (plist-get ,ctx :url))
+ (pearl-org-file-path (or (plist-get ,ctx :org-file)
+ pearl-org-file-path))
+ (pearl-default-team-id (plist-get ,ctx :default-team-id)))
+ ,@body)))))
+
+(defun pearl--read-buffer-account (&optional buffer)
+ "Return the `#+LINEAR-ACCOUNT' name BUFFER carries, or nil.
+BUFFER defaults to the current buffer; the search is bounded to the preamble so
+it stays cheap on large buffers, matching `pearl--read-active-source'."
+ (with-current-buffer (or buffer (current-buffer))
+ (save-excursion
+ (goto-char (point-min))
+ (when (re-search-forward "^#\\+LINEAR-ACCOUNT: \\(.*\\)$" 4000 t)
+ (string-trim (match-string 1))))))
+
+(defun pearl--require-account-context (&optional mutation)
+ "Guard a current-buffer command against the active Linear account.
+Returns the active account's context plist, or nil in legacy single-account
+mode (`pearl-accounts' unset), where there is nothing to guard.
+
+Otherwise enforces buffer ownership before any API call:
+- buffer owned by the active account -> proceed;
+- buffer owned by a *different* configured account -> refuse, naming both, so a
+ command can't run under one account against another's file;
+- no `#+LINEAR-ACCOUNT' marker and MUTATION non-nil -> refuse until a refresh
+ stamps ownership; a non-mutating render/list command proceeds (it will stamp
+ ownership on its next write)."
+ (when pearl-accounts
+ (let* ((ctx (pearl--current-account-context))
+ (active (plist-get ctx :name))
+ (buffer-account (pearl--read-buffer-account)))
+ (cond
+ ((and buffer-account (not (string-equal buffer-account active)))
+ (user-error (concat "This file belongs to %s; active account is %s. "
+ "Run M-x pearl-switch-account first")
+ buffer-account active))
+ ((and (null buffer-account) mutation)
+ (user-error "No LINEAR-ACCOUNT in this file; refresh it under an account first"))
+ (t ctx)))))
+
+(defun pearl--ensure-buffer-account-header ()
+ "Stamp `#+LINEAR-ACCOUNT' into the current buffer if missing (accounts mode).
+A no-op in legacy mode or when the marker is already present. The merge/refresh
+path doesn't rebuild the header through `pearl--build-org-content', so without
+this a legacy file (rendered before accounts were configured) would never
+acquire ownership -- leaving the buffer guard's \"refresh it under an account
+first\" advice unresolvable, since the refresh itself wouldn't stamp it."
+ (when (and pearl--account-context
+ (not (pearl--read-buffer-account)))
+ (save-excursion
+ (goto-char (point-min))
+ (when (re-search-forward "^#\\+LINEAR-SOURCE:" nil t)
+ (beginning-of-line)
+ (insert (format "#+LINEAR-ACCOUNT: %s\n"
+ (plist-get pearl--account-context :name)))))))
+
+(defun pearl--clear-account-scoped-state ()
+ "Clear every cache scoped to a single Linear workspace.
+The one place that lists the account-scoped caches -- the five Linear lookups
+plus any future account-derived state -- so a newly added cache is registered
+here and `pearl-switch-account' invalidates it without re-implementing cache
+semantics. A personal workspace's teams must never bleed into a work one."
+ (setq pearl--cache-teams nil
+ pearl--cache-states nil
+ pearl--cache-team-collections nil
+ pearl--cache-views nil
+ pearl--cache-viewer nil))
+
+(defun pearl--mode-line-lighter ()
+ "Return the `pearl-mode' mode-line lighter, naming the active account.
+\" Pearl[work]\" when accounts are configured and one is active; plain
+\" Pearl\" in legacy single-account mode."
+ (if (and pearl-accounts pearl-active-account)
+ (format " Pearl[%s]" pearl-active-account)
+ " Pearl"))
+
+(defun pearl-switch-account (name)
+ "Switch the active Linear account to NAME.
+Interactively, completes over the configured `pearl-accounts'. Resolves the
+account (erroring on an unknown name or missing key), makes it active, clears
+the account-scoped caches so the next lookup refetches against the new
+workspace, refreshes the mode-line indicator, and visits the account's org file
+when it exists."
+ (interactive
+ (list (if (null pearl-accounts)
+ (user-error "No accounts configured; set `pearl-accounts' first")
+ (completing-read "Switch to account: "
+ (mapcar #'car pearl-accounts) nil t))))
+ (let ((ctx (pearl--resolve-account name)))
+ (setq pearl-active-account name)
+ (pearl--clear-account-scoped-state)
+ (force-mode-line-update t)
+ (let ((file (plist-get ctx :org-file)))
+ (if (and file (file-exists-p file))
+ (progn (find-file file)
+ (message "Switched to account %s" name))
+ (message "Switched to account %s (no org file yet; run a fetch)" name)))))
+
;;; Core API Functions (Async-First Architecture)
(defun pearl--headers ()
@@ -428,38 +641,49 @@ If SUCCESS-FN or ERROR-FN are not provided, default handlers will be used."
(message "Linear API error: %s" error-thrown)
(pearl--log "Error response: %s" (prin1-to-string data)))))
- (let ((request-data (json-encode `(("query" . ,query)
- ,@(when variables `(("variables" . ,variables))))))
- ;; Build headers before bumping the counter: `pearl--headers' signals
- ;; when the API key is unset, and that must happen before the increment
- ;; or no callback ever runs to decrement it (leaking the count).
- (headers (pearl--headers)))
- (pearl--log "Request payload: %s" request-data)
- (setq pearl--active-requests (1+ pearl--active-requests))
-
- (request
- pearl-graphql-url
- :type "POST"
- :headers headers
- :data request-data
- :parser 'json-read
- :success (cl-function
- (lambda (&key data &allow-other-keys)
- (setq pearl--active-requests (1- pearl--active-requests))
- (pearl--log "Response received: %s" (prin1-to-string data))
- (funcall success-fn data)))
- :error (cl-function
- (lambda (&key error-thrown response data &allow-other-keys)
- (setq pearl--active-requests (1- pearl--active-requests))
- (pearl--log "Error: %s" error-thrown)
- ;; Guard the status-code read: `response' can be nil on some
- ;; transport failures, and the accessor errors on a non-struct.
- (when (request-response-p response)
- (pearl--log "Response status: %s"
- (request-response-status-code response)))
- (when data
- (pearl--log "Error response: %s" (prin1-to-string data)))
- (funcall error-fn error-thrown response data))))))
+ ;; Snapshot the account context once, at dispatch. Reuse the context already
+ ;; bound by an enclosing `pearl--with-account-context' (pagination, a wrapping
+ ;; command); otherwise resolve the active account now. The macro binds the
+ ;; legacy globals for the synchronous header build and is re-established around
+ ;; each callback, so a mid-flight `pearl-switch-account' can't bleed into a
+ ;; request that was already dispatched. In legacy mode (no `pearl-accounts')
+ ;; the context is nil and the macro is a passthrough.
+ (let ((ctx (or pearl--account-context (pearl--current-account-context))))
+ (pearl--with-account-context ctx
+ (let ((request-data (json-encode `(("query" . ,query)
+ ,@(when variables `(("variables" . ,variables))))))
+ ;; Build headers before bumping the counter: `pearl--headers' signals
+ ;; when the API key is unset, and that must happen before the increment
+ ;; or no callback ever runs to decrement it (leaking the count).
+ (headers (pearl--headers)))
+ (pearl--log "Request payload: %s" request-data)
+ (setq pearl--active-requests (1+ pearl--active-requests))
+
+ (request
+ pearl-graphql-url
+ :type "POST"
+ :headers headers
+ :data request-data
+ :parser 'json-read
+ :success (cl-function
+ (lambda (&key data &allow-other-keys)
+ (setq pearl--active-requests (1- pearl--active-requests))
+ (pearl--log "Response received: %s" (prin1-to-string data))
+ (pearl--with-account-context ctx
+ (funcall success-fn data))))
+ :error (cl-function
+ (lambda (&key error-thrown response data &allow-other-keys)
+ (setq pearl--active-requests (1- pearl--active-requests))
+ (pearl--log "Error: %s" error-thrown)
+ ;; Guard the status-code read: `response' can be nil on some
+ ;; transport failures, and the accessor errors on a non-struct.
+ (when (request-response-p response)
+ (pearl--log "Response status: %s"
+ (request-response-status-code response)))
+ (when data
+ (pearl--log "Error response: %s" (prin1-to-string data)))
+ (pearl--with-account-context ctx
+ (funcall error-fn error-thrown response data)))))))))
(defun pearl--graphql-request (query &optional variables)
"Synchronous wrapper for GraphQL requests (backward compatibility).
@@ -1620,11 +1844,7 @@ none, returns nil. FORCE refreshes the collection cache first."
"Clear the Linear lookup caches (teams, states, per-team collections, views).
Use after renaming things in Linear, or to force the next lookup to refetch."
(interactive)
- (setq pearl--cache-teams nil
- pearl--cache-states nil
- pearl--cache-team-collections nil
- pearl--cache-views nil
- pearl--cache-viewer nil)
+ (pearl--clear-account-scoped-state)
(message "Linear caches cleared"))
(defun pearl-update-issue-state (issue-id state-name team-id)
@@ -2677,6 +2897,7 @@ unpushed local edits, they are stashed first (kill ring + the conflict-backup
buffer) so the refresh can't silently lose them (decision 4), then the refresh
proceeds and the subtree is replaced with Linear's version."
(interactive)
+ (pearl--require-account-context)
(save-excursion
(pearl--goto-issue-heading-or-error)
(let ((issue-id (org-entry-get nil "LINEAR-ID"))
@@ -2822,6 +3043,7 @@ comment instead of the one-line minibuffer. BODY, when supplied
non-interactively, is sent as-is. Works from anywhere inside an issue subtree;
the new comment is the viewer's own, so it renders editable."
(interactive (list nil))
+ (pearl--require-account-context t)
(save-excursion
(pearl--goto-issue-heading-or-error)
(let ((issue-id (org-entry-get nil "LINEAR-ID"))
@@ -2843,6 +3065,7 @@ the issue body and syncs to Linear through the usual conflict gate, C-c C-k
cancels. An alternative to editing the description inline for anyone who wants
a dedicated buffer. Works from anywhere inside an issue subtree."
(interactive)
+ (pearl--require-account-context t)
(save-excursion
(pearl--goto-issue-heading-or-error)
(let ((issue-id (org-entry-get nil "LINEAR-ID"))
@@ -2909,6 +3132,7 @@ Works from anywhere inside an issue subtree. Confirms first, then issues a
soft delete (Linear moves the issue to Trash, recoverable for about 30 days);
on success the issue's Org subtree is removed from the buffer."
(interactive)
+ (pearl--require-account-context t)
(let* ((marker (save-excursion (pearl--goto-issue-heading-or-error) (point-marker)))
(issue-id (org-entry-get marker "LINEAR-ID"))
(ident (or (org-entry-get marker "LINEAR-IDENTIFIER") "this issue")))
@@ -2999,6 +3223,7 @@ including one a TODO-keyword cycle can't express."
(mapcar (lambda (s) (cdr (assoc 'name s)))
(and team-id (pearl--team-states team-id)))
nil t))))
+ (pearl--require-account-context t)
(save-excursion
(pearl--goto-issue-heading-or-error)
(let* ((team-id (org-entry-get nil "LINEAR-TEAM-ID"))
@@ -3030,6 +3255,7 @@ inside an issue subtree."
(list (completing-read "Assignee: "
(pearl--team-collection-names 'members team-id)
nil t))))
+ (pearl--require-account-context t)
(save-excursion
(pearl--goto-issue-heading-or-error)
(let* ((team-id (org-entry-get nil "LINEAR-TEAM-ID"))
@@ -3066,6 +3292,7 @@ inside an issue subtree."
(list (completing-read-multiple
"Labels (comma-separated, empty to clear): "
(pearl--team-collection-names 'labels team-id)))))
+ (pearl--require-account-context t)
(save-excursion
(pearl--goto-issue-heading-or-error)
(let* ((team-id (org-entry-get nil "LINEAR-TEAM-ID"))
@@ -3157,13 +3384,16 @@ only add the ones a failed team fetch left out."
issues))))
(append team-states issue-states))))
-(defun pearl--build-org-content (issues &optional source truncated states)
+(defun pearl--build-org-content (issues &optional source truncated states account)
"Build the Org content string for the linear org file from ISSUES.
SOURCE is the active-source descriptor recorded in the header so a later
`pearl-refresh-current-view' can re-run it; TRUNCATED marks that the page cap
was hit. STATES is the ordered workflow-state list (see
`pearl--gather-header-states') the `#+TODO:' line is derived from; a nil STATES
-yields the hardcoded default. Pure function, no side effects."
+yields the hardcoded default. ACCOUNT, when non-nil, is the Linear account name
+stamped as `#+LINEAR-ACCOUNT' so the buffer guard can tell which workspace owns
+this file; nil (legacy single-account mode) omits the marker. Pure function,
+no side effects."
(let* ((src (or source '(:type filter :name "Linear issues" :filter nil)))
(name (pearl--source-name src))
(filter (plist-get src :filter)))
@@ -3175,6 +3405,8 @@ yields the hardcoded default. Pure function, no side effects."
(insert (format "#+TODO: %s\n" (pearl--derive-todo-line states)))
;; Source-tracking metadata: the serialized source drives refresh; the
;; rest is human-readable provenance.
+ (when account
+ (insert (format "#+LINEAR-ACCOUNT: %s\n" account)))
(insert (format "#+LINEAR-SOURCE: %s\n" (prin1-to-string src)))
(insert (format "#+LINEAR-RUN-AT: %s\n" (format-time-string "%Y-%m-%d %H:%M")))
(insert (format "#+LINEAR-FILTER: %s\n" (pearl--summarize-filter filter)))
@@ -3227,8 +3459,10 @@ In every case the resulting buffer is surfaced (see `pearl--surface-buffer'),
so a fetch run from a menu or dashboard actually shows the issues instead of
just writing the file and leaving it off-screen."
(let* ((org-file-path pearl-org-file-path)
+ (account (and pearl--account-context
+ (plist-get pearl--account-context :name)))
(states (pearl--gather-header-states issues))
- (new-content (pearl--build-org-content issues source truncated states))
+ (new-content (pearl--build-org-content issues source truncated states account))
(existing-buf (find-buffer-visiting org-file-path)))
(cond
;; Branch A: no buffer visits the file -- atomic file write, then visit.
@@ -3429,6 +3663,16 @@ active file with the query recorded as the source."
(order (plist-get spec :order))
(source (list :type 'filter :name name :filter filter-plist
:sort sort :order order)))
+ ;; Account guard, checked before any filter compilation or network call:
+ ;; a query tagged for another account would otherwise resolve its team /
+ ;; state / label names against the wrong workspace. Inert in legacy mode
+ ;; and for an untagged (shared) query.
+ (let ((q-account (plist-get spec :account)))
+ (when (and pearl-accounts q-account)
+ (let ((active (plist-get (pearl--current-account-context) :name)))
+ (unless (string-equal q-account active)
+ (user-error "Saved query '%s' belongs to %s; switch accounts first"
+ name q-account)))))
;; Validate the saved query's authoring plist at the command boundary so
;; a typo'd key or bad value surfaces as a clear user-error instead of
;; being silently dropped or failing deeper in the compiler.
@@ -3988,6 +4232,7 @@ the user can re-sync with Replace to reconcile."
nil t))))
(when (pearl--filter-sentinel-value-p name)
(user-error "Cancelled; no saved query synced"))
+ (pearl--require-account-context t)
(let ((entry (assoc name pearl-saved-queries)))
(unless entry
(user-error "No saved query named %s" name))
@@ -4119,6 +4364,7 @@ name to sync under), or no local saved query matches the recorded name.
Bound under \\='C-; L f P\\=' inside `pearl-mode' buffers; reachable
through the transient as the `U' (upload) entry in the Fetch group."
(interactive)
+ (pearl--require-account-context t)
(let* ((source (pearl--read-active-source))
(type (and source (plist-get source :type)))
(name (and source (plist-get source :name))))
@@ -5024,6 +5270,7 @@ once, and only if comments are dirty; then each dirty field is saved
sequentially through its conflict gate and a single summary is reported. A
clean issue does no network at all."
(interactive)
+ (pearl--require-account-context t)
(save-excursion
(pearl--goto-issue-heading-or-error)
(let* ((marker (point-marker))
@@ -5154,6 +5401,7 @@ counts; declining mutates nothing. On confirm the dirty fields save
sequentially, continuing past any per-field conflict, and one summary is
reported. A clean file does nothing."
(interactive)
+ (pearl--require-account-context t)
(let* ((buffer (current-buffer))
(scan (pearl--scan-all-dirty)))
(if (null scan)
@@ -5246,7 +5494,10 @@ edit-then-merge flow keeps the user's expanded subtrees expanded."
"Refresh the active file's run-at, count, and truncation header lines.
ISSUE-COUNT is the new issue total; TRUNCATED marks a page-cap hit. The
`#+LINEAR-SOURCE:' descriptor is left untouched -- only the human-readable
-provenance advances on a refresh."
+provenance advances on a refresh. Under accounts mode an unmarked file also
+acquires its `#+LINEAR-ACCOUNT' stamp here (see
+`pearl--ensure-buffer-account-header')."
+ (pearl--ensure-buffer-account-header)
(save-excursion
(goto-char (point-min))
(when (re-search-forward "^#\\+LINEAR-RUN-AT: .*$" nil t)
@@ -5341,6 +5592,7 @@ are dropped, while any subtree with unpushed edits is kept. This is the
same-source counterpart to the replace-on-switch behavior of the query and view
commands. Errors if no source is recorded."
(interactive)
+ (pearl--require-account-context)
(let ((source (pearl--read-active-source))
(buffer (current-buffer)))
(unless source
@@ -5628,8 +5880,15 @@ resolves to a single label."
;;;###autoload
(defun pearl-load-api-key-from-env ()
- "Try to load Linear API key from environment variable."
+ "Try to load Linear API key from environment variable.
+Legacy single-account path only: once `pearl-accounts' is configured this
+refuses, so it can't bypass the per-account resolver and overwrite the active
+account's key in memory. Use `:api-key-source (:env \"...\")' in
+`pearl-accounts' instead."
(interactive)
+ (when pearl-accounts
+ (user-error
+ "Accounts are configured; put :api-key-source (:env \"...\") in `pearl-accounts'"))
(let ((env-key (getenv "LINEAR_API_KEY")))
(if env-key
(progn
@@ -5784,6 +6043,7 @@ description sync -- unchanged since fetch sends nothing, a local edit against
an unchanged remote pushes, and a both-sides-changed case is refused and
reported (refresh to reconcile)."
(interactive)
+ (pearl--require-account-context t)
(save-excursion
(pearl--goto-heading-or-error "Not on a Linear comment")
(let ((comment-id (org-entry-get nil "LINEAR-COMMENT-ID"))
@@ -5814,6 +6074,7 @@ with no stored hash, gets a stronger discard-local-edits confirmation. On
success the comment's Org subtree is removed; sibling comments and the issue
body stay."
(interactive)
+ (pearl--require-account-context t)
(save-excursion
(pearl--goto-heading-or-error "Not on a Linear comment")
(let ((comment-id (org-entry-get nil "LINEAR-COMMENT-ID"))
@@ -6035,7 +6296,7 @@ Binds `pearl-prefix-map' under `pearl-keymap-prefix' so every Pearl command is
reachable from the keyboard. Turns on automatically in any buffer Pearl
rendered (one carrying a `#+LINEAR-SOURCE' header); see
`pearl--maybe-enable-mode' on `org-mode-hook'."
- :lighter " Pearl"
+ :lighter (:eval (pearl--mode-line-lighter))
:keymap pearl-mode-map)
(defun pearl--buffer-is-pearl-p ()