Releases: lea-151107/pollen
v1.7.4
pollen v1.7.4
A single fix for a regression v1.7.1 introduced when it
added Ctrl+P as the Settings overlay binding.
The Settings overlay deliberately makes it hard to close
by accident while you are editing a field: q types into
the field and Esc only exits the editor back to
navigation, so closing always takes two deliberate steps.
Ctrl+P bypassed all of that. handleKey intercepts the
Settings binding while the overlay is open and calls
Close() before the keystroke ever reaches the panel, and
that interception never checked whether a field editor was
active. So pressing Ctrl+P (or the Ctrl+, alias) mid-edit
closed the whole overlay and silently discarded whatever
you had typed.
This is the mirror image of the v1.7.2 fix. That release
guarded the overlay-OPEN path so Ctrl+P wouldn't steal
bubbles' textarea LinePrevious / textinput PrevSuggestion
bindings while you were typing in the main UI. The
overlay-CLOSE path needed the same guard and didn't have
it. The close shortcut now consults a new
SettingsPanel.IsEditing(): while a field editor is active,
Ctrl+P falls through to the editor (a no-op for the plain
textinput) and the edit is preserved; from navigation mode
Ctrl+P still toggles the overlay closed as before.
The bug survived because the app's key-routing layer had
no key-driven tests at all. v1.7.4 adds the first one
(internal/app/update_settings_keys_test.go), which fails
on the pre-fix routing and passes after.
Fixed:
- Ctrl+P (and the Ctrl+, alias) no longer closes the
Settings overlay while a field editor is active; it
falls through to the editor and the in-progress edit is
preserved. From navigation mode it still toggles the
overlay closed - Added SettingsPanel.IsEditing() so the app routing can
honor the panel's existing "no accidental close while
editing" protection
Notes:
- v1.x SemVer-frozen surface unchanged. The Settings
binding's reach is narrowed (no longer fires while a
field editor is active), not extended - First app-layer test that drives Model.Update with key
messages; the routing layer had no key-driven coverage
before, which is why the regression slipped through - The applySettings httpx package-globals data race
flagged in v1.7.2 / v1.7.3 is unchanged and its deferred
fix plan still stands
See CHANGELOG.md for the full list.
v1.7.3
pollen v1.7.3
The Ctrl+/ help overlay gains two action buttons. Until
now it was strictly an informational accordion: ↑/↓ moved
between section headers, Enter expanded or collapsed
them. The two new buttons sit above the section list and
respond to Enter the way buttons should — they run an
action.
The first button, "Open Settings", opens the Settings
overlay directly. Same code path as Ctrl+P, but reachable
from inside the help so a user who opened Ctrl+/ looking
for "where do I change the timeout" doesn't have to close
the help, recall the Ctrl+P binding, and reopen another
overlay. The second button, "Reset settings to defaults",
resets every settings.json key to its built-in default.
Because this is destructive, the button does not fire
immediately — it switches the help body to a y/n
confirmation view ("Reset all settings to their default
values?"). Y runs the reset (applySettings on
settings.Defaults(), which dispatches the runtime globals
and persists the new file); N or Esc returns to the
normal help. A status-bar toast ("settings reset to
defaults") confirms the reset succeeded.
A small extraction supports the reset path:
internal/settings now exposes a Defaults() function
that returns the canonical *Settings, matching what
Load() yields when no file is present. Previously those
defaults lived inline in Load(); both startup and the
in-TUI reset now share the same source of truth.
Added:
- "Open Settings" button at the top of the Ctrl+/ help
overlay. Enter opens the Settings overlay (same as
Ctrl+P) - "Reset settings to defaults" button at the top of the
Ctrl+/ help overlay with a y/n confirm step. Y resets
every settings.json key to its default and applies it
immediately; N or Esc cancels settings.Defaults()returning the canonical default
*Settings, used by the new reset path
Changed:
- Help overlay focus now spans buttons + sections in a
single ↑/↓ cycle. Cursor starts on the first button
when Ctrl+/ opens. Enter on a button activates it;
Enter on a section header still expands or collapses - Help overlay footer hint updated to reflect the new
Enter semantics ("Enter: run / expand")
Notes:
- v1.x SemVer-frozen surface unchanged. No new key
bindings, no settings keys, no persistence format
change. The reset action is reachable only through the
help overlay's button, not from a global shortcut - The remaining
applySettingsdata race againsthttpx
package globals (flagged in v1.7.2) is unchanged here.
The new reset path goes through the sameapplySettings
call site, so it inherits the same lower-priority issue
and the same deferred fix plan (Snapshot pattern or
atomic primitives inhttpx)
See CHANGELOG.md for the full list.
v1.7.2
pollen v1.7.2
Three fixes spanning v1.7.0 and v1.7.1. The first two are
v1.7.0 defects exposed by a follow-up audit; the third is
a regression v1.7.1 introduced when it added Ctrl+P as the
Settings binding.
The OAuth DC status renderer evaluated its switch in the
wrong order. The static token != nil case came first,
which meant a re-fetch on a Device Code panel that already
held a hydrated token left the stale "Bearer …" preview on
screen for the entire flow. The new IdP-issued user_code
was hidden behind the old token, so the user couldn't
transcribe it on the second device, the Poll loop ran for
30 minutes returning authorization_pending, and the only
visible signal of failure was a 4-second toast at the very
end. A failed re-fetch had a parallel issue: the error
sat behind the stale token until the user pressed d to
forget it. The cases are now ordered the same way CC and
AC have always ordered theirs — transient state first
(polling with or without user_code), then error, then the
static Bearer preview.
The Settings overlay's float validator accepted "NaN".
strconv.ParseFloat parses "NaN" successfully into
math.NaN(), and IEEE 754 says NaN comparisons return
false against everything — so the f <= min || f >= max
range check let NaN slip past. The Response panel ratio
field then carried NaN into View's layout math
(width × ratio), and the column counts for the rest of
the session were undefined. (Saving to disk was prevented
by encoding/json, which refuses to marshal NaN, so the
on-disk file was unharmed.) The validator now rejects
NaN and ±Inf explicitly via math.IsNaN and math.IsInf
before the range check.
The Ctrl+P binding v1.7.1 added for the Settings overlay
silently broke two bubbles default bindings:
bubbles/textarea binds Ctrl+P to LinePrevious (move
cursor up one line, used in the Body editor) and
bubbles/textinput binds it to PrevSuggestion (cycle
backwards through autocompletions, used in Headers).
handleKey evaluated the Settings case before delegating
to the focused panel, so Ctrl+P never reached the
underlying widgets. Emacs-trained users editing a
multi-line body lost their cursor-up shortcut, and
Headers users lost suggestion cycling. The Settings
binding now carries the same isTextEditingFocus guard
that the u undo shortcut has used since v1.6 era —
inside a focused textinput or textarea, Ctrl+P falls
through to the widget; from any other focus, Ctrl+P
still opens Settings.
Fixed:
- renderOAuthDCStatus case order matches CC / AC: polling
beats stale token, error beats stale token. The user_code
is shown during re-fetch - Settings float validator rejects NaN, +Inf, and -Inf
before the range check - Ctrl+P (and Ctrl+, alias) Settings binding guarded by
isTextEditingFocus so bubbles textarea LinePrevious and
textinput PrevSuggestion defaults work again
Notes:
- v1.x SemVer-frozen surface unchanged. The Settings
binding's reach is narrowed (no longer fires inside text
editing), not extended - A real-but-lower-priority data race remains in
applySettings: it writes plain (non-atomic) httpx package
vars that the Send Cmd goroutine reads from httpx.Do.
Production occurrences are rare and effects are limited
to torn reads of primitive types. A Snapshot pattern or
atomic primitives in httpx is reserved for a future
release; CI's -race doesn't catch it because no test
exercises a parallel Send + Settings edit
See CHANGELOG.md for the full list.
v1.7.1
pollen v1.7.1
v1.7.0's Settings overlay was bound only to Ctrl+,,
mirroring VS Code and IntelliJ. That works fine in
graphical editors that intercept keyboard events at the OS
level, but inside a terminal it doesn't survive. The comma
key has no traditional ASCII control code — Ctrl+letter
maps to 0x01–0x1A, and Ctrl+[, ], ^, _ map to 0x1B–0x1F
because of historical Unix conventions, but Ctrl+, just
sends a plain comma (0x2C). bubbletea v1 doesn't enable
the kitty / CSI-u keyboard protocol that some modern
terminals (kitty, WezTerm, recent Alacritty) use to encode
the distinct sequence, so on xterm, GNOME Terminal,
macOS Terminal.app, Windows Terminal, and WSL — the
overwhelming majority of pollen's homes — Ctrl+, did
absolutely nothing.
The fix adds Ctrl+P (ASCII 0x10, recognised by every
terminal) as the primary binding. Ctrl+, is kept as an
alias so users on CSI-u-capable terminals still get the
VS Code muscle memory. The Help overlay's Global section
now reads "Ctrl+P / Ctrl+, : Open settings overlay",
reflecting both options.
The Settings overlay implementation itself is unchanged —
only the keybinding that opens it is fixed.
Fixed:
- Settings overlay primary keybinding switched to
Ctrl+P
for universal terminal compatibility;Ctrl+,retained
as an alias for kitty / CSI-u-capable terminals.
Pre-v1.7.1 the binding was effectively dead on every
standard terminal.
Notes:
- v1.x SemVer-frozen surface unchanged: additive only.
Ctrl+P is new, Ctrl+, remains documented. - The same kind of binding issue does not affect any other
pollen shortcut: every existing Ctrl+letter mapping uses
a key with a valid ASCII control code, and Ctrl+/ has the
Ctrl+_ alias precisely because both transmit the same
0x1F byte.
See CHANGELOG.md for the full list.
v1.7.0
pollen v1.7.0
The minor that completes pollen's OAuth roadmap and removes
the long-standing friction of editing settings.json by hand.
Two features, both reserved against scope-out items that
have been deferred since v1.5–v1.6.
OAuth Device Code (RFC 8628). pollen's previous OAuth
support — Client Credentials (v1.5.0) and Authorization
Code with PKCE (v1.6.0) — covered confidential machine-to-
machine clients and interactive desktop browsers. Neither
quite fits the environment pollen actually lives in:
SSH sessions on a remote server, WSL where the Linux
"open browser" command may not work, CI runners with no
display, containers. Device Authorization Grant is the
canonical OAuth flow for these cases — pollen never tries
to open a browser itself. It calls the IdP's device
authorization endpoint, displays a short user_code + URL,
and polls the token endpoint while the user completes the
authorization step on whatever device they already have
logged in (phone, laptop). When the user approves, pollen
gets the access token; the standard auto-refresh-on-send
machinery takes it from there.
The Auth panel gains a sixth tab, "OAuth DC", with five
fields (Device URL, Token URL, Client ID, Client Secret,
Scope) plus the action row. g starts the flow; Esc
cancels mid-flight. The user_code and verification URL
get prominent panel real estate during polling — three
dedicated lines because that's the user's transcription
target. RFC 8628 §3.5's state machine is honoured
verbatim: authorization_pending continues at the current
interval, slow_down adds 5 seconds, access_denied and
expired_token end the flow. The 30-minute total timeout
matches what typical IdPs allow for the verification
window.
Tokens land in ~/.config/pollen/oauth_tokens.json the
same way CC and AC tokens do (mode 0600, keyed by
URL+ClientID+Grant). The d-on-action-row "forget" key
works identically.
In-TUI Settings overlay (Ctrl+,). Until v1.7 the only
way to change pollen's behaviour beyond toggling TLS
verification or switching environments was to quit, edit
settings.json by hand, and restart. The new overlay
exposes all 17 settings keys as editable rows: bools
toggle on Enter, int/float/string fields drop into an
editor that validates against the same ranges Load()
clamps. Each commit lands in settings.json and is
applied to the matching runtime global immediately —
HTTP request timeout, response size cap, history limit,
intruder concurrency / delay / max requests, proxy URL,
disable-redirects, the OAuth-persist-tokens flag, and so
on. Two fields (CA cert file, Enable cookies) carry a
"restart" badge: they're consumed only at startup so the
change is saved but takes effect next launch.
Navigation matches the v1.6.1 accordion help — ↑/↓ or
j/k, g/G for first/last, PgUp/Dn to hop five, Esc or q to
close. Help section is updated so Ctrl+, is discoverable.
Added:
- OAuth DC auth type and Device Authorization Grant
implementation in internal/oauth/devicecode.go,
exposed via the AuthOAuthDC panel - Token persistence for device_code tokens (shares the
v1.6.4 oauth_tokens.json layer) - In-TUI Settings overlay covering all 17 settings.json
keys, opened with Ctrl+, - Three new intruder setter methods so settings changes
flow into the Intruder config defaults at runtime - Help section entries advertising both new features
Notes:
- v1.x SemVer-frozen surface gains the AuthOAuthDC value
and the Ctrl+, binding. Existing configuration files
load without modification - The v1.6.2-era openBrowser-failure recovery item is
closed by the Device Code addition: in any environment
where browser launch is unreliable, Device Code is the
right grant to pick. The plain Authorization Code path
is unchanged for environments where browser launch
works - CA cert file and Enable cookies still require restart;
their live-reload would require deeper transport-layer
surgery that is intentionally out of scope
See CHANGELOG.md for the full list.
v1.6.6
pollen v1.6.6
A tiny CI hygiene patch. No production code change, no
user-facing behavior change.
v1.6.4 shipped disk persistence for OAuth tokens with three
regression tests that verified the on-disk file ended up
with the requested POSIX file mode bits (0o600 for the
token store and SaveJSONSecure, 0o644 for the existing
SaveJSON). Those tests assumed POSIX semantics — which
they do hold on Linux and macOS. On Windows, however, file
permissions are ACL-based and Go's os.WriteFile reports
the resulting mode as 0o666 regardless of the requested
mode. Every windows-latest matrix job in pollen's CI has
therefore failed since v1.6.4, leaving the main branch CI
red for two releases.
The three tests are now guarded with runtime.GOOS == "windows"
skip so the Windows matrix passes again without removing
the POSIX coverage. Linux and macOS jobs continue to verify
that SaveJSON and SaveJSONSecure honour 0o644 and 0o600
respectively. The underlying production code is unchanged —
it was correct all along.
Fixed:
- POSIX file-mode regression tests now skip on Windows
instead of asserting 0o600 / 0o644 against the ACL-based
result. GitHub Actions CI returns to green on all six
matrix jobs (ubuntu-latest, macos-latest, windows-latest
× Go 1.21, stable)
Notes:
- No production code changed. Windows file protection is
still provided by the default ACL on the user's config
directory; pollen does not configure ACLs explicitly - v1.x SemVer surface unchanged. No settings, key bindings,
or persistence formats changed
See CHANGELOG.md for the full list.
v1.6.5
pollen v1.6.5
A small correctness patch closing an RFC 6749 §6 compliance
gap that v1.6.4 amplified. One fix, no new surface, no
behavior changes outside the bug it addresses.
The OAuth refresh path silently dropped the existing
refresh_token whenever an IdP omitted it from the refresh
response. Per RFC 6749 §6, omitting refresh_token in the
response means "keep using the one you sent us" — and a
number of mainstream IdPs do exactly that. Google OAuth
documents the behavior explicitly: "the refresh token might
not be returned with every access token". Microsoft Entra,
Auth0, and Okta also support non-rotating configurations.
Pre-v1.6.5 pollen's Refresh function returned whatever the
server gave back, including an empty refresh_token, and let
the caller (the auto-refresh-on-send path from v1.6.0) store
the empty value. v1.6.4's disk persistence then wrote that
empty value to ~/.config/pollen/oauth_tokens.json, so every
subsequent session lost the ability to refresh against the
same IdP — exactly the value proposition that v1.6.4 was
supposed to deliver, defeated.
The fix lives in oauth.Refresh: when postForm returns a
Token whose RefreshToken is empty, Refresh fills it from
the caller-supplied cfg.RefreshToken. This matches what
golang.org/x/oauth2 does and aligns with the RFC. The
rotation path (server returns a new refresh_token) is
unaffected — the new token still replaces the old.
Fixed:
- oauth.Refresh now preserves the input refresh_token when
the IdP omits it from the response. With v1.6.4's disk
persistence, this means refresh capability survives across
sessions for non-rotating IdPs like Google OAuth
Notes:
- v1.x SemVer-frozen surface unchanged. No settings, key
bindings, or on-disk file formats changed - Existing on-disk entries that already lost their
refresh_token from earlier (v1.6.4) sessions cannot be
recovered automatically — re-fetch viag(CC) or
re-authorize via browser (AC) once, and future refreshes
then persist correctly - The rotation path (server returns a fresh refresh_token)
is untouched and still propagates + persists the new token
See CHANGELOG.md for the full list.
v1.6.4
pollen v1.6.4
A patch release that fills in the last item from the OAuth
roadmap reserved back in v1.5.0: tokens now persist across
pollen sessions.
Authorization Code with PKCE shipped in v1.6.0 with a real
browser dance. Every restart re-opening pollen would have
required re-running that dance to get a fresh access token —
a meaningful friction. Client Credentials was lighter (one
keypress to re-fetch) but the same overall cost: tokens
existed only for the lifetime of the process.
This patch persists both grants. After a successful fetch
or refresh, the token (plus its config context — Token URL,
Client ID, Scope, Grant) is written to
~/.config/pollen/oauth_tokens.json with mode 0600 (owner
read/write only) via an atomic temp-file-then-rename. On
next start, when the Auth panel's Token URL and Client ID
match a stored entry, the access token and refresh token
are hydrated automatically — the "Bearer …" preview appears
without pressing g. The hydrated-but-expired case still
runs through the v1.6.0 auto-refresh-on-send path.
Entries are keyed by (Token URL, Client ID, Grant), so CC
and AC tokens for the same IdP/client coexist as
independent entries. Scope is recorded but not part of the
key — re-fetching with a different scope cleanly overwrites
the prior entry.
Default is opt-out: the value prop is real and the file
mode is industry-standard for dev-tool credential storage
(gh, gcloud, aws-cli all write similar files with 0600 in
~/.config or ~/.local). Users who want session-only OAuth
set "oauth_persist_tokens": false in settings.json.
Two recovery paths for users who want to forget a stored
token without editing files:
-
From inside pollen: press
don the Auth panel's
action row (the same row wheregtriggers fetch /
authorize / refresh). The current Token URL + Client
ID entry is removed from the on-disk store and the
in-memory token is cleared. A status toast confirms. -
From a shell:
rm ~/.config/pollen/oauth_tokens.json.
Added:
- OAuth token persistence to disk for both Client
Credentials and Authorization Code with PKCE. New file:
~/.config/pollen/oauth_tokens.json (0600). Hydrated on
start when Token URL + Client ID match don the OAuth / OAuth AC action row forgets the
persisted token for the current Token URL + Client IDoauth_persist_tokenssettings flag (default true)- userconfig.SaveJSONSecure internal helper for 0600-mode
atomic JSON writes
Notes:
- v1.x SemVer-frozen surface: only additive changes (a new
settings field, a new keybinding, a new on-disk file).
Existing configurations load unchanged. The Auth panel's
tab strip is unchanged - Default is opt-out (oauth_persist_tokens: true) because
the value prop is real and 0600 mode matches gh / gcloud /
aws-cli posture. Thedshortcut and the settings flag
give two opt-out paths - Token encryption at rest is intentionally not provided;
0600 in ~/.config is the same posture as other dev tools
See CHANGELOG.md for the full list.
v1.6.3
pollen v1.6.3
A small UX patch on top of v1.6.2. Two fixes in one
codepath, no new surface, no behavior changes outside the
bugs they address.
Both fixes are status-bar toast hygiene. pollen's status
line is updated via setStatus() (which just sets the
text) plus a separate statusTick() Cmd that emits a
delayed clearStatusMsg. The two are paired explicitly by
each arm of Update — but two arms had been missing the
tick, leaving toasts stuck on screen until something else
happened to set a new status.
The first miss is the y (copy response body) shortcut in
the response panel. The handler called deliverCopy(),
which sets "copied as response body" (or a clipboard-
fallback string), and then returned m, nil instead of
m, m.statusTick(2 * time.Second). Every other
deliverCopy caller in pollen (cURL copy, fetch copy,
collection ops, history ops) follows the pair-with-tick
pattern. The response-copy arm was an isolated omission.
The second miss is in the v1.6.0 auto-refresh-on-send
path. When the OAuth token is near expiry and a
refresh_token is available, pollen sets "refreshing OAuth
token…" and dispatches the refresh. On success, the
authRefreshedSendMsg handler ran the actual send and
returned. But sendResultMsg never updates the status, so
the "refreshing…" string stayed visible long after the
refresh and the send had completed — strongly suggesting
to the user that the refresh was still in flight. The
success handler now sets "OAuth token refreshed" and uses
tea.Batch to fire both the send and a 2-second tick
independently, so the actual response and the dismissal of
the confirmation toast can race without blocking each
other.
The failure path (authRefreshFailedMsg) was already
correct in v1.6.0 — it replaced the "refreshing…" string
with a "refresh failed: …" error and a tick. Only the
success branch had the trailing-toast bug.
Fixed:
yin the response panel: status now auto-clears after
2 seconds, matching every other copy shortcut- Auto-refresh-on-send success: "refreshing OAuth token…"
is replaced with "OAuth token refreshed" + 2-second tick
instead of persisting indefinitely
Notes:
- v1.x SemVer-frozen surface unchanged. No new keybindings,
no changes to settings/persistence formats - Audit coverage: 3 Explore agents ran in parallel over the
entire repo. 7 agent-flagged candidates were rejected as
false positives after primary-source verification; 1
more (env.json migration edge case) was downgraded to a
defensible improvement rather than a real bug. Only the
two status-toast fixes above remained as confirmed
defects
See CHANGELOG.md for the full list.
v1.6.2
pollen v1.6.2
A small correctness patch on top of v1.6.1. Two fixes, no
new surface, no behavior changes outside the bugs they
address.
The first fix closes a v1.6.0 latent bug in the
Authorization Code with PKCE flow: parseLoopback
accepted ::1 as a valid loopback host (and a unit test
even pinned that acceptance) but the matching net.Listen
call was hard-coded to 127.0.0.1. A user who set
Redirect URI: http://[::1]:8765/callback would have
parseLoopback accept it, the IdP redirect the browser to
[::1]:8765, and the IPv4-only listener never see the
callback — silent 5-minute timeout. Fixed by routing the
parsed host through net.JoinHostPort, so 127.0.0.1
binds IPv4, ::1 binds IPv6, and localhost follows
the kernel's dual-stack default. A new IPv6 end-to-end
regression test pins the fix and skips cleanly on
environments without IPv6 loopback.
The second fix refreshes the Ctrl+/ help overlay's Auth
section, which had been stuck on pre-v1.6.0 wording. The
type list still showed only "None / Bearer / Basic /
OAuth"; the g row read as Client-Credentials-only; and
Esc-on-action-row (the in-flight cancel for an OAuth AC
flow, added in v1.6.0) was completely undocumented. The
section now lists all five types, describes g as
covering both grants, and includes the Esc cancel as its
own row.
Also rewrites a comment in internal/oauth/authcode.go
that had been describing recovery behavior (URL printed
to stderr / shown in the status line) that the code never
implemented. The functional gap remains — there is still
no recovery path if openBrowser fails to launch — and
is reserved for a future release with the broader
URL-stashing redesign.
Fixed:
- OAuth Authorization Code: callback server now binds to
the host parsed from RedirectURI, fixing the
http://[::1]:port/...case that previously timed out - Ctrl+/ help: Auth section refreshed to document OAuth
AC, the dual-purposegkey, and Esc-on-action-row as
the in-flight cancel
Notes:
internal/oauth.parseLoopbackis unexported; its
signature change (adds a host return value) is internal- v1.x SemVer-frozen surface unchanged
- Recovery path for
openBrowserfailure remains a known
gap — see scope-out notes in the source comment
See CHANGELOG.md for the full list.