-
Notifications
You must be signed in to change notification settings - Fork 154
feat: add task-based model profiles and /profile command #231
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
rinadelph
wants to merge
14
commits into
mpfaffenberger:main
Choose a base branch
from
rinadelph:main
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 6 commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
2d744eb
feat: add task-based model profiles and /profile command
rinadelph dc7192e
style: ruff format two test files (ruff 0.15.5 compat)
rinadelph 6ade7e4
feat: /profile UX — rename task→agent, add tab-completion with model …
rinadelph fbb1c4c
fix: profile model override syncs to active profile JSON on disk
rinadelph 3f7a270
feat: /profile new — interactive TUI wizard for creating profiles
rinadelph 0dacc39
feat: /profile opens TUI directly, auto-detects edit vs create mode
rinadelph 359fe6f
fix: model picker renders inline in right panel, never pollutes scrol…
rinadelph 5403b6e
feat: /profile TUI — new, duplicate, export (E), import (I) actions
rinadelph 4ef3845
refactor: simplify /profile TUI to always-visible dual-panel layout
rinadelph ff49253
fix: address CodeRabbit PR review issues
rinadelph 5dfb2c9
style: run ruff format on test_summarization_agent.py
rinadelph ebffe71
fix: update test mocks for task_models function imports
rinadelph 5712cc8
fix: inline profile naming in TUI (no screen flash)
rinadelph b360b62
fix: eliminate screen flash and log overlay in /profiles TUI
rinadelph File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,226 @@ | ||
| """ | ||
| Tab-completion for the /profile command. | ||
|
|
||
| Provides context-aware completions: | ||
| /profile → subcommands + agent role shortcuts | ||
| /profile set → agent role names (compaction, subagent, …) | ||
| /profile set <role> → model names with provider hints | ||
| /profile reset → agent role names | ||
| /profile load|delete → saved profile names | ||
| """ | ||
|
|
||
| from typing import Iterable | ||
|
|
||
| from prompt_toolkit.completion import Completer, Completion | ||
| from prompt_toolkit.document import Document | ||
|
|
||
| # ── Agent roles that can be configured ──────────────────────────────────────── | ||
| # These match Task enum member names (lowercase) from task_models.py, | ||
| # excluding MAIN (the global default which isn't set through /profile). | ||
| AGENT_ROLES: dict[str, str] = { | ||
| "compaction": "Summarization / context-compaction model", | ||
| "subagent": "Sub-agent dispatch model", | ||
| } | ||
|
|
||
| # ── /profile subcommands ─────────────────────────────────────────────────────── | ||
| PROFILE_SUBCOMMANDS: dict[str, str] = { | ||
| "new": "Create a new profile with the TUI wizard", | ||
| "create": "Create a new profile with the TUI wizard (alias for new)", | ||
| "set": "Set model for an agent role", | ||
| "reset": "Reset an agent role to its default", | ||
| "save": "Save current config as a named profile", | ||
| "load": "Load a named profile", | ||
| "list": "List all saved profiles", | ||
| "delete": "Delete a named profile", | ||
| "guide": "Show configuration reference", | ||
| } | ||
|
|
||
|
|
||
| # ── Lazy data loaders (never raise) ─────────────────────────────────────────── | ||
|
|
||
|
|
||
| def _load_profile_names() -> list[str]: | ||
| try: | ||
| from code_puppy.task_models import list_profiles | ||
|
|
||
| return [p["name"] for p in list_profiles()] | ||
| except Exception: | ||
| return [] | ||
|
|
||
|
|
||
| def _load_model_names() -> list[str]: | ||
| try: | ||
| from code_puppy.command_line.model_picker_completion import load_model_names | ||
|
|
||
| return load_model_names() | ||
| except Exception: | ||
| return [] | ||
|
|
||
|
|
||
| def _model_provider_hint(model_name: str) -> str: | ||
| """Short provider label derived from the model name.""" | ||
| lower = model_name.lower() | ||
| if ( | ||
| model_name.startswith("openai:") | ||
| or "gpt" in lower | ||
| or "o1" in lower | ||
| or "o3" in lower | ||
| ): | ||
| return "OpenAI" | ||
| if model_name.startswith("anthropic:") or "claude" in lower: | ||
| return "Anthropic" | ||
| if model_name.startswith("google-gla:") or "gemini" in lower: | ||
| return "Google" | ||
| if model_name.startswith("groq:") or "llama" in lower or "mixtral" in lower: | ||
| return "Groq" | ||
| if model_name.startswith("mistral:"): | ||
| return "Mistral" | ||
| if model_name.startswith("cerebras:") or "cerebras" in lower or "glm" in lower: | ||
| return "Cerebras" | ||
| if model_name.startswith("xai:") or "grok" in lower: | ||
| return "xAI" | ||
| if ":" in model_name: | ||
| return model_name.split(":", 1)[0].title() | ||
| return "model" | ||
|
|
||
|
|
||
| # ── Completer ───────────────────────────────────────────────────────────────── | ||
|
|
||
|
|
||
| class ProfileCompleter(Completer): | ||
| """ | ||
| Context-aware tab-completion for ``/profile``. | ||
|
|
||
| Plugs into the prompt_toolkit completion pipeline alongside the existing | ||
| SlashCommandCompleter and ModelNameCompleter. | ||
| """ | ||
|
|
||
| TRIGGER = "/profile" | ||
|
|
||
| def get_completions( | ||
| self, document: Document, complete_event | ||
| ) -> Iterable[Completion]: | ||
| text = document.text_before_cursor | ||
| stripped = text.lstrip() | ||
|
|
||
| if not stripped.startswith(self.TRIGGER): | ||
| return | ||
|
|
||
| # Slice off everything before and including "/profile" | ||
| trigger_pos = text.find(self.TRIGGER) | ||
| after = text[trigger_pos + len(self.TRIGGER) :] | ||
|
|
||
| # Nothing typed yet (cursor right after "/profile") — don't complete | ||
| if not after: | ||
| return | ||
|
|
||
| tokens = after.split() | ||
| ends_with_space = after.endswith(" ") | ||
|
|
||
| # ── /profile <partial> → subcommands + role shortcuts ─────────────── | ||
| if len(tokens) == 0 or (len(tokens) == 1 and not ends_with_space): | ||
| partial = tokens[0] if tokens else "" | ||
| # Agent role shortcuts (e.g. /profile compaction gpt-4o) | ||
| for name, meta in AGENT_ROLES.items(): | ||
| if name.startswith(partial): | ||
| yield Completion( | ||
| name, | ||
| start_position=-len(partial), | ||
| display_meta=meta, | ||
| ) | ||
| # Subcommands | ||
| for name, meta in PROFILE_SUBCOMMANDS.items(): | ||
| if name.startswith(partial): | ||
| yield Completion( | ||
| name, | ||
| start_position=-len(partial), | ||
| display_meta=meta, | ||
| ) | ||
| return | ||
|
|
||
| sub = tokens[0] | ||
|
|
||
| # ── /profile set … ──────────────────────────────────────────────────── | ||
| if sub == "set": | ||
| if len(tokens) == 1 and ends_with_space: | ||
| # /profile set <TAB> → agent roles | ||
| for name, meta in AGENT_ROLES.items(): | ||
| yield Completion(name, display_meta=meta) | ||
|
|
||
| elif len(tokens) == 2 and not ends_with_space: | ||
| # /profile set comp<TAB> | ||
| partial = tokens[1] | ||
| for name, meta in AGENT_ROLES.items(): | ||
| if name.startswith(partial): | ||
| yield Completion( | ||
| name, | ||
| start_position=-len(partial), | ||
| display_meta=meta, | ||
| ) | ||
|
|
||
| elif (len(tokens) == 2 and ends_with_space) or ( | ||
| len(tokens) == 3 and not ends_with_space | ||
| ): | ||
| # /profile set compaction <TAB|partial> → model names | ||
| partial = tokens[2] if len(tokens) == 3 else "" | ||
| yield from _model_completions(partial) | ||
|
|
||
| return | ||
|
|
||
| # ── /profile <role> … (shorthand: /profile compaction gpt-4o) ─────── | ||
| if sub in AGENT_ROLES: | ||
| if len(tokens) == 1 and ends_with_space: | ||
| yield from _model_completions("") | ||
| elif len(tokens) == 2 and not ends_with_space: | ||
| yield from _model_completions(tokens[1]) | ||
| return | ||
|
|
||
| # ── /profile reset <TAB|partial> → agent roles ─────────────────────── | ||
| if sub == "reset": | ||
| if len(tokens) == 1 and ends_with_space: | ||
| for name, meta in AGENT_ROLES.items(): | ||
| yield Completion(name, display_meta=meta) | ||
| elif len(tokens) == 2 and not ends_with_space: | ||
| partial = tokens[1] | ||
| for name, meta in AGENT_ROLES.items(): | ||
| if name.startswith(partial): | ||
| yield Completion( | ||
| name, | ||
| start_position=-len(partial), | ||
| display_meta=meta, | ||
| ) | ||
| return | ||
|
|
||
| # ── /profile load|delete <TAB|partial> → saved profile names ───────── | ||
| if sub in ("load", "delete"): | ||
| profiles = _load_profile_names() | ||
| if len(tokens) == 1 and ends_with_space: | ||
| for name in profiles: | ||
| yield Completion(name, display_meta="saved profile") | ||
| elif len(tokens) == 2 and not ends_with_space: | ||
| partial = tokens[1] | ||
| for name in profiles: | ||
| if name.startswith(partial): | ||
| yield Completion( | ||
| name, | ||
| start_position=-len(partial), | ||
| display_meta="saved profile", | ||
| ) | ||
|
|
||
|
|
||
| def _model_completions(partial: str) -> Iterable[Completion]: | ||
| """Yield model name completions filtered by *partial*, with provider hints.""" | ||
| models = _load_model_names() | ||
| partial_lower = partial.lower() | ||
| for model in models: | ||
| model_lower = model.lower() | ||
| if ( | ||
| not partial | ||
| or model_lower.startswith(partial_lower) | ||
| or partial_lower in model_lower | ||
| ): | ||
| yield Completion( | ||
| model, | ||
| start_position=-len(partial), | ||
| display_meta=_model_provider_hint(model), | ||
| ) | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Completion coverage doesn't match the command surface.
handle_profile_command()is registered for/profilestoo, and the handler acceptsclear,rm, andremove. This completer only understands/profile,reset, anddelete, so supported syntax loses all profile-specific completions.Suggested fix
PROFILE_SUBCOMMANDS: dict[str, str] = { "new": "Create a new profile with the TUI wizard", "create": "Create a new profile with the TUI wizard (alias for new)", "set": "Set model for an agent role", "reset": "Reset an agent role to its default", + "clear": "Reset an agent role to its default (alias for reset)", "save": "Save current config as a named profile", "load": "Load a named profile", "list": "List all saved profiles", "delete": "Delete a named profile", + "rm": "Delete a named profile (alias for delete)", + "remove": "Delete a named profile (alias for delete)", "guide": "Show configuration reference", } @@ class ProfileCompleter(Completer): @@ - TRIGGER = "/profile" + TRIGGERS = ("/profiles", "/profile") @@ - if not stripped.startswith(self.TRIGGER): + trigger = next( + ( + candidate + for candidate in self.TRIGGERS + if stripped == candidate or stripped.startswith(f"{candidate} ") + ), + None, + ) + if not trigger: return - # Slice off everything before and including "/profile" - trigger_pos = text.find(self.TRIGGER) - after = text[trigger_pos + len(self.TRIGGER) :] + trigger_pos = text.find(trigger) + after = text[trigger_pos + len(trigger) :] @@ - if sub == "reset": + if sub in ("reset", "clear"): @@ - if sub in ("load", "delete"): + if sub in ("load", "delete", "rm", "remove"):Also applies to: 98-111, 178-208
🤖 Prompt for AI Agents