feat(menu): #140 — Ask opens a web chat, provider is in-flake state
All checks were successful
Check / eval (push) Successful in 3m57s
All checks were successful
Check / eval (push) Successful in 3m57s
Bernardo: "opening claude-code is too disruptive". SUPER+CTRL+A spent a terminal, an npm fetch and a REPL on "what's the syntax for…" — the prompt was the right surface, the destination was not. The query is now URL-encoded into the provider's web chat and handed to xdg-open. settings.ask.provider (claude|chatgpt|gemini|grok, default claude) with a Preferences row, read from state at RUN TIME so switching needs no rebuild, and any unknown/absent value falls back rather than opening nothing. He asked for chatgpt + grok alongside claude/gemini, and for the CLI to go: claude-code is deleted and pkgs.nodejs leaves with it (carried only for that npx). Keybind is "Ask AI" now the destination is the user's. The failure mode is the design point: prefill is undocumented on all four vendors, each can drop `?q=` unilaterally, and it fails SILENTLY — you land in an empty chat having already typed the question. So the query is also copied to the clipboard (a vendor breaking change costs one paste, not the question), and the URLs sit in one attrset so the repair is one line. Verified against the generated script: bash -n clean; every bad state value (unset/null/garbage) falls back to claude; a hostile query encodes correctly (& → %26, ? → %3F, # → %23, $(whoami) neutralised, café → caf%C3%A9). NOT verified on purpose: whether each vendor fills the box or auto-submits — that means posting into Bernardo's own accounts. Sweep: ROADMAP's shipped-modules line described the npx REPL; nix-ld's comment cited claude-code as its example; both fixed. Menu row + keybind renamed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -460,36 +460,6 @@ high-ROI, etc.) live in the journal + ROADMAP — not here.*
|
||||
|
||||
### Product / day-2
|
||||
|
||||
### 140. Ask Claude should hand off to a web chat, not open a coding agent
|
||||
|
||||
Bernardo 2026-07-16: "opening claude-code is too disruptive" — he would prefer
|
||||
the query go to a **web interface** (Gemini or Claude) instead. Today
|
||||
SUPER+CTRL+A → `nomarchy-menu ask` (`modules/home/rofi.nix:899–905`) takes the
|
||||
free-text line and does `${cfg.terminal} -e npx --yes @anthropic-ai/claude-code@latest "$q"`:
|
||||
a terminal window, an npm fetch on first run, and a REPL that stays open — a
|
||||
coding agent answering "what's the syntax for…". The prompt surface is right;
|
||||
the destination is wrong for the question the surface invites.
|
||||
|
||||
Cheap version: URL-encode `$q` and `xdg-open` a prefill URL
|
||||
(`https://claude.ai/new?q=…`, `https://gemini.google.com/app?q=…`), landing in
|
||||
the default browser (Chromium, per Decisions). That is a handful of lines and
|
||||
deletes the `npx`/network/REPL cost — plus `pkgs.nodejs`, carried in
|
||||
`home.packages` (`:1685`) **only** for this feature, so the closure shrinks
|
||||
with it. To settle before building:
|
||||
- **Which destination, and is it a choice?** Bernardo named Claude *or*
|
||||
Gemini. A `settings.ask.provider` key keeps it in-flake and menu-writable
|
||||
(the convention) and stops us hardcoding a vendor into a vendor-neutral
|
||||
distro; a fixed default is cheaper. Prefill-URL support is per-vendor and
|
||||
can be changed by them unilaterally — worth verifying each still prefills,
|
||||
since a silently-ignored `?q=` degrades to "opens a blank chat", which is
|
||||
survivable but should be a known cost, not a surprise.
|
||||
- **Keep the CLI at all?** A second row (Ask ▸ web / Ask ▸ agent) preserves
|
||||
today's behaviour for whoever wants it; deleting it is the simpler surface
|
||||
and the one the complaint argues for. Bernardo's call.
|
||||
- Renaming: "Ask Claude" is the keybind desc (`keybinds.nix:57`) and shows in
|
||||
the generated cheatsheet — if the destination becomes configurable, the
|
||||
label should stop naming one vendor.
|
||||
|
||||
### 134. `unstable.<pkg>` in the downstream — a newer app without a second flake
|
||||
|
||||
Bernardo 2026-07-15, wanting a newer LM Studio (pinned 0.4.15-2 vs unstable
|
||||
|
||||
@@ -31,10 +31,10 @@ Design/decision records and a running log of shipped work (items marked
|
||||
- ✓ shipped modules: `network` (nmtui in `$TERMINAL`) · `bluetooth`
|
||||
(blueman-manager) · `capture` (grim/slurp submenu: region/full →
|
||||
clipboard/file, saved to `~/Pictures/Screenshots`) · `keybinds` (the
|
||||
cheatsheet, see below) · `ask` (free-text → claude CLI in a terminal;
|
||||
auths via OAuth, no API key; pulled fresh via `npx
|
||||
@anthropic-ai/claude-code@latest` rather than the nixpkgs package, which
|
||||
lags model releases — `nodejs` is bundled for npx; REPL stays open)
|
||||
cheatsheet, see below) · `ask` (free-text → the provider's web chat in
|
||||
the default browser; provider from `settings.ask.provider`, Preferences ▸
|
||||
Ask provider — see the #140 ✓ entry below for what it used to be and why
|
||||
it changed)
|
||||
- ✓ keybindings cheatsheet: `modules/home/keybinds.nix` is the **single
|
||||
source** for both the Hyprland binds and the SUPER+? rofi list, so they
|
||||
can't drift; `nomarchy-menu keybinds` renders the padded two-column
|
||||
@@ -449,6 +449,28 @@ Design/decision records and a running log of shipped work (items marked
|
||||
(#131). Those windows therefore carry float/center rules and deliberately no
|
||||
`size` rule. Measured after: calendar 1536×936 = exactly 60%×65%, doctor
|
||||
1408×1008 = 55%×70%, both centred in the Waybar work area.
|
||||
- ✓ **Ask hands off to a web chat (#140, 2026-07-16):** Bernardo: "opening
|
||||
claude-code is too disruptive." SUPER+CTRL+A used to spend a terminal, an npm
|
||||
fetch and a REPL on "what's the syntax for…" — the prompt was the right
|
||||
surface, the destination was not. Now the free-text line is URL-encoded into
|
||||
the provider's web chat and handed to `xdg-open` (Chromium, per Decisions).
|
||||
Provider is `settings.ask.provider` (claude · chatgpt · gemini · grok,
|
||||
default claude) with a Preferences row, read from state **at run time**, so
|
||||
switching takes effect on the next question with no rebuild — and an unknown
|
||||
or absent value falls back rather than opening nothing. The `claude-code`
|
||||
path is deleted and `pkgs.nodejs` left with it (it was carried only for that
|
||||
`npx`); the keybind is "Ask AI" now that the destination is the user's.
|
||||
**The interesting bit is the failure mode.** Prefill is undocumented on all
|
||||
four vendors — each can drop `?q=` unilaterally, and it fails *silently*:
|
||||
you land in an empty chat having already typed the question. So the query is
|
||||
also copied to the clipboard, which turns a vendor's breaking change from
|
||||
"your question is gone" into one paste; and the URLs live in a single
|
||||
attrset (`askProviders`) so the repair is one line. Verified with the
|
||||
generated script: every bad state value falls back to claude, and a hostile
|
||||
query (`&`, `?`, `#`, quotes, `$(whoami)`, unicode) encodes correctly —
|
||||
`%26`, `%3F`, `%23`, `caf%C3%A9`. **Unverified on purpose:** whether each
|
||||
vendor's page fills the box or auto-submits — testing that means posting into
|
||||
Bernardo's own accounts.
|
||||
- ✓ **Menus are sized in `ch`, not in % of the monitor (#131, 2026-07-16):**
|
||||
Bernardo saw Recovery's labels ellipsize **on a 2560×1440 external** — which
|
||||
falsified the item's own model ("`width: 40%` + Inter 11 → only below ~1920;
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
{ mods = "$mod CTRL"; key = "K"; action = "exec, nomarchy-menu keyboard"; desc = "Keyboard layout"; group = "Menu"; }
|
||||
{ mods = "$mod CTRL"; key = "S"; action = "exec, nomarchy-menu capture"; desc = "Screenshot / capture"; group = "Media"; }
|
||||
{ mods = "$mod CTRL"; key = "P"; action = "exec, nomarchy-menu colorpicker"; desc = "Color picker (→ clipboard)"; group = "Menu"; }
|
||||
{ mods = "$mod CTRL"; key = "A"; action = "exec, nomarchy-menu ask"; desc = "Ask Claude"; group = "Menu"; }
|
||||
{ mods = "$mod CTRL"; key = "A"; action = "exec, nomarchy-menu ask"; desc = "Ask AI"; group = "Menu"; }
|
||||
{ mods = "$mod CTRL"; key = "D"; action = "exec, nomarchy-menu dnd"; desc = "Do Not Disturb toggle"; group = "Media"; }
|
||||
# I as in "settings" muscle memory (Super+I elsewhere); T for Tools.
|
||||
{ mods = "$mod CTRL"; key = "I"; action = "exec, nomarchy-menu system"; desc = "System menu"; group = "Menu"; }
|
||||
|
||||
@@ -12,6 +12,20 @@ let
|
||||
t = cfg.theme;
|
||||
c = t.colors;
|
||||
termSheet = import ./term-sheet.nix { inherit pkgs; };
|
||||
|
||||
# Ask ▸ web chat (#140). One table, because these URLs are the vendors' to
|
||||
# change: prefill is undocumented on every one of them, so when a `?q=`
|
||||
# stops working the fix must be a one-line edit here and nothing else.
|
||||
# Claude and ChatGPT are the well-attested pair; Gemini and Grok are
|
||||
# best-effort — hence the clipboard fallback in the `ask` handler, which is
|
||||
# what keeps a dropped parameter from costing the user their question.
|
||||
askProviders = {
|
||||
claude = "https://claude.ai/new?q=";
|
||||
chatgpt = "https://chatgpt.com/?q=";
|
||||
gemini = "https://gemini.google.com/app?q=";
|
||||
grok = "https://grok.com/?q=";
|
||||
};
|
||||
askDefaultProvider = "claude";
|
||||
inherit (config.lib.formats.rasi) mkLiteral;
|
||||
|
||||
px = n: mkLiteral "${toString n}px";
|
||||
@@ -897,13 +911,30 @@ ${themeRows}
|
||||
exit 0 ;;
|
||||
|
||||
ask)
|
||||
# Free-text question → claude CLI in a terminal (OAuth, no API key).
|
||||
# Pulled fresh from npm via npx rather than nixpkgs' claude-code,
|
||||
# which lags behind model releases; npx caches after first run. The
|
||||
# REPL stays open for follow-ups.
|
||||
q=$(rofi_menu -p claude < /dev/null) || exit 0
|
||||
# Free-text question → the provider's web chat in the default browser
|
||||
# (#140). It used to open claude-code in a terminal: a coding agent, an
|
||||
# npm fetch on first run and a REPL left sitting there, all to answer
|
||||
# "what's the syntax for…". The prompt was the right surface; the
|
||||
# destination was not.
|
||||
#
|
||||
# The provider is read from the in-flake state on every invocation, so
|
||||
# switching it in Preferences takes effect immediately — no rebuild —
|
||||
# and an unknown/absent value falls back rather than opening nothing.
|
||||
q=$(rofi_menu -p ask < /dev/null) || exit 0
|
||||
{ [ -n "$q" ] && [ "$q" != "$BACK" ]; } || exit 0
|
||||
exec ${cfg.terminal} -e npx --yes @anthropic-ai/claude-code@latest "$q" ;;
|
||||
p=$(nomarchy-state-sync get settings.ask.provider 2>/dev/null)
|
||||
case "$p" in ${lib.concatStringsSep "|" (lib.attrNames askProviders)}) ;; *) p=${askDefaultProvider} ;; esac
|
||||
enc=$(printf '%s' "$q" | ${pkgs.jq}/bin/jq -sRr @uri)
|
||||
case "$p" in
|
||||
${lib.concatStringsSep "\n" (lib.mapAttrsToList
|
||||
(name: url: " ${name}) url='${url}'\"$enc\" ;;") askProviders)}
|
||||
esac
|
||||
# Prefill is the *vendor's* feature, not ours: each can change or drop
|
||||
# `?q=` whenever they like, and the failure is silent — you land in an
|
||||
# empty chat having already typed the question. So the text goes to the
|
||||
# clipboard too. Worst case is one paste, not retyping.
|
||||
printf '%s' "$q" | ${pkgs.wl-clipboard}/bin/wl-copy 2>/dev/null || true
|
||||
exec xdg-open "$url" ;;
|
||||
|
||||
dnd)
|
||||
# Toggle swaync Do-Not-Disturb (-d prints the new state). With DND
|
||||
@@ -1223,7 +1254,7 @@ ${themeRows}
|
||||
row "Web search${menuHint "web"}" system-search
|
||||
row "Capture${menuHint "capture"}" applets-screenshooter
|
||||
command -v hyprpicker >/dev/null 2>&1 && row "Color picker${menuHint "colorpicker"}" preferences-desktop-color
|
||||
row "Ask Claude${menuHint "ask"}" internet-chat
|
||||
row "Ask AI${menuHint "ask"}" internet-chat
|
||||
back
|
||||
} | rofi_menu -show-icons -markup-rows -p Tools) || exit 0
|
||||
case "$choice" in
|
||||
@@ -1332,6 +1363,9 @@ ${themeRows}
|
||||
true|True) row "Update checks (on)" system-software-update ;;
|
||||
*) row "Update checks (off)" system-software-update ;;
|
||||
esac
|
||||
askp=$(nomarchy-state-sync get settings.ask.provider 2>/dev/null)
|
||||
case "$askp" in ${lib.concatStringsSep "|" (lib.attrNames askProviders)}) ;; *) askp=${askDefaultProvider} ;; esac
|
||||
row "Ask provider ($askp)" system-users
|
||||
case "$(nomarchy-state-sync get settings.bluetooth.enable 2>/dev/null)" in
|
||||
false|False) row "Bluetooth package (off)" bluetooth ;;
|
||||
*) row "Bluetooth package (on)" bluetooth ;;
|
||||
@@ -1361,6 +1395,7 @@ ${themeRows}
|
||||
*"Auto-login"*) exec "$0" autologin ;;
|
||||
*"Auto-commit"*) exec "$0" autocommit ;;
|
||||
*"Update checks"*) exec "$0" updates-toggle ;;
|
||||
*"Ask provider"*) exec "$0" ask-provider ;;
|
||||
*"Bluetooth package"*) exec "$0" bluetooth-package ;;
|
||||
*Printing*) exec "$0" printing-toggle ;;
|
||||
*"Power profile"*) exec "$0" power-profile ;;
|
||||
@@ -1369,6 +1404,25 @@ ${themeRows}
|
||||
*Doctor*) exec "$0" doctor ;;
|
||||
esac ;;
|
||||
|
||||
ask-provider)
|
||||
# settings.ask.provider → which web chat SUPER+CTRL+A opens (#140).
|
||||
# `--no-switch`: the ask handler reads this key at run time, so there
|
||||
# is nothing to rebuild — the next question just goes elsewhere.
|
||||
cur=$(nomarchy-state-sync get settings.ask.provider 2>/dev/null)
|
||||
case "$cur" in ${lib.concatStringsSep "|" (lib.attrNames askProviders)}) ;; *) cur=${askDefaultProvider} ;; esac
|
||||
choice=$( {
|
||||
${lib.concatStringsSep "\n" (map
|
||||
(name: " row \"${name}\" system-users") (lib.attrNames askProviders))}
|
||||
back
|
||||
} | rofi_menu -show-icons -p "Ask provider (now: $cur)") || exit 0
|
||||
[ "$choice" = "$BACK" ] && exec "$0" system-preferences
|
||||
case "$choice" in
|
||||
${lib.concatStringsSep "|" (lib.attrNames askProviders)})
|
||||
nomarchy-state-sync --quiet set settings.ask.provider "$choice" --no-switch
|
||||
notify-send "Ask" "SUPER+CTRL+A now opens $choice." ;;
|
||||
esac
|
||||
exec "$0" system-preferences ;;
|
||||
|
||||
updates-toggle)
|
||||
# settings.updates.enable → HM nomarchy.updates (Waybar checker).
|
||||
case "$(nomarchy-state-sync get settings.updates.enable 2>/dev/null)" in
|
||||
@@ -1684,7 +1738,6 @@ in
|
||||
nomarchy-vpn # VPN submenu (NM connect/import + Tailscale)
|
||||
pkgs.fd # files module (fuzzy search over $HOME)
|
||||
pkgs.xdg-utils # xdg-open for the files + web modules
|
||||
pkgs.nodejs # npx, for the Ask Claude module (claude-code from npm)
|
||||
pkgs.networkmanager_dmenu # network module: rofi wifi/VPN picker
|
||||
pkgs.rofi-pulse-select # audio module: sink/source switcher
|
||||
# Capture › OCR region: English only — the unscoped wrapper drags
|
||||
|
||||
@@ -288,9 +288,9 @@ in
|
||||
|
||||
# ── Foreign binaries: nix-ld ─────────────────────────────────────
|
||||
# An ld.so shim so dynamically-linked binaries not built for NixOS
|
||||
# (downloaded tools, language servers, pip/npm-installed ELFs, the
|
||||
# npx-fetched claude-code) run without manual patchelf. On by default —
|
||||
# a pragmatic-desktop expectation.
|
||||
# (downloaded tools, language servers, pip/npm-installed ELFs) run
|
||||
# without manual patchelf. On by default — a pragmatic-desktop
|
||||
# expectation.
|
||||
programs.nix-ld.enable = lib.mkDefault true;
|
||||
|
||||
# ── Firmware ─────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user