From 3eed2e185773451574c51a4bad591a2753dac662 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Mon, 13 Jul 2026 23:21:32 -0500 Subject: feat(launcher): bare ai picks the agent before the projects The interactive flow now opens with a runtime picker: claude first (Enter-Enter preserves the old two-keystroke habit), codex labeled as ChatGPT, and one local: line per ollama model, queried live with a short timeout so a dead server just drops those lines. The pick feeds the existing runtime map, then the annotated project multi-select runs as before. --runtime and AI_RUNTIME skip the question, single-directory mode is unchanged, and --print-runtimes exposes the choice list for the two new bats tests. --- claude-templates/bin/ai | 59 ++++++++++++++++++++++++++++++---- scripts/tests/ai-launcher-runtime.bats | 34 +++++++++++++++++++- todo.org | 3 ++ 3 files changed, 88 insertions(+), 8 deletions(-) diff --git a/claude-templates/bin/ai b/claude-templates/bin/ai index c805e8b..cf17875 100755 --- a/claude-templates/bin/ai +++ b/claude-templates/bin/ai @@ -2,10 +2,12 @@ # ai — agent session launcher (unified aix + hey) # # Usage: -# ai Select one or more projects via fzf and open each in -# an 'ai' tmux session window (creates session if needed). +# ai Pick the agent first (claude, codex/ChatGPT, or any +# local ollama model), then select one or more projects +# via fzf; each opens in an 'ai' tmux session window. # Git-aware: fetches, annotates with ↑/↓/dirty, auto-pulls -# clean-and-behind repos before opening. +# clean-and-behind repos before opening. --runtime or +# AI_RUNTIME skips the agent pick. # # ai ... Single-project mode. Opens each given directory directly # in the 'ai' session (new window or switch to existing). @@ -51,6 +53,36 @@ resolve_agent_cmd() { esac } +# One line per launchable agent, claude first (Enter-Enter keeps the old +# muscle memory). Local models appear only when both codex (the CLI that +# drives them) and a live ollama answer; a dead server just drops the lines. +build_runtime_choices() { + command -v claude >/dev/null 2>&1 && echo "claude — Claude Code" + command -v codex >/dev/null 2>&1 && echo "codex — ChatGPT (Codex CLI)" + if command -v codex >/dev/null 2>&1 && command -v ollama >/dev/null 2>&1; then + timeout 3 ollama list 2>/dev/null | tail -n +2 | awk 'NF {print "local:" $1 " — ollama"}' + fi +} + +# Interactive runtime pick for the bare-`ai` flow. Sets RUNTIME (and +# LOCAL_MODEL for a local pick) and re-resolves the agent command. +# Returns 1 when the pick is cancelled. +pick_runtime() { + local choice + choice=$(build_runtime_choices | fzf --height=30% --reverse --prompt='agent> ') || return 1 + [ -z "$choice" ] && return 1 + case "$choice" in + claude*) RUNTIME="claude" ;; + codex*) RUNTIME="codex" ;; + local:*) + RUNTIME="local" + LOCAL_MODEL="${choice#local:}" + LOCAL_MODEL="${LOCAL_MODEL%% *}" + ;; + esac + resolve_agent_cmd +} + # Run in the pane's shell just before Claude launches. `stty susp undef` clears # the tty's SIGTSTP (C-z) character for this pane only, so an accidental C-z is # passed through to Claude as input rather than suspending the session to the @@ -70,7 +102,7 @@ build_instructions() { } usage() { - sed -n '2,20p' "$0" | sed 's|^# \?||' + sed -n '2,23p' "$0" | sed 's|^# \?||' exit 0 } @@ -447,24 +479,31 @@ print_launch_mode() { # ---------- dispatch ---------- print_launch="" +runtime_explicit="${AI_RUNTIME:+1}" while [ $# -gt 0 ]; do case "$1" in -h|--help) usage ;; --runtime) - [ -z "${2:-}" ] && { echo "ai: --runtime needs a value — valid runtimes: claude, codex" >&2; exit 2; } + [ -z "${2:-}" ] && { echo "ai: --runtime needs a value — valid runtimes: claude, codex, local" >&2; exit 2; } RUNTIME="$2" + runtime_explicit=1 shift 2 ;; --runtime=*) RUNTIME="${1#--runtime=}" + runtime_explicit=1 shift ;; --print-launch) print_launch=1 shift ;; + --print-runtimes) + build_runtime_choices + exit 0 + ;; *) break ;; @@ -478,16 +517,22 @@ if [ -n "$print_launch" ]; then print_launch_mode "$1" fi -check_deps - case "${1:-}" in --attach) + check_deps attach_mode ;; "") + # Bare `ai`: pick the agent first (skipped when --runtime or AI_RUNTIME + # already chose), then the familiar project multi-select. + if [ -z "$runtime_explicit" ]; then + pick_runtime || exit 0 + fi + check_deps multi_mode ;; *) + check_deps for arg in "$@"; do single_mode "$arg" done diff --git a/scripts/tests/ai-launcher-runtime.bats b/scripts/tests/ai-launcher-runtime.bats index b93f778..3f0ad69 100644 --- a/scripts/tests/ai-launcher-runtime.bats +++ b/scripts/tests/ai-launcher-runtime.bats @@ -11,10 +11,11 @@ setup() { PROJ="$(mktemp -d)" mkdir -p "$PROJ/.ai" touch "$PROJ/.ai/protocols.org" + STUB_BIN="$(mktemp -d)" } teardown() { - rm -rf "$PROJ" + rm -rf "$PROJ" "$STUB_BIN" } @test "default runtime launches claude" { @@ -63,3 +64,34 @@ teardown() { [ "$status" -eq 1 ] [[ "$output" == *"protocols.org"* ]] } + +@test "--print-runtimes lists claude, codex, and one line per ollama model" { + cat > "$STUB_BIN/ollama" <<'STUB' +#!/bin/bash +if [ "$1" = "list" ]; then + printf 'NAME ID SIZE MODIFIED\n' + printf 'gpt-oss:120b aaa 65 GB 1 hour ago\n' + printf 'qwen3-coder:30b bbb 18 GB 1 hour ago\n' +fi +STUB + chmod +x "$STUB_BIN/ollama" + PATH="$STUB_BIN:$PATH" run bash "$AI" --print-runtimes + [ "$status" -eq 0 ] + [ "${lines[0]%% *}" = "claude" ] + [[ "$output" == *"codex"* ]] + [[ "$output" == *"local:gpt-oss:120b"* ]] + [[ "$output" == *"local:qwen3-coder:30b"* ]] +} + +@test "a dead ollama server just drops the local lines" { + cat > "$STUB_BIN/ollama" <<'STUB' +#!/bin/bash +echo "Error: could not connect to a running Ollama instance" >&2 +exit 1 +STUB + chmod +x "$STUB_BIN/ollama" + PATH="$STUB_BIN:$PATH" run bash "$AI" --print-runtimes + [ "$status" -eq 0 ] + [[ "$output" == *"claude"* ]] + [[ "$output" != *"local:"* ]] +} diff --git a/todo.org b/todo.org index 42dd8f0..00cb024 100644 --- a/todo.org +++ b/todo.org @@ -293,6 +293,9 @@ Analysis in [[file:docs/design/2026-07-13-runtime-portability-inventories.org]] *** 2026-07-13 Mon @ 13:34:17 -0500 Hook parity inventoried — only two hooks carry real porting work Full mapping in [[file:docs/design/2026-07-13-runtime-portability-inventories.org]]. AskUserQuestion deny is moot off Claude (no popup tool to deny); PostToolUse validators survive via the bundles' git pre-commit hooks; clear-resume folds into the session-plumbing child; session-title is cosmetic. Real gaps: PreCompact priority-save (prose downgrade) and Stop wrap-teardown (Codex notify / manual elsewhere) — decisions in the VERIFY below. +*** 2026-07-13 Mon @ 23:21:08 -0500 Interactive agent picker — bare =ai= asks which brain first +Craig's ask: the fzf flow should offer the LLMs before the projects. Bare =ai= now runs a runtime picker (claude first so Enter-Enter keeps the old muscle memory; codex labeled ChatGPT; one =local:= line per ollama model, live-queried with a 3s timeout — a dead server just drops the local lines), then the familiar annotated project multi-select. =--runtime= / =AI_RUNTIME= skip the pick; single-dir mode unchanged. New =--print-runtimes= test seam; 9/9 launcher bats, suite 441/0. This substantially delivers the "easy lightweight way to change agents / agentically democratic" roam ask — the launcher-hardening task keeps the deeper refactor. + *** 2026-07-13 Mon @ 23:04:21 -0500 Local runtime wired — ai --runtime local runs codex --oss over ollama The reserved lane went live the same day: =local= maps to =codex --oss --local-provider=ollama -m $AI_LOCAL_MODEL= (default gpt-oss:120b; qwen3-coder:30b via the env var). Verified end to end: =codex exec --oss --local-provider=ollama -m gpt-oss:120b= returned a correct completion through the local model (~7K tokens). The explicit provider flag avoids config.toml dependence (a root-level =oss_provider= key would also work; appending to the file lands in the last TOML table and silently does nothing — learned live). Dependency check keys on AGENT_BIN (codex) now that AGENT_CMD carries flags. This partially answers the spec's "first supported local editing CLI" blocker: codex-over-ollama is the de-facto v1. The model-floor child's live trial (a takuzu session under =ai --runtime local=) is the next step. -- cgit v1.2.3