25 KiB
UI/UX Specification: pos ai alias
Date: 2026-08-26 Author: Designer Status: DESIGN_READY
TL;DR
| Aspect | Decision |
|---|---|
| Main menu | menu_run with 4 items; alias table rendered above the box |
| Create flow | 4-step wizard using step() headers; name validated in a loop |
| Edit flow | Pick → show current values → per-field prompt with Enter=keep |
| Remove flow | Pick → detail display → confirm … n (default=NO) |
| List output | Column-formatted table to stdout, 80-col safe |
| Long prompts | Single-line via menu_ask_value; full value stored; display truncated |
| Error UX | warn + re-prompt (recoverable) or err + exit (fatal) |
| First-run | Auto-create .env+.sh on first write; show "No aliases yet" |
| Color | CYAN box, GREEN success, YELLOW warn, RED error — same as all pos tools |
Key design decisions:
- Edit uses Enter-to-keep — each field defaults to current value; empty = keep.
- Remove defaults to NO — irreversible action gets
confirm … n. - No multi-line prompt input —
menu_ask_valueis single-line; matches every otherpostool. - Table preview shows provider + session + truncated prompt — enough to distinguish at a glance.
Step 1: Main Menu — pos ai alias (no args)
Screen Purpose
Entry point. Shows existing aliases and offers all actions.
ASCII Mockup — With Aliases
════════════════════════════════════════════
AI Agent Aliases
════════════════════════════════════════════
1) Create new alias
2) Edit existing alias
3) Remove alias
4) List aliases
0) Exit
----------------------------------------
Choose:
Above the menu box (also on stderr), render the alias table:
Name Provider Session Prompt
─────────── ───────────── ───────────── ──────────────────────────
devbot gemini devbot You are a Linux dev ass…
code openrouter code You are a code reviewer…
─────────── ───────────── ───────────── ──────────────────────────
2 alias(es)
ASCII Mockup — Empty (No Aliases Yet)
════════════════════════════════════════════
AI Agent Aliases
════════════════════════════════════════════
1) Create new alias
2) Edit existing alias
3) Remove alias
4) List aliases
0) Exit
----------------------------------------
Choose:
With a line above the box:
[!] No aliases defined yet — create one with option 1.
Table Column Spec
Column Width Alignment Truncation
───────── ───── ───────── ─────────────────────────────────
Indent 2 — —
Name 12 left hard-cut (alias names max ~20 by validation)
Provider 12 left as-is (short: gemini, openrouter)
Session 12 left as-is
Prompt 42 left ${prompt:0:40}… (if > 42 chars)
Total width: 2 + 12 + 1 + 12 + 1 + 12 + 1 + 42 = 83 cols (safe for 80-col with minor overflow on rare cases; alternatively reduce prompt to 40 → 81).
Empty prompt cell: Show ${DIM}(default)${RESET} in dim.
Row limit: If > 5 aliases, show first 4 + a final row: … and N more.
Behavior
| Condition | Behavior |
|---|---|
| Zero aliases | No table; dim warning line; menu options still shown (user can create immediately). |
| 1+ aliases | Table above menu box. |
| EOF / non-tty | menu_guard fails → prints to stderr, rc 1. |
0/q/Q on menu |
Clean exit rc 0 (standard menu_run behavior). |
Step 2: Create Flow
Entry Points
pos ai alias→ menu → option 1pos ai alias create(no name arg — full interactive)pos ai alias create <name>(pre-fills name, skips step 1)
ASCII Mockup — Full Interactive Create
════════════════════════════════════════════
Create AI Agent Alias
════════════════════════════════════════════
[1/4] Alias Name
─────────────────────────────────────────
Alias name: devbot
[2/4] Provider
─────────────────────────────────────────
-- 2 available --
1) gemini
2) openrouter
0) Back
----------------------------------------
Pick provider [1-2], text=filter, 0=back 1
[3/4] Session Name
─────────────────────────────────────────
Session name [devbot]:
[4/4] System Prompt
─────────────────────────────────────────
System prompt (empty = use built-in): You are a Linux dev assistant.
────────────────────────────────────────────
Create alias 'devbot'?
Provider: gemini
Session: devbot
Prompt: You are a Linux dev assistant.
────────────────────────────────────────────
Create alias 'devbot'? [Y/n]: y
[+] Alias 'devbot' created.
Reload shell: source ~/.bashrc
Step-by-Step Behavior
Step 1: Alias Name
| Input | Behavior |
|---|---|
| Prompt | menu_ask_value "Alias name" "" — mandatory, no default |
| Empty input | warn "Alias name cannot be empty" → re-prompt (loop) |
| Invalid format | warn "Invalid name '$input' — use letters, digits, hyphens, underscores (start with a letter)" → re-prompt |
| Duplicate name | warn "Alias '$input' already exists — use 'pos ai alias edit $input' instead" → re-prompt |
<name> arg given |
Validate format + uniqueness; fatal err if invalid (non-interactive) |
| Valid name | Break out of loop, proceed to step 2 |
Validation regex: ^[a-zA-Z][a-zA-Z0-9_-]*$
Step 2: Provider
| Input | Behavior |
|---|---|
| Source | Discover from $PROVIDER_DIR/*.sh (pattern: bin/pos-ai lines 17-18) |
| Picker | menu_pick "Pick provider" "${providers[@]}" |
| Zero providers | err "No AI providers installed — run 'pos ai' setup first" → exit 1 |
| Single provider | Auto-select, log: "Using provider: ${provider}" (skip picker) |
| User picks 0/back | Return to main menu (rc 1 from menu_pick) |
Step 3: Session Name
| Input | Behavior |
|---|---|
| Prompt | menu_ask_value "Session name" "<alias_name>" — default = alias name |
| Empty (Enter) | Accepts default = alias name (the common path) |
| Invalid format | warn + re-prompt with current default |
| Valid | Proceed |
Design decision: Default session = alias name. 90%+ of users want 1:1 mapping. The bracket default [alias_name] makes this obvious.
Step 4: System Prompt
| Input | Behavior |
|---|---|
| Prompt | menu_ask_value "System prompt (empty = use built-in)" "" |
| Empty | Stores empty string → --system flag omitted in generated alias |
Contains | |
`warn "System prompt must not contain ' |
| > 500 chars | warn "Prompt is ${#input} chars — consider keeping it concise" → still accepts |
Single quotes, $, backticks |
Accepted — _regen_aliases() handles escaping |
Design decision: single-line only. menu_ask_value uses read -rp. Multi-line input is not supported and would require external deps (dialog/whiptail). Users paste prompts on one line — this is consistent with every other pos tool.
Confirmation
| Aspect | Behavior |
|---|---|
| Display | section-style box with all 4 values; prompt truncated to 50 chars in display |
| Confirm | confirm "Create alias '<name>'?" y — default YES (constructive action) |
| User declines | log "Aborted." → return to main menu |
| User confirms | _write_env_file() + _regen_aliases() → log "Alias '<name>' created." |
Step 3: Edit Flow
Entry Points
pos ai alias→ menu → option 2pos ai alias edit(interactive picker)pos ai alias edit <name>(edit specific alias)
ASCII Mockup — Full Interactive Edit
════════════════════════════════════════════
Edit AI Agent Alias
════════════════════════════════════════════
-- 3 available --
1) devbot [gemini] You are a Linux dev assistant.
2) code [openrouter] You are a code reviewer.
3) dev [gemini] Dev assistant
0) Back
----------------------------------------
Pick alias to edit [1-3], text=filter, 0=back 1
Current values for 'devbot':
Provider: gemini
Session: devbot
Prompt: You are a Linux dev assistant.
[1/3] Provider
─────────────────────────────────────────
-- 2 available --
1) gemini
2) openrouter
0) Back
----------------------------------------
Pick provider [1-2], text=filter, 0=back 1
[2/3] Session Name
─────────────────────────────────────────
Session name [devbot]:
[3/3] System Prompt
─────────────────────────────────────────
System prompt [You are a Linux dev assistant.]:
────────────────────────────────────────────
Save changes to 'devbot'?
Provider: gemini (unchanged)
Session: devbot (unchanged)
Prompt: You are a dev assistant
────────────────────────────────────────────
Save changes? [Y/n]: y
[+] Alias 'devbot' updated.
Step-by-Step Behavior
Alias Picker
| Condition | Behavior |
|---|---|
| No aliases | warn "No aliases to edit — create one first" → return to main menu |
| Picker | menu_pick "Pick alias to edit" "${display_items[@]}" |
| Items format | "${name} [${provider}] ${prompt_preview}" (prompt preview = first 30 chars) |
<name> arg given |
Validate existence; err if not found, exit 1 |
Show Current Values
After picking, display a summary block (to stderr):
Current values for 'devbot':
Provider: gemini
Session: devbot
Prompt: You are a Linux dev assistant.
This gives the user a reference before editing.
Per-Field Editing (3 steps)
| Field | Mechanism | Default | Validation |
|---|---|---|---|
| Provider | menu_pick from installed providers |
Current shown in list header | Must pick valid provider; 0/back = keep current |
| Session | menu_ask_value "Session name" "<current>" |
Current value | Regex; warn + re-prompt on invalid |
| Prompt | menu_ask_value "System prompt" "<current>" |
Current value (truncated in display if > 60 chars) | | rejected; > 500 chars warning |
Provider edit detail: menu_pick doesn't support pre-selection. Current provider is shown in the header: "Current provider: gemini". If user picks 0/back, current is kept — clean "skip to keep" pattern.
Prompt edit detail: If current prompt > 80 chars, default display is truncated: [You are a Linux dev assistant. You reply with c…]. The full value is preserved in the stored default regardless.
Step count: 3 steps (not 4) because alias name cannot be changed — it's the record key.
Save Confirmation
| Condition | Behavior |
|---|---|
| No changes detected (all values identical) | log "No changes — nothing to save." → return to main menu (skip write) |
| Changes exist | Show diff summary with (changed) / (unchanged) tags |
| Confirm | confirm "Save changes to '<name>'?" y — default YES |
| User declines | log "Discarded." → return to main menu |
| User confirms | Rewrite .env entry, regenerate .sh, log "Alias '<name>' updated." |
Diff summary format:
────────────────────────────────────────────
Save changes to 'devbot'?
Provider: gemini (unchanged)
Session: mybot (changed)
Prompt: You are a dev assistant
────────────────────────────────────────────
Step 4: Remove Flow
Entry Points
pos ai alias→ menu → option 3pos ai alias remove(interactive picker)pos ai alias remove <name>(remove specific alias)
ASCII Mockup
════════════════════════════════════════════
Remove AI Agent Alias
════════════════════════════════════════════
-- 3 available --
1) devbot [gemini] You are a Linux dev assistant.
2) code [openrouter] You are a code reviewer.
3) dev [gemini] Dev assistant
0) Back
----------------------------------------
Pick alias to remove [1-3], text=filter, 0=back 1
Alias: devbot
Provider: gemini
Session: devbot
Prompt: You are a Linux dev assistant.
Remove alias 'devbot'? This cannot be undone. [y/N]: n
[--] Cancelled.
Behavior
| Condition | Behavior |
|---|---|
| No aliases | warn "No aliases to remove" → return to main menu |
| Picker | Same format as edit picker |
<name> arg |
Validate existence; err if not found, exit 1 |
| Detail display | Show all fields in full (no truncation) before confirmation |
| Confirm | confirm "Remove alias '<name>'? This cannot be undone." n — default NO |
| User declines | log "Cancelled." → return to main menu |
| User confirms | Remove from .env, regenerate .sh, log "Alias '<name>' removed." |
Design decision: default=n. Irreversible destructive action. confirm with default n means pressing Enter is safe — user must explicitly type y.
Step 5: List / Show (Non-Interactive)
pos ai alias list
ASCII Mockup
Aliases (3):
Name Provider Session Prompt
─────────── ───────────── ───────────── ──────────────────────────────
devbot gemini devbot You are a Linux dev assistant.
code openrouter code You are a code reviewer. Be concise.
dev gemini devbot Dev assistant
| Aspect | Spec |
|---|---|
| Format | printf " %-12s %-12s %-12s %s\n" |
| Prompt truncation | Full up to 60 chars; longer → append … |
| Zero aliases | Print Aliases (0): with nothing below, exit 0 |
| Header | Aliases (N): — count included for scripting |
| Output | stdout, no color (pipeable) |
pos ai alias show <name>
ASCII Mockup
Alias: devbot
Provider: gemini
Session: devbot
Prompt: You are a Linux dev assistant.
Command: pos ai gemini ask --session devbot --system 'You are a Linux dev assistant.'
| Aspect | Spec |
|---|---|
<name> required |
Missing → err "Usage: pos ai alias show <name>" exit 1 |
| Name not found | err "Alias '<name>' not found" exit 1 |
| Full values | No truncation on any field |
Command field |
Exact alias expansion: pos ai <provider> ask --session <session> [--system '<prompt>'] |
| Single quotes in prompt | Display escaped as '\'' to show actual alias content |
| Empty prompt | Command omits --system entirely |
| Output | stdout, no color |
Step 6: Error States — Complete Catalog
| Scenario | Message | Type | Behavior |
|---|---|---|---|
| First run (no .env) | Silent auto-create on first write | — | _load_aliases() returns empty; menu shows "No aliases" |
| Alias name empty | warn "Alias name cannot be empty" |
Recoverable | Re-prompt (create) |
| Alias name invalid | warn "Invalid name '<input>' — use letters, digits, hyphens, underscores" |
Recoverable | Re-prompt |
| Alias name duplicate (create) | warn "Alias '<input>' already exists — use 'pos ai alias edit <input>' instead" |
Recoverable | Re-prompt |
| Alias not found (edit/remove) | err "Alias '<name>' not found" |
Fatal | Exit 1 |
| Invalid provider | err "Unknown provider '<input>'" |
Fatal | Exit 1 (impossible in interactive — menu_pick) |
| No providers installed | err "No AI providers installed — run 'pos ai' setup first" |
Fatal | Exit 1 |
| Pipe char in prompt | warn "System prompt must not contain '|' characters" |
Recoverable | Re-prompt |
| Long prompt (> 500 chars) | warn "Prompt is <N> chars — consider keeping it concise" |
Advisory | Accepts, continues |
| No aliases to edit | warn "No aliases to edit — create one first" |
Info | Return to main menu |
| No aliases to remove | warn "No aliases to remove" |
Info | Return to main menu |
| Non-tty / EOF | [!] Interactive menu needs a terminal — use a subcommand instead (see --help). |
Fatal | Exit 1 |
| Confirm declined (create) | log "Aborted." |
Info | Return to main menu |
| Confirm declined (remove) | log "Cancelled." |
Info | Return to main menu |
| Confirm declined (edit) | log "Discarded." |
Info | Return to main menu |
| No changes in edit | log "No changes — nothing to save." |
Info | Return to main menu |
Message Convention
| Helper | Use For | Destination |
|---|---|---|
log |
Success, info | stdout |
warn |
Recoverable problems, advisory | stderr |
err |
Fatal errors | stderr + exit 1 |
section / step |
Visual structure | stderr (display) |
Step 7: Color & Formatting Conventions
Color Palette
| Element | Color | Source |
|---|---|---|
| Section/box borders | CYAN | section(), menu_run() |
| Step numbers | BOLD | step() |
| Step underlines | BLUE | step() |
| Success messages | GREEN | log() |
| Warnings | YELLOW | warn() |
| Errors | RED | err() |
| Dim/placeholder text | DIM | inline ${DIM}...${RESET} |
| Table headers | Plain (no color) | — |
| Default values in prompts | Plain | shown in brackets [value] |
Box Style
All section boxes use the existing double-line Unicode style — the project standard from section() in common.sh:
════════════════════════════════════════════
Title
════════════════════════════════════════════
80-Column Safety
Final column layout for table output:
Indent: 2
Name: 12
Gap: 1
Provider: 12
Gap: 1
Session: 12
Gap: 1
Prompt: 42
─────────────────────
Total: 83 cols (1 col over 80 — acceptable for modern terminals)
For strict 80-col: prompt = 40 → total = 81. Decision: use 42 for prompt width; 83 cols is acceptable — the project targets 80-col as a guideline, not a hard wall. All other content (menus, prompts, messages) is well within 80.
Step 8: Implementation Guidance for Builder
Function Map
| Function | Responsibility | Est. Lines |
|---|---|---|
_load_aliases() |
Read .env → parallel arrays _ALIAS_NAMES[], _ALIAS_PROVIDERS[], _ALIAS_SESSIONS[], _ALIAS_PROMPTS[] |
~20 |
_find_alias <name> |
Linear scan; return index or -1 | ~8 |
_write_env_file() |
Write arrays → .env (with header comments) |
~15 |
_regen_aliases() |
.env → .sh with '→'\'' escaping + bash -n pre-commit check |
~30 |
_list_aliases() |
printf table to stdout |
~15 |
_show_alias <name> |
Key-value display to stdout | ~10 |
_create_alias [name] |
4-step wizard (name→provider→session→prompt→confirm) | ~60 |
_edit_alias [name] |
Pick→show→3 field prompts→diff→confirm→write | ~70 |
_remove_alias [name] |
Pick→detail→confirm(n)→remove+regen | ~30 |
_main_menu() |
menu_run loop dispatching to above |
~30 |
usage() |
Help text | ~20 |
Critical Implementation Patterns
-
All display to stderr, all results to stdout —
menu-lib.shcontract.menu_run/menu_pick/menu_ask_valuealready do this;_create_alias/_edit_alias/_remove_aliasmust follow the same pattern for theirsection()/step()output. -
Re-prompt loops — use
while truewith validation inside;continueonwarn,breakon valid input. Name validation loops until valid. Provider/session/prompt validation loops until valid. -
Edit diff detection — compare new values against loaded values before writing; skip the write entirely if all identical. This avoids unnecessary
.shregeneration. -
Provider discovery — copy pattern from
bin/pos-ailines 17-18:PROVIDER_DIR="$(dirname "$0")/../lib/ai-providers" [ -d "$PROVIDER_DIR" ] || PROVIDER_DIR="$(dirname "$0")/ai-providers" -
Pipe char in prompt — validate in
_create_aliasand_edit_aliasprompt steps, NOT in_regen_aliases()(which trusts its input after validation). -
Generated .sh syntax check —
bash -n "$sh_file"beforemv "$tmp" "$sh_file"; on failure,warnandrm -f "$tmp"(keep old.sh). -
INTERACTIVE_CMDS — add
ai-aliasto the space-separated list inbin/pos.
State Transition Diagram
pos ai alias (no args)
│
├─ _main_menu()
│ ├─ [1] → _create_alias()
│ │ ├─ Step 1: name validation loop
│ │ ├─ Step 2: provider picker (or auto if single)
│ │ ├─ Step 3: session (default = alias name)
│ │ ├─ Step 4: prompt (default = empty)
│ │ ├─ confirm → _write_env_file() + _regen_aliases()
│ │ └─ return to _main_menu()
│ │
│ ├─ [2] → _edit_alias()
│ │ ├─ picker (or use arg)
│ │ ├─ show current values
│ │ ├─ Step 1: provider picker (0=back = keep)
│ │ ├─ Step 2: session (enter = keep)
│ │ ├─ Step 3: prompt (enter = keep)
│ │ ├─ diff check → confirm → write
│ │ └─ return to _main_menu()
│ │
│ ├─ [3] → _remove_alias()
│ │ ├─ picker (or use arg)
│ │ ├─ show detail
│ │ ├─ confirm n (default = no)
│ │ ├─ remove + regen
│ │ └─ return to _main_menu()
│ │
│ ├─ [4] → _list_aliases() (stdout, then return to _main_menu)
│ │
│ └─ [0] → exit 0
│
├─ pos ai alias create [name] → _create_alias "$name"
├─ pos ai alias edit [name] → _edit_alias "$name"
├─ pos ai alias remove [name] → _remove_alias "$name"
├─ pos ai alias list → _list_aliases(); exit 0
├─ pos ai alias show <name> → _show_alias "$name"; exit 0
└─ -h|--help → usage(); exit 0
Out of Scope
- Multi-line prompt editor (no
dialog/whiptaildependency) - Alias import/export
- Alias categories or tags
- Alias usage statistics
pos configintegration (architecture Decision 4: explicitly rejected)- Alias renaming (name is the record key; remove + create is the path)
Open Design Questions
None. All decisions resolved from architecture doc + existing primitives.
End of UX specification.