@@ -941,32 +941,53 @@ def main(args: list[str] | None = None) -> None:
941941
942942
943943def _interactive_shell (opts : argparse .Namespace , config : "Config" , cache ) -> None :
944- """Interactive CLI shell — Claude Code style experience."""
944+ """Interactive CLI shell — Claude Code style experience with readline support ."""
945945 import time as _time
946+ import readline as _readline
947+
948+ # ---- Readline setup for arrow-key history & tab completion ----
949+ _HISTORY_FILE = os .path .expanduser ("~/.deepworm_history" )
950+ _COMMANDS = [
951+ "/help" , "/compare" , "/polish" , "/graph" , "/config" ,
952+ "/models" , "/history" , "/set" , "/clear" , "/exit" ,
953+ ]
954+
955+ def _completer (text : str , state : int ) -> str | None :
956+ if text .startswith ("/" ):
957+ matches = [c for c in _COMMANDS if c .startswith (text )]
958+ else :
959+ matches = []
960+ return matches [state ] if state < len (matches ) else None
961+
962+ _readline .set_completer (_completer )
963+ _readline .parse_and_bind ("tab: complete" )
964+ _readline .set_completer_delims (" \t \n " )
965+ _readline .set_history_length (500 )
966+ try :
967+ _readline .read_history_file (_HISTORY_FILE )
968+ except (FileNotFoundError , OSError ):
969+ pass
970+
971+ def _save_history () -> None :
972+ try :
973+ _readline .write_history_file (_HISTORY_FILE )
974+ except OSError :
975+ pass
946976
947977 # Detect provider info
948978 provider_info = f"{ config .provider } /{ config .model } "
949979
950980 # Welcome banner
951981 console .print ()
952982 console .print (Panel (
953- f"[bold]deepworm[/bold] v{ __version__ } \n \n "
983+ f"[bold white ]deepworm[/bold white ] v{ __version__ } \n \n "
954984 f"AI-powered deep research agent\n "
955985 f"Provider: [cyan]{ provider_info } [/cyan]" ,
956- border_style = "blue " ,
986+ border_style = "bright_blue " ,
957987 expand = False ,
958988 ))
959989 console .print ()
960- console .print (" [dim]Type a topic to research, or use a command:[/dim]" )
961- console .print ()
962- console .print (" [cyan]/help[/cyan] Show all commands" )
963- console .print (" [cyan]/compare[/cyan] Compare multiple topics" )
964- console .print (" [cyan]/polish[/cyan] Analyze a report file" )
965- console .print (" [cyan]/graph[/cyan] Extract knowledge graph" )
966- console .print (" [cyan]/config[/cyan] Show current configuration" )
967- console .print (" [cyan]/models[/cyan] List available models" )
968- console .print (" [cyan]/history[/cyan] Show research history" )
969- console .print (" [cyan]/exit[/cyan] Exit deepworm" )
990+ console .print (" [dim]Just type a topic to start researching. Use[/dim] [cyan]/help[/cyan] [dim]for commands.[/dim]" )
970991 console .print ()
971992
972993 total_tokens = 0
@@ -975,23 +996,27 @@ def _interactive_shell(opts: argparse.Namespace, config: "Config", cache) -> Non
975996
976997 while True :
977998 try :
978- # Prompt with token count if any work was done
999+ # Build prompt — plain text for readline (no Rich markup)
9791000 if total_tokens > 0 :
9801001 elapsed = _time .time () - session_start
981- prompt_text = f"[bold blue]deepworm[/bold blue] [dim] ({ total_tokens :,} tokens | { elapsed :.0f} s)[/dim] > "
1002+ prompt_str = f"\033 [1;34mdeepworm \033 [0m \033 [2m ({ total_tokens :,} tokens | { elapsed :.0f} s)\033 [0m > "
9821003 else :
983- prompt_text = "[bold blue]deepworm[/bold blue] > "
1004+ prompt_str = "\033 [1;34mdeepworm \033 [0m > "
9841005
985- user_input = console . input (prompt_text ).strip ()
1006+ user_input = input (prompt_str ).strip ()
9861007 except (KeyboardInterrupt , EOFError ):
1008+ _save_history ()
9871009 console .print ("\n [dim]Goodbye![/dim]" )
9881010 break
9891011
9901012 if not user_input :
9911013 continue
9921014
9931015 # --- Commands ---
994- if user_input .lower () in ("/exit" , "/quit" , "/q" , "exit" , "quit" ):
1016+ cmd_lower = user_input .lower ()
1017+
1018+ if cmd_lower in ("/exit" , "/quit" , "/q" , "exit" , "quit" ):
1019+ _save_history ()
9951020 elapsed = _time .time () - session_start
9961021 console .print ()
9971022 if researches_done > 0 :
@@ -1005,23 +1030,31 @@ def _interactive_shell(opts: argparse.Namespace, config: "Config", cache) -> Non
10051030 console .print ("[dim]Goodbye![/dim]" )
10061031 break
10071032
1008- if user_input . lower () in ("/help" , "/h" , "help" ):
1033+ if cmd_lower in ("/help" , "/h" , "help" ):
10091034 _show_help ()
10101035 continue
10111036
1012- if user_input . lower () in ("/config" , "/cfg" ):
1037+ if cmd_lower in ("/config" , "/cfg" ):
10131038 _show_config (config )
10141039 continue
10151040
1016- if user_input . lower () in ("/history" , "/hist" ):
1041+ if cmd_lower in ("/history" , "/hist" ):
10171042 _show_history_interactive ()
10181043 continue
10191044
1020- if user_input .lower () in ("/models" ,):
1021- _show_models (config )
1045+ if cmd_lower in ("/models" ,):
1046+ _show_models_interactive (config )
1047+ continue
1048+
1049+ if cmd_lower .startswith ("/set " ):
1050+ _handle_set_command (user_input , config )
1051+ continue
1052+
1053+ if cmd_lower in ("/clear" ,):
1054+ os .system ("cls" if os .name == "nt" else "clear" )
10221055 continue
10231056
1024- if user_input . lower () .startswith ("/compare" ):
1057+ if cmd_lower .startswith ("/compare" ):
10251058 parts = user_input .split (maxsplit = 1 )
10261059 if len (parts ) < 2 :
10271060 console .print ("[dim]Usage: /compare topic1, topic2, topic3[/dim]" )
@@ -1043,7 +1076,7 @@ def _interactive_shell(opts: argparse.Namespace, config: "Config", cache) -> Non
10431076 console .print (f"[red]Error: { e } [/red]" )
10441077 continue
10451078
1046- if user_input . lower () .startswith ("/polish" ):
1079+ if cmd_lower .startswith ("/polish" ):
10471080 parts = user_input .split (maxsplit = 1 )
10481081 if len (parts ) < 2 :
10491082 console .print ("[dim]Usage: /polish <file.md>[/dim]" )
@@ -1057,7 +1090,7 @@ def _interactive_shell(opts: argparse.Namespace, config: "Config", cache) -> Non
10571090 _run_polish_inline (text )
10581091 continue
10591092
1060- if user_input . lower () .startswith ("/graph" ):
1093+ if cmd_lower .startswith ("/graph" ):
10611094 parts = user_input .split (maxsplit = 1 )
10621095 if len (parts ) < 2 :
10631096 console .print ("[dim]Usage: /graph <file.md>[/dim]" )
@@ -1187,15 +1220,17 @@ def _show_help() -> None:
11871220 console .print ()
11881221
11891222 cmds = Table (show_header = False , box = None , padding = (0 , 2 ))
1190- cmds .add_column (style = "cyan" , min_width = 20 )
1223+ cmds .add_column (style = "cyan" , min_width = 24 )
11911224 cmds .add_column (style = "dim" )
11921225 cmds .add_row ("/help" , "Show this help" )
1226+ cmds .add_row ("/models" , "List & switch models interactively" )
1227+ cmds .add_row ("/set <key> <value>" , "Change config (provider, model, depth, breadth)" )
1228+ cmds .add_row ("/config" , "Show current configuration" )
11931229 cmds .add_row ("/compare t1, t2, t3" , "Compare multiple topics" )
11941230 cmds .add_row ("/polish file.md" , "Run polish pipeline on a file" )
11951231 cmds .add_row ("/graph file.md" , "Extract knowledge graph from a file" )
1196- cmds .add_row ("/config" , "Show current configuration" )
1197- cmds .add_row ("/models" , "List available models" )
11981232 cmds .add_row ("/history" , "Show research history" )
1233+ cmds .add_row ("/clear" , "Clear screen" )
11991234 cmds .add_row ("/exit" , "Exit deepworm" )
12001235 console .print (cmds )
12011236
@@ -1204,6 +1239,12 @@ def _show_help() -> None:
12041239 console .print (" [dim]AI safety --output report.md --polish[/dim]" )
12051240 console .print ()
12061241
1242+ console .print (" [bold]Quick config:[/bold]" )
1243+ console .print (" [dim]/set provider google[/dim]" )
1244+ console .print (" [dim]/set model gemini-2.5-flash[/dim]" )
1245+ console .print (" [dim]/set depth 3[/dim]" )
1246+ console .print ()
1247+
12071248 console .print (" [bold]Examples:[/bold]" )
12081249 console .print (" [dim]> What is WebAssembly and why does it matter?[/dim]" )
12091250 console .print (" [dim]> /compare React, Vue, Svelte[/dim]" )
@@ -1254,27 +1295,112 @@ def _show_history_interactive() -> None:
12541295
12551296
12561297def _show_models (config : "Config" ) -> None :
1257- """Show available models for current provider."""
1298+ """Show available models for current provider (non-interactive fallback)."""
1299+ _show_models_interactive (config )
1300+
1301+
1302+ def _show_models_interactive (config : "Config" ) -> None :
1303+ """Show available models with numbered selection."""
12581304 console .print ()
1259- models = {
1305+ all_models = {
12601306 "openai" : ["gpt-4o" , "gpt-4o-mini" , "gpt-4-turbo" , "gpt-3.5-turbo" ],
12611307 "anthropic" : ["claude-sonnet-4-20250514" , "claude-3-5-sonnet-20241022" , "claude-3-haiku-20240307" ],
12621308 "google" : ["gemini-2.5-flash" , "gemini-2.5-pro" , "gemini-2.0-flash" , "gemini-3-flash-preview" ],
1263- "ollama" : ["llama3.1" , "mistral" , "codellama" , "phi3" ],
1309+ "ollama" : ["llama3.2" , "llama3. 1" , "mistral" , "codellama" , "phi3" , "qwen2.5 " ],
12641310 }
1311+
1312+ # Build flat numbered list
1313+ entries : list [tuple [str , str ]] = []
1314+ for provider , models in all_models .items ():
1315+ for model in models :
1316+ entries .append ((provider , model ))
1317+
12651318 tbl = Table (title = "Available Models" , header_style = "bold" , padding = (0 , 1 ))
1266- tbl .add_column ("Provider" , style = "cyan" )
1267- tbl .add_column ("Models" )
1319+ tbl .add_column ("#" , style = "bold cyan" , justify = "right" , width = 3 )
1320+ tbl .add_column ("Provider" , style = "dim" )
1321+ tbl .add_column ("Model" )
12681322 tbl .add_column ("" , style = "dim" )
1269- for provider , model_list in models .items ():
1270- is_current = provider == config .provider
1271- marker = "[green]active[/green]" if is_current else ""
1272- tbl .add_row (provider , ", " .join (model_list ), marker )
1323+
1324+ for idx , (provider , model ) in enumerate (entries , 1 ):
1325+ is_current = provider == config .provider and model == config .model
1326+ marker = "[green bold]\u2713 active[/green bold]" if is_current else ""
1327+ model_style = "[bold green]" if is_current else ""
1328+ model_end = "[/bold green]" if is_current else ""
1329+ tbl .add_row (str (idx ), provider , f"{ model_style } { model } { model_end } " , marker )
1330+
12731331 console .print (tbl )
12741332 console .print (f"\n [dim]Current: { config .provider } /{ config .model } [/dim]" )
1333+ console .print (" [dim]Type a number to switch, or press Enter to cancel:[/dim]" )
1334+
1335+ try :
1336+ choice = input (" > " ).strip ()
1337+ except (KeyboardInterrupt , EOFError ):
1338+ console .print ()
1339+ return
1340+
1341+ if not choice :
1342+ return
1343+
1344+ try :
1345+ idx = int (choice )
1346+ if 1 <= idx <= len (entries ):
1347+ new_provider , new_model = entries [idx - 1 ]
1348+ config .provider = new_provider
1349+ config .model = new_model
1350+ console .print (f" [green]\u2713 Switched to { new_provider } /{ new_model } [/green]" )
1351+ else :
1352+ console .print (f" [yellow]Invalid number. Choose 1-{ len (entries )} .[/yellow]" )
1353+ except ValueError :
1354+ # Maybe they typed a model name directly
1355+ for provider , model in entries :
1356+ if model == choice or choice == f"{ provider } /{ model } " :
1357+ config .provider = provider
1358+ config .model = model
1359+ console .print (f" [green]\u2713 Switched to { provider } /{ model } [/green]" )
1360+ break
1361+ else :
1362+ console .print (f" [yellow]Unknown model: { choice } [/yellow]" )
12751363 console .print ()
12761364
12771365
1366+ def _handle_set_command (user_input : str , config : "Config" ) -> None :
1367+ """Handle /set key value commands."""
1368+ parts = user_input .split (maxsplit = 2 )
1369+ if len (parts ) < 3 :
1370+ console .print (" [dim]Usage: /set <key> <value>[/dim]" )
1371+ console .print (" [dim]Keys: provider, model, depth, breadth[/dim]" )
1372+ return
1373+
1374+ key = parts [1 ].lower ()
1375+ value = parts [2 ].strip ()
1376+
1377+ if key == "provider" :
1378+ valid = ("openai" , "anthropic" , "google" , "ollama" )
1379+ if value not in valid :
1380+ console .print (f" [yellow]Valid providers: { ', ' .join (valid )} [/yellow]" )
1381+ return
1382+ config .provider = value
1383+ console .print (f" [green]\u2713 provider = { value } [/green]" )
1384+ elif key == "model" :
1385+ config .model = value
1386+ console .print (f" [green]\u2713 model = { value } [/green]" )
1387+ elif key == "depth" :
1388+ try :
1389+ config .depth = int (value )
1390+ console .print (f" [green]\u2713 depth = { value } [/green]" )
1391+ except ValueError :
1392+ console .print (" [yellow]depth must be a number.[/yellow]" )
1393+ elif key == "breadth" :
1394+ try :
1395+ config .breadth = int (value )
1396+ console .print (f" [green]\u2713 breadth = { value } [/green]" )
1397+ except ValueError :
1398+ console .print (" [yellow]breadth must be a number.[/yellow]" )
1399+ else :
1400+ console .print (f" [yellow]Unknown key: { key } [/yellow]" )
1401+ console .print (" [dim]Valid keys: provider, model, depth, breadth[/dim]" )
1402+
1403+
12781404def _run_polish_inline (text : str , topic : str = "" ) -> None :
12791405 """Run polish pipeline inline in interactive mode."""
12801406 from .readability import analyze_readability
0 commit comments