Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Not on zsh? The Python CLIs (`shellllm-comma`, `shellllm-ask`, `shellllm-recall`
| Command | What it does | Example |
|---|---|---|
| `, <prompt>` | Propose 3–5 shell commands, pick one in fzf, drop on prompt. Never executes. | `, the five largest files here` |
| `,, [prompt]` | Same, but with terminal context attached. Bare `,,` repairs the previous command. | `,,` after a typo'd command |
| `,,` | Repair the previous command. Top fix lands on your prompt line — no picker. `,, <intent>` for "I meant X"; `,, --pick` to see alternatives. | `,,` after a typo |
| `? <question>` | Ask the model. Streams markdown, keeps a per-pane conversation. Reads piped input. | `? what does git stash do` |
| `???` | Memory and recall. Bare query searches the archive; flags pin long-term facts. | `??? --add I prefer ripgrep` |
| `??` | Start, stop, or check the local `llama-server`. | `?? --start fast` |
Expand Down
5 changes: 4 additions & 1 deletion bash/shellllm.bash
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ _shellllm_with_ctx() {

llmc() { local s=$?; "$SHELLLM_COMMA" "$@"; }
llmcc() { local s=$?; _shellllm_with_ctx "$s" "$SHELLLM_COMMA" --ctx "$@"; }
llmf() { local s=$?; _shellllm_with_ctx "$s" "$SHELLLM_COMMA" --fix; }
# Bare `llmf` repairs; `llmf <intent>` repairs with your stated intent;
# `llmf --pick [intent]` shows the picker. Top suggestion is printed to
# stdout — the user pastes / runs it, never the function.
llmf() { local s=$?; _shellllm_with_ctx "$s" "$SHELLLM_COMMA" --fix "$@"; }
llma() { local s=$?; _shellllm_with_ctx "$s" "$SHELLLM_ASK" "$@"; }
llmm() { "$SHELLLM_RECALL" "$@"; }
Binary file modified demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 9 additions & 8 deletions demo.tape
Original file line number Diff line number Diff line change
Expand Up @@ -123,23 +123,24 @@ Sleep 1s
Enter
Sleep 3s

# ── Beat 2 — `,,` diagnoses then fixes the previous command ─────────────
# ── Beat 2 — `,,` diagnoses, then drops the fix on the prompt line ──────
# A real flag typo on `find`. BSD and GNU find both accept `-type`, so
# the corrected command works on every platform — `,,` diagnoses the
# typo in one stderr line and proposes the corrected command first.
# the corrected command works everywhere. `,,` prints the diagnosis on
# stderr and drops the model's top fix directly on the next prompt
# line — no picker. You read both, then Enter to run, or edit / Ctrl-C.
# `,, --pick` would show the full picker when alternatives matter.
Type "find . -tpye f -name '*.py'"
Sleep 500ms
Enter
Sleep 1500ms
Type ",,"
Sleep 800ms
Enter
Wait+Screen@30s /enter: drop on prompt/
Sleep 800ms
# Wait for the diagnosis line and the dropped fix; give the audience
# time to read both before pressing Enter to run.
Sleep 9s
Enter
Sleep 1s
Enter
Sleep 4s
Sleep 3s
Type "clear"
Enter
Sleep 400ms
Expand Down
37 changes: 28 additions & 9 deletions src/shellllm/comma.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,15 +171,20 @@ def _fix_system_prompt() -> str:
def _print_usage(*, to: Any = None) -> None:
out = to or sys.stdout
out.write(
"usage: , <what you want to do> propose commands (no terminal context)\n"
" ,, <what you want to do> same, with terminal context (, --ctx)\n"
" ,, fix the previous command (, --fix [hint])\n"
" , --new <what you want to do> start a fresh session\n"
"usage: , <prompt> propose commands via fzf picker\n"
" ,, fix the previous command (top fix → prompt line)\n"
" ,, <intent> fix using your stated intent\n"
" ,, --pick [intent] same, but show the picker (see alternatives)\n"
" , --ctx <prompt> propose with terminal context as background\n"
" , --new <prompt> start a fresh session\n"
" , --reset drop current session\n"
" , --history print session transcript\n"
" , --fast|--balanced|--smart … route this call to a tier (zsh)\n"
" , --help show this message\n"
"\n"
"`,,` never executes anything — the suggestion lands on your prompt;\n"
"you confirm with Enter, edit it, or cancel with Ctrl-C.\n"
"\n"
"For facts and cross-session recall, see `??? --help`.\n"
)

Expand Down Expand Up @@ -400,10 +405,15 @@ def _consume_flag(flag: str) -> bool:
if _consume_flag("--new"):
session.archive_and_reset(archive=archive, embed_fn=_safe_embed)

# `,` is the context-free verb; `--ctx` (the zsh `,, <prompt>`) brings
# the terminal context along, and `--fix` (bare `,,`) implies it.
# `,` is the context-free verb; `, --ctx <prompt>` brings the
# terminal context along (the zsh `,,` glyph routes here when run
# with a prompt that the user wants treated as a refinement, not a
# repair); `, --fix` (the zsh bare `,,`) repairs the previous
# command. Fix-mode drops the model's top suggestion straight on
# the prompt — pass `--pick` to see the full picker instead.
ctx_mode = _consume_flag("--ctx")
fix_mode = _consume_flag("--fix")
pick_mode = _consume_flag("--pick")
shell_ctx = ctx_mode or fix_mode

prompt = " ".join(argv).strip()
Expand Down Expand Up @@ -459,9 +469,18 @@ def _consume_flag(flag: str) -> bool:
return 1
content, items = result

chosen = _pick(items)
if not chosen:
return 1
# In fix mode (the typical `,,` flow) we trust the model's top
# suggestion and drop it straight on the user's prompt line — the
# diagnose-then-suggest design already surfaces *why* the model
# thinks this is the fix above. The picker is one keystroke too
# many for obvious typos. `,, --pick` (or `, --fix --pick`) opts
# back into the picker when alternatives matter.
if fix_mode and not pick_mode:
chosen = items[0]["command"]
else:
chosen = _pick(items)
if not chosen:
return 1

# Persist the turn for the next refinement. We store the raw JSON
# the model produced so it sees its own prior list verbatim.
Expand Down
56 changes: 56 additions & 0 deletions tests/test_comma_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,62 @@ def test_fix_appends_user_hint(monkeypatch, capsys, isolated, fake_model, auto_p
assert "I meant the dev branch" in user_msg


def test_fix_default_skips_picker_and_prints_top(monkeypatch, capsys, isolated, fake_model):
"""Bare `,,` must drop the model's top suggestion to stdout without invoking the picker."""
_enable_shell_ctx(monkeypatch)
pick_called = {"hit": False}

def fail_pick(items):
pick_called["hit"] = True
return None

monkeypatch.setattr(comma, "_pick", fail_pick)
assert _run(["--fix"], monkeypatch) == 0
out = capsys.readouterr().out
# `fake_model` fixture returns commands=[{ls -lh, "list with sizes"}, ...]
# so the top is "ls -lh" and it must reach stdout untouched.
assert out.strip() == "ls -lh"
assert pick_called["hit"] is False


def test_fix_pick_flag_opts_back_into_picker(monkeypatch, capsys, isolated, fake_model):
"""`,, --pick` must restore the picker for cases where alternatives matter."""
_enable_shell_ctx(monkeypatch)
pick_calls: list = []

def record_pick(items):
pick_calls.append(items)
return items[1]["command"] # pick the second one to prove we routed through

monkeypatch.setattr(comma, "_pick", record_pick)
assert _run(["--fix", "--pick"], monkeypatch) == 0
assert len(pick_calls) == 1
assert capsys.readouterr().out.strip() == "ls -la"


def test_fix_pick_with_intent(monkeypatch, capsys, isolated, fake_model, auto_pick):
"""`--pick` must coexist with a free-form intent — neither swallows the other."""
_enable_shell_ctx(monkeypatch)
assert _run(["--fix", "--pick", "use", "ripgrep"], monkeypatch) == 0
user_msg = next(m["content"] for m in fake_model[0] if m["role"] == "user")
assert "use ripgrep" in user_msg


def test_plain_comma_unchanged_still_uses_picker(
monkeypatch, capsys, isolated, fake_model, auto_pick
):
"""No-picker behavior is fix-mode-only; plain `,` keeps the fzf flow."""
pick_calls: list = []

def record_pick(items):
pick_calls.append(items)
return items[0]["command"]

monkeypatch.setattr(comma, "_pick", record_pick)
assert _run(["list", "files"], monkeypatch) == 0
assert len(pick_calls) == 1, "plain , must go through the picker"


def test_fix_uses_fix_schema(monkeypatch, capsys, isolated, fake_model, auto_pick):
"""Fix mode must request the diagnose-then-suggest schema, not the plain one."""
_enable_shell_ctx(monkeypatch)
Expand Down
20 changes: 12 additions & 8 deletions zsh/shellllm.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -205,17 +205,21 @@ function ,() {
# default `cmd`) controls how much context rides along.
function ,,() {
local __last_status=$_SHELLLM_PREV_STATUS
# A leading tier flag (`,, --fast …`) is routing, not the prompt —
# pop it so the fix-vs-ctx switch only sees real arguments.
local -a tier
# `,,` is always fix mode. A leading tier flag is routing, not the
# prompt; `--pick` (anywhere) opts into the picker; everything else
# becomes "intent" — a hint the model uses to narrow the repair.
local -a tier pick rest
if [[ "${1:-}" == --fast || "${1:-}" == --balanced || "${1:-}" == --smart ]]; then
tier=("$1"); shift
fi
if (( $# )); then
_shellllm_comma_run $__last_status ${tier[@]} --ctx "$@"
else
_shellllm_comma_run $__last_status ${tier[@]} --fix
fi
local a
for a in "$@"; do
case "$a" in
--pick) pick=(--pick) ;;
*) rest+=("$a") ;;
esac
done
_shellllm_comma_run $__last_status ${tier[@]} --fix ${pick[@]} ${rest[@]}
}

# ─── `?` — answer. `noglob` is required because `?` is a zsh glob char.
Expand Down
Loading