diff --git a/agent/BACKLOG.md b/agent/BACKLOG.md index a7e8dfa..17fa4e0 100644 --- a/agent/BACKLOG.md +++ b/agent/BACKLOG.md @@ -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.` 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 diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 8da3351..aced5a0 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -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; diff --git a/modules/home/keybinds.nix b/modules/home/keybinds.nix index bd24b61..1b26e02 100644 --- a/modules/home/keybinds.nix +++ b/modules/home/keybinds.nix @@ -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"; } diff --git a/modules/home/rofi.nix b/modules/home/rofi.nix index ca71d0d..ea0e1a9 100644 --- a/modules/home/rofi.nix +++ b/modules/home/rofi.nix @@ -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 diff --git a/modules/nixos/default.nix b/modules/nixos/default.nix index f9a6f50..4bc4d75 100644 --- a/modules/nixos/default.nix +++ b/modules/nixos/default.nix @@ -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 ─────────────────────────────────────────────────────